Lag bygning.toml av IFC-modellen og en tilleggsfil
«enok import modell.ifc --tillegg prosjekt.toml» fletter inn det modellen ikke beskriver, som varmeanlegg og tiltak, og rettelser der den mangler noe. Tilleggsfilen kan holde rom utenfor oppvarmet BRA, og et navn med «*» treffer alle elementer det passer på, også i scenarioer. Importen er nå deterministisk: entitetene leses i fast rekkefølge, helningen arealvektes når flater slås sammen, og skrå tak får alltid normalen oppover. Vegger får himmelretningen i navnet, slik at tilleggsfiler kan vise til dem. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
This commit is contained in:
+92
-3
@@ -11,6 +11,7 @@
|
||||
|
||||
use crate::inndata::ifcgeom;
|
||||
use crate::inndata::step::{self, Arg, Entitet, Fil};
|
||||
use crate::inndata::toml_inn::Importvalg;
|
||||
use crate::klima::geo;
|
||||
use crate::modell::*;
|
||||
use crate::Result;
|
||||
@@ -29,17 +30,26 @@ const VEGGTYPER: [&str; 3] = ["IFCWALL", "IFCWALLSTANDARDCASE", "IFCCURTAINWALL"
|
||||
const PLATETYPER: [&str; 3] = ["IFCSLAB", "IFCROOF", "IFCSLABSTANDARDCASE"];
|
||||
|
||||
pub fn les(sti: &Path) -> Result<Importrapport> {
|
||||
les_med(sti, &Importvalg::default())
|
||||
}
|
||||
|
||||
pub fn les_med(sti: &Path, valg: &Importvalg) -> Result<Importrapport> {
|
||||
let raa = std::fs::read(sti).with_context(|| format!("leser {}", sti.display()))?;
|
||||
let tekst = String::from_utf8_lossy(&raa);
|
||||
fra_tekst(
|
||||
fra_tekst_med(
|
||||
&tekst,
|
||||
sti.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("Bygning"),
|
||||
valg,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
fra_tekst_med(tekst, standardnavn, &Importvalg::default())
|
||||
}
|
||||
|
||||
pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Result<Importrapport> {
|
||||
let fil = step::les(tekst);
|
||||
let mut merknader = Vec::new();
|
||||
|
||||
@@ -88,6 +98,7 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
let mut rom: Vec<(String, f64)> = Vec::new();
|
||||
let mut kjellerrom: Vec<(String, f64)> = Vec::new();
|
||||
let mut mistenkelige: Vec<String> = Vec::new();
|
||||
let mut uoppvarmede: Vec<String> = 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);
|
||||
@@ -111,6 +122,14 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
kjellervolum += romvolum;
|
||||
} else {
|
||||
let lavt = navn.to_lowercase();
|
||||
if valg
|
||||
.uoppvarmede_rom
|
||||
.iter()
|
||||
.any(|u| lavt.contains(&u.to_lowercase()))
|
||||
{
|
||||
uoppvarmede.push(format!("{navn} {areal:.1} m²"));
|
||||
continue;
|
||||
}
|
||||
if ["kott", "bod", "loft", "garasje", "carport", "uoppvarmet"]
|
||||
.iter()
|
||||
.any(|o| lavt.contains(o))
|
||||
@@ -136,6 +155,12 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
mistenkelige.join(", ")
|
||||
));
|
||||
}
|
||||
if !uoppvarmede.is_empty() {
|
||||
merknader.push(format!(
|
||||
"Disse rommene er uoppvarmet etter tilleggsfilen og holdt utenfor oppvarmet BRA: {}.",
|
||||
uoppvarmede.join(", ")
|
||||
));
|
||||
}
|
||||
|
||||
// ── plassering og geometri for alle bygningsdeler ───────────────
|
||||
let typer: Vec<&str> = VEGGTYPER.iter().chain(PLATETYPER.iter()).copied().collect();
|
||||
@@ -398,6 +423,10 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
));
|
||||
}
|
||||
|
||||
// Tilleggsfiler og scenarioer viser til flatene med navn, så navnene må være entydige.
|
||||
unike_navn(&mut flater);
|
||||
unike_navn(&mut kjellerflater);
|
||||
|
||||
// ── merknader ───────────────────────────────────────────────────
|
||||
if bra <= 0.0 {
|
||||
bra = flater
|
||||
@@ -571,6 +600,8 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
}
|
||||
};
|
||||
|
||||
vinduer.sort_by(|a, b| a.navn.cmp(&b.navn));
|
||||
|
||||
let bygning = Bygning {
|
||||
bygning: BygningInfo {
|
||||
navn,
|
||||
@@ -668,9 +699,13 @@ fn slaa_sammen(flater: &mut Vec<Flate>) {
|
||||
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.
|
||||
// Arealvektet himmelretning og helning, slik at summen peker riktig vei og ikke
|
||||
// avhenger av rekkefølgen elementene kommer i.
|
||||
let sum = g.areal + f.areal;
|
||||
g.orientering = (g.orientering * g.areal + f.orientering * f.areal) / sum;
|
||||
if let (Some(a), Some(b)) = (g.helning, f.helning) {
|
||||
g.helning = Some((a * g.areal + b * f.areal) / sum);
|
||||
}
|
||||
g.areal = sum;
|
||||
}
|
||||
None => samlet.push(f),
|
||||
@@ -689,6 +724,36 @@ fn slaa_sammen(flater: &mut Vec<Flate>) {
|
||||
*flater = samlet;
|
||||
}
|
||||
|
||||
/// Gjør flatenavnene entydige. Vegger får himmelretningen etter konstruksjonsnavnet, og andre
|
||||
/// flater som fortsatt deler navn, nummereres.
|
||||
fn unike_navn(flater: &mut [Flate]) {
|
||||
// Fast rekkefølge, slik at samme modell alltid gir samme fil og samme nummerering.
|
||||
flater.sort_by(|a, b| {
|
||||
(a.typ as u8, &a.navn)
|
||||
.cmp(&(b.typ as u8, &b.navn))
|
||||
.then(a.orientering.total_cmp(&b.orientering))
|
||||
.then(b.areal.total_cmp(&a.areal))
|
||||
});
|
||||
for f in flater.iter_mut().filter(|f| f.typ == FlateType::Vegg) {
|
||||
f.navn = format!("{}, {}", f.navn, himmelretning(f.orientering));
|
||||
}
|
||||
let mut antall: HashMap<String, usize> = HashMap::new();
|
||||
for f in flater.iter_mut() {
|
||||
let n = antall.entry(f.navn.clone()).or_insert(0);
|
||||
*n += 1;
|
||||
if *n > 1 {
|
||||
f.navn = format!("{} ({n})", f.navn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn himmelretning(grader: f64) -> &'static str {
|
||||
const RETNINGER: [&str; 8] = [
|
||||
"nord", "nordøst", "øst", "sørøst", "sør", "sørvest", "vest", "nordvest",
|
||||
];
|
||||
RETNINGER[((grader / 45.0).round() as i64).rem_euclid(8) as usize]
|
||||
}
|
||||
|
||||
/// En etasje i modellen, med navn og høyde over byggets referansenivå.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Etasje {
|
||||
@@ -1150,7 +1215,10 @@ fn retning_av(d: &Del, cx: f64, cy: f64, sant_nord: f64) -> (f64, f64) {
|
||||
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 {
|
||||
// Et skrått tak har også en vannrett komponent, men skal alltid ha normalen oppover;
|
||||
// ellers kan undersiden bli hovedflaten og gi helning 180° minus takvinkelen.
|
||||
let skraa_plate = PLATETYPER.contains(&d.typ.as_str()) && g[2].abs() > 0.1;
|
||||
if vannrett > 0.2 && !skraa_plate {
|
||||
// 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 {
|
||||
@@ -1686,6 +1754,27 @@ mod tester {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vegger_faar_himmelretning_i_navnet() {
|
||||
let r = fra_tekst(&prove(), "prove").unwrap();
|
||||
let mut navn: Vec<&str> = r.bygning.flate.iter().map(|f| f.navn.as_str()).collect();
|
||||
navn.sort();
|
||||
assert_eq!(navn, vec!["Nordvegg, nord", "Sorvegg, sør"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uoppvarmede_rom_fra_tilleggsfilen_holdes_utenfor_bra() {
|
||||
let valg = Importvalg {
|
||||
uoppvarmede_rom: vec!["STUE".into()],
|
||||
};
|
||||
let r = fra_tekst_med(&prove(), "prove", &valg).unwrap();
|
||||
assert_ne!(r.bygning.bygning.oppvarmet_bra, 100.0);
|
||||
assert!(r
|
||||
.merknader
|
||||
.iter()
|
||||
.any(|m| m.contains("uoppvarmet etter tilleggsfilen")));
|
||||
}
|
||||
|
||||
/// Lengdeenheten skal hentes gjennom prosjektets enhetsoppsett, ikke ved å lete
|
||||
/// gjennom alle enheter i filen. Prøvefilen har både millimeter og meter.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user