Skip to main content

falcon_library/model3d/
ifc.rs

1//! IFC (`.ifc`, the STEP text form). Geometry comes from web-ifc, the
2//! engine Eagle's viewer uses, when falcon is built with it
3//! (`scripts/fetch-web-ifc.sh`; see [`super::ifc_native`]): extruded,
4//! revolved and swept solids, booleans and openings, CSG primitives,
5//! advanced breps and everything tessellated.
6//!
7//! Without web-ifc, or when it cannot read a file, falcon's own reader
8//! below draws the tessellated part: `IfcTriangulatedFaceSet`,
9//! `IfcPolygonalFaceSet` (faces with holes) and faceted breps
10//! (`IfcFacetedBrep`, shells, `IfcFace` with `IfcPolyLoop` bounds), also
11//! inside shell- and face-based surface models. Products are placed
12//! through `IfcProductDefinitionShape`, their shape representations (Body
13//! first), `IfcMappedItem` (representation map origin and transformation
14//! operator) and the `IfcLocalPlacement` chain. Lengths are converted to
15//! metres from the project's length unit and the model is turned from
16//! IFC's Z-up to the viewer's Y-up. Colours come from `IfcStyledItem`
17//! surface styles on the geometry, else from the product's (or its
18//! type's) material style, else from a default per entity type. Swept
19//! solids, CSG, booleans and advanced breps are not read by it: such
20//! products are counted as skipped, and a file with nothing drawable is
21//! an error ([`NO_GEOMETRY`]), so the item keeps its QuickLook still.
22
23use std::collections::{HashMap, HashSet};
24
25use super::earclip::{self, P3};
26use super::math::V3;
27use super::step::{self, Entity, StepFile, Value};
28use super::{DEFAULT_COLOR, Material, Mesh, Model, ParseError, Result, Surface};
29
30/// The reason given for a file with nothing falcon can draw.
31pub const NO_GEOMETRY: &str = "IFC: no tessellated geometry";
32
33/// Whether falcon reads IFC geometry through web-ifc (every solid kind)
34/// rather than only the tessellated part.
35pub fn full_geometry() -> bool {
36    super::ifc_native::available()
37}
38
39/// Files larger than this are not read (the whole instance graph is held
40/// in memory).
41pub const MAX_BYTES: usize = 256 << 20;
42
43/// How deeply mapped items may nest (also stops cycles).
44const MAX_MAP_DEPTH: usize = 8;
45
46/// A placed model this far from the origin (in metres) is moved to it, so
47/// georeferenced coordinates keep their precision as `f32`.
48const RECENTRE_BEYOND: f64 = 10_000.0;
49
50/// What reading a file found, for the inspector.
51#[derive(Debug, Clone, Default, PartialEq)]
52pub struct Report {
53    /// The schema named in the header, e.g. `IFC4`.
54    pub schema: String,
55    /// Products drawn (with at least one triangle).
56    pub shown: usize,
57    /// Products with a shape that has no tessellated geometry.
58    pub skipped: usize,
59    pub triangles: usize,
60    /// The geometry types that were not read, by display name, most
61    /// common first.
62    pub unsupported: Vec<(String, usize)>,
63    /// web-ifc is built in but could not read this file, so falcon's own
64    /// tessellated reader made this report.
65    pub fallback: bool,
66}
67
68/// Reads an IFC file as a model.
69pub fn parse(bytes: &[u8]) -> Result<Model> {
70    let (model, _) = read(bytes)?;
71    model.ok_or_else(|| ParseError::new(NO_GEOMETRY))
72}
73
74/// Reads the model and the report; the model is `None` when nothing could
75/// be drawn. Only unreadable files are errors.
76pub fn read(bytes: &[u8]) -> Result<(Option<Model>, Report)> {
77    if bytes.len() > MAX_BYTES {
78        return Err(ParseError::new("IFC: the file is too large"));
79    }
80    let file = step::parse(bytes)?;
81    if let Some(r) = super::ifc_native::read(&file, bytes)? {
82        return Ok(r);
83    }
84    let (model, mut report) = Reader::new(&file).run();
85    report.fallback = full_geometry();
86    Ok((model, report))
87}
88
89/// Reads the file with falcon's own (tessellated-only) reader, never
90/// through web-ifc.
91pub fn read_tessellated(bytes: &[u8]) -> Result<(Option<Model>, Report)> {
92    if bytes.len() > MAX_BYTES {
93        return Err(ParseError::new("IFC: the file is too large"));
94    }
95    let file = step::parse(bytes)?;
96    Ok(Reader::new(&file).run())
97}
98
99/// The report for the file at `path`, without keeping the model.
100pub fn summary(path: &std::path::Path) -> Result<Report> {
101    let bytes = std::fs::read(path).map_err(ParseError::new)?;
102    read(&bytes).map(|(_, r)| r)
103}
104
105/// Entity types never drawn: voids, spaces and helpers that would hide
106/// the building.
107pub(super) const HIDDEN: &[&str] = &[
108    "IFCOPENINGELEMENT",
109    "IFCOPENINGSTANDARDCASE",
110    "IFCVOIDINGFEATURE",
111    "IFCSPACE",
112    "IFCSPATIALZONE",
113    "IFCVIRTUALELEMENT",
114    "IFCANNOTATION",
115    "IFCGRID",
116];
117
118/// Display names and default colours for common entity types.
119const TYPES: &[(&str, &str, Option<V3>)] = &[
120    ("IFCWALL", "IfcWall", Some([0.92, 0.90, 0.85])),
121    ("IFCWALLSTANDARDCASE", "IfcWallStandardCase", Some([0.92, 0.90, 0.85])),
122    ("IFCWALLELEMENTEDCASE", "IfcWallElementedCase", Some([0.92, 0.90, 0.85])),
123    ("IFCCURTAINWALL", "IfcCurtainWall", Some([0.55, 0.70, 0.80])),
124    ("IFCSLAB", "IfcSlab", Some([0.68, 0.68, 0.68])),
125    ("IFCSLABSTANDARDCASE", "IfcSlabStandardCase", Some([0.68, 0.68, 0.68])),
126    ("IFCSLABELEMENTEDCASE", "IfcSlabElementedCase", Some([0.68, 0.68, 0.68])),
127    ("IFCROOF", "IfcRoof", Some([0.64, 0.36, 0.30])),
128    ("IFCWINDOW", "IfcWindow", Some([0.55, 0.75, 0.92])),
129    ("IFCWINDOWSTANDARDCASE", "IfcWindowStandardCase", Some([0.55, 0.75, 0.92])),
130    ("IFCDOOR", "IfcDoor", Some([0.60, 0.42, 0.28])),
131    ("IFCDOORSTANDARDCASE", "IfcDoorStandardCase", Some([0.60, 0.42, 0.28])),
132    ("IFCPLATE", "IfcPlate", Some([0.62, 0.80, 0.90])),
133    ("IFCCOLUMN", "IfcColumn", Some([0.62, 0.65, 0.72])),
134    ("IFCCOLUMNSTANDARDCASE", "IfcColumnStandardCase", Some([0.62, 0.65, 0.72])),
135    ("IFCBEAM", "IfcBeam", Some([0.56, 0.60, 0.70])),
136    ("IFCBEAMSTANDARDCASE", "IfcBeamStandardCase", Some([0.56, 0.60, 0.70])),
137    ("IFCMEMBER", "IfcMember", Some([0.50, 0.55, 0.62])),
138    ("IFCMEMBERSTANDARDCASE", "IfcMemberStandardCase", Some([0.50, 0.55, 0.62])),
139    ("IFCSTAIR", "IfcStair", Some([0.76, 0.72, 0.64])),
140    ("IFCSTAIRFLIGHT", "IfcStairFlight", Some([0.76, 0.72, 0.64])),
141    ("IFCRAMP", "IfcRamp", Some([0.76, 0.72, 0.64])),
142    ("IFCRAMPFLIGHT", "IfcRampFlight", Some([0.76, 0.72, 0.64])),
143    ("IFCRAILING", "IfcRailing", Some([0.35, 0.35, 0.38])),
144    ("IFCCOVERING", "IfcCovering", Some([0.85, 0.82, 0.74])),
145    ("IFCFOOTING", "IfcFooting", Some([0.55, 0.55, 0.52])),
146    ("IFCPILE", "IfcPile", Some([0.55, 0.55, 0.52])),
147    ("IFCFURNISHINGELEMENT", "IfcFurnishingElement", Some([0.72, 0.55, 0.38])),
148    ("IFCFURNITURE", "IfcFurniture", Some([0.72, 0.55, 0.38])),
149    ("IFCSITE", "IfcSite", Some([0.46, 0.62, 0.38])),
150    ("IFCGEOGRAPHICELEMENT", "IfcGeographicElement", Some([0.46, 0.62, 0.38])),
151    ("IFCBUILDING", "IfcBuilding", None),
152    ("IFCBUILDINGSTOREY", "IfcBuildingStorey", None),
153    ("IFCBUILDINGELEMENTPROXY", "IfcBuildingElementProxy", None),
154    ("IFCFLOWSEGMENT", "IfcFlowSegment", Some([0.55, 0.66, 0.56])),
155    ("IFCFLOWFITTING", "IfcFlowFitting", Some([0.55, 0.66, 0.56])),
156    ("IFCFLOWTERMINAL", "IfcFlowTerminal", Some([0.80, 0.80, 0.84])),
157    ("IFCPIPESEGMENT", "IfcPipeSegment", Some([0.55, 0.66, 0.56])),
158    ("IFCDUCTSEGMENT", "IfcDuctSegment", Some([0.66, 0.70, 0.74])),
159    ("IFCTRIANGULATEDFACESET", "IfcTriangulatedFaceSet", None),
160    ("IFCPOLYGONALFACESET", "IfcPolygonalFaceSet", None),
161    ("IFCFACETEDBREP", "IfcFacetedBrep", None),
162    ("IFCEXTRUDEDAREASOLID", "IfcExtrudedAreaSolid", None),
163    ("IFCEXTRUDEDAREASOLIDTAPERED", "IfcExtrudedAreaSolidTapered", None),
164    ("IFCREVOLVEDAREASOLID", "IfcRevolvedAreaSolid", None),
165    ("IFCSWEPTDISKSOLID", "IfcSweptDiskSolid", None),
166    ("IFCSURFACECURVESWEPTAREASOLID", "IfcSurfaceCurveSweptAreaSolid", None),
167    ("IFCFIXEDREFERENCESWEPTAREASOLID", "IfcFixedReferenceSweptAreaSolid", None),
168    ("IFCBOOLEANRESULT", "IfcBooleanResult", None),
169    ("IFCBOOLEANCLIPPINGRESULT", "IfcBooleanClippingResult", None),
170    ("IFCADVANCEDBREP", "IfcAdvancedBrep", None),
171    ("IFCADVANCEDBREPWITHVOIDS", "IfcAdvancedBrepWithVoids", None),
172    ("IFCBSPLINESURFACEWITHKNOTS", "IfcBSplineSurfaceWithKnots", None),
173    ("IFCCSGSOLID", "IfcCsgSolid", None),
174    ("IFCBLOCK", "IfcBlock", None),
175    ("IFCSPHERE", "IfcSphere", None),
176    ("IFCHALFSPACESOLID", "IfcHalfSpaceSolid", None),
177    ("IFCPOLYGONALBOUNDEDHALFSPACE", "IfcPolygonalBoundedHalfSpace", None),
178    ("IFCBOUNDINGBOX", "IfcBoundingBox", None),
179    ("IFCPOLYLINE", "IfcPolyline", None),
180    ("IFCGEOMETRICCURVESET", "IfcGeometricCurveSet", None),
181    ("IFCGEOMETRICSET", "IfcGeometricSet", None),
182    ("IFCINDEXEDPOLYCURVE", "IfcIndexedPolyCurve", None),
183];
184
185/// An entity name as IFC writes it (`IFCWALL` → `IfcWall`); names not in
186/// the table keep only their first letter after `Ifc` in capitals.
187pub fn display_name(upper: &str) -> String {
188    if let Some(t) = TYPES.iter().find(|t| t.0 == upper) {
189        return t.1.to_string();
190    }
191    match upper.strip_prefix("IFC") {
192        Some(rest) if !rest.is_empty() => {
193            let lower = rest.to_ascii_lowercase();
194            format!("Ifc{}{}", lower[..1].to_ascii_uppercase(), &lower[1..])
195        }
196        _ => upper.to_string(),
197    }
198}
199
200/// How a shape representation ranks by its identifier: Body first, then
201/// Facetation, then anything else; `None` for the ones that are never
202/// drawn (axis, footprint, bounding box, annotation and the like).
203pub(super) fn rep_rank(identifier: &str) -> Option<u8> {
204    match identifier.to_ascii_lowercase().as_str() {
205        "body" => Some(0),
206        "facetation" => Some(1),
207        "axis" | "footprint" | "box" | "annotation" | "profile" | "clearance" | "cog" | "lighting" | "reference" | "plan" => None,
208        _ => Some(2),
209    }
210}
211
212fn default_color(upper: &str) -> V3 {
213    TYPES.iter().find(|t| t.0 == upper).and_then(|t| t.2).unwrap_or(DEFAULT_COLOR)
214}
215
216fn sub(a: P3, b: P3) -> P3 {
217    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
218}
219
220fn dot(a: P3, b: P3) -> f64 {
221    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
222}
223
224fn cross(a: P3, b: P3) -> P3 {
225    [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
226}
227
228fn normalize(a: P3) -> Option<P3> {
229    let l = dot(a, a).sqrt();
230    (l > 1e-12 && l.is_finite()).then(|| [a[0] / l, a[1] / l, a[2] / l])
231}
232
233/// An affine transform: three rows of a 4×4 matrix.
234#[derive(Debug, Clone, Copy, PartialEq)]
235struct Xf([[f64; 4]; 3]);
236
237const IDENTITY: Xf = Xf([[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0]]);
238
239impl Xf {
240    /// The frame with axes `x`, `y`, `z` (as columns) at `o`.
241    fn from_axes(o: P3, x: P3, y: P3, z: P3) -> Xf {
242        Xf([[x[0], y[0], z[0], o[0]], [x[1], y[1], z[1], o[1]], [x[2], y[2], z[2], o[2]]])
243    }
244
245    /// A right-handed frame at `o` with Z along `z` and X as close to `x`
246    /// as possible.
247    fn frame(o: P3, z: Option<P3>, x: Option<P3>) -> Xf {
248        let z = z.and_then(normalize).unwrap_or([0.0, 0.0, 1.0]);
249        let x0 = x.and_then(normalize).unwrap_or([1.0, 0.0, 0.0]);
250        let x = normalize(sub(x0, scale(z, dot(x0, z))))
251            .or_else(|| normalize(sub([1.0, 0.0, 0.0], scale(z, z[0]))))
252            .or_else(|| normalize(sub([0.0, 1.0, 0.0], scale(z, z[1]))))
253            .unwrap_or([1.0, 0.0, 0.0]);
254        let y = cross(z, x);
255        Xf::from_axes(o, x, y, z)
256    }
257
258    /// `self` after `inner`: `inner` is applied first.
259    fn then(&self, inner: &Xf) -> Xf {
260        let (a, b) = (&self.0, &inner.0);
261        let mut m = [[0.0; 4]; 3];
262        for r in 0..3 {
263            for c in 0..4 {
264                m[r][c] = (0..3).map(|k| a[r][k] * b[k][c]).sum::<f64>() + if c == 3 { a[r][3] } else { 0.0 };
265            }
266        }
267        Xf(m)
268    }
269
270    fn point(&self, p: P3) -> P3 {
271        let m = &self.0;
272        [0, 1, 2].map(|r| m[r][0] * p[0] + m[r][1] * p[1] + m[r][2] * p[2] + m[r][3])
273    }
274
275    fn vector(&self, v: P3) -> P3 {
276        let m = &self.0;
277        [0, 1, 2].map(|r| m[r][0] * v[0] + m[r][1] * v[1] + m[r][2] * v[2])
278    }
279}
280
281fn scale(a: P3, s: f64) -> P3 {
282    [a[0] * s, a[1] * s, a[2] * s]
283}
284
285/// A list of two or three numbers as a point (2D points get Z = 0).
286fn coords(v: &Value) -> Option<P3> {
287    let l = v.as_list()?;
288    let n = |k: usize| l.get(k).and_then(Value::as_f64);
289    let p = [n(0)?, n(1)?, n(2).unwrap_or(0.0)];
290    p.iter().all(|c| c.is_finite()).then_some(p)
291}
292
293/// Triangles and vertices built for one product, in file units and world
294/// (IFC) axes, with the material they are drawn in.
295#[derive(Default)]
296struct Piece {
297    material: Option<Material>,
298    positions: Vec<P3>,
299    normals: Option<Vec<P3>>,
300    indices: Vec<u32>,
301}
302
303impl Piece {
304    fn new(material: Option<Material>) -> Piece {
305        Piece { material, ..Default::default() }
306    }
307}
308
309/// What meshes are grouped by: entity type, material name and colour.
310type GroupKey = (String, String, [u32; 3]);
311
312struct Reader<'a> {
313    f: &'a StepFile,
314    /// World transforms of local placements, by id.
315    placements: HashMap<u64, Xf>,
316    /// Surface styles by the id of the geometry they are attached to.
317    item_styles: HashMap<u64, Material>,
318    /// Material styles by product (or type) id.
319    product_styles: HashMap<u64, Material>,
320    /// Points of `IfcCartesianPoint` by id.
321    points: HashMap<u64, Option<P3>>,
322}
323
324impl<'a> Reader<'a> {
325    fn new(f: &'a StepFile) -> Reader<'a> {
326        let mut r = Reader { f, placements: HashMap::new(), item_styles: HashMap::new(), product_styles: HashMap::new(), points: HashMap::new() };
327        r.collect_styles();
328        r
329    }
330
331    fn entities_named(&self, name: &'a str) -> impl Iterator<Item = (u64, &'a Entity)> + 'a {
332        self.f.entities.iter().filter(move |(_, e)| e.name == name).map(|(&id, e)| (id, e))
333    }
334
335    fn point(&mut self, v: &Value) -> Option<P3> {
336        let id = v.as_ref_id()?;
337        if let Some(p) = self.points.get(&id) {
338            return *p;
339        }
340        let p = self.f.deref_named(v, "IFCCARTESIANPOINT").and_then(|e| coords(e.arg(0)));
341        self.points.insert(id, p);
342        p
343    }
344
345    fn direction(&self, v: &Value) -> Option<P3> {
346        self.f.deref_named(v, "IFCDIRECTION").and_then(|e| coords(e.arg(0)))
347    }
348
349    /// An `IfcAxis2Placement3D` (or 2D) as a transform.
350    fn axis_placement(&mut self, v: &Value) -> Xf {
351        let Some(e) = self.f.deref(v) else { return IDENTITY };
352        match e.name.as_str() {
353            "IFCAXIS2PLACEMENT3D" => {
354                let o = self.point(e.arg(0)).unwrap_or([0.0; 3]);
355                Xf::frame(o, self.direction(e.arg(1)), self.direction(e.arg(2)))
356            }
357            "IFCAXIS2PLACEMENT2D" => {
358                let o = self.point(e.arg(0)).unwrap_or([0.0; 3]);
359                Xf::frame(o, None, self.direction(e.arg(1)))
360            }
361            "IFCAXIS1PLACEMENT" => {
362                let o = self.point(e.arg(0)).unwrap_or([0.0; 3]);
363                Xf::frame(o, self.direction(e.arg(1)), None)
364            }
365            _ => IDENTITY,
366        }
367    }
368
369    /// The world transform of an `IfcLocalPlacement`, composed through its
370    /// `PlacementRelTo` chain.
371    fn placement(&mut self, v: &Value) -> Xf {
372        let mut chain = Vec::new();
373        let mut seen = HashSet::new();
374        let mut base = IDENTITY;
375        let mut cur = v.as_ref_id();
376        while let Some(id) = cur {
377            if let Some(x) = self.placements.get(&id) {
378                base = *x;
379                break;
380            }
381            let Some(e) = self.f.get(id).filter(|e| e.name == "IFCLOCALPLACEMENT") else { break };
382            if !seen.insert(id) {
383                break;
384            }
385            chain.push(id);
386            cur = e.arg(0).as_ref_id();
387        }
388        let mut x = base;
389        for id in chain.into_iter().rev() {
390            let e = self.f.get(id).expect("found above");
391            x = x.then(&self.axis_placement(e.arg(1)));
392            self.placements.insert(id, x);
393        }
394        x
395    }
396
397    /// An `IfcCartesianTransformationOperator3D` (uniform or not, or 2D)
398    /// as a transform.
399    fn operator(&mut self, v: &Value) -> Xf {
400        let Some(e) = self.f.deref(v) else { return IDENTITY };
401        if !e.name.starts_with("IFCCARTESIANTRANSFORMATIONOPERATOR") {
402            return IDENTITY;
403        }
404        let three_d = e.name.contains("3D");
405        let o = self.point(e.arg(2)).unwrap_or([0.0; 3]);
406        let s = e.arg(3).as_f64().unwrap_or(1.0);
407        let z = if three_d { self.direction(e.arg(4)) } else { None };
408        let mut x = Xf::frame(o, z, self.direction(e.arg(0)));
409        let (s2, s3) = match e.name.as_str() {
410            "IFCCARTESIANTRANSFORMATIONOPERATOR3DNONUNIFORM" => (e.arg(5).as_f64().unwrap_or(s), e.arg(6).as_f64().unwrap_or(s)),
411            "IFCCARTESIANTRANSFORMATIONOPERATOR2DNONUNIFORM" => (e.arg(4).as_f64().unwrap_or(s), s),
412            _ => (s, s),
413        };
414        for row in &mut x.0 {
415            row[0] *= s;
416            row[1] *= s2;
417            row[2] *= s3;
418        }
419        x
420    }
421
422    /// The colour of a list of presentation styles (or one style).
423    fn style_material(&self, v: &Value, depth: usize) -> Option<Material> {
424        if depth > 4 {
425            return None;
426        }
427        let refs: Vec<&Value> = match v {
428            Value::List(l) => l.iter().collect(),
429            other => vec![other],
430        };
431        for r in refs {
432            let Some(e) = self.f.deref(r) else { continue };
433            match e.name.as_str() {
434                "IFCPRESENTATIONSTYLEASSIGNMENT" => {
435                    if let Some(m) = self.style_material(e.arg(0), depth + 1) {
436                        return Some(m);
437                    }
438                }
439                "IFCSURFACESTYLE" => {
440                    let name = e.arg(0).as_str().unwrap_or("").to_string();
441                    for s in e.arg(2).as_list().unwrap_or_default() {
442                        let Some(se) = self.f.deref(s) else { continue };
443                        if se.name != "IFCSURFACESTYLERENDERING" && se.name != "IFCSURFACESTYLESHADING" {
444                            continue;
445                        }
446                        let Some(c) = self.f.deref_named(se.arg(0), "IFCCOLOURRGB") else { continue };
447                        let ch = |k: usize| c.arg(k).as_f64().unwrap_or(0.8).clamp(0.0, 1.0) as f32;
448                        let transparency = se.arg(1).as_f64().filter(|t| t.is_finite()).unwrap_or(0.0).clamp(0.0, 1.0) as f32;
449                        return Some(Material { name, color: [ch(1), ch(2), ch(3)], opacity: 1.0 - transparency });
450                    }
451                }
452                _ => {}
453            }
454        }
455        None
456    }
457
458    /// Styles on geometry, and material styles of products and types.
459    fn collect_styles(&mut self) {
460        for (_, e) in self.entities_named("IFCSTYLEDITEM") {
461            if let (Some(item), Some(m)) = (e.arg(0).as_ref_id(), self.style_material(e.arg(1), 0)) {
462                self.item_styles.entry(item).or_insert(m);
463            }
464        }
465        // IfcMaterial → its style, through its definition representation.
466        let mut material_styles: HashMap<u64, Material> = HashMap::new();
467        for (_, e) in self.entities_named("IFCMATERIALDEFINITIONREPRESENTATION") {
468            let Some(mat_id) = e.arg(3).as_ref_id() else { continue };
469            let mat_name = self.f.get(mat_id).and_then(|m| m.arg(0).as_str()).unwrap_or("").to_string();
470            'reps: for rep in e.arg(2).as_list().unwrap_or_default() {
471                let Some(re) = self.f.deref(rep) else { continue };
472                for item in re.arg(3).as_list().unwrap_or_default() {
473                    let Some(si) = self.f.deref_named(item, "IFCSTYLEDITEM") else { continue };
474                    if let Some(mut m) = self.style_material(si.arg(1), 0) {
475                        if m.name.is_empty() {
476                            m.name = mat_name.clone();
477                        }
478                        material_styles.insert(mat_id, m);
479                        break 'reps;
480                    }
481                }
482            }
483        }
484        if material_styles.is_empty() {
485            return;
486        }
487        for (_, e) in self.entities_named("IFCRELASSOCIATESMATERIAL") {
488            let Some(m) = self.material_of(e.arg(5), 0).and_then(|id| material_styles.get(&id)) else { continue };
489            for o in e.arg(4).as_list().unwrap_or_default() {
490                if let Some(id) = o.as_ref_id() {
491                    self.product_styles.entry(id).or_insert_with(|| m.clone());
492                }
493            }
494        }
495        // Products take their type's material when they have none.
496        for (_, e) in self.entities_named("IFCRELDEFINESBYTYPE") {
497            let Some(m) = e.arg(5).as_ref_id().and_then(|t| self.product_styles.get(&t)).cloned() else { continue };
498            for o in e.arg(4).as_list().unwrap_or_default() {
499                if let Some(id) = o.as_ref_id() {
500                    self.product_styles.entry(id).or_insert_with(|| m.clone());
501                }
502            }
503        }
504    }
505
506    /// The first `IfcMaterial` behind a material select (layer sets,
507    /// lists, constituents, profiles).
508    fn material_of(&self, v: &Value, depth: usize) -> Option<u64> {
509        if depth > 4 {
510            return None;
511        }
512        let id = v.as_ref_id()?;
513        let e = self.f.get(id)?;
514        let first = |k: usize| e.arg(k).as_list().and_then(|l| l.first());
515        match e.name.as_str() {
516            "IFCMATERIAL" => Some(id),
517            "IFCMATERIALLAYERSETUSAGE" | "IFCMATERIALPROFILESETUSAGE" => self.material_of(e.arg(0), depth + 1),
518            "IFCMATERIALLAYERSET" | "IFCMATERIALLIST" => self.material_of(first(0)?, depth + 1),
519            "IFCMATERIALLAYER" => self.material_of(e.arg(0), depth + 1),
520            "IFCMATERIALCONSTITUENTSET" | "IFCMATERIALPROFILESET" => self.material_of(first(2)?, depth + 1),
521            "IFCMATERIALCONSTITUENT" | "IFCMATERIALPROFILE" => self.material_of(e.arg(2), depth + 1),
522            _ => None,
523        }
524    }
525
526    /// The scale from the project's length unit to metres.
527    fn length_scale(&self) -> f64 {
528        let project_units = self.entities_named("IFCPROJECT").next().and_then(|(_, p)| p.arg(8).as_ref_id());
529        let assignments: Vec<&Entity> = project_units
530            .and_then(|id| self.f.get(id))
531            .into_iter()
532            .chain(self.entities_named("IFCUNITASSIGNMENT").map(|(_, e)| e))
533            .collect();
534        for a in assignments {
535            for u in a.arg(0).as_list().unwrap_or_default() {
536                if let Some(s) = self.unit_scale(u, 0) {
537                    return s;
538                }
539            }
540        }
541        1.0
542    }
543
544    /// The size of a length unit in metres; `None` for other units.
545    fn unit_scale(&self, v: &Value, depth: usize) -> Option<f64> {
546        let e = self.f.deref(v)?;
547        if e.arg(1).as_enum() != Some("LENGTHUNIT") || depth > 4 {
548            return None;
549        }
550        match e.name.as_str() {
551            "IFCSIUNIT" => {
552                if e.arg(3).as_enum() != Some("METRE") {
553                    return None;
554                }
555                Some(match e.arg(2).as_enum() {
556                    Some("KILO") => 1e3,
557                    Some("HECTO") => 1e2,
558                    Some("DECA") => 1e1,
559                    Some("DECI") => 1e-1,
560                    Some("CENTI") => 1e-2,
561                    Some("MILLI") => 1e-3,
562                    Some("MICRO") => 1e-6,
563                    _ => 1.0,
564                })
565            }
566            "IFCCONVERSIONBASEDUNIT" | "IFCCONVERSIONBASEDUNITWITHOFFSET" => {
567                let m = self.f.deref_named(e.arg(3), "IFCMEASUREWITHUNIT")?;
568                let value = m.arg(0).as_f64()?;
569                let base = self.unit_scale(m.arg(1), depth + 1).unwrap_or(1.0);
570                (value > 0.0 && value.is_finite()).then_some(value * base)
571            }
572            _ => None,
573        }
574    }
575
576    /// `IfcCartesianPointList3D` coordinates.
577    fn point_list(&self, v: &Value) -> Option<Vec<P3>> {
578        let e = self.f.deref(v)?;
579        if !e.name.starts_with("IFCCARTESIANPOINTLIST") {
580            return None;
581        }
582        e.arg(0).as_list()?.iter().map(coords).collect()
583    }
584
585    /// Adds the triangles of one representation item to `out`, and the
586    /// geometry types it can't read to `skipped`.
587    fn item(&mut self, v: &Value, xf: &Xf, depth: usize, inherited: Option<&Material>, out: &mut Vec<Piece>, skipped: &mut Vec<String>) {
588        let Some(id) = v.as_ref_id() else { return };
589        let Some(e) = self.f.get(id) else { return };
590        let style = self.item_styles.get(&id).cloned().or_else(|| inherited.cloned());
591        match e.name.as_str() {
592            "IFCTRIANGULATEDFACESET" | "IFCTRIANGULATEDIRREGULARNETWORK" => {
593                if let Some(p) = self.triangulated(e, xf, style) {
594                    out.push(p);
595                }
596            }
597            "IFCPOLYGONALFACESET" => {
598                if let Some(p) = self.polygonal(e, xf, style) {
599                    out.push(p);
600                }
601            }
602            "IFCFACETEDBREP" | "IFCFACETEDBREPWITHVOIDS" => {
603                let mut p = Piece::new(style);
604                let mut shells = vec![e.arg(0).clone()];
605                shells.extend(e.arg(1).as_list().unwrap_or_default().iter().cloned());
606                for s in &shells {
607                    self.shell(s, xf, &mut p);
608                }
609                out.push(p);
610            }
611            "IFCCLOSEDSHELL" | "IFCOPENSHELL" | "IFCCONNECTEDFACESET" => {
612                let mut p = Piece::new(style);
613                self.shell(v, xf, &mut p);
614                out.push(p);
615            }
616            "IFCSHELLBASEDSURFACEMODEL" | "IFCFACEBASEDSURFACEMODEL" => {
617                let mut p = Piece::new(style);
618                for s in e.arg(0).as_list().unwrap_or_default() {
619                    self.shell(s, xf, &mut p);
620                }
621                out.push(p);
622            }
623            "IFCMAPPEDITEM" => {
624                if depth >= MAX_MAP_DEPTH {
625                    return;
626                }
627                let Some(map) = self.f.deref_named(e.arg(0), "IFCREPRESENTATIONMAP") else { return };
628                let origin = self.axis_placement(map.arg(0));
629                let target = self.operator(e.arg(1));
630                let inner = xf.then(&target).then(&origin);
631                let Some(rep) = self.f.deref(map.arg(1)) else { return };
632                for it in rep.arg(3).as_list().unwrap_or_default() {
633                    self.item(it, &inner, depth + 1, style.as_ref(), out, skipped);
634                }
635            }
636            other => skipped.push(display_name(other)),
637        }
638    }
639
640    /// An `IfcTriangulatedFaceSet`. The coordinate index, normals and point
641    /// index are told apart by their shape, which covers both the IFC4
642    /// and IFC4.3 attribute orders.
643    fn triangulated(&mut self, e: &Entity, xf: &Xf, style: Option<Material>) -> Option<Piece> {
644        let pts = self.point_list(e.arg(0))?;
645        fn nested(v: &Value) -> Option<&[Value]> {
646            v.as_list().filter(|l| !l.is_empty() && l.iter().all(|x| x.as_list().is_some()))
647        }
648        let mut index = None;
649        let mut normals = None;
650        let mut pn = None;
651        for a in e.args.iter().skip(1) {
652            if let Some(l) = nested(a) {
653                let ints = l.iter().all(|t| t.as_list().is_some_and(|t| t.iter().all(|x| x.as_int().is_some())));
654                if ints && index.is_none() {
655                    index = Some(l);
656                } else if !ints && normals.is_none() {
657                    normals = Some(l);
658                }
659            } else if let Some(l) = a.as_list().filter(|l| !l.is_empty() && l.iter().all(|x| x.as_int().is_some())) {
660                pn = Some(l);
661            }
662        }
663        let index = index?;
664        let lookup = |k: i64| -> Option<u32> {
665            let k = match pn {
666                Some(pn) => pn.get(usize::try_from(k - 1).ok()?)?.as_int()?,
667                None => k,
668            };
669            let k = usize::try_from(k - 1).ok()?;
670            (k < pts.len()).then_some(k as u32)
671        };
672        let mut p = Piece::new(style);
673        p.positions = pts.iter().map(|&q| xf.point(q)).collect();
674        for t in index {
675            let t = t.as_list().unwrap_or_default();
676            if t.len() < 3 {
677                continue;
678            }
679            let ids: Option<Vec<u32>> = t.iter().take(3).map(|x| x.as_int().and_then(lookup)).collect();
680            if let Some(ids) = ids {
681                p.indices.extend(ids);
682            }
683        }
684        if pn.is_none()
685            && let Some(n) = normals
686            && n.len() == pts.len()
687        {
688            let ns: Option<Vec<P3>> = n.iter().map(|v| coords(v).map(|c| xf.vector(c))).collect();
689            p.normals = ns;
690        }
691        Some(p)
692    }
693
694    /// An `IfcPolygonalFaceSet`: each indexed face (with voids) cut into
695    /// triangles over the shared points.
696    fn polygonal(&mut self, e: &Entity, xf: &Xf, style: Option<Material>) -> Option<Piece> {
697        let pts = self.point_list(e.arg(0))?;
698        let pn: Option<Vec<i64>> = e.arg(3).as_list().map(|l| l.iter().filter_map(Value::as_int).collect());
699        let resolve = |k: &Value| -> Option<usize> {
700            let k = k.as_int()?;
701            let k = match &pn {
702                Some(pn) if !pn.is_empty() => *pn.get(usize::try_from(k - 1).ok()?)?,
703                _ => k,
704            };
705            let k = usize::try_from(k - 1).ok()?;
706            (k < pts.len()).then_some(k)
707        };
708        let loop_of = |v: &Value| -> Option<Vec<usize>> { v.as_list()?.iter().map(resolve).collect() };
709        let mut p = Piece::new(style);
710        p.positions = pts.iter().map(|&q| xf.point(q)).collect();
711        for f in e.arg(2).as_list().unwrap_or_default() {
712            let Some(fe) = self.f.deref(f) else { continue };
713            if !fe.name.starts_with("IFCINDEXEDPOLYGONALFACE") {
714                continue;
715            }
716            let Some(outer) = loop_of(fe.arg(0)) else { continue };
717            let holes: Vec<Vec<usize>> = if fe.name == "IFCINDEXEDPOLYGONALFACEWITHVOIDS" {
718                fe.arg(1).as_list().unwrap_or_default().iter().filter_map(loop_of).collect()
719            } else {
720                Vec::new()
721            };
722            let all: Vec<usize> = outer.iter().chain(holes.iter().flatten()).copied().collect();
723            let o3: Vec<P3> = outer.iter().map(|&k| pts[k]).collect();
724            let h3: Vec<Vec<P3>> = holes.iter().map(|h| h.iter().map(|&k| pts[k]).collect()).collect();
725            let hr: Vec<&[P3]> = h3.iter().map(Vec::as_slice).collect();
726            for t in earclip::triangulate_3d(&o3, &hr) {
727                p.indices.extend(t.map(|k| all[k] as u32));
728            }
729        }
730        Some(p)
731    }
732
733    /// The faces of a shell (or connected face set) into `p`.
734    fn shell(&mut self, v: &Value, xf: &Xf, p: &mut Piece) {
735        let Some(e) = self.f.deref(v) else { return };
736        if !matches!(e.name.as_str(), "IFCCLOSEDSHELL" | "IFCOPENSHELL" | "IFCCONNECTEDFACESET") {
737            return;
738        }
739        for face in e.arg(0).as_list().unwrap_or_default() {
740            self.face(face, xf, p);
741        }
742    }
743
744    /// One planar face with its poly-loop bounds, cut into triangles.
745    fn face(&mut self, v: &Value, xf: &Xf, p: &mut Piece) {
746        let Some(e) = self.f.deref(v) else { return };
747        if e.name != "IFCFACE" && e.name != "IFCFACESURFACE" {
748            return;
749        }
750        let mut outer: Option<Vec<P3>> = None;
751        let mut loops: Vec<Vec<P3>> = Vec::new();
752        for b in e.arg(0).as_list().unwrap_or_default() {
753            let Some(be) = self.f.deref(b) else { continue };
754            let Some(pl) = self.f.deref_named(be.arg(0), "IFCPOLYLOOP") else { continue };
755            let refs = pl.arg(0).as_list().unwrap_or_default();
756            let mut pts: Vec<P3> = refs.iter().filter_map(|r| self.point(r)).collect();
757            // A closing point that repeats the first.
758            if pts.len() > 3 && pts.first() == pts.last() {
759                pts.pop();
760            }
761            if pts.len() < 3 {
762                continue;
763            }
764            if be.arg(1).as_enum() == Some("F") {
765                pts.reverse();
766            }
767            if be.name == "IFCFACEOUTERBOUND" && outer.is_none() {
768                outer = Some(pts);
769            } else {
770                loops.push(pts);
771            }
772        }
773        let outer = match outer {
774            Some(o) => o,
775            None => {
776                // No outer bound marked: the largest loop is the outline.
777                let area = |l: &Vec<P3>| {
778                    let n = earclip::newell(l);
779                    dot(n, n)
780                };
781                let Some(k) = (0..loops.len()).max_by(|&a, &b| area(&loops[a]).total_cmp(&area(&loops[b]))) else { return };
782                loops.swap_remove(k)
783            }
784        };
785        let hr: Vec<&[P3]> = loops.iter().map(Vec::as_slice).collect();
786        let tris = earclip::triangulate_3d(&outer, &hr);
787        if tris.is_empty() {
788            return;
789        }
790        let base = p.positions.len() as u32;
791        p.positions.extend(outer.iter().chain(loops.iter().flatten()).map(|&q| xf.point(q)));
792        for t in tris {
793            p.indices.extend(t.map(|k| base + k as u32));
794        }
795    }
796
797    /// The triangles of a product's shape: the Body representation first,
798    /// then other drawable ones if Body has none.
799    fn product(&mut self, e: &Entity, pds: &Entity, style: Option<&Material>, skipped: &mut Vec<String>) -> Vec<Piece> {
800        let xf = self.placement(e.arg(5));
801        let mut ranked: Vec<(u8, &Entity)> = Vec::new();
802        for r in pds.arg(2).as_list().unwrap_or_default() {
803            let Some(re) = self.f.deref(r) else { continue };
804            let Some(rank) = rep_rank(re.arg(1).as_str().unwrap_or("")) else { continue };
805            ranked.push((rank, re));
806        }
807        ranked.sort_by_key(|r| r.0);
808        let mut k = 0;
809        while k < ranked.len() {
810            let rank = ranked[k].0;
811            let mut out = Vec::new();
812            while k < ranked.len() && ranked[k].0 == rank {
813                for it in ranked[k].1.arg(3).as_list().unwrap_or_default() {
814                    self.item(it, &xf, 0, style, &mut out, skipped);
815                }
816                k += 1;
817            }
818            out.retain(|p| !p.indices.is_empty());
819            if !out.is_empty() {
820                return out;
821            }
822        }
823        Vec::new()
824    }
825
826    fn run(mut self) -> (Option<Model>, Report) {
827        let f = self.f;
828        let mut report = Report { schema: f.schemas.first().cloned().unwrap_or_default(), ..Default::default() };
829        let unit = self.length_scale();
830        let mut ids: Vec<u64> = f.entities.keys().copied().collect();
831        ids.sort_unstable();
832        // Meshes in progress (with whether every piece had normals), by
833        // (type, material name, colour bits).
834        let mut groups: Vec<(GroupKey, Mesh, bool)> = Vec::new();
835        let mut where_: HashMap<GroupKey, usize> = HashMap::new();
836        let mut unsupported: HashMap<String, usize> = HashMap::new();
837        let mut world: Vec<Vec<[f64; 3]>> = Vec::new();
838        for id in ids {
839            let e = &f.entities[&id];
840            if !e.name.starts_with("IFC") || HIDDEN.contains(&e.name.as_str()) {
841                continue;
842            }
843            let Some(pds) = f.deref_named(e.arg(6), "IFCPRODUCTDEFINITIONSHAPE") else { continue };
844            let style = self.product_styles.get(&id).cloned();
845            let mut skipped = Vec::new();
846            let pieces = self.product(e, pds, style.as_ref(), &mut skipped);
847            if pieces.is_empty() {
848                report.skipped += 1;
849                for s in skipped {
850                    *unsupported.entry(s).or_default() += 1;
851                }
852                continue;
853            }
854            report.shown += 1;
855            let type_name = display_name(&e.name);
856            for piece in pieces {
857                let mat = piece.material.unwrap_or_else(|| Material { name: type_name.clone(), color: default_color(&e.name), opacity: 1.0 });
858                let key = (type_name.clone(), mat.name.clone(), mat.color.map(f32::to_bits));
859                let g = *where_.entry(key.clone()).or_insert_with(|| {
860                    let surface = Surface { blend: mat.opacity.is_finite() && (mat.opacity - 1.0).abs() > 1e-6, ..Surface::default() };
861                    groups.push((key, Mesh { name: type_name.clone(), material: Some(mat.clone()), surface, normals: Some(Vec::new()), ..Default::default() }, true));
862                    world.push(Vec::new());
863                    groups.len() - 1
864                });
865                let (_, mesh, with_normals) = &mut groups[g];
866                let base = world[g].len() as u32;
867                report.triangles += piece.indices.len() / 3;
868                mesh.indices.extend(piece.indices.iter().map(|i| base + i));
869                // Z-up to Y-up, in metres.
870                world[g].extend(piece.positions.iter().map(|q| [q[0] * unit, q[2] * unit, -q[1] * unit]));
871                match (piece.normals, mesh.normals.as_mut()) {
872                    (Some(ns), Some(out)) if *with_normals => out.extend(ns.iter().map(|n| {
873                        let n = normalize(*n).unwrap_or([0.0, 0.0, 1.0]);
874                        [n[0] as f32, n[2] as f32, -n[1] as f32]
875                    })),
876                    _ => {
877                        *with_normals = false;
878                        mesh.normals = None;
879                    }
880                }
881            }
882        }
883        let mut unsupported: Vec<(String, usize)> = unsupported.into_iter().collect();
884        unsupported.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
885        report.unsupported = unsupported;
886        if report.triangles == 0 {
887            return (None, report);
888        }
889        // Move far-off (georeferenced) models to the origin.
890        let (mut lo, mut hi) = ([f64::MAX; 3], [f64::MIN; 3]);
891        for q in world.iter().flatten() {
892            for k in 0..3 {
893                lo[k] = lo[k].min(q[k]);
894                hi[k] = hi[k].max(q[k]);
895            }
896        }
897        let centre = [0, 1, 2].map(|k| (lo[k] + hi[k]) / 2.0);
898        let shift = if centre.iter().any(|c| c.abs() > RECENTRE_BEYOND) { centre } else { [0.0; 3] };
899        let mut meshes = Vec::with_capacity(groups.len());
900        for ((_, mut mesh, _), pts) in groups.into_iter().zip(world) {
901            mesh.positions = pts.iter().map(|q| [(q[0] - shift[0]) as f32, (q[1] - shift[1]) as f32, (q[2] - shift[2]) as f32]).collect();
902            meshes.push(mesh);
903        }
904        match (Model { meshes }).finish() {
905            Ok(m) => {
906                report.triangles = m.triangle_count();
907                (Some(m), report)
908            }
909            Err(_) => {
910                report.triangles = 0;
911                (None, report)
912            }
913        }
914    }
915}
916
917#[cfg(test)]
918#[path = "ifc_tests.rs"]
919mod tests;