//! 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 { 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 { let mut flater: Vec = 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 = 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) -> Vec { 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 = 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 = (start..topp).collect(); let topp_i: Vec = (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); } }