2026-09-14 18:03:00 +02:00
//! Import fra IFC (IFC2x3 og IFC4), som er formatet Vectorworks eksporterer med
//! «Export IFC Project».
//!
//! Importen henter klimaskjermen: yttervegger, tak, gulv, vinduer og dører, med areal,
//! U-verdi og himmelretning. Areal og U-verdi hentes fra mengder (`IfcElementQuantity`) og
//! egenskapssett (`Pset_WallCommon.ThermalTransmittance` m.fl.) når modellen har dem, ellers
//! av geometriske attributter. Himmelretningen utledes av elementets plassering: veggens
//! lokale x-akse gir planet, og ytre normal velges som den som peker bort fra bygningens
//! tyngdepunkt. Prosjektets sanne nord (`IfcGeometricRepresentationContext.TrueNorth`)
//! legges til.
2026-09-14 19:16:18 +02:00
use crate ::inndata ::ifcgeom ;
2026-09-14 18:03:00 +02:00
use crate ::inndata ::step ::{ self , Arg , Entitet , Fil };
2026-09-15 15:38:04 +02:00
use crate ::klima ::geo ;
2026-09-14 18:03:00 +02:00
use crate ::modell ::* ;
use crate ::Result ;
use anyhow ::Context ;
use std ::collections ::HashMap ;
use std ::path ::Path ;
pub struct Importrapport {
pub bygning : Bygning ,
pub merknader : Vec < String > ,
2026-09-14 19:16:18 +02:00
/// Bygningsdeler som ikke er tatt med i klimaskjermen: navn, areal og U-verdi.
pub utelatt : Vec < ( String , f64 , Option < f64 > ) > ,
2026-09-14 18:03:00 +02:00
}
const VEGGTYPER : [ & str ; 3 ] = [ "IFCWALL" , "IFCWALLSTANDARDCASE" , "IFCCURTAINWALL" ];
const PLATETYPER : [ & str ; 3 ] = [ "IFCSLAB" , "IFCROOF" , "IFCSLABSTANDARDCASE" ];
pub fn les ( sti : & Path ) -> Result < Importrapport > {
let raa = std ::fs ::read ( sti ). with_context ( || format! ( "leser {} " , sti . display ())) ? ;
let tekst = String ::from_utf8_lossy ( & raa );
fra_tekst (
& tekst ,
sti . file_stem ()
. and_then ( | s | s . to_str ())
. unwrap_or ( "Bygning" ),
)
}
pub fn fra_tekst ( tekst : & str , standardnavn : & str ) -> Result < Importrapport > {
let fil = step ::les ( tekst );
let mut merknader = Vec ::new ();
2026-09-14 19:16:18 +02:00
let skala = ifcgeom ::lengdeenhet ( & fil );
2026-09-14 18:03:00 +02:00
let sant_nord = sant_nord ( & fil );
let egenskaper = samle_egenskaper ( & fil );
let mengder = samle_mengder ( & fil );
let ( vindu_vert , dor_vert ) = samle_apninger ( & fil );
2026-09-14 22:36:54 +02:00
let ( etasjer , etasje_for ) = les_etasjer ( & fil , skala );
2026-09-14 18:03:00 +02:00
2026-09-15 15:38:04 +02:00
// En eksport uten bygningsdeler gir ingen klimaskjerm å regne på. Det skjer typisk når
// designlagene ikke er knyttet til etasjer i eksportdialogen, og da er det bedre å stoppe
// enn å skrive en tom bygningsbeskrivelse.
let antall_deler : usize = [
"IFCWALL" ,
"IFCWALLSTANDARDCASE" ,
"IFCCURTAINWALL" ,
"IFCSLAB" ,
"IFCROOF" ,
"IFCWINDOW" ,
"IFCDOOR" ,
"IFCSPACE" ,
]
. iter ()
. map ( | t | fil . av_type ( t ). count ())
. sum ();
if antall_deler == 0 {
anyhow ::bail! (
"IFC-filen har ingen bygningsdeler: ingen vegger, dekker, tak, vinduer, dører eller rom. \
Den inneholder bare prosjektstrukturen{}. I Vectorworks skjer dette gjerne når designlagene \
ikke er med under «Layer Mapping» i «Export IFC Project»." ,
if etasjer . is_empty () {
String ::new ()
} else {
format! ( " med {} etasjer" , etasjer . len ())
}
);
}
2026-09-14 19:16:18 +02:00
// ── oppvarmet areal fra rommene ─────────────────────────────────
2026-09-14 22:36:54 +02:00
// Rom i en etasje under terreng hører til kjelleren og ikke til oppvarmet BRA.
2026-09-14 18:03:00 +02:00
let mut bra = 0.0 ;
let mut volum = 0.0 ;
2026-09-14 22:36:54 +02:00
let mut kjellerareal = 0.0 ;
let mut kjellervolum = 0.0 ;
2026-09-14 19:16:18 +02:00
let mut rom : Vec < ( String , f64 ) > = Vec ::new ();
2026-09-14 22:36:54 +02:00
let mut kjellerrom : Vec < ( String , f64 ) > = Vec ::new ();
let mut mistenkelige : Vec < String > = Vec ::new ();
2026-09-14 19:16:18 +02:00
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" ));
2026-09-14 22:36:54 +02:00
if areal <= 0.0 || maalerom {
continue ;
}
let romvolum = mengder
. get ( & r )
. and_then ( | m | m . get ( "NetVolume" ). or_else ( || m . get ( "GrossVolume" )). copied ())
. unwrap_or ( 0.0 );
let under = etasje_for
. get ( & r )
. and_then ( | i | etasjer . get ( * i ))
. map ( | e | e . under_terreng )
. unwrap_or ( false );
if under {
kjellerrom . push (( navn , areal ));
kjellerareal += areal ;
kjellervolum += romvolum ;
} else {
let lavt = navn . to_lowercase ();
if [ "kott" , "bod" , "loft" , "garasje" , "carport" , "uoppvarmet" ]
. iter ()
. any ( | o | lavt . contains ( o ))
{
mistenkelige . push ( format! ( " {navn} {areal:.1} m²" ));
}
2026-09-14 19:16:18 +02:00
rom . push (( navn , areal ));
bra += areal ;
2026-09-14 22:36:54 +02:00
volum += romvolum ;
2026-09-14 18:03:00 +02:00
}
}
2026-09-14 19:16:18 +02:00
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! (
2026-09-14 22:36:54 +02:00
"Oppvarmet BRA er satt til {bra:.1} m², summen av rommene i etasjene over terreng. Rom [m²]: {} ." ,
2026-09-14 19:16:18 +02:00
liste . join ( ", " )
));
}
2026-09-14 22:36:54 +02:00
if ! mistenkelige . is_empty () {
merknader . push ( format! (
"Disse rommene er regnet som oppvarmet, men navnet tyder på at de ikke er det: {} . Trekk dem fra oppvarmet_bra om de står uoppvarmet." ,
mistenkelige . join ( ", " )
));
}
2026-09-14 18:03:00 +02:00
2026-09-14 19:16:18 +02:00
// ── 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 )),
2026-09-14 22:36:54 +02:00
etasje : etasje_for . get ( & r ). copied (),
2026-09-14 19:16:18 +02:00
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 ),
});
2026-09-14 18:03:00 +02:00
}
2026-09-14 19:16:18 +02:00
// 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 ))
2026-09-14 18:03:00 +02:00
. collect ();
2026-09-14 19:16:18 +02:00
let referanse : Vec <& Del > = if ytre_deler . is_empty () {
deler
. iter ()
. filter ( | d | VEGGTYPER . contains ( & d . typ . as_str ()))
. collect ()
} else {
ytre_deler
};
2026-09-14 18:03:00 +02:00
let ( mut cx , mut cy , mut n ) = ( 0.0 , 0.0 , 0.0 );
2026-09-14 19:16:18 +02:00
for d in & referanse {
if let Some (( x , y , _ , _ , _ )) = d . plass {
2026-09-14 18:03:00 +02:00
cx += x ;
cy += y ;
n += 1.0 ;
}
}
if n > 0.0 {
cx /= n ;
cy /= n ;
}
2026-09-14 22:36:54 +02:00
// ── kjeller som egen sone ───────────────────────────────────────
// Finnes det en etasje under terreng med rom, er den nesten alltid uoppvarmet.
// Bygningsdelene der hører til kjellerens klimaskjerm, og etasjeskilleren over blir
// grensen mellom den oppvarmede delen og sonen.
let kjelleretasje = ( ! kjellerrom . is_empty ())
. then ( || etasjer . iter (). position ( | e | e . under_terreng ))
. flatten ();
let kjellernavn = kjelleretasje
. and_then ( | i | etasjer . get ( i ))
. map ( | e | e . navn . clone ())
. unwrap_or_default ();
// Etasjehøyden gir hvor dypt kjelleren ligger.
let kjellerdybde = kjelleretasje
. and_then ( | i | {
let denne = etasjer . get ( i ) ? . hoyde ;
let over = etasjer . get ( i + 1 ). map ( | e | e . hoyde ). unwrap_or ( denne + 2.4 );
Some ( over - denne )
})
. unwrap_or ( 0.0 );
// Den laveste etasjen over terreng, der etasjeskilleren mot kjelleren ligger.
let forste_over = etasjer . iter (). position ( | e | ! e . under_terreng );
2026-09-15 15:38:04 +02:00
// Terrenghøyden på IfcBuilding sier hvor mye av grunnmuren som står over bakken.
let terreng = terrenghoyder ( & fil , skala );
let kjeller_over_terreng = match ( terreng , forste_over . and_then ( | i | etasjer . get ( i ))) {
( Some (( referanse , terrengnivaa )), Some ( forste )) => {
over_terreng_for_kjeller ( referanse , terrengnivaa , forste . hoyde , kjellerdybde )
}
_ => 0.0 ,
};
2026-09-14 22:36:54 +02:00
2026-09-14 19:16:18 +02:00
// ── klimaskjerm ─────────────────────────────────────────────────
2026-09-14 22:36:54 +02:00
let mut kjellerflater : Vec < Flate > = Vec ::new ();
2026-09-14 18:03:00 +02:00
let mut flater : Vec < Flate > = Vec ::new ();
2026-09-14 19:16:18 +02:00
let mut utelatt : Vec < ( String , f64 , Option < f64 > ) > = Vec ::new ();
2026-09-14 18:03:00 +02:00
let mut uten_uverdi = 0 usize ;
2026-09-14 19:16:18 +02:00
let mut uten_geometri = 0 usize ;
2026-09-14 18:03:00 +02:00
let mut asimut_for_vegg : HashMap < u64 , f64 > = HashMap ::new ();
2026-09-14 22:36:54 +02:00
// Laveste etasje og laveste gulvnivå skiller dekke mot grunn fra etasjeskillere.
let etasjekontekst = Etasjekontekst {
nederste : ( ! etasjer . is_empty ()). then_some ( 0 ),
kjeller : kjelleretasje ,
forste_over ,
};
2026-09-14 19:16:18 +02:00
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 );
2026-09-14 18:03:00 +02:00
2026-09-14 19:16:18 +02:00
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 ;
2026-09-14 18:03:00 +02:00
continue ;
}
2026-09-14 22:36:54 +02:00
let i_kjeller = kjelleretasje . is_some () && d . etasje == kjelleretasje ;
let Some (( typ , mot )) = klassifiser ( d , laveste , helning , & etasjekontekst ) else {
2026-09-14 19:16:18 +02:00
utelatt . push (( d . navn . clone (), d . areal , d . u_verdi ));
2026-09-14 18:03:00 +02:00
continue ;
};
2026-09-14 22:36:54 +02:00
// En etasjeskiller i den laveste etasjen over terreng er grensen mot kjelleren.
let ( mot , sone ) = if kjelleretasje . is_some ()
&& ! i_kjeller
&& d . etasje == forste_over
&& typ == FlateType ::Gulv
{
( Mot ::Uoppvarmet , Some ( kjellernavn . clone ()))
} else {
( mot , None )
};
2026-09-14 19:16:18 +02:00
if d . u_verdi . is_none () {
2026-09-14 18:03:00 +02:00
uten_uverdi += 1 ;
}
2026-09-14 22:36:54 +02:00
let mal = if i_kjeller {
& mut kjellerflater
} else {
& mut flater
};
mal . push ( Flate {
2026-09-14 19:16:18 +02:00
navn : d . navn . clone (),
2026-09-14 18:03:00 +02:00
typ ,
2026-09-14 19:16:18 +02:00
areal : d . areal ,
orientering : if typ == FlateType ::Vegg { asimut } else { 0.0 },
2026-09-14 18:03:00 +02:00
helning : Some ( helning ),
2026-09-14 19:16:18 +02:00
u_verdi : d . u_verdi ,
2026-09-14 18:03:00 +02:00
lag : None ,
2026-09-14 22:36:54 +02:00
masseklasse : masseklasse_av_materialer ( & fil , d . nr ),
2026-09-14 18:03:00 +02:00
masseplassering : Masseplassering ::Fordelt ,
mot ,
b_faktor : None ,
absorptans : None ,
omkrets : None ,
2026-09-15 15:38:04 +02:00
dybde : ( i_kjeller && mot == Mot ::Grunn ). then_some ( kjellerdybde - kjeller_over_terreng ),
over_terreng : ( i_kjeller && mot == Mot ::Grunn ). then_some ( kjeller_over_terreng ),
2026-09-14 22:36:54 +02:00
sone ,
kommentar : d
. etasje
. and_then ( | i | etasjer . get ( i ))
. map ( | e | format! ( " {} , importert fra IFC" , e . navn ))
. or_else ( || Some ( "importert fra IFC" . into ())),
2026-09-14 18:03:00 +02:00
});
}
2026-09-14 19:16:18 +02:00
slaa_sammen ( & mut flater );
2026-09-14 22:36:54 +02:00
slaa_sammen ( & mut kjellerflater );
2026-09-14 19:16:18 +02:00
2026-09-14 18:03:00 +02:00
// ── vinduer og dører ────────────────────────────────────────────
2026-09-14 19:16:18 +02:00
let mut vinduer : Vec < Vindu > = Vec ::new ();
2026-09-14 18:03:00 +02:00
for ( r , e ) in fil . av_type ( "IFCWINDOW" ). chain ( fil . av_type ( "IFCDOOR" )) {
let dor = e . typ == "IFCDOOR" ;
2026-09-14 19:16:18 +02:00
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 ;
2026-09-14 18:03:00 +02:00
let areal = mengder
. get ( & r )
2026-09-14 19:16:18 +02:00
. map ( arealmengde )
. filter ( | a | * a > 0.05 )
2026-09-14 18:03:00 +02:00
. unwrap_or ( hoyde * bredde );
2026-09-14 19:16:18 +02:00
if areal <= 0.05 {
2026-09-14 18:03:00 +02:00
continue ;
}
let vert = if dor {
dor_vert . get ( & r )
} else {
vindu_vert . get ( & r )
};
2026-09-14 19:16:18 +02:00
// 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 ;
}
2026-09-14 18:03:00 +02:00
let asimut = vert
. and_then ( | v | asimut_for_vegg . get ( v ))
. copied ()
. unwrap_or ( 0.0 );
let u = u_verdi ( & egenskaper , r ). unwrap_or_else ( || {
uten_uverdi += 1 ;
if dor {
2.0
} else {
2.4
}
});
vinduer . push ( Vindu {
navn : navn_av ( e , r , if dor { "Dør" } else { "Vindu" }),
areal ,
antall : 1.0 ,
orientering : asimut ,
helning : 90.0 ,
u_verdi : u ,
g_verdi : egenskap ( & egenskaper , r , "SolarHeatGainTransmittance" )
. or_else ( || egenskap ( & egenskaper , r , "SolarTransmittance" )),
karmandel : egenskap ( & egenskaper , r , "GlazingAreaFraction" ). map ( | g | 1.0 - g ),
solskjerming : None ,
horisont : None ,
dor ,
kommentar : Some ( "importert fra IFC" . into ()),
});
}
2026-09-14 19:16:18 +02:00
// 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 ;
2026-09-14 18:03:00 +02:00
}
2026-09-14 19:16:18 +02:00
let mut trukket_fra = 0 usize ;
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 ;
2026-09-14 18:03:00 +02:00
}
}
}
2026-09-14 19:16:18 +02:00
if trukket_fra > 0 {
merknader . push ( format! (
"Vindus- og dørareal er trukket fra {trukket_fra} vegger. Kontroller veggarealene mot tegningen."
));
}
2026-09-14 18:03:00 +02:00
2026-09-14 19:16:18 +02:00
// ── merknader ───────────────────────────────────────────────────
2026-09-14 18:03:00 +02:00
if bra <= 0.0 {
bra = flater
. iter ()
. filter ( | f | f . typ == FlateType ::Gulv )
. map ( | f | f . areal )
. sum ::< f64 > ();
2026-09-14 19:16:18 +02:00
merknader . push ( if bra > 0.0 {
format! ( "IFC-modellen mangler romarealer; oppvarmet BRA er anslått til {bra:.0} m² av gulvflatene. Kontroller verdien." )
2026-09-14 18:03:00 +02:00
} else {
2026-09-14 19:16:18 +02:00
"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."
));
2026-09-14 18:03:00 +02:00
}
if uten_uverdi > 0 {
merknader . push ( format! (
2026-09-14 19:16:18 +02:00
" {uten_uverdi} bygningsdeler mangler U-verdi i IFC-filen. Legg inn U-verdier, eller la enok bruke typiske verdier for byggeåret."
2026-09-14 18:03:00 +02:00
));
}
merknader . push (
2026-09-14 19:16:18 +02:00
"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."
2026-09-14 18:03:00 +02:00
. into (),
);
2026-09-14 22:36:54 +02:00
let ( data , funnet ) = les_byggdata ( & fil , & egenskaper );
if ! funnet . is_empty () {
merknader . push ( format! (
"Modellen har egenskaper for drift og forsyning, og disse er tatt i bruk: {} ." ,
funnet . join ( ", " )
));
} else {
merknader . push (
"Modellen har ingen egenskaper for byggeår, tetthet, ventilasjon eller varmekilde. \
Festes et postformat med slike felter på bygningen i BIM-verktøyet og knyttes til et \
IFC-egenskapssett, kommer de med hit. Se «Egenskaper enok leser» i enoks dokumentasjon."
. into (),
);
}
2026-09-15 15:38:04 +02:00
let ubrukelig = | s : & str | {
let s = s . trim ();
s . is_empty () || s == "Default Building" || s == "Vectorworks Project"
};
// Bygningens navn, ellers prosjektets langnavn eller navn, ellers filnavnet.
2026-09-14 18:03:00 +02:00
let navn = fil
. av_type ( "IFCBUILDING" )
. next ()
. and_then ( | ( _ , e ) | e . navn (). map ( | s | s . to_string ()))
2026-09-15 15:38:04 +02:00
. filter ( | s | ! ubrukelig ( s ))
. or_else ( || {
fil . av_type ( "IFCPROJECT" ). next (). and_then ( | ( _ , p ) | {
[ p . arg ( 5 ), p . arg ( 2 )]
. into_iter ()
. flatten ()
. filter_map ( | a | a . som_tekst ())
. find ( | s | ! ubrukelig ( s ))
. map ( | s | s . trim (). to_string ())
})
})
2026-09-14 18:03:00 +02:00
. unwrap_or_else ( || standardnavn . to_string ());
2026-09-15 15:38:04 +02:00
// Vernede bygninger er unntatt plikten til å ha energiattest.
let vernet = fil
. av_type ( "IFCBUILDING" )
. next ()
. and_then ( | ( r , _ ) | egenskaper . get ( & r ))
. and_then ( | e | e . get ( "IsLandmarked" ))
. map ( | a | matches! ( a . som_tekst (), Some ( "T" ) | Some ( "TRUE" ) | Some ( "true" )))
. unwrap_or ( false );
if vernet {
merknader . push (
"Bygningen er merket som vernet i modellen. Vernede bygninger etter kulturminneloven og \
plan- og bygningsloven er unntatt plikten til å ha energiattest, men beregningen kan \
fortsatt brukes til å vurdere tiltak."
. into (),
);
}
2026-09-14 22:36:54 +02:00
// Kjellersonen settes sammen av flatene i etasjen under terreng.
let mut soner : Vec < Sone > = Vec ::new ();
if let Some ( i ) = kjelleretasje {
if ! kjellerflater . is_empty () {
let volum = if kjellervolum > 0.0 {
kjellervolum
} else {
kjellerareal * kjellerdybde . max ( 2.0 )
};
let rom : Vec < String > = kjellerrom
. iter ()
. map ( | ( n , a ) | format! ( " {n} {a:.1} " ))
. collect ();
soner . push ( Sone {
navn : kjellernavn . clone (),
volum : ( volum > 0.0 ). then_some ( volum ),
luftveksling : None ,
flater : kjellerflater ,
andel_distribusjonstap : None ,
andel_tappevannstap : None ,
fast_effekt : 0.0 ,
kommentar : Some ( format! (
"Etasje « {} » ligger {:.2} m under referansenivået. Rom [m²]: {} " ,
etasjer [ i ]. navn ,
- etasjer [ i ]. hoyde ,
rom . join ( ", " )
)),
});
2026-09-15 15:38:04 +02:00
let grunnmur = if terreng . is_some () {
format! (
"Etter terrenghøyden i modellen står grunnmuren {kjeller_over_terreng:.2} m over bakken og \
{:.2} m under." ,
kjellerdybde - kjeller_over_terreng
)
} else {
format! (
"Kjellerveggene er regnet {kjellerdybde:.1} m under terreng uten noe over. Står en del av \
grunnmuren over bakken, sett `over_terreng` i meter på veggene, eller sett terrenghøyden \
på bygningen i modellen. Utslaget er lite, om lag én prosent per meter."
)
};
2026-09-14 22:36:54 +02:00
merknader . push ( format! (
"Etasjen « {} » ligger under terreng og er satt opp som en uoppvarmet sone med {:.0} m² gulv. \
2026-09-15 15:38:04 +02:00
{grunnmur} " ,
2026-09-14 22:36:54 +02:00
etasjer [ i ]. navn , kjellerareal
));
merknader . push ( "Er kjelleren oppvarmet likevel, slett [[sone]] og legg flatene inn i klimaskjermen. \
Går det uisolerte varmerør der, sett `andel_distribusjonstap = 1.0` på sonen." . to_string ());
}
}
2026-09-15 15:38:04 +02:00
let klimaposisjon = match posisjon ( & fil ) {
Some ( p ) if p . er_standardplassering () => {
merknader . push (
"Modellen står på Vectorworks' standardplassering i Washington, så posisjonen er ikke \
brukt. Georefereres tegningen eller settes prosjektstedet, velger beregningen \
klimasted ut fra den."
. into (),
);
None
}
Some ( p ) if ! p . i_norge () => {
merknader . push ( format! (
"Modellen oppgir posisjon {:.4} ° nord, {:.4} ° øst, som ligger utenfor Norge. \
Posisjonen er ikke brukt til å velge klimasted." ,
p . bredde , p . lengde
));
None
}
Some ( p ) => {
merknader . push ( format! (
"Modellen oppgir posisjon {:.4} ° nord, {:.4} ° øst. Uten kommune velger beregningen \
klimastedet nærmest denne." ,
p . bredde , p . lengde
));
Some ([ p . bredde , p . lengde ])
}
None => {
merknader . push (
"Modellen oppgir ingen posisjon. Georefereres tegningen eller settes prosjektstedet, \
velger beregningen klimasted ut fra den."
. into (),
);
None
}
};
2026-09-14 22:36:54 +02:00
2026-09-14 18:03:00 +02:00
let bygning = Bygning {
bygning : BygningInfo {
navn ,
2026-09-14 22:36:54 +02:00
kategori : data
. kategori
. as_deref ()
. and_then ( kategori_av_tekst )
. unwrap_or ( Kategori ::Smaahus ),
byggeaar : data
. byggeaar
. unwrap_or ( crate ::modell ::kontroll ::IMPORTERT_BYGGEAAR ),
oppvarmet_bra : data . oppvarmet_bra . unwrap_or ( bra ),
2026-09-14 18:03:00 +02:00
oppvarmet_volum : ( volum > 0.0 ). then_some ( volum ),
antall_boenheter : 1 ,
bygningens_oppvarmede_bra : None ,
adresse : adresse ( & fil ),
gnr_bnr : None ,
beskrivelse : Some ( "Importert fra IFC" . into ()),
2026-09-15 15:38:04 +02:00
energiraadgiver : forfatter ( & fil ),
2026-09-14 18:03:00 +02:00
oppdragsgiver : None ,
},
2026-09-14 22:36:54 +02:00
klima : Klima {
kommune : data . kommune . clone (),
2026-09-15 15:38:04 +02:00
posisjon : klimaposisjon ,
2026-09-14 22:36:54 +02:00
.. Klima ::default ()
},
2026-09-14 18:03:00 +02:00
flate : flater ,
2026-09-14 22:36:54 +02:00
sone : soner ,
2026-09-14 19:16:18 +02:00
grunn : Grunn ::default (),
2026-09-14 18:03:00 +02:00
vindu : vinduer ,
kuldebroer : Kuldebroer ::default (),
2026-09-14 22:36:54 +02:00
infiltrasjon : Infiltrasjon {
n50 : data . n50 ,
.. Infiltrasjon ::default ()
},
ventilasjon : Ventilasjon {
typ : data
. ventilasjon
. as_deref ()
. and_then ( ventilasjon_av_tekst )
. unwrap_or_default (),
gjenvinning : data
. gjenvinning
. map ( | v | if v > 1.0 { v / 100.0 } else { v }),
.. Ventilasjon ::default ()
},
2026-09-14 18:03:00 +02:00
tappevann : Tappevann ::default (),
2026-09-14 22:36:54 +02:00
oppvarming : Oppvarming {
kilde : data
. varmekilde
. as_deref ()
. and_then ( varmekilde_av_tekst )
. map ( | t | {
vec! [ Varmekilde {
typ : t ,
andel_rom : 1.0 ,
andel_ventilasjon : None ,
andel_tappevann : 1.0 ,
spf : None ,
turtemperatur : None ,
lukket : true ,
kommentar : Some ( "fra egenskap i IFC-modellen" . into ()),
}]
})
. unwrap_or_default (),
.. Oppvarming ::default ()
},
2026-09-14 18:03:00 +02:00
distribusjon : Distribusjon ::default (),
solcelle : None ,
kjoling : Kjoling ::default (),
lufting : Lufting ::default (),
drift : Drift ::default (),
scenario : Vec ::new (),
};
2026-09-14 19:16:18 +02:00
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 ;
}
2026-09-14 22:36:54 +02:00
/// En etasje i modellen, med navn og høyde over byggets referansenivå.
#[derive(Clone, Debug)]
pub struct Etasje {
pub navn : String ,
/// Høyde over byggets referansenivå, m. Negativ for kjeller.
pub hoyde : f64 ,
/// Ligger etasjen under terreng, slik navn eller høyde tyder på?
pub under_terreng : bool ,
}
/// Leser etasjene og hvilken etasje hver bygningsdel og hvert rom hører til.
///
/// Etasjeinndelingen er den sikreste opplysningen en BIM-modell har om hva som er hva.
/// En etasje som heter «Kjeller» og ligger under referansenivået er nesten alltid
/// uoppvarmet, og bygningsdelene der hører til kjellerens klimaskjerm og ikke husets.
fn les_etasjer ( fil : & Fil , skala : f64 ) -> ( Vec < Etasje > , HashMap < u64 , usize > ) {
let mut etasjer : Vec < ( u64 , Etasje ) > = fil
. av_type ( "IFCBUILDINGSTOREY" )
. map ( | ( r , e ) | {
let navn = e
. navn ()
. filter ( | s | ! s . is_empty ())
. unwrap_or ( "etasje" )
. to_string ();
let hoyde = e . arg ( 9 ). and_then ( | a | a . som_tall ()). unwrap_or ( 0.0 ) * skala ;
let lavt = navn . to_lowercase ();
let under_terreng = hoyde < - 0.5
|| [ "kjeller" , "basement" , "souterreng" , "underetasje" ]
. iter ()
. any ( | o | lavt . contains ( o ));
(
r ,
Etasje {
navn ,
hoyde ,
under_terreng ,
},
)
})
. collect ();
etasjer . sort_by ( | a , b | {
a . 1. hoyde
. partial_cmp ( & b . 1. hoyde )
. unwrap_or ( std ::cmp ::Ordering ::Equal )
});
let indeks : HashMap < u64 , usize > = etasjer
. iter ()
. enumerate ()
. map ( | ( i , ( r , _ )) | ( * r , i ))
. collect ();
let mut for_element : HashMap < u64 , usize > = HashMap ::new ();
// Bygningsdeler knyttes til etasjen med IfcRelContainedInSpatialStructure.
for ( _ , rel ) in fil . av_type ( "IFCRELCONTAINEDINSPATIALSTRUCTURE" ) {
let Some ( etasje ) = rel
. arg ( 5 )
. and_then ( | a | a . som_ref ())
. and_then ( | r | indeks . get ( & r ))
else {
continue ;
};
for e in rel . arg ( 4 ). map ( | a | a . som_liste ()). unwrap_or ( & []) {
if let Some ( r ) = e . som_ref () {
for_element . insert ( r , * etasje );
}
}
}
// Rom knyttes til etasjen med IfcRelAggregates.
for ( _ , rel ) in fil . av_type ( "IFCRELAGGREGATES" ) {
let Some ( etasje ) = rel
. arg ( 4 )
. and_then ( | a | a . som_ref ())
. and_then ( | r | indeks . get ( & r ))
else {
continue ;
};
for e in rel . arg ( 5 ). map ( | a | a . som_liste ()). unwrap_or ( & []) {
if let Some ( r ) = e . som_ref () {
for_element . insert ( r , * etasje );
}
}
}
( etasjer . into_iter (). map ( | ( _ , e ) | e ). collect (), for_element )
}
/// Masseklasse ut fra materialnavnene i konstruksjonen.
///
/// IFC-filer fra Vectorworks oppgir materialene som andeler uten tykkelse, så U-verdien
/// kan ikke regnes av dem. Navnene sier likevel nok om massen: betong og tegl er tungt,
/// bindingsverk og isolasjon er lett.
fn masseklasse_av_materialer ( fil : & Fil , element : u64 ) -> Option < Masseklasse > {
let mut navn : Vec < String > = Vec ::new ();
for ( _ , rel ) in fil . av_type ( "IFCRELASSOCIATESMATERIAL" ) {
let treff = rel
. arg ( 4 )
. map ( | a | a . som_liste ())
. unwrap_or ( & [])
. iter ()
. any ( | o | o . som_ref () == Some ( element ));
if ! treff {
continue ;
}
let Some ( m ) = rel
. arg ( 5 )
. and_then ( | a | a . som_ref ())
. and_then ( | r | fil . hent ( r ))
else {
continue ;
};
samle_materialnavn ( fil , m , & mut navn , 0 );
}
if navn . is_empty () {
return None ;
}
let tekst = navn . join ( " " ). to_lowercase ();
let tungt = [
"concrete" , "betong" , "brick" , "tegl" , "masonry" , "mur" , "stone" , "stein" , "screed" ,
];
let lett = [
"insulat" ,
"isolasjon" ,
"timber" ,
"wood" ,
"tre" ,
"panel" ,
"gypsum" ,
"gips" ,
"air" ,
];
let t = tungt . iter (). filter ( | o | tekst . contains ( ** o )). count ();
let l = lett . iter (). filter ( | o | tekst . contains ( ** o )). count ();
Some ( match ( t , l ) {
( 0 , 0 ) => return None ,
( t , l ) if t > l => Masseklasse ::Tung ,
( t , l ) if t == l && t > 0 => Masseklasse ::Middels ,
_ => Masseklasse ::Lett ,
})
}
fn samle_materialnavn ( fil : & Fil , e : & Entitet , ut : & mut Vec < String > , dybde : u32 ) {
if dybde > 4 {
return ;
}
if e . typ == "IFCMATERIAL" {
if let Some ( n ) = e . arg ( 0 ). and_then ( | a | a . som_tekst ()) {
ut . push ( n . to_string ());
}
return ;
}
for a in & e . args {
match a {
Arg ::Ref ( r ) => {
if let Some ( n ) = fil . hent ( * r ) {
samle_materialnavn ( fil , n , ut , dybde + 1 );
}
}
Arg ::Liste ( l ) => {
for x in l {
if let Some ( n ) = x . som_ref (). and_then ( | r | fil . hent ( r )) {
samle_materialnavn ( fil , n , ut , dybde + 1 );
}
}
}
_ => {}
}
}
}
/// Opplysninger om drift og forsyning som IFC ikke har noen fast plass til.
///
/// IFC beskriver geometri og konstruksjon, men ikke byggeår, tetthet, ventilasjon eller
/// hvordan huset varmes opp. Det er nettopp de opplysningene som skiller en god
/// energiberegning fra en middelmådig. De kan likevel følge med modellen: i Vectorworks
/// lager man et postformat, fester det på bygningen og knytter det til et IFC-egenskapssett.
/// Feltnavnene under leses fra et hvilket som helst egenskapssett på `IfcBuilding`,
/// `IfcSite` eller `IfcProject`, på norsk eller engelsk.
#[derive(Default, Debug)]
pub struct Byggdata {
pub byggeaar : Option < i32 > ,
pub kategori : Option < String > ,
pub kommune : Option < String > ,
pub n50 : Option < f64 > ,
pub ventilasjon : Option < String > ,
pub gjenvinning : Option < f64 > ,
pub varmekilde : Option < String > ,
pub oppvarmet_bra : Option < f64 > ,
}
/// Leser `Byggdata` fra egenskapssettene på bygningen, tomta eller prosjektet.
fn les_byggdata ( fil : & Fil , egenskaper : & Egenskaper ) -> ( Byggdata , Vec < String > ) {
let mut d = Byggdata ::default ();
let mut funnet = Vec ::new ();
let baerere : Vec < u64 > = [ "IFCBUILDING" , "IFCSITE" , "IFCPROJECT" ]
. iter ()
. flat_map ( | t | fil . av_type ( t ). map ( | ( r , _ ) | r ). collect ::< Vec < _ >> ())
. collect ();
let mut hent = | navn : & [ & str ] | -> Option < Arg > {
for b in & baerere {
let Some ( sett ) = egenskaper . get ( b ) else {
continue ;
};
for n in navn {
if let Some (( nokkel , v )) = sett . iter (). find ( | ( k , _ ) | {
k . eq_ignore_ascii_case ( n ) || k . replace ( ' ' , "" ). eq_ignore_ascii_case ( n )
}) {
funnet . push ( nokkel . clone ());
return Some ( v . clone ());
}
}
}
None
};
let tall = | a : Option < Arg >| -> Option < f64 > {
let a = a ? ;
a . som_tall (). or_else ( || {
a . som_tekst ()
. and_then ( | s | s . replace ( ',' , "." ). trim (). parse (). ok ())
})
};
let tekst = | a : Option < Arg >| -> Option < String > {
a . and_then ( | x | x . som_tekst (). map ( | s | s . trim (). to_string ()))
. filter ( | s | ! s . is_empty ())
};
d . byggeaar = tall ( hent ( & [
"Byggeaar" ,
"Byggeår" ,
"YearOfConstruction" ,
"ConstructionYear" ,
]))
. map ( | v | v as i32 );
d . kategori = tekst ( hent ( & [ "Bygningskategori" , "Kategori" , "BuildingCategory" ]));
d . kommune = tekst ( hent ( & [ "Kommune" , "Municipality" , "Klimasted" ]));
d . n50 = tall ( hent ( & [
"Lekkasjetall" ,
"n50" ,
"AirPermeability" ,
"AirTightness" ,
]));
d . ventilasjon = tekst ( hent ( & [
"Ventilasjonstype" ,
"Ventilasjon" ,
"VentilationType" ,
]));
d . gjenvinning = tall ( hent ( & [
"Varmegjenvinning" ,
"Gjenvinning" ,
"HeatRecoveryEfficiency" ,
"TemperaturVirkningsgrad" ,
]));
d . varmekilde = tekst ( hent ( & [
"Varmekilde" ,
"Oppvarming" ,
"HeatingSource" ,
"HeatSource" ,
]));
d . oppvarmet_bra = tall ( hent ( & [ "OppvarmetBRA" , "Oppvarmet BRA" , "HeatedFloorArea" ]));
( d , funnet )
}
2026-09-15 15:38:04 +02:00
/// Byggets posisjon.
///
/// Georeferering gjennom `IfcMapConversion` og `IfcProjectedCRS` brukes først, siden den er
/// mest presis. Ellers brukes breddegrad og lengdegrad på `IfcSite`. Vectorworks fyller begge
/// med en standardplassering i Washington når ingenting er satt, og den velges bare når ingen
/// annen posisjon finnes, slik at den kan kjennes igjen og oversees.
fn posisjon ( fil : & Fil ) -> Option < geo ::Posisjon > {
let fra_kart = fil . av_type ( "IFCMAPCONVERSION" ). next (). and_then ( | ( _ , m ) | {
let crs = m
. arg ( 1 )
. and_then ( | a | a . som_ref ())
. and_then ( | r | fil . hent ( r )) ? ;
let navn = crs . arg ( 0 ). and_then ( | a | a . som_tekst ()) ? ;
let x = m . arg ( 2 ). and_then ( | a | a . som_tall ()) ? ;
let y = m . arg ( 3 ). and_then ( | a | a . som_tall ()) ? ;
geo ::fra_koordinatsystem ( navn , x , y )
});
let fra_tomt = fil . av_type ( "IFCSITE" ). next (). and_then ( | ( _ , e ) | {
let grad = | a : Option <& Arg >| -> Option < f64 > {
let l = a ? . som_liste ();
if l . is_empty () {
return None ;
}
let g = | i : usize | l . get ( i ). and_then ( | x | x . som_tall ()). unwrap_or ( 0.0 );
let verdi = g ( 0 ). abs () + g ( 1 ). abs () / 60.0 + g ( 2 ). abs () / 3600.0 + g ( 3 ). abs () / 3.6e9 ;
Some ( if g ( 0 ) < 0.0 { - verdi } else { verdi })
};
Some ( geo ::Posisjon {
bredde : grad ( e . arg ( 9 )) ? ,
lengde : grad ( e . arg ( 10 )) ? ,
})
});
[ fra_kart , fra_tomt ]
. into_iter ()
. flatten ()
. find ( | p | ! p . er_standardplassering ())
. or ( fra_kart )
. or ( fra_tomt )
}
/// Referansehøyden og terrenghøyden på bygningen, begge over havet, m.
fn terrenghoyder ( fil : & Fil , skala : f64 ) -> Option < ( f64 , f64 ) > {
let ( _ , e ) = fil . av_type ( "IFCBUILDING" ). next () ? ;
let referanse = e . arg ( 9 ). and_then ( | a | a . som_tall ()) ? * skala ;
let terreng = e . arg ( 10 ). and_then ( | a | a . som_tall ()) ? * skala ;
Some (( referanse , terreng ))
}
/// Hvor mye av grunnmuren som står over bakken, m.
///
/// Toppen av grunnmuren ligger ved gulvet i første etasje over kjelleren. Den står over
/// terrenget med differansen mellom etasjens høyde over havet og terrengets, begrenset til
/// kjellerens høyde.
fn over_terreng_for_kjeller (
referanse : f64 ,
terreng : f64 ,
forste_etasje : f64 ,
kjellerdybde : f64 ,
) -> f64 {
(( referanse + forste_etasje ) - terreng ). clamp ( 0.0 , kjellerdybde . max ( 0.0 ))
2026-09-14 22:36:54 +02:00
}
2026-09-14 19:16:18 +02:00
/// En bygningsdel slik den er lest av IFC-filen.
struct Del {
nr : u64 ,
typ : String ,
navn : String ,
2026-09-14 22:36:54 +02:00
etasje : Option < usize > ,
2026-09-14 19:16:18 +02:00
ytre : Option < bool > ,
u_verdi : Option < f64 > ,
plass : Option < ( f64 , f64 , f64 , f64 , f64 ) > ,
areal : f64 ,
normal : Option < [ f64 ; 3 ] > ,
}
2026-09-14 22:36:54 +02:00
/// Oversetter en fritekstverdi fra modellen til en bygningskategori.
fn kategori_av_tekst ( s : & str ) -> Option < Kategori > {
let t = s . to_lowercase ();
Some ( match t . as_str () {
x if x . contains ( "småhus" ) || x . contains ( "smaahus" ) || x . contains ( "enebolig" ) => {
Kategori ::Smaahus
}
x if x . contains ( "boligblokk" ) || x . contains ( "leilighet" ) => Kategori ::Boligblokk ,
x if x . contains ( "kontor" ) => Kategori ::Kontorbygg ,
x if x . contains ( "skole" ) => Kategori ::Skolebygg ,
x if x . contains ( "barnehage" ) => Kategori ::Barnehage ,
x if x . contains ( "hotell" ) => Kategori ::Hotell ,
x if x . contains ( "sykehus" ) => Kategori ::Sykehus ,
x if x . contains ( "sykehjem" ) => Kategori ::Sykehjem ,
x if x . contains ( "idrett" ) => Kategori ::Idrettsbygg ,
x if x . contains ( "forretning" ) || x . contains ( "butikk" ) => Kategori ::Forretningsbygg ,
x if x . contains ( "kultur" ) => Kategori ::Kulturbygg ,
x if x . contains ( "industri" ) || x . contains ( "verksted" ) => Kategori ::Industribygg ,
_ => return None ,
})
}
fn ventilasjon_av_tekst ( s : & str ) -> Option < VentilasjonType > {
let t = s . to_lowercase ();
Some ( match t . as_str () {
x if x . contains ( "balansert" ) || x . contains ( "balanced" ) || x . contains ( "mekanisk til" ) => {
VentilasjonType ::Balansert
}
x if x . contains ( "avtrekk" ) || x . contains ( "exhaust" ) => VentilasjonType ::Avtrekk ,
x if x . contains ( "naturlig" ) || x . contains ( "natural" ) => VentilasjonType ::Naturlig ,
_ => return None ,
})
}
fn varmekilde_av_tekst ( s : & str ) -> Option < VarmekildeType > {
let t = s . to_lowercase ();
Some ( match t . as_str () {
x if x . contains ( "luft-luft" ) || x . contains ( "luft/luft" ) || x . contains ( "air-air" ) => {
VarmekildeType ::LuftLuftVp
}
x if x . contains ( "luft-vann" ) || x . contains ( "luft/vann" ) => VarmekildeType ::LuftVannVp ,
x if x . contains ( "berg" )
|| x . contains ( "væske" )
|| x . contains ( "vaeske" )
|| x . contains ( "ground" ) =>
{
VarmekildeType ::VaeskeVannVp
}
x if x . contains ( "avtrekksvarmepumpe" ) => VarmekildeType ::AvtrekkVp ,
x if x . contains ( "vedovn" ) || x . contains ( "ved" ) || x . contains ( "wood" ) => {
VarmekildeType ::Vedovn
}
x if x . contains ( "pellet" ) => VarmekildeType ::Pellets ,
x if x . contains ( "fjernvarme" ) || x . contains ( "district" ) => VarmekildeType ::Fjernvarme ,
x if x . contains ( "olje" ) => VarmekildeType ::Olje ,
x if x . contains ( "gass" ) || x . contains ( "gas" ) => VarmekildeType ::Gass ,
x if x . contains ( "elkjel" ) || x . contains ( "elektrokjel" ) || x . contains ( "kjel" ) => {
VarmekildeType ::ElKjel
}
x if x . contains ( "elektr" ) || x . contains ( "panelovn" ) || x . contains ( "electric" ) => {
VarmekildeType ::DirekteEl
}
_ => return None ,
})
}
2026-09-14 19:16:18 +02:00
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.
2026-09-14 22:36:54 +02:00
/// Etasjeinndelingen som klassifiseringen trenger.
struct Etasjekontekst {
/// Nederste etasje i modellen.
nederste : Option < usize > ,
/// Etasjen under terreng, dersom modellen har en.
kjeller : Option < usize > ,
/// Laveste etasje over terreng, der etasjeskilleren mot kjelleren ligger.
forste_over : Option < usize > ,
}
fn klassifiser (
d : & Del ,
laveste : f64 ,
helning : f64 ,
e : & Etasjekontekst ,
) -> Option < ( FlateType , Mot ) > {
2026-09-14 19:16:18 +02:00
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
},
));
}
2026-09-14 22:36:54 +02:00
// Et utkraget dekke eller balkonggulv er merket som ytre og vender mot det fri.
2026-09-14 19:16:18 +02:00
if d . ytre == Some ( true ) {
return Some (( FlateType ::Gulv , Mot ::Ute ));
}
2026-09-14 22:36:54 +02:00
// Mot grunnen bare i nederste etasje. Er etasjen ukjent, brukes høyden.
let nederst = match ( d . etasje , e . nederste ) {
( Some ( a ), Some ( b )) => a == b ,
_ => d
. plass
. map ( | p | ( p . 2 - laveste ). abs () < 0.5 )
. unwrap_or ( false ),
};
if nederst || navn . contains ( "kjellergulv" ) {
return Some (( FlateType ::Gulv , Mot ::Grunn ));
}
// Dekket i laveste etasje over en kjeller er grensen mot den uoppvarmede sonen.
if e . kjeller . is_some () && d . etasje . is_some () && d . etasje == e . forste_over {
return Some (( FlateType ::Gulv , Mot ::Uoppvarmet ));
}
// Dekker lenger opp er etasjeskillere inne i bygningen.
2026-09-14 19:16:18 +02:00
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
2026-09-14 18:03:00 +02:00
}
fn navn_av ( e : & Entitet , r : u64 , standard : & str ) -> String {
match e . navn () {
Some ( n ) if ! n . trim (). is_empty () => format! ( " {n} # {r} " ),
_ => format! ( " {standard} # {r} " ),
}
}
2026-09-15 15:38:04 +02:00
/// Byggets adresse fra `IfcBuilding.BuildingAddress`, eller fra tomta.
///
/// Adresser knyttet til personer og organisasjoner er forfatterens, ikke byggets, og brukes
/// ikke. Plassholdere uten gate eller poststed, som Vectorworks setter inn, hoppes over.
2026-09-14 18:03:00 +02:00
fn adresse ( fil : & Fil ) -> Option < String > {
2026-09-15 15:38:04 +02:00
let bygning = fil
. av_type ( "IFCBUILDING" )
. filter_map ( | ( _ , e ) | e . arg ( 11 ). and_then ( | a | a . som_ref ()));
let tomt = fil
. av_type ( "IFCSITE" )
. filter_map ( | ( _ , e ) | e . arg ( 13 ). and_then ( | a | a . som_ref ()));
bygning
. chain ( tomt )
. filter_map ( | r | fil . hent ( r ))
. filter ( | e | e . typ == "IFCPOSTALADDRESS" )
. find_map ( formater_adresse )
}
/// `IfcPostalAddress` har feltene Purpose, Description, UserDefinedPurpose, InternalLocation,
/// AddressLines, PostalBox, Town, Region, PostalCode og Country.
fn formater_adresse ( e : & Entitet ) -> Option < String > {
2026-09-14 18:03:00 +02:00
let linjer : Vec < String > = e
2026-09-15 15:38:04 +02:00
. arg ( 4 )
. map ( | a | a . som_liste ())
. unwrap_or ( & [])
2026-09-14 18:03:00 +02:00
. iter ()
2026-09-15 15:38:04 +02:00
. filter_map ( | a | a . som_tekst ())
. map ( | s | s . trim (). to_string ())
. filter ( | s | ! s . is_empty ())
2026-09-14 18:03:00 +02:00
. collect ();
2026-09-15 15:38:04 +02:00
let postnummer = e . arg ( 8 ). and_then ( | a | a . som_tekst ()). unwrap_or ( "" ). trim ();
let sted = e . arg ( 6 ). and_then ( | a | a . som_tekst ()). unwrap_or ( "" ). trim ();
if linjer . is_empty () && sted . is_empty () {
return None ;
}
let poststed : Vec <& str > = [ postnummer , sted ]
. into_iter ()
. filter ( | s | ! s . is_empty ())
. collect ();
Some (
linjer
. into_iter ()
. chain (( ! poststed . is_empty ()). then ( || poststed . join ( " " )))
. collect ::< Vec < _ >> ()
. join ( ", " ),
)
}
/// Forfatteren av modellen, som person og organisasjon.
///
/// I en modell laget for energirådgivning er det energirådgiveren. Vectorworks' egen
/// organisasjon, som står når ingenting er fylt inn, tas ikke med.
fn forfatter ( fil : & Fil ) -> Option < String > {
let ( _ , po ) = fil . av_type ( "IFCPERSONANDORGANIZATION" ). next () ? ;
let person = po
. arg ( 0 )
. and_then ( | a | a . som_ref ())
. and_then ( | r | fil . hent ( r ));
let organisasjon = po
. arg ( 1 )
. and_then ( | a | a . som_ref ())
. and_then ( | r | fil . hent ( r ));
// IfcPerson: Identification, FamilyName, GivenName, …
let personnavn = person
. map ( | p | {
[ p . arg ( 2 ), p . arg ( 1 )]
. into_iter ()
. flatten ()
. filter_map ( | a | a . som_tekst ())
. map ( str ::trim )
. filter ( | s | ! s . is_empty ())
. collect ::< Vec < _ >> ()
. join ( " " )
})
. filter ( | s | ! s . is_empty ());
// IfcOrganization: Identification, Name, …
let organisasjonsnavn = organisasjon
. and_then ( | o | o . arg ( 1 ). and_then ( | a | a . som_tekst ()))
. map ( str ::trim )
. filter ( | s | ! s . is_empty () && ! s . to_lowercase (). contains ( "vectorworks" ))
. map ( str ::to_string );
match ( personnavn , organisasjonsnavn ) {
( Some ( p ), Some ( o )) => Some ( format! ( " {p} , {o} " )),
( Some ( p ), None ) => Some ( p ),
( None , Some ( o )) => Some ( o ),
( None , None ) => None ,
2026-09-14 18:03:00 +02:00
}
}
/// Prosjektets dreining mot sant nord, i grader med klokka.
fn sant_nord ( fil : & Fil ) -> f64 {
for ( _ , e ) in fil . av_type ( "IFCGEOMETRICREPRESENTATIONCONTEXT" ) {
if let Some ( r ) = e . arg ( 5 ). and_then ( | a | a . som_ref ()) {
if let Some ( d ) = fil . hent ( r ) {
let v = d . arg ( 0 ). map ( | a | a . som_liste ()). unwrap_or ( & []);
if v . len () >= 2 {
let ( x , y ) = (
v [ 0 ]. som_tall (). unwrap_or ( 0.0 ),
v [ 1 ]. som_tall (). unwrap_or ( 1.0 ),
);
// TrueNorth peker mot nord i prosjektets koordinater.
return x . atan2 ( y ). to_degrees ();
}
}
}
}
0.0
}
/// Sammensatt global plassering: (x, y, z, dx, dy) der (dx, dy) er lokal x-akse i planet.
fn global_plassering ( fil : & Fil , r : u64 , skala : f64 ) -> Option < ( f64 , f64 , f64 , f64 , f64 ) > {
let mut x = 0.0 ;
let mut y = 0.0 ;
let mut z = 0.0 ;
let mut dx = 1.0 ;
let mut dy = 0.0 ;
let mut kjede = Vec ::new ();
let mut gjeldende = Some ( r );
let mut vakt = 0 ;
while let Some ( p ) = gjeldende {
vakt += 1 ;
if vakt > 64 {
break ;
}
let e = fil . hent ( p ) ? ;
if e . typ != "IFCLOCALPLACEMENT" {
break ;
}
kjede . push ( p );
gjeldende = e . arg ( 0 ). and_then ( | a | a . som_ref ());
}
for p in kjede . into_iter (). rev () {
let e = fil . hent ( p ) ? ;
let ax = e
. arg ( 1 )
. and_then ( | a | a . som_ref ())
. and_then ( | a | fil . hent ( a )) ? ;
let ( lx , ly , lz ) = punkt ( fil , ax . arg ( 0 ). and_then ( | a | a . som_ref ()), skala );
let ( rx , ry ) = retning ( fil , ax . arg ( 2 ). and_then ( | a | a . som_ref ())). unwrap_or (( 1.0 , 0.0 ));
// Roter det lokale punktet med foreldrenes akse og legg det til.
let nx = x + lx * dx - ly * dy ;
let ny = y + lx * dy + ly * dx ;
x = nx ;
y = ny ;
z += lz ;
let ndx = dx * rx - dy * ry ;
let ndy = dy * rx + dx * ry ;
let lengde = ( ndx * ndx + ndy * ndy ). sqrt (). max ( 1e-12 );
dx = ndx / lengde ;
dy = ndy / lengde ;
}
Some (( x , y , z , dx , dy ))
}
fn punkt ( fil : & Fil , r : Option < u64 > , skala : f64 ) -> ( f64 , f64 , f64 ) {
let Some ( e ) = r . and_then ( | r | fil . hent ( r )) else {
return ( 0.0 , 0.0 , 0.0 );
};
let v = e . arg ( 0 ). map ( | a | a . som_liste ()). unwrap_or ( & []);
let g = | i : usize | v . get ( i ). and_then ( | a | a . som_tall ()). unwrap_or ( 0.0 ) * skala ;
( g ( 0 ), g ( 1 ), g ( 2 ))
}
fn retning ( fil : & Fil , r : Option < u64 > ) -> Option < ( f64 , f64 ) > {
let e = r . and_then ( | r | fil . hent ( r )) ? ;
let v = e . arg ( 0 ). map ( | a | a . som_liste ()). unwrap_or ( & []);
if v . len () < 2 {
return None ;
}
Some ((
v [ 0 ]. som_tall (). unwrap_or ( 1.0 ),
v [ 1 ]. som_tall (). unwrap_or ( 0.0 ),
))
}
type Egenskaper = HashMap < u64 , HashMap < String , Arg >> ;
/// Samler alle egenskaper fra `IfcRelDefinesByProperties` per objekt.
fn samle_egenskaper ( fil : & Fil ) -> Egenskaper {
let mut ut : Egenskaper = HashMap ::new ();
for ( _ , rel ) in fil . av_type ( "IFCRELDEFINESBYPROPERTIES" ) {
let objekter = rel . arg ( 4 ). map ( | a | a . som_liste ()). unwrap_or ( & []);
let Some ( def ) = rel
. arg ( 5 )
. and_then ( | a | a . som_ref ())
. and_then ( | r | fil . hent ( r ))
else {
continue ;
};
if def . typ != "IFCPROPERTYSET" {
continue ;
}
let mut verdier = HashMap ::new ();
for p in def . arg ( 4 ). map ( | a | a . som_liste ()). unwrap_or ( & []) {
let Some ( pe ) = p . som_ref (). and_then ( | r | fil . hent ( r )) else {
continue ;
};
if pe . typ != "IFCPROPERTYSINGLEVALUE" {
continue ;
}
let ( Some ( navn ), Some ( v )) = ( pe . arg ( 0 ). and_then ( | a | a . som_tekst ()), pe . arg ( 2 )) else {
continue ;
};
verdier . insert ( navn . to_string (), v . clone ());
}
for o in objekter {
if let Some ( r ) = o . som_ref () {
ut . entry ( r ). or_default (). extend ( verdier . clone ());
}
}
}
// Typeobjektenes egenskaper arves av forekomstene.
let mut arv : Vec < ( u64 , HashMap < String , Arg > ) > = Vec ::new ();
for ( _ , rel ) in fil . av_type ( "IFCRELDEFINESBYTYPE" ) {
let Some ( typ ) = rel . arg ( 5 ). and_then ( | a | a . som_ref ()) else {
continue ;
};
let Some ( verdier ) = ut . get ( & typ ). cloned () else {
continue ;
};
for o in rel . arg ( 4 ). map ( | a | a . som_liste ()). unwrap_or ( & []) {
if let Some ( r ) = o . som_ref () {
arv . push (( r , verdier . clone ()));
}
}
}
for ( r , verdier ) in arv {
let e = ut . entry ( r ). or_default ();
for ( k , v ) in verdier {
e . entry ( k ). or_insert ( v );
}
}
ut
}
/// Samler alle mengder fra `IfcElementQuantity` per objekt.
fn samle_mengder ( fil : & Fil ) -> HashMap < u64 , HashMap < String , f64 >> {
let mut ut : HashMap < u64 , HashMap < String , f64 >> = HashMap ::new ();
for ( _ , rel ) in fil . av_type ( "IFCRELDEFINESBYPROPERTIES" ) {
let objekter = rel . arg ( 4 ). map ( | a | a . som_liste ()). unwrap_or ( & []);
let Some ( def ) = rel
. arg ( 5 )
. and_then ( | a | a . som_ref ())
. and_then ( | r | fil . hent ( r ))
else {
continue ;
};
if def . typ != "IFCELEMENTQUANTITY" {
continue ;
}
let mut verdier = HashMap ::new ();
for q in def . arg ( 5 ). map ( | a | a . som_liste ()). unwrap_or ( & []) {
let Some ( qe ) = q . som_ref (). and_then ( | r | fil . hent ( r )) else {
continue ;
};
let Some ( navn ) = qe . arg ( 0 ). and_then ( | a | a . som_tekst ()) else {
continue ;
};
if let Some ( v ) = qe . arg ( 3 ). and_then ( | a | a . som_tall ()) {
verdier . insert ( navn . to_string (), v );
}
}
for o in objekter {
if let Some ( r ) = o . som_ref () {
ut . entry ( r ). or_default (). extend ( verdier . clone ());
}
}
}
ut
}
/// Kobler vinduer og dører til veggen de sitter i, via åpningene.
fn samle_apninger ( fil : & Fil ) -> ( HashMap < u64 , u64 > , HashMap < u64 , u64 > ) {
// åpning → vertselement
let mut vert_for_apning : HashMap < u64 , u64 > = HashMap ::new ();
for ( _ , rel ) in fil . av_type ( "IFCRELVOIDSELEMENT" ) {
if let ( Some ( vert ), Some ( apning )) = (
rel . arg ( 4 ). and_then ( | a | a . som_ref ()),
rel . arg ( 5 ). and_then ( | a | a . som_ref ()),
) {
vert_for_apning . insert ( apning , vert );
}
}
let mut vindu = HashMap ::new ();
let mut dor = HashMap ::new ();
for ( _ , rel ) in fil . av_type ( "IFCRELFILLSELEMENT" ) {
let ( Some ( apning ), Some ( element )) = (
rel . arg ( 4 ). and_then ( | a | a . som_ref ()),
rel . arg ( 5 ). and_then ( | a | a . som_ref ()),
) else {
continue ;
};
let Some ( vert ) = vert_for_apning . get ( & apning ) else {
continue ;
};
match fil . hent ( element ). map ( | e | e . typ . as_str ()) {
Some ( "IFCDOOR" ) => {
dor . insert ( element , * vert );
}
Some ( "IFCWINDOW" ) => {
vindu . insert ( element , * vert );
}
_ => {}
}
}
( vindu , dor )
}
fn egenskap ( e : & Egenskaper , r : u64 , navn : & str ) -> Option < f64 > {
e . get ( & r ) ? . get ( navn ) ? . som_tall ()
}
fn u_verdi ( e : & Egenskaper , r : u64 ) -> Option < f64 > {
[
"ThermalTransmittance" ,
"UValue" ,
"U-Value" ,
"Varmegjennomgangskoeffisient" ,
]
. iter ()
. find_map ( | n | egenskap ( e , r , n ))
. filter ( | u | * u > 0.0 && * u < 20.0 )
}
fn er_ytre ( e : & Egenskaper , r : u64 ) -> Option < bool > {
let v = e . get ( & r ) ? . get ( "IsExternal" ) ? ;
match v {
Arg ::Enum ( s ) => Some ( s == "T" || s . eq_ignore_ascii_case ( "true" )),
Arg ::Tall ( n ) => Some ( * n != 0.0 ),
Arg ::Tekst ( s ) => Some ( s == "T" || s . eq_ignore_ascii_case ( "true" )),
_ => None ,
}
}
#[cfg(test)]
mod tester {
use super ::* ;
2026-09-14 19:16:18 +02:00
/// 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; \n HEADER;ENDSEC; \n DATA; \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; \n END-ISO-10303-21;" );
s
}
2026-09-14 18:03:00 +02:00
#[test]
2026-09-14 19:16:18 +02:00
fn leser_areal_uverdi_og_himmelretning_av_geometrien () {
let r = fra_tekst ( & prove (), "prove" ). unwrap ();
2026-09-14 18:03:00 +02:00
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 ));
2026-09-14 19:16:18 +02:00
assert_eq! ( b . flate . len (), 2 , "flater: {:?}" , b . flate );
2026-09-14 18:03:00 +02:00
for f in & b . flate {
2026-09-14 19:16:18 +02:00
// 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
);
2026-09-14 18:03:00 +02:00
assert_eq! ( f . u_verdi , Some ( 0.18 ));
2026-09-14 19:16:18 +02:00
assert_eq! ( f . mot , Mot ::Ute );
2026-09-14 18:03:00 +02:00
}
let sor = b
. flate
. iter ()
. find ( | f | f . navn . starts_with ( "Sorvegg" ))
. unwrap ();
let nord = b
. flate
. iter ()
. find ( | f | f . navn . starts_with ( "Nordvegg" ))
. unwrap ();
assert! (
( sor . orientering - 180.0 ). abs () < 1.0 ,
"sør = {}" ,
sor . orientering
);
assert! (
nord . orientering < 1.0 || nord . orientering > 359.0 ,
"nord = {}" ,
nord . orientering
);
}
2026-09-14 19:16:18 +02:00
/// Lengdeenheten skal hentes gjennom prosjektets enhetsoppsett, ikke ved å lete
/// gjennom alle enheter i filen. Prøvefilen har både millimeter og meter.
2026-09-14 18:03:00 +02:00
#[test]
2026-09-14 19:16:18 +02:00
fn lengdeenheten_hentes_fra_prosjektet () {
let fil = step ::les ( & prove ());
assert_eq! ( ifcgeom ::lengdeenhet ( & fil ), 0.001 );
}
2026-09-14 22:36:54 +02:00
/// Et postformat festet på bygningen og eksportert som IFC-egenskapssett skal leses,
/// slik at byggeår, tetthet, ventilasjon og varmekilde kommer med fra modellen.
#[test]
fn egenskapssett_paa_bygningen_leses () {
let t = prove (). replace (
"#300= IFCSPACE(" ,
"#400= IFCPROPERTYSINGLEVALUE('Byggeaar',$,IFCINTEGER(1948),$); \n\
#401= IFCPROPERTYSINGLEVALUE('Lekkasjetall',$,IFCREAL(8.0),$); \n\
#402= IFCPROPERTYSINGLEVALUE('Ventilasjonstype',$,IFCLABEL('Balansert'),$); \n\
#403= IFCPROPERTYSINGLEVALUE('Varmegjenvinning',$,IFCREAL(80.0),$); \n\
#404= IFCPROPERTYSINGLEVALUE('Varmekilde',$,IFCLABEL('Bergvarmepumpe'),$); \n\
#405= IFCPROPERTYSINGLEVALUE('Kommune',$,IFCLABEL('Indre \u{d8} stfold'),$); \n\
#406= IFCPROPERTYSET('ps2',$,'Enok',$,(#400,#401,#402,#403,#404,#405)); \n\
#407= IFCRELDEFINESBYPROPERTIES('r4',$,$,$,(#10),#406); \n\
#300= IFCSPACE(" ,
);
let r = fra_tekst ( & t , "prove" ). unwrap ();
let b = & r . bygning ;
assert_eq! ( b . bygning . byggeaar , 1948 );
assert_eq! ( b . infiltrasjon . n50 , Some ( 8.0 ));
assert_eq! ( b . ventilasjon . typ , VentilasjonType ::Balansert );
assert_eq! ( b . ventilasjon . gjenvinning , Some ( 0.8 ));
assert_eq! ( b . klima . kommune . as_deref (), Some ( "Indre Østfold" ));
assert_eq! ( b . oppvarming . kilde . len (), 1 );
assert_eq! ( b . oppvarming . kilde [ 0 ]. typ , VarmekildeType ::VaeskeVannVp );
assert! ( r . merknader . iter (). any ( | m | m . contains ( "tatt i bruk" )));
}
2026-09-15 15:38:04 +02:00
#[test]
fn eksport_uten_bygningsdeler_gir_tydelig_feil () {
let tom = "ISO-10303-21; \n HEADER;ENDSEC; \n DATA; \n\
#1= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.); \n\
#2= IFCUNITASSIGNMENT((#1)); \n\
#3= IFCPROJECT('p',$,'Prosjekt',$,$,$,$,$,#2); \n\
#10= IFCBUILDING('b',$,'Hus',$,$,$,$,$,.ELEMENT.,$,$,$); \n\
#11= IFCBUILDINGSTOREY('k',$,'Kjeller',$,$,$,$,$,.ELEMENT.,-2350.); \n\
ENDSEC; \n END-ISO-10303-21;" ;
let feil = fra_tekst ( tom , "tom" )
. err ()
. expect ( "en tom eksport skal gi feil" );
let tekst = feil . to_string ();
assert! ( tekst . contains ( "ingen bygningsdeler" ), "{tekst}" );
assert! ( tekst . contains ( "1 etasjer" ), "{tekst}" );
}
/// Adressen på firmaet som laget modellen skal ikke bli byggets adresse.
#[test]
fn adressen_hentes_fra_bygningen_og_ikke_fra_forfatteren () {
let t = prove (). replace (
"#10= IFCBUILDING('b',$,'Testhus',$,$,#20,$,$,.ELEMENT.,$,$,$);" ,
2026-09-15 15:45:08 +02:00
"#501= IFCPOSTALADDRESS($,$,$,$,('Firmaveien 1'),$,'Firmaby',$,'9999','Norge'); \n\
#502= IFCORGANIZATION('999999999','Eksempel AS',$,$,(#501)); \n\
#503= IFCPERSON($,'Nordmann','Kari',$,$,$,$,$); \n\
2026-09-15 15:38:04 +02:00
#504= IFCPERSONANDORGANIZATION(#503,#502,$); \n\
2026-09-15 15:45:08 +02:00
#500= IFCPOSTALADDRESS($,$,$,$,('Eksempelveien 12'),$,'Eksempelby',$,'1234','Norge'); \n\
2026-09-15 15:38:04 +02:00
#10= IFCBUILDING('b',$,'Testhus',$,$,#20,$,$,.ELEMENT.,$,$,#500);" ,
);
let r = fra_tekst ( & t , "prove" ). unwrap ();
assert_eq! (
r . bygning . bygning . adresse . as_deref (),
2026-09-15 15:45:08 +02:00
Some ( "Eksempelveien 12, 1234 Eksempelby" )
2026-09-15 15:38:04 +02:00
);
assert_eq! (
r . bygning . bygning . energiraadgiver . as_deref (),
2026-09-15 15:45:08 +02:00
Some ( "Kari Nordmann, Eksempel AS" )
2026-09-15 15:38:04 +02:00
);
}
#[test]
fn plassholderadresse_og_standardnavn_brukes_ikke () {
let t = prove ()
. replace (
"#10= IFCBUILDING('b',$,'Testhus',$,$,#20,$,$,.ELEMENT.,$,$,$);" ,
"#500= IFCPOSTALADDRESS($,$,$,'Vectorworks Architect',$,$,$,$,$,$); \n\
#10= IFCBUILDING('b',$,'Default Building',$,$,#20,$,$,.ELEMENT.,$,$,#500);" ,
)
. replace (
"#3= IFCPROJECT('p',$,'Prosjekt',$,$,$,$,(#40),#2);" ,
2026-09-15 15:45:08 +02:00
"#3= IFCPROJECT('p',$,'Eksempelhuset','Energianalyse',$,'Eksempelhuset','Eksisterende',(#40),#2);" ,
2026-09-15 15:38:04 +02:00
);
let r = fra_tekst ( & t , "prove" ). unwrap ();
assert_eq! ( r . bygning . bygning . adresse , None );
2026-09-15 15:45:08 +02:00
assert_eq! ( r . bygning . bygning . navn , "Eksempelhuset" );
2026-09-15 15:38:04 +02:00
}
#[test]
fn georeferering_i_utm_gir_posisjon_for_klimavalg () {
let t = prove (). replace (
"#4= IFCDIRECTION((0.,1.));" ,
"#4= IFCDIRECTION((0.,1.)); \n\
#600= IFCPROJECTEDCRS('EPSG:25832','ETRS89 / UTM zone 32N','ETRS89',$,$,$,$); \n\
#601= IFCMAPCONVERSION(#40,#600,613351.291,6611921.980,0.,1.,0.,$);" ,
);
let r = fra_tekst ( & t , "prove" ). unwrap ();
let [ b , l ] = r
. bygning
. klima
. posisjon
. expect ( "posisjon fra georefereringen" );
assert! (
( b - 59.63 ). abs () < 1e-4 && ( l - 11.01 ). abs () < 1e-4 ,
"{b}, {l}"
);
}
#[test]
fn vectorworks_standardplassering_brukes_ikke () {
let t = prove (). replace (
"#4= IFCDIRECTION((0.,1.));" ,
"#4= IFCDIRECTION((0.,1.)); \n\
#600= IFCPROJECTEDCRS('EPSG:3857',$,'WGS 84',$,$,$,$); \n\
#601= IFCMAPCONVERSION(#40,#600,-8575524.35228859,4705852.25746973,0.,1.,0.,$);" ,
);
let r = fra_tekst ( & t , "prove" ). unwrap ();
assert_eq! ( r . bygning . klima . posisjon , None );
assert! ( r . merknader . iter (). any ( | m | m . contains ( "standardplassering" )));
}
#[test]
fn grunnmur_over_terreng_av_hoydene () {
// ±0 ligger 131,2 moh., terrenget 130,6 moh. og første etasje på ±0: 0,6 m over bakken.
assert! (( over_terreng_for_kjeller ( 131.2 , 130.6 , 0.0 , 2.35 ) - 0.6 ). abs () < 1e-9 );
// Aldri mer enn kjellerhøyden, og aldri under null.
assert_eq! ( over_terreng_for_kjeller ( 131.2 , 125.0 , 0.0 , 2.35 ), 2.35 );
assert_eq! ( over_terreng_for_kjeller ( 130.0 , 131.0 , 0.0 , 2.35 ), 0.0 );
}
2026-09-14 19:16:18 +02:00
#[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" )));
2026-09-14 18:03:00 +02:00
}
}