ifc-lite-processing 10.5.0

Shared IFC processing pipeline and types used by server and FFI
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! Unit tests for `element.rs` (kept in a sibling `_tests.rs` module so the
//! production file stays under the module-size ratchet). Included via
//! `#[path = "element_tests.rs"] mod tests;`.

use super::*;
// Read back through the module under test, NOT straight from core. Importing it
// from core here shadows the glob and makes the assertion below compare the
// constant with itself -- proven by mutation: a private `const
// MAX_MAPPED_ITEM_DEPTH: u32 = 5;` in element_color.rs left this test green.
use super::element_color::MAX_MAPPED_ITEM_DEPTH;

fn refs(ids: &[u32]) -> FxHashSet<u32> {
    ids.iter().copied().collect()
}

/// #4122 — `build_mesh_data` trusts `mesh.instance_meta.is_some()` as a proxy
/// for "already welded in the object frame" (see its doc). The debug_assert
/// it now carries checks that trust instead of silently relying on it: a
/// `Mesh` with `instance_meta` set but `welded_in_object_frame` still
/// `false` — exactly the shape `router::voids::probe::get_opening_item_meshes_world`
/// produces (it bakes with `transform_mesh_world_framed` directly, never
/// through `weld_mesh`/`weld_sub_mesh`) — must trip it if it were ever handed
/// to `build_mesh_data`.
#[test]
#[should_panic(expected = "welded_in_object_frame is false")]
fn build_mesh_data_asserts_instance_meta_implies_welded_in_object_frame() {
    const IFC: &str = r#"ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('m.ifc','2026-09-08T00:00:00',(''),(''),'','','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#1=IFCWALL('1234567890123456789012',$,'Wall',$,$,#20,$,$,$);
#20=IFCLOCALPLACEMENT($,#21);
#21=IFCAXIS2PLACEMENT3D(#22,$,$);
#22=IFCCARTESIANPOINT((0.,0.,0.));
ENDSEC;
END-ISO-10303-21;
"#;
    let mut decoder = EntityDecoder::new(IFC);
    let entity = decoder.decode_by_id(1).expect("wall decodes");

    let job = ElementMeshJob {
        id: 1,
        ifc_type: IfcType::IfcWall,
        entity: &entity,
        kind: ElementJobKind::Product,
        element_color: None,
        metadata: None,
    };
    let void_index = FxHashMap::default();
    let geometry_style_index = FxHashMap::default();
    let indexed_colour_full = FxHashMap::default();
    let element_material_colors = FxHashMap::default();
    let texture_index = FxHashMap::default();
    let ctx = MeshProductionContext {
        void_index: &void_index,
        geometry_style_index: &geometry_style_index,
        indexed_colour_full: &indexed_colour_full,
        element_material_colors: &element_material_colors,
        texture_index: &texture_index,
        site_local_rotation: None,
    };

    let mut mesh = ifc_lite_geometry::Mesh::new();
    mesh.positions = vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0];
    mesh.normals = vec![0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0];
    mesh.indices = vec![0, 1, 2];
    mesh.instance_meta = Some(ifc_lite_geometry::InstanceMeta {
        transform: [
            1.0, 0.0, 0.0, 0.0, //
            0.0, 1.0, 0.0, 0.0, //
            0.0, 0.0, 1.0, 0.0, //
            0.0, 0.0, 0.0, 1.0,
        ],
        local_transform: None,
        canonical_transform: None,
        rep_identity: 0,
        instanceable: true,
    });
    // Deliberately left `false`: this mesh never went through
    // `apply_placement`/`apply_submesh_placement`, matching probe.rs's shape.
    assert!(!mesh.welded_in_object_frame);

    let _ = build_mesh_data(&job, mesh, [1.0, 1.0, 1.0, 1.0], None, None, false, 0, &ctx, None);
}

#[test]
fn plan_type_geometry_orphan_type_emits_unreferenced_maps_as_class_1() {
    for mode in [TypeGeometryMode::SuppressInstanced, TypeGeometryMode::EmitTagged] {
        let planned = plan_type_geometry(&[10, 11, 12], &refs(&[11]), false, mode);
        assert_eq!(
            planned,
            vec![(10, 1), (12, 1)],
            "orphan type: unreferenced maps render as class 1 in {mode:?}",
        );
    }
}

#[test]
fn plan_type_geometry_instantiated_type_suppressed_for_export_tagged_for_viewer() {
    let suppress = plan_type_geometry(
        &[10, 11],
        &refs(&[]),
        true,
        TypeGeometryMode::SuppressInstanced,
    );
    assert!(
        suppress.is_empty(),
        "an export must never duplicate an instanced type's geometry"
    );

    let tagged =
        plan_type_geometry(&[10, 11], &refs(&[]), true, TypeGeometryMode::EmitTagged);
    assert_eq!(
        tagged,
        vec![(10, 2), (11, 2)],
        "the viewer renders instanced type maps tagged class 2 for the Types view"
    );
}

