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:
@@ -538,6 +538,8 @@ fn bygg(
|
||||
absorptans: None,
|
||||
omkrets: None,
|
||||
dybde: None,
|
||||
over_terreng: None,
|
||||
sone: None,
|
||||
kommentar: konstruksjoner
|
||||
.get(&o.konstruksjon)
|
||||
.map(|k| k.navn.clone())
|
||||
@@ -580,6 +582,8 @@ fn bygg(
|
||||
},
|
||||
klima: Klima::default(),
|
||||
flate: flater,
|
||||
sone: Vec::new(),
|
||||
grunn: Grunn::default(),
|
||||
vindu: vinduer,
|
||||
kuldebroer: Kuldebroer::default(),
|
||||
infiltrasjon: Infiltrasjon::default(),
|
||||
|
||||
+424
-250
@@ -9,6 +9,7 @@
|
||||
//! tyngdepunkt. Prosjektets sanne nord (`IfcGeometricRepresentationContext.TrueNorth`)
|
||||
//! legges til.
|
||||
|
||||
use crate::inndata::ifcgeom;
|
||||
use crate::inndata::step::{self, Arg, Entitet, Fil};
|
||||
use crate::modell::*;
|
||||
use crate::Result;
|
||||
@@ -19,6 +20,8 @@ use std::path::Path;
|
||||
pub struct Importrapport {
|
||||
pub bygning: Bygning,
|
||||
pub merknader: Vec<String>,
|
||||
/// Bygningsdeler som ikke er tatt med i klimaskjermen: navn, areal og U-verdi.
|
||||
pub utelatt: Vec<(String, f64, Option<f64>)>,
|
||||
}
|
||||
|
||||
const VEGGTYPER: [&str; 3] = ["IFCWALL", "IFCWALLSTANDARDCASE", "IFCCURTAINWALL"];
|
||||
@@ -39,48 +42,77 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
let fil = step::les(tekst);
|
||||
let mut merknader = Vec::new();
|
||||
|
||||
let lengdeskala = lengdeenhet(&fil);
|
||||
let skala = ifcgeom::lengdeenhet(&fil);
|
||||
let sant_nord = sant_nord(&fil);
|
||||
let egenskaper = samle_egenskaper(&fil);
|
||||
let mengder = samle_mengder(&fil);
|
||||
let (vindu_vert, dor_vert) = samle_apninger(&fil);
|
||||
|
||||
// Areal og volum fra rom.
|
||||
// ── oppvarmet areal fra rommene ─────────────────────────────────
|
||||
let mut bra = 0.0;
|
||||
let mut volum = 0.0;
|
||||
for (r, _) in fil.av_type("IFCSPACE") {
|
||||
if let Some(m) = mengder.get(&r) {
|
||||
bra += m
|
||||
.get("NetFloorArea")
|
||||
.or_else(|| m.get("GrossFloorArea"))
|
||||
.copied()
|
||||
.unwrap_or(0.0);
|
||||
volum += m
|
||||
.get("NetVolume")
|
||||
.or_else(|| m.get("GrossVolume"))
|
||||
.copied()
|
||||
let mut rom: Vec<(String, f64)> = Vec::new();
|
||||
for (r, e) in fil.av_type("IFCSPACE") {
|
||||
let navn = romnavn(e);
|
||||
let areal = mengder.get(&r).map(arealmengde).unwrap_or(0.0);
|
||||
// Rom som beskriver bebygget areal eller bruttoareal er ikke gulvareal.
|
||||
let maalerom = matches!(navn.to_uppercase().as_str(), n if n.starts_with("BYA") || n.starts_with("BTA"));
|
||||
if areal > 0.0 && !maalerom {
|
||||
rom.push((navn, areal));
|
||||
bra += areal;
|
||||
volum += mengder
|
||||
.get(&r)
|
||||
.and_then(|m| m.get("NetVolume").or_else(|| m.get("GrossVolume")).copied())
|
||||
.unwrap_or(0.0);
|
||||
}
|
||||
}
|
||||
rom.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
||||
if !rom.is_empty() {
|
||||
let liste: Vec<String> = rom.iter().map(|(n, a)| format!("{n} {a:.1}")).collect();
|
||||
merknader.push(format!(
|
||||
"Oppvarmet BRA er satt til summen av alle rom, {bra:.1} m². IFC sier ikke hvilke rom som er oppvarmet, så trekk fra kjeller, kott, loft og garasje. Rom [m²]: {}.",
|
||||
liste.join(", ")
|
||||
));
|
||||
}
|
||||
|
||||
// Plasseringer for alle bygningsdeler.
|
||||
let mut plassering: HashMap<u64, (f64, f64, f64, f64, f64)> = HashMap::new();
|
||||
for (r, e) in fil.av_typer(&VEGGTYPER).chain(fil.av_typer(&PLATETYPER)) {
|
||||
if let Some(p) = e.arg(5).and_then(|a| a.som_ref()) {
|
||||
if let Some(v) = global_plassering(&fil, p, lengdeskala) {
|
||||
plassering.insert(r, v);
|
||||
}
|
||||
}
|
||||
// ── plassering og geometri for alle bygningsdeler ───────────────
|
||||
let typer: Vec<&str> = VEGGTYPER.iter().chain(PLATETYPER.iter()).copied().collect();
|
||||
let mut deler: Vec<Del> = Vec::new();
|
||||
for (r, e) in fil.av_typer(&typer) {
|
||||
let plass = e
|
||||
.arg(5)
|
||||
.and_then(|a| a.som_ref())
|
||||
.and_then(|p| global_plassering(&fil, p, skala));
|
||||
let netter = ifcgeom::netter_for_produkt(&fil, e.arg(6).and_then(|a| a.som_ref()));
|
||||
let flate = ifcgeom::samlet_hovedflate(&netter, skala);
|
||||
deler.push(Del {
|
||||
nr: r,
|
||||
typ: e.typ.clone(),
|
||||
navn: navn_av(e, r, standardnavn_for(&e.typ)),
|
||||
ytre: er_ytre(&egenskaper, r),
|
||||
u_verdi: u_verdi(&egenskaper, r),
|
||||
plass,
|
||||
areal: flate.map(|f| f.areal).unwrap_or(0.0),
|
||||
normal: flate.map(|f| f.normal),
|
||||
});
|
||||
}
|
||||
// Tyngdepunkt av ytterveggene, for å bestemme hvilken vei normalen peker.
|
||||
let ytre: Vec<u64> = fil
|
||||
.av_typer(&VEGGTYPER)
|
||||
.filter(|(r, _)| er_ytre(&egenskaper, *r).unwrap_or(true))
|
||||
.map(|(r, _)| r)
|
||||
|
||||
// Tyngdepunkt av de ytre veggene bestemmer hvilken vei normalen peker.
|
||||
let ytre_deler: Vec<&Del> = deler
|
||||
.iter()
|
||||
.filter(|d| VEGGTYPER.contains(&d.typ.as_str()) && d.ytre.unwrap_or(false))
|
||||
.collect();
|
||||
let referanse: Vec<&Del> = if ytre_deler.is_empty() {
|
||||
deler
|
||||
.iter()
|
||||
.filter(|d| VEGGTYPER.contains(&d.typ.as_str()))
|
||||
.collect()
|
||||
} else {
|
||||
ytre_deler
|
||||
};
|
||||
let (mut cx, mut cy, mut n) = (0.0, 0.0, 0.0);
|
||||
for r in &ytre {
|
||||
if let Some((x, y, _, _, _)) = plassering.get(r) {
|
||||
for d in &referanse {
|
||||
if let Some((x, y, _, _, _)) = d.plass {
|
||||
cx += x;
|
||||
cy += y;
|
||||
n += 1.0;
|
||||
@@ -91,93 +123,44 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
cy /= n;
|
||||
}
|
||||
|
||||
// ── klimaskjerm ─────────────────────────────────────────────────
|
||||
let mut flater: Vec<Flate> = Vec::new();
|
||||
let mut vinduer: Vec<Vindu> = Vec::new();
|
||||
let mut utelatt: Vec<(String, f64, Option<f64>)> = Vec::new();
|
||||
let mut uten_uverdi = 0usize;
|
||||
let mut uten_geometri = 0usize;
|
||||
let mut asimut_for_vegg: HashMap<u64, f64> = HashMap::new();
|
||||
|
||||
// ── vegger ──────────────────────────────────────────────────────
|
||||
for (r, e) in fil.av_typer(&VEGGTYPER) {
|
||||
let ytre_vegg = er_ytre(&egenskaper, r).unwrap_or(true);
|
||||
let asimut = plassering
|
||||
.get(&r)
|
||||
.map(|(x, y, _, dx, dy)| asimut_av(*x, *y, *dx, *dy, cx, cy, sant_nord))
|
||||
.unwrap_or(0.0);
|
||||
asimut_for_vegg.insert(r, asimut);
|
||||
if !ytre_vegg {
|
||||
continue;
|
||||
}
|
||||
let m = mengder.get(&r);
|
||||
let brutto = m
|
||||
.and_then(|m| {
|
||||
m.get("NetSideArea")
|
||||
.or_else(|| m.get("GrossSideArea"))
|
||||
.copied()
|
||||
})
|
||||
.unwrap_or(0.0);
|
||||
if brutto <= 0.01 {
|
||||
continue;
|
||||
}
|
||||
let u = u_verdi(&egenskaper, r);
|
||||
if u.is_none() {
|
||||
uten_uverdi += 1;
|
||||
}
|
||||
flater.push(Flate {
|
||||
navn: navn_av(e, r, "Vegg"),
|
||||
typ: FlateType::Vegg,
|
||||
areal: brutto,
|
||||
orientering: asimut,
|
||||
helning: Some(90.0),
|
||||
u_verdi: u,
|
||||
lag: None,
|
||||
masseklasse: None,
|
||||
masseplassering: Masseplassering::Fordelt,
|
||||
mot: Mot::Ute,
|
||||
b_faktor: None,
|
||||
absorptans: None,
|
||||
omkrets: None,
|
||||
dybde: None,
|
||||
kommentar: Some("importert fra IFC".into()),
|
||||
});
|
||||
}
|
||||
// Laveste gulvnivå brukes til å skille dekke mot grunn fra etasjeskillere.
|
||||
let laveste = deler
|
||||
.iter()
|
||||
.filter(|d| PLATETYPER.contains(&d.typ.as_str()))
|
||||
.filter_map(|d| d.plass.map(|p| p.2))
|
||||
.fold(f64::INFINITY, f64::min);
|
||||
|
||||
// ── plater: tak, gulv, dekke mot grunn ──────────────────────────
|
||||
for (r, e) in fil.av_typer(&PLATETYPER) {
|
||||
if !er_ytre(&egenskaper, r).unwrap_or(true) {
|
||||
for d in &deler {
|
||||
let er_vegg = VEGGTYPER.contains(&d.typ.as_str());
|
||||
let (asimut, helning) = retning_av(d, cx, cy, sant_nord);
|
||||
if er_vegg {
|
||||
asimut_for_vegg.insert(d.nr, asimut);
|
||||
}
|
||||
if d.areal <= 0.05 {
|
||||
uten_geometri += 1;
|
||||
continue;
|
||||
}
|
||||
let m = mengder.get(&r);
|
||||
let a = m
|
||||
.and_then(|m| m.get("NetArea").or_else(|| m.get("GrossArea")).copied())
|
||||
.unwrap_or(0.0);
|
||||
if a <= 0.01 {
|
||||
let Some((typ, mot)) = klassifiser(d, laveste, helning) else {
|
||||
utelatt.push((d.navn.clone(), d.areal, d.u_verdi));
|
||||
continue;
|
||||
}
|
||||
let type_tekst = e
|
||||
.args
|
||||
.iter()
|
||||
.filter_map(|x| x.som_tekst())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
.to_uppercase();
|
||||
// Plater uten ROOF-merking regnes som dekke mot grunn; kontroller `mot` etter importen
|
||||
// dersom modellen har dekker mot det fri eller mot uoppvarmet kjeller.
|
||||
let (typ, mot, helning) = if e.typ == "IFCROOF" || type_tekst.contains("ROOF") {
|
||||
(FlateType::Tak, Mot::Ute, 0.0)
|
||||
} else {
|
||||
(FlateType::Gulv, Mot::Grunn, 180.0)
|
||||
};
|
||||
let u = u_verdi(&egenskaper, r);
|
||||
if u.is_none() {
|
||||
if d.u_verdi.is_none() {
|
||||
uten_uverdi += 1;
|
||||
}
|
||||
flater.push(Flate {
|
||||
navn: navn_av(e, r, if typ == FlateType::Tak { "Tak" } else { "Gulv" }),
|
||||
navn: d.navn.clone(),
|
||||
typ,
|
||||
areal: a,
|
||||
orientering: 0.0,
|
||||
areal: d.areal,
|
||||
orientering: if typ == FlateType::Vegg { asimut } else { 0.0 },
|
||||
helning: Some(helning),
|
||||
u_verdi: u,
|
||||
u_verdi: d.u_verdi,
|
||||
lag: None,
|
||||
masseklasse: None,
|
||||
masseplassering: Masseplassering::Fordelt,
|
||||
@@ -186,21 +169,26 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
absorptans: None,
|
||||
omkrets: None,
|
||||
dybde: None,
|
||||
over_terreng: None,
|
||||
sone: None,
|
||||
kommentar: Some("importert fra IFC".into()),
|
||||
});
|
||||
}
|
||||
|
||||
slaa_sammen(&mut flater);
|
||||
|
||||
// ── vinduer og dører ────────────────────────────────────────────
|
||||
let mut vinduer: Vec<Vindu> = Vec::new();
|
||||
for (r, e) in fil.av_type("IFCWINDOW").chain(fil.av_type("IFCDOOR")) {
|
||||
let dor = e.typ == "IFCDOOR";
|
||||
let hoyde = e.arg(8).and_then(|a| a.som_tall()).unwrap_or(0.0) * lengdeskala;
|
||||
let bredde = e.arg(9).and_then(|a| a.som_tall()).unwrap_or(0.0) * lengdeskala;
|
||||
let hoyde = e.arg(8).and_then(|a| a.som_tall()).unwrap_or(0.0) * skala;
|
||||
let bredde = e.arg(9).and_then(|a| a.som_tall()).unwrap_or(0.0) * skala;
|
||||
let areal = mengder
|
||||
.get(&r)
|
||||
.and_then(|m| m.get("Area").or_else(|| m.get("GrossArea")).copied())
|
||||
.filter(|a| *a > 0.01)
|
||||
.map(arealmengde)
|
||||
.filter(|a| *a > 0.05)
|
||||
.unwrap_or(hoyde * bredde);
|
||||
if areal <= 0.01 {
|
||||
if areal <= 0.05 {
|
||||
continue;
|
||||
}
|
||||
let vert = if dor {
|
||||
@@ -208,6 +196,19 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
} else {
|
||||
vindu_vert.get(&r)
|
||||
};
|
||||
// Vinduer i innervegger hører ikke til klimaskjermen.
|
||||
let i_ytre_vegg = vert
|
||||
.and_then(|v| deler.iter().find(|d| d.nr == *v))
|
||||
.map(|d| d.ytre.unwrap_or(true))
|
||||
.unwrap_or(true);
|
||||
if !i_ytre_vegg {
|
||||
utelatt.push((
|
||||
format!("{} (i innervegg)", navn_av(e, r, "Vindu")),
|
||||
areal,
|
||||
u_verdi(&egenskaper, r),
|
||||
));
|
||||
continue;
|
||||
}
|
||||
let asimut = vert
|
||||
.and_then(|v| asimut_for_vegg.get(v))
|
||||
.copied()
|
||||
@@ -237,84 +238,75 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
});
|
||||
}
|
||||
|
||||
// Trekk vindus- og dørareal fra veggene de sitter i.
|
||||
for (v, vegg) in vindu_vert.iter().chain(dor_vert.iter()) {
|
||||
let areal = vinduer
|
||||
.iter()
|
||||
.find(|x| x.kommentar.is_some() && x.navn.ends_with(&format!("#{v}")))
|
||||
.map(|x| x.areal);
|
||||
let _ = (areal, vegg);
|
||||
// Vindus- og dørarealet trekkes fra veggene de sitter i, dersom geometrien ikke
|
||||
// allerede har hull der åpningene er.
|
||||
let mut apning_per_vegg: HashMap<u64, f64> = HashMap::new();
|
||||
for (a, vegg) in vindu_vert.iter().chain(dor_vert.iter()) {
|
||||
let e = fil.hent(*a);
|
||||
let h = e
|
||||
.and_then(|e| e.arg(8))
|
||||
.and_then(|x| x.som_tall())
|
||||
.unwrap_or(0.0)
|
||||
* skala;
|
||||
let b = e
|
||||
.and_then(|e| e.arg(9))
|
||||
.and_then(|x| x.som_tall())
|
||||
.unwrap_or(0.0)
|
||||
* skala;
|
||||
*apning_per_vegg.entry(*vegg).or_default() += h * b;
|
||||
}
|
||||
let apningsareal_per_vegg = {
|
||||
let mut m: HashMap<u64, f64> = HashMap::new();
|
||||
for (a, vegg) in vindu_vert.iter().chain(dor_vert.iter()) {
|
||||
let e = fil.hent(*a);
|
||||
let hoyde = e
|
||||
.and_then(|e| e.arg(8))
|
||||
.and_then(|x| x.som_tall())
|
||||
.unwrap_or(0.0)
|
||||
* lengdeskala;
|
||||
let bredde = e
|
||||
.and_then(|e| e.arg(9))
|
||||
.and_then(|x| x.som_tall())
|
||||
.unwrap_or(0.0)
|
||||
* lengdeskala;
|
||||
let areal = mengder
|
||||
.get(a)
|
||||
.and_then(|q| q.get("Area").or_else(|| q.get("GrossArea")).copied())
|
||||
.filter(|v| *v > 0.01)
|
||||
.unwrap_or(hoyde * bredde);
|
||||
*m.entry(*vegg).or_default() += areal;
|
||||
}
|
||||
m
|
||||
};
|
||||
for (r, _) in fil.av_typer(&VEGGTYPER) {
|
||||
if let Some(ap) = apningsareal_per_vegg.get(&r) {
|
||||
let navn = fil.hent(r).map(|e| navn_av(e, r, "Vegg"));
|
||||
if let Some(f) = navn.and_then(|n| flater.iter_mut().find(|f| f.navn == n)) {
|
||||
f.areal = (f.areal - ap).max(0.0);
|
||||
let mut trukket_fra = 0usize;
|
||||
for d in &deler {
|
||||
let Some(ap) = apning_per_vegg.get(&d.nr) else {
|
||||
continue;
|
||||
};
|
||||
let Some(f) = flater.iter_mut().find(|f| f.navn == d.navn) else {
|
||||
continue;
|
||||
};
|
||||
// Ligger veggarealet nær bruttoarealet, har geometrien ikke hull, og åpningene trekkes fra.
|
||||
if f.areal > *ap * 1.5 && d.normal.is_some() {
|
||||
let netto = (f.areal - ap).max(0.0);
|
||||
if netto > 0.05 {
|
||||
f.areal = netto;
|
||||
trukket_fra += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
flater.retain(|f| f.areal > 0.01);
|
||||
if trukket_fra > 0 {
|
||||
merknader.push(format!(
|
||||
"Vindus- og dørareal er trukket fra {trukket_fra} vegger. Kontroller veggarealene mot tegningen."
|
||||
));
|
||||
}
|
||||
|
||||
// ── merknader ───────────────────────────────────────────────────
|
||||
if bra <= 0.0 {
|
||||
bra = flater
|
||||
.iter()
|
||||
.filter(|f| f.typ == FlateType::Gulv)
|
||||
.map(|f| f.areal)
|
||||
.sum::<f64>();
|
||||
if bra > 0.0 {
|
||||
merknader.push(format!(
|
||||
"IFC-modellen har ingen romarealer; oppvarmet BRA er anslått til {bra:.0} m² av gulvarealet. Kontroller verdien."
|
||||
));
|
||||
merknader.push(if bra > 0.0 {
|
||||
format!("IFC-modellen mangler romarealer; oppvarmet BRA er anslått til {bra:.0} m² av gulvflatene. Kontroller verdien.")
|
||||
} else {
|
||||
merknader.push(
|
||||
"IFC-modellen har verken romarealer eller gulvflater; fyll inn oppvarmet_bra."
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
"IFC-modellen har verken romarealer eller gulvflater; fyll inn oppvarmet_bra.".into()
|
||||
});
|
||||
}
|
||||
if uten_geometri > 0 {
|
||||
merknader.push(format!(
|
||||
"{uten_geometri} bygningsdeler er utelatt fordi geometrien ikke ga noe areal."
|
||||
));
|
||||
}
|
||||
if uten_uverdi > 0 {
|
||||
merknader.push(format!(
|
||||
"{uten_uverdi} bygningsdeler mangler U-verdi i IFC-filen. Legg inn U-verdier, eller la enok \
|
||||
bruke typiske verdier for byggeåret."
|
||||
"{uten_uverdi} bygningsdeler mangler U-verdi i IFC-filen. Legg inn U-verdier, eller la enok bruke typiske verdier for byggeåret."
|
||||
));
|
||||
}
|
||||
if n == 0.0 {
|
||||
merknader.push(
|
||||
"Fant ingen plasseringer for ytterveggene; alle himmelretninger er satt til nord."
|
||||
.into(),
|
||||
);
|
||||
} else {
|
||||
merknader.push(
|
||||
"Himmelretningene er utledet av elementenes plassering og bygningens tyngdepunkt. Kontroller dem mot tegningen."
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
merknader.push(
|
||||
"IFC beskriver ikke drift, lekkasjetall eller energiforsyning. Fyll inn [ventilasjon], \
|
||||
[infiltrasjon], [oppvarming], [tappevann] og [klima] før beregning."
|
||||
"Arealene er regnet av geometrien: trekantene er gruppert etter retning, og den største gruppen er delens hovedflate. Himmelretningene følger flatenormalen. Kontroller begge mot tegningen."
|
||||
.into(),
|
||||
);
|
||||
merknader.push(
|
||||
"IFC beskriver ikke drift, lekkasjetall eller energiforsyning. Fyll inn [ventilasjon], [infiltrasjon], [oppvarming], [tappevann] og [klima] før beregning."
|
||||
.into(),
|
||||
);
|
||||
|
||||
@@ -322,7 +314,7 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
.av_type("IFCBUILDING")
|
||||
.next()
|
||||
.and_then(|(_, e)| e.navn().map(|s| s.to_string()))
|
||||
.filter(|s| !s.is_empty())
|
||||
.filter(|s| !s.is_empty() && s != "Default Building")
|
||||
.unwrap_or_else(|| standardnavn.to_string());
|
||||
|
||||
let bygning = Bygning {
|
||||
@@ -342,6 +334,8 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
},
|
||||
klima: Klima::default(),
|
||||
flate: flater,
|
||||
sone: Vec::new(),
|
||||
grunn: Grunn::default(),
|
||||
vindu: vinduer,
|
||||
kuldebroer: Kuldebroer::default(),
|
||||
infiltrasjon: Infiltrasjon::default(),
|
||||
@@ -355,7 +349,185 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
drift: Drift::default(),
|
||||
scenario: Vec::new(),
|
||||
};
|
||||
Ok(Importrapport { bygning, merknader })
|
||||
Ok(Importrapport {
|
||||
bygning,
|
||||
merknader,
|
||||
utelatt,
|
||||
})
|
||||
}
|
||||
|
||||
/// Slår sammen flater med samme konstruksjon, type og himmelretning til én oppføring.
|
||||
///
|
||||
/// En BIM-modell deler gjerne den samme veggen i mange elementer, ett per rom eller etasje.
|
||||
/// For en energiberegning er det konstruksjonen og himmelretningen som teller, så oppføringene
|
||||
/// summeres. Himmelretningen rundes til nærmeste 45 grader før sammenligningen.
|
||||
fn slaa_sammen(flater: &mut Vec<Flate>) {
|
||||
let nokkel = |f: &Flate| {
|
||||
let navn = f.navn.split(" #").next().unwrap_or(&f.navn).to_string();
|
||||
let sektor = ((f.orientering / 45.0).round() as i64).rem_euclid(8);
|
||||
let u = (f.u_verdi.unwrap_or(-1.0) * 100.0).round() as i64;
|
||||
(navn, f.typ, f.mot, sektor, u)
|
||||
};
|
||||
let mut samlet: Vec<Flate> = Vec::new();
|
||||
for f in flater.drain(..) {
|
||||
match samlet.iter_mut().find(|g| nokkel(g) == nokkel(&f)) {
|
||||
Some(g) => {
|
||||
// Arealvektet himmelretning, slik at summen peker riktig vei.
|
||||
let sum = g.areal + f.areal;
|
||||
g.orientering = (g.orientering * g.areal + f.orientering * f.areal) / sum;
|
||||
g.areal = sum;
|
||||
}
|
||||
None => samlet.push(f),
|
||||
}
|
||||
}
|
||||
for f in &mut samlet {
|
||||
f.navn = f
|
||||
.navn
|
||||
.split(" #")
|
||||
.next()
|
||||
.unwrap_or(&f.navn)
|
||||
.trim()
|
||||
.to_string();
|
||||
f.orientering = (f.orientering / 45.0).round() * 45.0;
|
||||
}
|
||||
*flater = samlet;
|
||||
}
|
||||
|
||||
/// En bygningsdel slik den er lest av IFC-filen.
|
||||
struct Del {
|
||||
nr: u64,
|
||||
typ: String,
|
||||
navn: String,
|
||||
ytre: Option<bool>,
|
||||
u_verdi: Option<f64>,
|
||||
plass: Option<(f64, f64, f64, f64, f64)>,
|
||||
areal: f64,
|
||||
normal: Option<[f64; 3]>,
|
||||
}
|
||||
|
||||
fn standardnavn_for(typ: &str) -> &'static str {
|
||||
if typ == "IFCROOF" {
|
||||
"Tak"
|
||||
} else if PLATETYPER.contains(&typ) {
|
||||
"Dekke"
|
||||
} else {
|
||||
"Vegg"
|
||||
}
|
||||
}
|
||||
|
||||
fn romnavn(e: &Entitet) -> String {
|
||||
e.arg(7)
|
||||
.and_then(|a| a.som_tekst())
|
||||
.filter(|s| !s.is_empty())
|
||||
.or_else(|| e.arg(2).and_then(|a| a.som_tekst()))
|
||||
.unwrap_or("rom")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Arealet fra en mengdesamling. Godtar både standardnavnene og verktøyspesifikke navn.
|
||||
fn arealmengde(m: &HashMap<String, f64>) -> f64 {
|
||||
for navn in [
|
||||
"NetFloorArea",
|
||||
"GrossFloorArea",
|
||||
"NetArea",
|
||||
"GrossArea",
|
||||
"Area",
|
||||
] {
|
||||
if let Some(v) = m.get(navn) {
|
||||
return *v;
|
||||
}
|
||||
}
|
||||
// Ellers: et hvilket som helst areal, slik at verktøyspesifikke navn som
|
||||
// «GSA BIM Area» også virker.
|
||||
m.iter()
|
||||
.filter(|(k, _)| k.to_lowercase().contains("area"))
|
||||
.map(|(_, v)| *v)
|
||||
.fold(0.0, f64::max)
|
||||
}
|
||||
|
||||
/// Himmelretning og helning ut fra hovedflatens normal.
|
||||
fn retning_av(d: &Del, cx: f64, cy: f64, sant_nord: f64) -> (f64, f64) {
|
||||
let Some(n) = d.normal else {
|
||||
return (
|
||||
0.0,
|
||||
if PLATETYPER.contains(&d.typ.as_str()) {
|
||||
0.0
|
||||
} else {
|
||||
90.0
|
||||
},
|
||||
);
|
||||
};
|
||||
// Normalen er gitt i lokale koordinater; roter den med delens plassering.
|
||||
let (dx, dy) = d.plass.map(|p| (p.3, p.4)).unwrap_or((1.0, 0.0));
|
||||
let mut g = [n[0] * dx - n[1] * dy, n[0] * dy + n[1] * dx, n[2]];
|
||||
|
||||
let vannrett = (g[0] * g[0] + g[1] * g[1]).sqrt();
|
||||
if vannrett > 0.2 {
|
||||
// Veggflate: velg den normalen som peker bort fra bygningens tyngdepunkt.
|
||||
if let Some((x, y, _, _, _)) = d.plass {
|
||||
if g[0] * (x - cx) + g[1] * (y - cy) < 0.0 {
|
||||
g = [-g[0], -g[1], -g[2]];
|
||||
}
|
||||
}
|
||||
} else if g[2] < 0.0 {
|
||||
// Vannrett flate: normalen oppover gir helning 0, nedover 180.
|
||||
g = [-g[0], -g[1], -g[2]];
|
||||
}
|
||||
let helning = g[2].clamp(-1.0, 1.0).acos().to_degrees();
|
||||
let asimut = (g[0].atan2(g[1]).to_degrees() + sant_nord).rem_euclid(360.0);
|
||||
(asimut, helning)
|
||||
}
|
||||
|
||||
/// Avgjør om en bygningsdel hører til klimaskjermen, og hva den vender mot.
|
||||
fn klassifiser(d: &Del, laveste: f64, helning: f64) -> Option<(FlateType, Mot)> {
|
||||
let navn = d.navn.to_lowercase();
|
||||
let grunnord = [
|
||||
"kjellermur",
|
||||
"grunnmur",
|
||||
"ringmur",
|
||||
"såle",
|
||||
"sale",
|
||||
"fundament",
|
||||
];
|
||||
let takord = ["tak", "roof", "loft"];
|
||||
|
||||
if PLATETYPER.contains(&d.typ.as_str()) {
|
||||
if d.typ == "IFCROOF" || takord.iter().any(|o| navn.contains(o)) {
|
||||
return Some((
|
||||
FlateType::Tak,
|
||||
if navn.contains("loft") {
|
||||
Mot::Uoppvarmet
|
||||
} else {
|
||||
Mot::Ute
|
||||
},
|
||||
));
|
||||
}
|
||||
let nederst = d
|
||||
.plass
|
||||
.map(|p| (p.2 - laveste).abs() < 0.5)
|
||||
.unwrap_or(false);
|
||||
if nederst || navn.contains("grunn") || navn.contains("kjellergulv") {
|
||||
return Some((FlateType::Gulv, Mot::Grunn));
|
||||
}
|
||||
// Etasjeskillere og balkongdekker er ikke klimaskjerm med mindre de er merket ytre.
|
||||
if d.ytre == Some(true) {
|
||||
return Some((FlateType::Gulv, Mot::Ute));
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
// Vegger.
|
||||
if d.ytre == Some(true) {
|
||||
return Some((FlateType::Vegg, Mot::Ute));
|
||||
}
|
||||
if grunnord.iter().any(|o| navn.contains(o)) {
|
||||
return Some((FlateType::Vegg, Mot::Grunn));
|
||||
}
|
||||
if d.ytre.is_none() && helning > 60.0 {
|
||||
// Uten `IsExternal` må vi anta at veggen er ytre.
|
||||
return Some((FlateType::Vegg, Mot::Ute));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn navn_av(e: &Entitet, r: u64, standard: &str) -> String {
|
||||
@@ -384,25 +556,6 @@ fn adresse(fil: &Fil) -> Option<String> {
|
||||
(!ut.is_empty()).then_some(ut)
|
||||
}
|
||||
|
||||
/// Faktor som gjør lengder om til meter.
|
||||
fn lengdeenhet(fil: &Fil) -> f64 {
|
||||
for (_, e) in fil.av_type("IFCSIUNIT") {
|
||||
let er_lengde = e.arg(1).and_then(|a| a.som_tekst()) == Some("LENGTHUNIT");
|
||||
if !er_lengde {
|
||||
continue;
|
||||
}
|
||||
let prefiks = e.arg(2).and_then(|a| a.som_tekst()).unwrap_or("");
|
||||
return match prefiks {
|
||||
"MILLI" => 0.001,
|
||||
"CENTI" => 0.01,
|
||||
"DECI" => 0.1,
|
||||
"KILO" => 1000.0,
|
||||
_ => 1.0,
|
||||
};
|
||||
}
|
||||
1.0
|
||||
}
|
||||
|
||||
/// Prosjektets dreining mot sant nord, i grader med klokka.
|
||||
fn sant_nord(fil: &Fil) -> f64 {
|
||||
for (_, e) in fil.av_type("IFCGEOMETRICREPRESENTATIONCONTEXT") {
|
||||
@@ -489,22 +642,6 @@ fn retning(fil: &Fil, r: Option<u64>) -> Option<(f64, f64)> {
|
||||
))
|
||||
}
|
||||
|
||||
/// Himmelretning for en vegg: normalen står vinkelrett på veggens akse, og velges slik at
|
||||
/// den peker bort fra bygningens tyngdepunkt.
|
||||
fn asimut_av(x: f64, y: f64, dx: f64, dy: f64, cx: f64, cy: f64, sant_nord: f64) -> f64 {
|
||||
let kandidater = [(dy, -dx), (-dy, dx)];
|
||||
let ut = (x - cx, y - cy);
|
||||
let valgt = if kandidater[0].0 * ut.0 + kandidater[0].1 * ut.1
|
||||
>= kandidater[1].0 * ut.0 + kandidater[1].1 * ut.1
|
||||
{
|
||||
kandidater[0]
|
||||
} else {
|
||||
kandidater[1]
|
||||
};
|
||||
// Asimut fra nord med klokka; prosjektets y-akse peker mot nord etter TrueNorth-dreining.
|
||||
(valgt.0.atan2(valgt.1).to_degrees() + sant_nord).rem_euclid(360.0)
|
||||
}
|
||||
|
||||
type Egenskaper = HashMap<u64, HashMap<String, Arg>>;
|
||||
|
||||
/// Samler alle egenskaper fra `IfcRelDefinesByProperties` per objekt.
|
||||
@@ -668,54 +805,83 @@ fn er_ytre(e: &Egenskaper, r: u64) -> Option<bool> {
|
||||
mod tester {
|
||||
use super::*;
|
||||
|
||||
const PROVE: &str = r#"ISO-10303-21;
|
||||
HEADER;ENDSEC;
|
||||
DATA;
|
||||
#1= IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
|
||||
#2= IFCDIRECTION((0.,1.));
|
||||
#3= IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-5,#50,#2);
|
||||
#10= IFCBUILDING('b',$,'Testhus',$,$,#20,$,$,.ELEMENT.,$,$,$);
|
||||
#20= IFCLOCALPLACEMENT($,#21);
|
||||
#21= IFCAXIS2PLACEMENT3D(#22,$,$);
|
||||
#22= IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#100= IFCWALLSTANDARDCASE('w1',$,'Sorvegg',$,$,#110,$,$);
|
||||
#110= IFCLOCALPLACEMENT(#20,#111);
|
||||
#111= IFCAXIS2PLACEMENT3D(#112,$,#113);
|
||||
#112= IFCCARTESIANPOINT((0.,-5.,0.));
|
||||
#113= IFCDIRECTION((1.,0.,0.));
|
||||
#120= IFCWALLSTANDARDCASE('w2',$,'Nordvegg',$,$,#130,$,$);
|
||||
#130= IFCLOCALPLACEMENT(#20,#131);
|
||||
#131= IFCAXIS2PLACEMENT3D(#132,$,#133);
|
||||
#132= IFCCARTESIANPOINT((0.,5.,0.));
|
||||
#133= IFCDIRECTION((1.,0.,0.));
|
||||
#200= IFCPROPERTYSINGLEVALUE('ThermalTransmittance',$,IFCREAL(0.18),$);
|
||||
#201= IFCPROPERTYSINGLEVALUE('IsExternal',$,IFCBOOLEAN(.T.),$);
|
||||
#202= IFCPROPERTYSET('ps',$,'Pset_WallCommon',$,(#200,#201));
|
||||
#203= IFCRELDEFINESBYPROPERTIES('r',$,$,$,(#100,#120),#202);
|
||||
#210= IFCQUANTITYAREA('NetSideArea',$,$,25.0,$);
|
||||
#211= IFCELEMENTQUANTITY('q',$,'BaseQuantities',$,$,(#210));
|
||||
#212= IFCRELDEFINESBYPROPERTIES('r2',$,$,$,(#100,#120),#211);
|
||||
#300= IFCSPACE('s',$,'Stue',$,$,#20,$,$,.ELEMENT.,.INTERNAL.,$);
|
||||
#310= IFCQUANTITYAREA('NetFloorArea',$,$,100.0,$);
|
||||
#311= IFCQUANTITYVOLUME('NetVolume',$,$,250.0,$);
|
||||
#312= IFCELEMENTQUANTITY('q2',$,'BaseQuantities',$,$,(#310,#311));
|
||||
#313= IFCRELDEFINESBYPROPERTIES('r3',$,$,$,(#300),#312);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;"#;
|
||||
/// To yttervegger på 5 × 2,5 m med hver sin trekantgeometri, i millimeter.
|
||||
/// Sørveggen ligger ved y = −5 m, nordveggen ved y = +5 m.
|
||||
fn prove() -> String {
|
||||
let mut s = String::from(
|
||||
"ISO-10303-21;\nHEADER;ENDSEC;\nDATA;\n #1= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);\n #2= IFCUNITASSIGNMENT((#1));\n #3= IFCPROJECT('p',$,'Prosjekt',$,$,$,$,(#40),#2);\n #4= IFCDIRECTION((0.,1.));\n #40= IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-5,#50,#4);\n #5= IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);\n #10= IFCBUILDING('b',$,'Testhus',$,$,#20,$,$,.ELEMENT.,$,$,$);\n #20= IFCLOCALPLACEMENT($,#21);\n #21= IFCAXIS2PLACEMENT3D(#22,$,$);\n #22= IFCCARTESIANPOINT((0.,0.,0.));\n #200= IFCPROPERTYSINGLEVALUE('ThermalTransmittance',$,IFCREAL(0.18),$);\n #201= IFCPROPERTYSINGLEVALUE('IsExternal',$,IFCBOOLEAN(.T.),$);\n #202= IFCPROPERTYSET('ps',$,'Pset_WallCommon',$,(#200,#201));\n #300= IFCSPACE('s',$,'Stue',$,$,#20,$,'Stue',.ELEMENT.,.INTERNAL.,$);\n #310= IFCQUANTITYAREA('NetFloorArea',$,$,100.0,$);\n #311= IFCQUANTITYVOLUME('NetVolume',$,$,250.0,$);\n #312= IFCELEMENTQUANTITY('q2',$,'BaseQuantities',$,$,(#310,#311));\n #313= IFCRELDEFINESBYPROPERTIES('r3',$,$,$,(#300),#312);\n",
|
||||
);
|
||||
// En vegg som en tynn kasse: 5 m lang, 2,5 m høy, 0,2 m tykk, i millimeter.
|
||||
let mut vegg = |nr: u64, navn: &str, y: f64| {
|
||||
let (p, l, h, tj) = (nr, 5000.0, 2500.0, 200.0);
|
||||
s.push_str(&format!(
|
||||
"#{}= IFCWALL('w{p}',$,'{navn}',$,$,#{},#{},$,$);\n",
|
||||
p,
|
||||
p + 1,
|
||||
p + 4
|
||||
));
|
||||
s.push_str(&format!("#{}= IFCLOCALPLACEMENT(#20,#{});\n", p + 1, p + 2));
|
||||
s.push_str(&format!(
|
||||
"#{}= IFCAXIS2PLACEMENT3D(#{},$,#{});\n",
|
||||
p + 2,
|
||||
p + 3,
|
||||
p + 9
|
||||
));
|
||||
s.push_str(&format!(
|
||||
"#{}= IFCCARTESIANPOINT((0.,{:.1},0.));\n",
|
||||
p + 3,
|
||||
y * 1000.0
|
||||
));
|
||||
s.push_str(&format!(
|
||||
"#{}= IFCPRODUCTDEFINITIONSHAPE($,$,(#{}));\n",
|
||||
p + 4,
|
||||
p + 5
|
||||
));
|
||||
s.push_str(&format!(
|
||||
"#{}= IFCSHAPEREPRESENTATION(#40,'Body','Tessellation',(#{}));\n",
|
||||
p + 5,
|
||||
p + 6
|
||||
));
|
||||
s.push_str(&format!(
|
||||
"#{}= IFCTRIANGULATEDFACESET(#{},$,.T.,((1,2,3),(1,3,4),(5,7,6),(5,8,7),(1,5,6),(1,6,2),(2,6,7),(2,7,3),(3,7,8),(3,8,4),(4,8,5),(4,5,1)),$);\n",
|
||||
p + 6,
|
||||
p + 7
|
||||
));
|
||||
s.push_str(&format!(
|
||||
"#{}= IFCCARTESIANPOINTLIST3D(((0.,0.,0.),({l:.1},0.,0.),({l:.1},{tj:.1},0.),(0.,{tj:.1},0.),(0.,0.,{h:.1}),({l:.1},0.,{h:.1}),({l:.1},{tj:.1},{h:.1}),(0.,{tj:.1},{h:.1})));\n",
|
||||
p + 7
|
||||
));
|
||||
s.push_str(&format!("#{}= IFCDIRECTION((1.,0.,0.));\n", p + 9));
|
||||
s.push_str(&format!(
|
||||
"#{}= IFCRELDEFINESBYPROPERTIES('rel{p}',$,$,$,(#{p}),#202);\n",
|
||||
p + 8
|
||||
));
|
||||
};
|
||||
vegg(100, "Sorvegg", -5.0);
|
||||
vegg(120, "Nordvegg", 5.0);
|
||||
s.push_str("ENDSEC;\nEND-ISO-10303-21;");
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leser_vegger_areal_uverdi_og_himmelretning() {
|
||||
let r = fra_tekst(PROVE, "prove").unwrap();
|
||||
fn leser_areal_uverdi_og_himmelretning_av_geometrien() {
|
||||
let r = fra_tekst(&prove(), "prove").unwrap();
|
||||
let b = &r.bygning;
|
||||
assert_eq!(b.bygning.navn, "Testhus");
|
||||
assert_eq!(b.bygning.oppvarmet_bra, 100.0);
|
||||
assert_eq!(b.bygning.oppvarmet_volum, Some(250.0));
|
||||
assert_eq!(b.flate.len(), 2);
|
||||
assert_eq!(b.flate.len(), 2, "flater: {:?}", b.flate);
|
||||
for f in &b.flate {
|
||||
assert_eq!(f.areal, 25.0);
|
||||
// Hovedflaten er 5 × 2,5 m, ikke summen av alle sider.
|
||||
assert!(
|
||||
(f.areal - 12.5).abs() < 0.01,
|
||||
"{} fikk areal {}",
|
||||
f.navn,
|
||||
f.areal
|
||||
);
|
||||
assert_eq!(f.u_verdi, Some(0.18));
|
||||
assert_eq!(f.mot, Mot::Ute);
|
||||
}
|
||||
// Veggen ved y = −5 vender sør (180°), den ved y = +5 vender nord (0°).
|
||||
let sor = b
|
||||
.flate
|
||||
.iter()
|
||||
@@ -738,14 +904,22 @@ END-ISO-10303-21;"#;
|
||||
);
|
||||
}
|
||||
|
||||
/// Lengdeenheten skal hentes gjennom prosjektets enhetsoppsett, ikke ved å lete
|
||||
/// gjennom alle enheter i filen. Prøvefilen har både millimeter og meter.
|
||||
#[test]
|
||||
fn millimeterenhet_skaleres() {
|
||||
let t = PROVE.replace(
|
||||
"#1= IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);",
|
||||
"#1= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);",
|
||||
);
|
||||
let r = fra_tekst(&t, "prove").unwrap();
|
||||
// Arealene kommer fra mengder og er alltid i m², så de endres ikke.
|
||||
assert_eq!(r.bygning.flate[0].areal, 25.0);
|
||||
fn lengdeenheten_hentes_fra_prosjektet() {
|
||||
let fil = step::les(&prove());
|
||||
assert_eq!(ifcgeom::lengdeenhet(&fil), 0.001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vegg_uten_geometri_utelates() {
|
||||
let uten = prove().replace("'Body','Tessellation'", "'Axis','Curve2D'");
|
||||
let r = fra_tekst(&uten, "prove").unwrap();
|
||||
assert!(r.bygning.flate.is_empty());
|
||||
assert!(r
|
||||
.merknader
|
||||
.iter()
|
||||
.any(|m| m.contains("geometrien ikke ga noe areal")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
pub mod gbxml;
|
||||
pub mod ifc;
|
||||
pub mod ifcgeom;
|
||||
pub mod regneark;
|
||||
pub mod step;
|
||||
pub mod toml_inn;
|
||||
|
||||
@@ -127,6 +127,8 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
absorptans: None,
|
||||
omkrets: None,
|
||||
dybde: None,
|
||||
over_terreng: None,
|
||||
sone: None,
|
||||
kommentar: None,
|
||||
});
|
||||
}
|
||||
@@ -197,6 +199,8 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
},
|
||||
klima: Klima::default(),
|
||||
flate: flater,
|
||||
sone: Vec::new(),
|
||||
grunn: Grunn::default(),
|
||||
vindu: vinduer,
|
||||
kuldebroer: Kuldebroer::default(),
|
||||
infiltrasjon: Infiltrasjon::default(),
|
||||
|
||||
Reference in New Issue
Block a user