Geometri fra IFC, grunnmodell etter ISO 13370 og uoppvarmede soner
test / test (push) Failing after 3s
test / test (push) Failing after 3s
Importen er prøvd mot en virkelig IFC-fil fra Vectorworks Architect 2026 og avdekket tre feil og ett hull. IFC-import - Lengdeenheten hentes nå gjennom prosjektets enhetsoppsett. Filen inneholder to lengdeenheter, og oppslaget traff tilfeldig den ene. - Arealer regnes av geometrien, siden modellen ikke har mengder for vegger og dekker. Trekantene grupperes etter retning, og den største gruppen er delens hovedflate. Metoden gir riktig areal også når vinduer er skåret ut av veggen. - Materialsjikt eksporteres som hver sin geometridel. Deler som ligger i samme plan og overlapper hverandre telles én gang, ellers ble en vegg talt sju ganger. - Himmelretningen følger flatenormalen i stedet for et anslag ut fra tyngdepunktet. - Flater med samme konstruksjon og himmelretning slås sammen, og rommene listes med areal, siden IFC ikke sier hvilke som er oppvarmet. Grunnen og kjelleren - Flater mot grunnen regnes etter NS-EN ISO 13370. En kjellervegg deles i en del over terreng og en nedgravd del, etter høydene `over_terreng` og `dybde`. - Uoppvarmede soner får egen temperatur time for time etter NS-EN ISO 13789, med varmetilskudd fra distribusjonstap og beredere som står der. Uisolerte varmerør i en kjeller teller dermed både som tap og som varme som holder etasjeskilleren varm. - Varmetapsbudsjettet oppgir konstruksjonens egen U-verdi, slik NS 3031 Tabell 3 og energikravene i TEK er formulert, mens timesberegningen bruker grunnens demping. Nordhusstykket 5 er modellert av IFC-geometrien med kjelleren som egen sone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
This commit is contained in:
@@ -0,0 +1,586 @@
|
||||
//! Geometri fra IFC: arealer og flatenormaler fra trekantnett, brep og ekstruderte profiler.
|
||||
//!
|
||||
//! BIM-verktøy eksporterer sjelden mengder for alle bygningsdeler, og IFC-visningen
|
||||
//! ReferenceView bruker tessellert geometri. Denne modulen regner derfor arealet av en
|
||||
//! bygningsdel direkte av geometrien: trekantene grupperes etter retning, og den største
|
||||
//! gruppen av parallelle flater er delens hovedflate. Summen deles på to fordi et lukket
|
||||
//! volum har en forside og en bakside.
|
||||
//!
|
||||
//! Metoden gir riktig areal også når vinduer og dører er skåret ut av veggen, siden hullene
|
||||
//! rett og slett mangler trekanter.
|
||||
|
||||
use crate::inndata::step::{Arg, Fil};
|
||||
|
||||
/// Et trekantnett i bygningsdelens lokale koordinater.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Nett {
|
||||
pub punkter: Vec<[f64; 3]>,
|
||||
pub trekanter: Vec<[usize; 3]>,
|
||||
}
|
||||
|
||||
/// Hovedflaten til en bygningsdel.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Hovedflate {
|
||||
/// Areal av én side, m².
|
||||
pub areal: f64,
|
||||
/// Enhetsnormal i lokale koordinater.
|
||||
pub normal: [f64; 3],
|
||||
/// Utstrekning i flatens plan: (u_min, u_maks, v_min, v_maks), m.
|
||||
plan: [f64; 4],
|
||||
}
|
||||
|
||||
impl Nett {
|
||||
pub fn tom(&self) -> bool {
|
||||
self.trekanter.is_empty()
|
||||
}
|
||||
|
||||
fn trekant(&self, t: [usize; 3]) -> Option<([f64; 3], f64)> {
|
||||
let (a, b, c) = (
|
||||
*self.punkter.get(t[0])?,
|
||||
*self.punkter.get(t[1])?,
|
||||
*self.punkter.get(t[2])?,
|
||||
);
|
||||
let u = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
||||
let v = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
|
||||
let n = [
|
||||
u[1] * v[2] - u[2] * v[1],
|
||||
u[2] * v[0] - u[0] * v[2],
|
||||
u[0] * v[1] - u[1] * v[0],
|
||||
];
|
||||
let lengde = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
|
||||
if lengde < 1e-12 {
|
||||
return None;
|
||||
}
|
||||
Some(([n[0] / lengde, n[1] / lengde, n[2] / lengde], 0.5 * lengde))
|
||||
}
|
||||
|
||||
/// Samlet overflate, m². Tar med begge sider av et lukket volum.
|
||||
pub fn overflate(&self, skala: f64) -> f64 {
|
||||
let s2 = skala * skala;
|
||||
self.trekanter
|
||||
.iter()
|
||||
.filter_map(|t| self.trekant(*t))
|
||||
.map(|(_, a)| a * s2)
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Finner den største gruppen av parallelle trekanter, som er bygningsdelens hovedflate.
|
||||
///
|
||||
/// `skala` gjør lengdene om til meter. Trekanter regnes som parallelle når normalene
|
||||
/// er innenfor omtrent 18 grader, målt uten hensyn til retning, slik at for- og bakside
|
||||
/// havner i samme gruppe.
|
||||
pub fn hovedflate(&self, skala: f64) -> Option<Hovedflate> {
|
||||
let s2 = skala * skala;
|
||||
let mut flater: Vec<([f64; 3], f64)> = self
|
||||
.trekanter
|
||||
.iter()
|
||||
.filter_map(|t| self.trekant(*t))
|
||||
.map(|(n, a)| (n, a * s2))
|
||||
.collect();
|
||||
if flater.is_empty() {
|
||||
return None;
|
||||
}
|
||||
flater.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||
|
||||
let mut grupper: Vec<([f64; 3], f64)> = Vec::new();
|
||||
for (n, a) in &flater {
|
||||
let treff = grupper
|
||||
.iter_mut()
|
||||
.find(|(g, _)| (g[0] * n[0] + g[1] * n[1] + g[2] * n[2]).abs() > 0.95);
|
||||
match treff {
|
||||
Some((_, sum)) => *sum += a,
|
||||
None => grupper.push((*n, *a)),
|
||||
}
|
||||
}
|
||||
let (normal, sum) = grupper
|
||||
.into_iter()
|
||||
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))?;
|
||||
Some(Hovedflate {
|
||||
areal: sum / 2.0,
|
||||
normal,
|
||||
plan: self.utstrekning(normal, skala),
|
||||
})
|
||||
}
|
||||
|
||||
/// Punktenes utstrekning i planet vinkelrett på `normal`, m.
|
||||
fn utstrekning(&self, normal: [f64; 3], skala: f64) -> [f64; 4] {
|
||||
let (u, v) = planakser(normal);
|
||||
let mut ut = [
|
||||
f64::INFINITY,
|
||||
f64::NEG_INFINITY,
|
||||
f64::INFINITY,
|
||||
f64::NEG_INFINITY,
|
||||
];
|
||||
for p in &self.punkter {
|
||||
let a = (p[0] * u[0] + p[1] * u[1] + p[2] * u[2]) * skala;
|
||||
let b = (p[0] * v[0] + p[1] * v[1] + p[2] * v[2]) * skala;
|
||||
ut[0] = ut[0].min(a);
|
||||
ut[1] = ut[1].max(a);
|
||||
ut[2] = ut[2].min(b);
|
||||
ut[3] = ut[3].max(b);
|
||||
}
|
||||
ut
|
||||
}
|
||||
}
|
||||
|
||||
/// To akser som spenner planet vinkelrett på `n`.
|
||||
fn planakser(n: [f64; 3]) -> ([f64; 3], [f64; 3]) {
|
||||
let hjelp = if n[2].abs() < 0.9 {
|
||||
[0.0, 0.0, 1.0]
|
||||
} else {
|
||||
[1.0, 0.0, 0.0]
|
||||
};
|
||||
let u = enhet([
|
||||
n[1] * hjelp[2] - n[2] * hjelp[1],
|
||||
n[2] * hjelp[0] - n[0] * hjelp[2],
|
||||
n[0] * hjelp[1] - n[1] * hjelp[0],
|
||||
])
|
||||
.unwrap_or([1.0, 0.0, 0.0]);
|
||||
let v = [
|
||||
n[1] * u[2] - n[2] * u[1],
|
||||
n[2] * u[0] - n[0] * u[2],
|
||||
n[0] * u[1] - n[1] * u[0],
|
||||
];
|
||||
(u, v)
|
||||
}
|
||||
|
||||
/// Slår sammen hovedflatene til en bygningsdels geometrideler til ett areal.
|
||||
///
|
||||
/// BIM-verktøy eksporterer ofte hvert materialsjikt i en vegg eller et dekke som sin egen
|
||||
/// geometridel. Sjiktene ligger parallelt og dekker den samme flaten, så de skal telles én
|
||||
/// gang. Deler som ligger i samme plan og overlapper hverandre regnes derfor som én flate,
|
||||
/// der den største av dem gjelder, mens deler som dekker forskjellige områder legges sammen.
|
||||
pub fn samlet_hovedflate(deler: &[Nett], skala: f64) -> Option<Hovedflate> {
|
||||
let mut flater: Vec<Hovedflate> = deler.iter().filter_map(|n| n.hovedflate(skala)).collect();
|
||||
if flater.is_empty() {
|
||||
return None;
|
||||
}
|
||||
flater.sort_by(|a, b| {
|
||||
b.areal
|
||||
.partial_cmp(&a.areal)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
let mut grupper: Vec<Hovedflate> = Vec::new();
|
||||
for f in flater {
|
||||
let samme = grupper.iter().any(|g| {
|
||||
let parallell =
|
||||
(g.normal[0] * f.normal[0] + g.normal[1] * f.normal[1] + g.normal[2] * f.normal[2])
|
||||
.abs()
|
||||
> 0.95;
|
||||
parallell && overlapper(&g.plan, &f.plan)
|
||||
});
|
||||
if !samme {
|
||||
grupper.push(f);
|
||||
}
|
||||
}
|
||||
let areal = grupper.iter().map(|g| g.areal).sum();
|
||||
Some(Hovedflate {
|
||||
areal,
|
||||
..grupper[0]
|
||||
})
|
||||
}
|
||||
|
||||
/// Sant når to utstrekninger dekker mer enn halvparten av den minste i begge retninger.
|
||||
fn overlapper(a: &[f64; 4], b: &[f64; 4]) -> bool {
|
||||
let dekning = |a0: f64, a1: f64, b0: f64, b1: f64| {
|
||||
let felles = (a1.min(b1) - a0.max(b0)).max(0.0);
|
||||
let minste = (a1 - a0).min(b1 - b0);
|
||||
if minste <= 1e-9 {
|
||||
felles > 0.0
|
||||
} else {
|
||||
felles / minste > 0.5
|
||||
}
|
||||
};
|
||||
dekning(a[0], a[1], b[0], b[1]) && dekning(a[2], a[3], b[2], b[3])
|
||||
}
|
||||
|
||||
/// Henter geometrien til et produkt via `Representation`, med én oppføring per geometridel.
|
||||
pub fn netter_for_produkt(fil: &Fil, representasjon: Option<u64>) -> Vec<Nett> {
|
||||
let mut deler = Vec::new();
|
||||
let Some(r) = representasjon else {
|
||||
return deler;
|
||||
};
|
||||
let Some(form) = fil.hent(r) else {
|
||||
return deler;
|
||||
};
|
||||
// IfcProductDefinitionShape.Representations
|
||||
for rep in form.arg(2).map(|a| a.som_liste()).unwrap_or(&[]) {
|
||||
let Some(sr) = rep.som_ref().and_then(|x| fil.hent(x)) else {
|
||||
continue;
|
||||
};
|
||||
if sr.typ != "IFCSHAPEREPRESENTATION" {
|
||||
continue;
|
||||
}
|
||||
// Bare kroppen beskriver volumet; akse- og fotavtrykksrepresentasjoner hoppes over.
|
||||
if sr.arg(1).and_then(|a| a.som_tekst()) != Some("Body") {
|
||||
continue;
|
||||
}
|
||||
for post in sr.arg(3).map(|a| a.som_liste()).unwrap_or(&[]) {
|
||||
if let Some(n) = post.som_ref() {
|
||||
let mut nett = Nett::default();
|
||||
les_element(fil, n, &mut nett, 0);
|
||||
if !nett.tom() {
|
||||
deler.push(nett);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
deler
|
||||
}
|
||||
|
||||
fn les_element(fil: &Fil, r: u64, nett: &mut Nett, dybde: u32) {
|
||||
if dybde > 8 {
|
||||
return;
|
||||
}
|
||||
let Some(e) = fil.hent(r) else {
|
||||
return;
|
||||
};
|
||||
match e.typ.as_str() {
|
||||
"IFCTRIANGULATEDFACESET" => les_trekantnett(fil, r, nett),
|
||||
"IFCPOLYGONALFACESET" => les_polygonnett(fil, r, nett),
|
||||
"IFCFACETEDBREP" | "IFCADVANCEDBREP" => {
|
||||
if let Some(skall) = e.arg(0).and_then(|a| a.som_ref()) {
|
||||
les_skall(fil, skall, nett);
|
||||
}
|
||||
}
|
||||
"IFCEXTRUDEDAREASOLID" => les_ekstrudering(fil, r, nett),
|
||||
"IFCMAPPEDITEM" => {
|
||||
// Kartlagt geometri: følg kilden. Transformasjonen ses bort fra, siden
|
||||
// arealet er uavhengig av flytting og rotasjon.
|
||||
if let Some(kilde) = e.arg(0).and_then(|a| a.som_ref()).and_then(|k| fil.hent(k)) {
|
||||
if let Some(rep) = kilde
|
||||
.arg(1)
|
||||
.and_then(|a| a.som_ref())
|
||||
.and_then(|x| fil.hent(x))
|
||||
{
|
||||
for post in rep.arg(3).map(|a| a.som_liste()).unwrap_or(&[]) {
|
||||
if let Some(n) = post.som_ref() {
|
||||
les_element(fil, n, nett, dybde + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"IFCBOOLEANRESULT" | "IFCBOOLEANCLIPPINGRESULT" => {
|
||||
// Bruk førsteoperanden; utsparinger gir uansett litt for stort areal.
|
||||
if let Some(f) = e.arg(1).and_then(|a| a.som_ref()) {
|
||||
les_element(fil, f, nett, dybde + 1);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn punktliste(fil: &Fil, r: u64) -> Vec<[f64; 3]> {
|
||||
let Some(e) = fil.hent(r) else {
|
||||
return Vec::new();
|
||||
};
|
||||
e.arg(0)
|
||||
.map(|a| a.som_liste())
|
||||
.unwrap_or(&[])
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let k = p.som_liste();
|
||||
let g = |i: usize| k.get(i).and_then(|x| x.som_tall()).unwrap_or(0.0);
|
||||
[g(0), g(1), g(2)]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn les_trekantnett(fil: &Fil, r: u64, nett: &mut Nett) {
|
||||
let Some(e) = fil.hent(r) else { return };
|
||||
let Some(liste) = e.arg(0).and_then(|a| a.som_ref()) else {
|
||||
return;
|
||||
};
|
||||
let start = nett.punkter.len();
|
||||
nett.punkter.extend(punktliste(fil, liste));
|
||||
// CoordIndex er 1-basert.
|
||||
for t in e.arg(3).map(|a| a.som_liste()).unwrap_or(&[]) {
|
||||
let k = t.som_liste();
|
||||
if k.len() < 3 {
|
||||
continue;
|
||||
}
|
||||
let g = |i: usize| k[i].som_tall().unwrap_or(1.0) as usize;
|
||||
nett.trekanter.push([
|
||||
start + g(0).saturating_sub(1),
|
||||
start + g(1).saturating_sub(1),
|
||||
start + g(2).saturating_sub(1),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
fn les_polygonnett(fil: &Fil, r: u64, nett: &mut Nett) {
|
||||
let Some(e) = fil.hent(r) else { return };
|
||||
let Some(liste) = e.arg(0).and_then(|a| a.som_ref()) else {
|
||||
return;
|
||||
};
|
||||
let start = nett.punkter.len();
|
||||
nett.punkter.extend(punktliste(fil, liste));
|
||||
for f in e.arg(2).map(|a| a.som_liste()).unwrap_or(&[]) {
|
||||
let Some(fe) = f.som_ref().and_then(|x| fil.hent(x)) else {
|
||||
continue;
|
||||
};
|
||||
let indekser: Vec<usize> = fe
|
||||
.arg(0)
|
||||
.map(|a| a.som_liste())
|
||||
.unwrap_or(&[])
|
||||
.iter()
|
||||
.filter_map(|x| x.som_tall())
|
||||
.map(|v| start + (v as usize).saturating_sub(1))
|
||||
.collect();
|
||||
vifte(&indekser, nett);
|
||||
}
|
||||
}
|
||||
|
||||
fn les_skall(fil: &Fil, r: u64, nett: &mut Nett) {
|
||||
let Some(skall) = fil.hent(r) else { return };
|
||||
for f in skall.arg(0).map(|a| a.som_liste()).unwrap_or(&[]) {
|
||||
let Some(flate) = f.som_ref().and_then(|x| fil.hent(x)) else {
|
||||
continue;
|
||||
};
|
||||
for b in flate.arg(0).map(|a| a.som_liste()).unwrap_or(&[]) {
|
||||
let Some(grense) = b.som_ref().and_then(|x| fil.hent(x)) else {
|
||||
continue;
|
||||
};
|
||||
// Bare ytre grense; hull i flaten gir litt for stort areal.
|
||||
if grense.typ != "IFCFACEOUTERBOUND" {
|
||||
continue;
|
||||
}
|
||||
let Some(lokke) = grense
|
||||
.arg(0)
|
||||
.and_then(|a| a.som_ref())
|
||||
.and_then(|x| fil.hent(x))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let mut indekser = Vec::new();
|
||||
for p in lokke.arg(0).map(|a| a.som_liste()).unwrap_or(&[]) {
|
||||
if let Some(pr) = p.som_ref() {
|
||||
if let Some(pe) = fil.hent(pr) {
|
||||
let k = pe.arg(0).map(|a| a.som_liste()).unwrap_or(&[]);
|
||||
let g = |i: usize| k.get(i).and_then(|x| x.som_tall()).unwrap_or(0.0);
|
||||
indekser.push(nett.punkter.len());
|
||||
nett.punkter.push([g(0), g(1), g(2)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
vifte(&indekser, nett);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Deler en polygonløkke i trekanter fra første hjørne.
|
||||
fn vifte(indekser: &[usize], nett: &mut Nett) {
|
||||
for i in 1..indekser.len().saturating_sub(1) {
|
||||
nett.trekanter
|
||||
.push([indekser[0], indekser[i], indekser[i + 1]]);
|
||||
}
|
||||
}
|
||||
|
||||
fn les_ekstrudering(fil: &Fil, r: u64, nett: &mut Nett) {
|
||||
let Some(e) = fil.hent(r) else { return };
|
||||
let Some(profil) = e.arg(0).and_then(|a| a.som_ref()).and_then(|x| fil.hent(x)) else {
|
||||
return;
|
||||
};
|
||||
let dybde = e.arg(3).and_then(|a| a.som_tall()).unwrap_or(0.0);
|
||||
let retning = e
|
||||
.arg(2)
|
||||
.and_then(|a| a.som_ref())
|
||||
.and_then(|x| fil.hent(x))
|
||||
.map(|d| {
|
||||
let k = d.arg(0).map(|a| a.som_liste()).unwrap_or(&[]);
|
||||
let g = |i: usize| k.get(i).and_then(|x| x.som_tall()).unwrap_or(0.0);
|
||||
[g(0), g(1), g(2)]
|
||||
})
|
||||
.unwrap_or([0.0, 0.0, 1.0]);
|
||||
|
||||
// Profilet ligger i xy-planet. Hent ytre kurve som punkter.
|
||||
let punkter = profilpunkter(fil, profil);
|
||||
if punkter.len() < 3 {
|
||||
return;
|
||||
}
|
||||
let start = nett.punkter.len();
|
||||
for p in &punkter {
|
||||
nett.punkter.push([p[0], p[1], 0.0]);
|
||||
}
|
||||
let topp = nett.punkter.len();
|
||||
for p in &punkter {
|
||||
nett.punkter.push([
|
||||
p[0] + retning[0] * dybde,
|
||||
p[1] + retning[1] * dybde,
|
||||
retning[2] * dybde,
|
||||
]);
|
||||
}
|
||||
// Bunn og topp.
|
||||
let bunn: Vec<usize> = (start..topp).collect();
|
||||
let topp_i: Vec<usize> = (topp..nett.punkter.len()).collect();
|
||||
vifte(&bunn, nett);
|
||||
vifte(&topp_i, nett);
|
||||
// Sideflater.
|
||||
let n = punkter.len();
|
||||
for i in 0..n {
|
||||
let j = (i + 1) % n;
|
||||
nett.trekanter.push([start + i, start + j, topp + j]);
|
||||
nett.trekanter.push([start + i, topp + j, topp + i]);
|
||||
}
|
||||
}
|
||||
|
||||
fn profilpunkter(fil: &Fil, profil: &crate::inndata::step::Entitet) -> Vec<[f64; 2]> {
|
||||
match profil.typ.as_str() {
|
||||
"IFCARBITRARYCLOSEDPROFILEDEF" | "IFCARBITRARYPROFILEDEFWITHVOIDS" => {
|
||||
let Some(kurve) = profil
|
||||
.arg(2)
|
||||
.and_then(|a| a.som_ref())
|
||||
.and_then(|x| fil.hent(x))
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
if kurve.typ != "IFCPOLYLINE" {
|
||||
return Vec::new();
|
||||
}
|
||||
kurve
|
||||
.arg(0)
|
||||
.map(|a| a.som_liste())
|
||||
.unwrap_or(&[])
|
||||
.iter()
|
||||
.filter_map(|p| p.som_ref().and_then(|x| fil.hent(x)))
|
||||
.map(|pe| {
|
||||
let k = pe.arg(0).map(|a| a.som_liste()).unwrap_or(&[]);
|
||||
let g = |i: usize| k.get(i).and_then(|x| x.som_tall()).unwrap_or(0.0);
|
||||
[g(0), g(1)]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
"IFCRECTANGLEPROFILEDEF" => {
|
||||
let x = profil.arg(3).and_then(|a| a.som_tall()).unwrap_or(0.0) / 2.0;
|
||||
let y = profil.arg(4).and_then(|a| a.som_tall()).unwrap_or(0.0) / 2.0;
|
||||
vec![[-x, -y], [x, -y], [x, y], [-x, y]]
|
||||
}
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Lengdeenheten i meter, hentet gjennom prosjektets enhetsoppsett.
|
||||
///
|
||||
/// En IFC-fil kan inneholde flere `IfcSIUnit` med lengdeenhet, for eksempel når et
|
||||
/// underoppsett bruker meter mens modellen er i millimeter. Derfor må enheten slås opp
|
||||
/// gjennom `IfcProject.UnitsInContext` og ikke ved å lete gjennom alle enheter.
|
||||
pub fn lengdeenhet(fil: &Fil) -> f64 {
|
||||
let fra_oppsett = fil
|
||||
.av_type("IFCPROJECT")
|
||||
.next()
|
||||
.and_then(|(_, p)| p.arg(8).and_then(|a| a.som_ref()))
|
||||
.and_then(|r| fil.hent(r))
|
||||
.and_then(|oppsett| {
|
||||
oppsett
|
||||
.arg(0)
|
||||
.map(|a| a.som_liste())
|
||||
.unwrap_or(&[])
|
||||
.iter()
|
||||
.filter_map(|u| u.som_ref().and_then(|r| fil.hent(r)))
|
||||
.find(|e| {
|
||||
e.typ == "IFCSIUNIT"
|
||||
&& e.arg(1).and_then(|a| a.som_tekst()) == Some("LENGTHUNIT")
|
||||
})
|
||||
.map(prefiksfaktor)
|
||||
});
|
||||
fra_oppsett.unwrap_or(1.0)
|
||||
}
|
||||
|
||||
fn prefiksfaktor(e: &crate::inndata::step::Entitet) -> f64 {
|
||||
match e.arg(2).and_then(|a| a.som_tekst()).unwrap_or("") {
|
||||
"MILLI" => 0.001,
|
||||
"CENTI" => 0.01,
|
||||
"DECI" => 0.1,
|
||||
"KILO" => 1000.0,
|
||||
_ => 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Enhetsvektor, eller `None` for en nullvektor.
|
||||
pub fn enhet(v: [f64; 3]) -> Option<[f64; 3]> {
|
||||
let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
|
||||
(l > 1e-12).then(|| [v[0] / l, v[1] / l, v[2] / l])
|
||||
}
|
||||
|
||||
/// Sant når et argument er en liste med minst ett element.
|
||||
pub fn har_innhold(a: Option<&Arg>) -> bool {
|
||||
a.map(|x| !x.som_liste().is_empty()).unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tester {
|
||||
use super::*;
|
||||
|
||||
/// En kasse på 4 × 3 × 0,2 m skal gi hovedflate 12 m² med normal langs z.
|
||||
#[test]
|
||||
fn hovedflate_av_kasse() {
|
||||
let mut n = Nett::default();
|
||||
let (x, y, z) = (4.0, 3.0, 0.2);
|
||||
let p = [
|
||||
[0.0, 0.0, 0.0],
|
||||
[x, 0.0, 0.0],
|
||||
[x, y, 0.0],
|
||||
[0.0, y, 0.0],
|
||||
[0.0, 0.0, z],
|
||||
[x, 0.0, z],
|
||||
[x, y, z],
|
||||
[0.0, y, z],
|
||||
];
|
||||
n.punkter.extend_from_slice(&p);
|
||||
let flater = [
|
||||
[0, 1, 2],
|
||||
[0, 2, 3], // bunn
|
||||
[4, 6, 5],
|
||||
[4, 7, 6], // topp
|
||||
[0, 4, 5],
|
||||
[0, 5, 1],
|
||||
[1, 5, 6],
|
||||
[1, 6, 2],
|
||||
[2, 6, 7],
|
||||
[2, 7, 3],
|
||||
[3, 7, 4],
|
||||
[3, 4, 0],
|
||||
];
|
||||
n.trekanter.extend_from_slice(&flater);
|
||||
let h = n.hovedflate(1.0).unwrap();
|
||||
assert!((h.areal - 12.0).abs() < 1e-9, "areal ble {}", h.areal);
|
||||
assert!(h.normal[2].abs() > 0.99, "normal ble {:?}", h.normal);
|
||||
}
|
||||
|
||||
/// Et hull i flaten skal trekkes fra, siden hullet mangler trekanter.
|
||||
#[test]
|
||||
fn hull_reduserer_hovedflaten() {
|
||||
let mut n = Nett::default();
|
||||
// To flater på 4 × 3 med et hull på 1 × 1 er modellert som fire striper.
|
||||
for z in [0.0, 0.2] {
|
||||
let base = n.punkter.len();
|
||||
n.punkter.extend_from_slice(&[
|
||||
[0.0, 0.0, z],
|
||||
[4.0, 0.0, z],
|
||||
[4.0, 1.0, z],
|
||||
[0.0, 1.0, z],
|
||||
]);
|
||||
n.trekanter.push([base, base + 1, base + 2]);
|
||||
n.trekanter.push([base, base + 2, base + 3]);
|
||||
}
|
||||
let h = n.hovedflate(1.0).unwrap();
|
||||
assert!((h.areal - 4.0).abs() < 1e-9, "areal ble {}", h.areal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skala_gjor_om_millimeter_til_kvadratmeter() {
|
||||
let mut n = Nett::default();
|
||||
n.punkter.extend_from_slice(&[
|
||||
[0.0, 0.0, 0.0],
|
||||
[2000.0, 0.0, 0.0],
|
||||
[2000.0, 1000.0, 0.0],
|
||||
[0.0, 1000.0, 0.0],
|
||||
]);
|
||||
n.trekanter.push([0, 1, 2]);
|
||||
n.trekanter.push([0, 2, 3]);
|
||||
// Én enkelt flate: hovedflaten deler på to, så 2 m² blir 1 m².
|
||||
let h = n.hovedflate(0.001).unwrap();
|
||||
assert!((h.areal - 1.0).abs() < 1e-9, "areal ble {}", h.areal);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user