#[test]
fn plan_type_geometry_referenced_maps_never_emit() {
    let planned = plan_type_geometry(
        &[10],
        &refs(&[10]),
        false,
        TypeGeometryMode::EmitTagged,
    );
    assert!(
        planned.is_empty(),
        "a map an IfcMappedItem instantiates draws through its occurrence"
    );
}

#[test]
fn find_geometry_item_color_follows_mapped_item() {
    // #100 IfcMappedItem → #101 IfcRepresentationMap → #103
    // IfcShapeRepresentation whose Items = (#110). The style lives on the
    // underlying item #110, not on the mapped item, so a flat lookup of
    // #100 misses it — the resolver must chase the mapping (#913 §2.7).
    const IFC: &str = r#"ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('m.ifc','2026-06-04T00:00:00',(''),(''),'','','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#2=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.0E-5,$,$);
#100=IFCMAPPEDITEM(#101,#105);
#101=IFCREPRESENTATIONMAP(#102,#103);
#102=IFCAXIS2PLACEMENT3D(#104,$,$);
#103=IFCSHAPEREPRESENTATION(#2,'Body','MappedRepresentation',(#110));
#104=IFCCARTESIANPOINT((0.,0.,0.));
#105=IFCCARTESIANTRANSFORMATIONOPERATOR3D($,$,#104,$,$);
ENDSEC;
END-ISO-10303-21;
"#;
    let blue = [0.1, 0.2, 0.9, 1.0];
    let mut styles: FxHashMap<u32, GeometryStyleInfo> = FxHashMap::default();
    styles.insert(110, GeometryStyleInfo::from_color(blue));

    let mut decoder = EntityDecoder::new(IFC);

    // Mapped item, no direct style → inherits the underlying item's colour.
    assert_eq!(find_geometry_item_color(100, &styles, &mut decoder), Some(blue));
    // A direct style still wins.
    assert_eq!(find_geometry_item_color(110, &styles, &mut decoder), Some(blue));
    // A non-mapped, unstyled item (the representation map itself) → None.
    assert_eq!(find_geometry_item_color(101, &styles, &mut decoder), None);
}

#[test]
fn infer_opening_material_names_glass_vs_frame() {
    let glass =
        infer_opening_subpart_material_name(&IfcType::IfcWindow, [0.7, 0.9, 0.5, 0.3], 42);
    assert_eq!(glass.as_deref(), Some("Window_Glass"));

    let frame =
        infer_opening_subpart_material_name(&IfcType::IfcDoor, [0.5, 0.5, 0.5, 1.0], 7);
    assert_eq!(frame.as_deref(), Some("Door_Frame_7"));

    let none = infer_opening_subpart_material_name(&IfcType::IfcWall, [1.0; 4], 1);
    assert!(none.is_none(), "only windows/doors get inferred part names");
}

#[test]
fn find_geometry_item_color_terminates_on_cyclic_mapping() {
    // #100's mapped representation lists #100 itself as an item, so the
    // chase re-enters where it started. The chain is entirely file-supplied,
    // so a malformed or hostile file controls the recursion depth.
    const IFC: &str = r#"ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('m.ifc','2026-06-04T00:00:00',(''),(''),'','','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#2=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.0E-5,$,$);
#100=IFCMAPPEDITEM(#101,$);
#101=IFCREPRESENTATIONMAP($,#103);
#103=IFCSHAPEREPRESENTATION(#2,'Body','MappedRepresentation',(#100));
ENDSEC;
END-ISO-10303-21;
"#;
    let styles: FxHashMap<u32, GeometryStyleInfo> = FxHashMap::default();
    let mut decoder = EntityDecoder::new(IFC);
    assert_eq!(find_geometry_item_color(100, &styles, &mut decoder), None);
}

/// Build a chain of `hops` nested `IfcMappedItem`s whose innermost mapped
/// representation lists the styled leaf `#999`. Entry point is `#200`.
fn nested_mapped_chain(hops: u32) -> String {
    let mut s = String::from(
        "ISO-10303-21;\nHEADER;\nFILE_DESCRIPTION((''),'2;1');\n\
         FILE_NAME('m.ifc','2026-06-04T00:00:00',(''),(''),'','','');\n\
         FILE_SCHEMA(('IFC4'));\nENDSEC;\nDATA;\n\
         #2=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.0E-5,$,$);\n",
    );
    for i in 0..hops {
        let map_item = 200 + i;
        let rep_map = 1000 + i;
        let shape = 2000 + i;
        // The last hop's representation holds the styled leaf; the others
        // hold the next mapped item down.
        let inner = if i + 1 == hops { 999 } else { 200 + i + 1 };
        s.push_str(&format!("#{map_item}=IFCMAPPEDITEM(#{rep_map},$);\n"));
        s.push_str(&format!("#{rep_map}=IFCREPRESENTATIONMAP($,#{shape});\n"));
        s.push_str(&format!(
            "#{shape}=IFCSHAPEREPRESENTATION(#2,'Body','MappedRepresentation',(#{inner}));\n"
        ));
    }
    s.push_str("ENDSEC;\nEND-ISO-10303-21;\n");
    s
}

