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 };
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 19:16:18 +02:00
// ── oppvarmet areal fra rommene ─────────────────────────────────
2026-09-14 18:03:00 +02:00
let mut bra = 0.0 ;
let mut volum = 0.0 ;
2026-09-14 19:16:18 +02:00
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 ())
2026-09-14 18:03:00 +02:00
. unwrap_or ( 0.0 );
}
}
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! (
"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 ( ", " )
));
}
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 )),
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 19:16:18 +02:00
// ── klimaskjerm ─────────────────────────────────────────────────
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 19:16:18 +02:00
// 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 );
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 19:16:18 +02:00
let Some (( typ , mot )) = klassifiser ( d , laveste , helning ) else {
utelatt . push (( d . navn . clone (), d . areal , d . u_verdi ));
2026-09-14 18:03:00 +02:00
continue ;
};
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 ;
}
flater . 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 ,
masseklasse : None ,
masseplassering : Masseplassering ::Fordelt ,
mot ,
b_faktor : None ,
absorptans : None ,
omkrets : None ,
dybde : None ,
2026-09-14 19:16:18 +02:00
over_terreng : None ,
sone : None ,
2026-09-14 18:03:00 +02:00
kommentar : Some ( "importert fra IFC" . into ()),
});
}
2026-09-14 19:16:18 +02:00
slaa_sammen ( & mut flater );
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 (),
);
let navn = fil
. av_type ( "IFCBUILDING" )
. next ()
. and_then ( | ( _ , e ) | e . navn (). map ( | s | s . to_string ()))
2026-09-14 19:16:18 +02:00
. filter ( | s | ! s . is_empty () && s != "Default Building" )
2026-09-14 18:03:00 +02:00
. unwrap_or_else ( || standardnavn . to_string ());
let bygning = Bygning {
bygning : BygningInfo {
navn ,
kategori : Kategori ::Smaahus ,
byggeaar : 2020 ,
oppvarmet_bra : bra ,
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 ()),
energiraadgiver : None ,
oppdragsgiver : None ,
},
klima : Klima ::default (),
flate : flater ,
2026-09-14 19:16:18 +02:00
sone : Vec ::new (),
grunn : Grunn ::default (),
2026-09-14 18:03:00 +02:00
vindu : vinduer ,
kuldebroer : Kuldebroer ::default (),
infiltrasjon : Infiltrasjon ::default (),
ventilasjon : Ventilasjon ::default (),
tappevann : Tappevann ::default (),
oppvarming : Oppvarming ::default (),
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 ;
}
/// 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
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} " ),
}
}
fn adresse ( fil : & Fil ) -> Option < String > {
let ( _ , e ) = fil . av_type ( "IFCPOSTALADDRESS" ). next () ? ;
let linjer : Vec < String > = e
. arg ( 4 ) ?
. som_liste ()
. iter ()
. filter_map ( | a | a . som_tekst (). map ( | s | s . to_string ()))
. collect ();
let by = e . arg ( 6 ). and_then ( | a | a . som_tekst ()). unwrap_or ( "" );
let mut ut = linjer . join ( ", " );
if ! by . is_empty () {
if ! ut . is_empty () {
ut . push_str ( ", " );
}
ut . push_str ( by );
}
( ! ut . is_empty ()). then_some ( ut )
}
/// 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 );
}
#[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
}
}