#[test]
fn find_geometry_item_color_resolves_exactly_at_the_depth_cap() {
    let green = [0.0, 0.8, 0.2, 1.0];
    let mut styles: FxHashMap<u32, GeometryStyleInfo> = FxHashMap::default();
    styles.insert(999, GeometryStyleInfo::from_color(green));

    let ifc = nested_mapped_chain(MAX_MAPPED_ITEM_DEPTH);
    let mut decoder = EntityDecoder::new(&ifc);
    assert_eq!(
        find_geometry_item_color(200, &styles, &mut decoder),
        Some(green),
        "a chain exactly MAX_MAPPED_ITEM_DEPTH hops deep must still resolve"
    );
}

#[test]
fn find_geometry_item_color_stops_one_hop_past_the_depth_cap() {
    let green = [0.0, 0.8, 0.2, 1.0];
    let mut styles: FxHashMap<u32, GeometryStyleInfo> = FxHashMap::default();
    styles.insert(999, GeometryStyleInfo::from_color(green));

    let ifc = nested_mapped_chain(MAX_MAPPED_ITEM_DEPTH + 1);
    let mut decoder = EntityDecoder::new(&ifc);
    assert_eq!(
        find_geometry_item_color(200, &styles, &mut decoder),
        None,
        "one hop past the cap the chase gives up rather than recursing on"
    );
}

/// The two boundary tests above build their chains *from*
/// `MAX_MAPPED_ITEM_DEPTH`, so they follow the constant wherever it moves and
/// stay green even if it is tuned down to 1 — they pin the boundary's shape,
/// not its position. This one uses a literal depth: nesting this shallow is
/// ordinary in real assemblies (a mapped item inside a mapped type inside an
/// aggregate), and colour must survive it whatever the cap is set to.
#[test]
fn find_geometry_item_color_resolves_ordinary_nesting_depth() {
    let green = [0.0, 0.8, 0.2, 1.0];
    let mut styles: FxHashMap<u32, GeometryStyleInfo> = FxHashMap::default();
    styles.insert(999, GeometryStyleInfo::from_color(green));

    let ifc = nested_mapped_chain(8);
    let mut decoder = EntityDecoder::new(&ifc);
    assert_eq!(
        find_geometry_item_color(200, &styles, &mut decoder),
        Some(green),
        "8 mapped-item hops is ordinary nesting; the cap must not swallow it"
    );
}

/// `resolve_color_for_representation_map` (#957, the type-geometry path) is a
/// second entry point into the same chase, reaching it from a rep map rather
/// than from a mapped item. The cap lives inside `find_geometry_item_color`
/// so it covers both; a guard at either call site would not have.
#[test]
fn resolve_color_for_representation_map_terminates_on_cyclic_mapping() {
    const IFC: &str = r#"ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('m.ifc','2026-06-04T00:00:00',(''),(''),'','','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#2=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.0E-5,$,$);
#100=IFCMAPPEDITEM(#101,$);
#101=IFCREPRESENTATIONMAP($,#103);
#103=IFCSHAPEREPRESENTATION(#2,'Body','MappedRepresentation',(#100));
ENDSEC;
END-ISO-10303-21;
"#;
    let styles: FxHashMap<u32, GeometryStyleInfo> = FxHashMap::default();
    let mut decoder = EntityDecoder::new(IFC);
    assert_eq!(
        resolve_color_for_representation_map(101, &styles, &mut decoder),
        None
    );
}

/// A depth cap alone bounds the chain LENGTH but not its BREADTH. This
/// representation holds four items that each lead back into the cycle, so a
/// depth-only guard explores `4^MAX_MAPPED_ITEM_DEPTH` paths — no stack
/// overflow, just a worker pinned forever. Trading the abort for a hang would
/// not have been a fix.
///
/// The assertion is on the returned colour, as everywhere else here. Its
/// failure mode without the visited set is a hang rather than a wrong value,
/// which CI reports as a lane timeout; that is stated plainly rather than
/// dressed up as a fast assertion, because there is no way to assert "this
/// returned before the heat death of the universe" that is not a timing test.
#[test]
fn find_geometry_item_color_bounds_cyclic_fan_out_not_just_depth() {
    const IFC: &str = r#"ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('m.ifc','2026-06-04T00:00:00',(''),(''),'','','');
FILE_SCHEMA(('IFC4'));
ENDSEC;
DATA;
#2=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.0E-5,$,$);
#100=IFCMAPPEDITEM(#101,$);
#101=IFCREPRESENTATIONMAP($,#103);
#103=IFCSHAPEREPRESENTATION(#2,'Body','MappedRepresentation',(#110,#111,#112,#113));
#110=IFCMAPPEDITEM(#101,$);
#111=IFCMAPPEDITEM(#101,$);
#112=IFCMAPPEDITEM(#101,$);
#113=IFCMAPPEDITEM(#101,$);
ENDSEC;
END-ISO-10303-21;
"#;
    let styles: FxHashMap<u32, GeometryStyleInfo> = FxHashMap::default();
    let mut decoder = EntityDecoder::new(IFC);
    assert_eq!(find_geometry_item_color(100, &styles, &mut decoder), None);
}

/// The cap is not a free parameter: it must match the geometry router's, or a
/// chain longer than this one but shorter than the router's renders its
/// geometry and silently loses its leaf's style.
///
/// Agreement is now STRUCTURAL: all three walks import one constant from
/// `ifc_lite_core::limits`, so they cannot disagree. This test used to assert
/// `MAX_MAPPED_ITEM_DEPTH == 32` against a literal, with a message claiming it
/// "must equal ifc_lite_geometry::router::processing::MAX_MAPPED_ITEM_DEPTH" —
/// but it never read the router's value, so it would have stayed green while
/// the router moved to any other number. It pinned agreement in its message and
/// a literal in its assertion, which is the illusion of enforcement rather than
/// enforcement. The value itself is pinned once, in core, next to the constant.
///
/// What is worth keeping here is the identity of the import, so that swapping
/// back to a private copy is a visible change rather than a silent one.
#[test]
fn mapped_item_depth_cap_is_the_shared_constant() {
    assert_eq!(
        MAX_MAPPED_ITEM_DEPTH,
        ifc_lite_core::MAX_MAPPED_ITEM_DEPTH,
        "element.rs must use the shared cap, not a private copy"
    );
}

/// An item shared between a DEEP branch (where the cap cuts its subtree before
/// the styled leaf) and a SHORT one (where it resolves). A plain visited SET
/// marks it on the deep visit and skips the short one, silently losing the
/// colour — a WRONG VALUE rather than a crash, so nothing reports it
/// (Codex, #2868 review; same shape here).
#[test]
fn a_shared_item_resolves_via_the_shallow_branch() {
    let mut body = String::from(
        "#200=IFCMAPPEDITEM(#201,$);\n\
         #201=IFCREPRESENTATIONMAP($,#202);\n\
         #202=IFCSHAPEREPRESENTATION(#2,'Body','MappedRepresentation',(#300,#900));\n",
    );
    for i in 0..30u32 {
        let (item, rm, sh) = (300 + i, 400 + i, 500 + i);
        let inner = if i == 29 { 900 } else { 300 + i + 1 };
        body.push_str(&format!("#{item}=IFCMAPPEDITEM(#{rm},$);\n"));
        body.push_str(&format!("#{rm}=IFCREPRESENTATIONMAP($,#{sh});\n"));
        body.push_str(&format!(
            "#{sh}=IFCSHAPEREPRESENTATION(#2,'Body','MappedRepresentation',(#{inner}));\n"
        ));
    }
    body.push_str(
        "#900=IFCMAPPEDITEM(#901,$);\n\
         #901=IFCREPRESENTATIONMAP($,#902);\n\
         #902=IFCSHAPEREPRESENTATION(#2,'Body','MappedRepresentation',(#950));\n\
         #950=IFCMAPPEDITEM(#951,$);\n\
         #951=IFCREPRESENTATIONMAP($,#952);\n\
         #952=IFCSHAPEREPRESENTATION(#2,'Body','MappedRepresentation',(#999));\n",
    );
    let ifc = format!(
        "ISO-10303-21;\nHEADER;\nFILE_DESCRIPTION((''),'2;1');\n\
         FILE_NAME('m.ifc','2026-06-04T00:00:00',(''),(''),'','','');\n\
         FILE_SCHEMA(('IFC4'));\nENDSEC;\nDATA;\n\
         #2=IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.0E-5,$,$);\n{body}ENDSEC;\nEND-ISO-10303-21;\n"
    );
    let green = [0.0, 0.8, 0.2, 1.0];
    let mut styles: FxHashMap<u32, GeometryStyleInfo> = FxHashMap::default();
    styles.insert(999, GeometryStyleInfo::from_color(green));
    let mut decoder = EntityDecoder::new(&ifc);
    assert_eq!(
        find_geometry_item_color(200, &styles, &mut decoder),
        Some(green),
        "the shallow branch reaches the styled leaf well inside the cap"
    );
}

#[path = "element_reference_opening_tests.rs"]
mod reference_openings;