@xeokit/xeokit-sdk 2.6.95 → 2.6.97

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,900 @@
1
+ import {math, Plugin, SceneModel, worldToRTCPositions} from "../../viewer/index.js";
2
+
3
+ import {IFCOpenShellDefaultDataSource} from "./IFCOpenShellDefaultDataSource.js";
4
+
5
+
6
+ /**
7
+ * {@link Viewer} plugin that uses [IfcOpenShell](https://ifcopenshell.org/) to load BIM models directly from IFC files.
8
+ *
9
+ * <a href="https://xeokit.github.io/xeokit-sdk/examples/index.html#BIMOffline_IFCOpenShellLoaderPlugin_Duplex"><img src="https://xeokit.io/img/docs/IFCOpenShellLoaderPlugin/IFCOpenShellLoaderPlugin.png"></a>
10
+ *
11
+ * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/index.html#BIMOffline_IFCOpenShellLoaderPlugin_Duplex)]
12
+ *
13
+ * ## Overview
14
+ *
15
+ * * Loads small-to-medium sized BIM models directly from IFC files.
16
+ * * Uses [IfcOpenShell API](https://ifcopenshell.org/) to parse IFC files in the browser.
17
+ * * Loads IFC geometry, element structure metadata, and property sets.
18
+ * * Not for large models. For best performance with large models, we recommend using {@link XKTLoaderPlugin}.
19
+ * * Loads double-precision coordinates, enabling models to be viewed at global coordinates without accuracy loss.
20
+ * * Filter which IFC types don't get loaded.
21
+ * * Configure initial appearances of specified IFC types.
22
+ * * Set a custom data source for IFC files.
23
+ *
24
+ * ## Limitations
25
+ *
26
+ * Loading and parsing huge IFC STEP files can be slow, and can overwhelm the browser, however. To view your
27
+ * largest IFC models, we recommend instead pre-converting those to xeokit's compressed native .XKT format, then
28
+ * loading them with {@link XKTLoaderPlugin} instead.</p>
29
+ *
30
+ * ## Scene representation
31
+ *
32
+ * When loading a model, IFCOpenShellLoaderPlugin creates an {@link Entity} that represents the model, which
33
+ * will have {@link Entity#isModel} set ````true```` and will be registered by {@link Entity#id}
34
+ * in {@link Scene#models}. The IFCOpenShellLoaderPlugin also creates an {@link Entity} for each object within the
35
+ * model. Those Entities will have {@link Entity#isObject} set ````true```` and will be registered
36
+ * by {@link Entity#id} in {@link Scene#objects}.
37
+ *
38
+ * ## Metadata
39
+ *
40
+ * When loading a model, IFCOpenShellLoaderPlugin also creates a {@link MetaModel} that represents the model, which contains
41
+ * a {@link MetaObject} for each IFC element, plus a {@link PropertySet} for each IFC property set. Loading metadata
42
+ * can be very slow, so we can also optionally disable it if we don't need it.
43
+ *
44
+ * ## Usage
45
+ *
46
+ * In the example below we'll load the Duplex BIM model from
47
+ * an [IFC file](https://github.com/xeokit/xeokit-sdk/tree/master/assets/models/ifc). Within our {@link Viewer}, this
48
+ * will create a bunch of {@link Entity}s that represents the model and its objects, along with a {@link MetaModel},
49
+ * {@link MetaObject}s and {@link PropertySet}s that hold their metadata.
50
+ *
51
+ * ````javascript
52
+ * import {Viewer, IFCOpenShellLoaderPlugin, NavCubePlugin, TreeViewPlugin} from "../../dist/xeokit-sdk.es.js";
53
+ *
54
+ * //------------------------------------------------------------------------------------------------------------------
55
+ * // 1. Create a Viewer,
56
+ * // 2. Arrange the camera
57
+ * //------------------------------------------------------------------------------------------------------------------
58
+ *
59
+ * // 1
60
+ * const viewer = new Viewer({
61
+ * canvasId: "myCanvas",
62
+ * transparent: true
63
+ * });
64
+ *
65
+ * // 2
66
+ * viewer.camera.eye = [-3.933, 2.855, 27.018];
67
+ * viewer.camera.look = [4.400, 3.724, 8.899];
68
+ * viewer.camera.up = [-0.018, 0.999, 0.039];
69
+ *
70
+ * //------------------------------------------------------------------------------------------------------------------
71
+ * // 1. Create the IFCOpenShellLoaderPlugin,
72
+ * // 2. Load an IFC model
73
+ * //------------------------------------------------------------------------------------------------------------------
74
+ *
75
+ * // 1
76
+ *
77
+ * const ifcLoader = new IFCOpenShellLoaderPlugin(viewer, {
78
+ * workerSrc: "./my/directory/IFCOpenShellWorker.js",
79
+ * ifcOpenShellURL: "./my/directory/ifcopenshell-0.8.3+34a1bc6-cp313-cp313-emscripten_4_0_9_wasm32.whl"
80
+ * });
81
+ *
82
+ * // 2
83
+ * const model = ifcLoader.load({ // Returns an Entity that represents the model
84
+ * id: "myModel",
85
+ * src: "../assets/models/ifc/Duplex.ifc",
86
+ * excludeTypes: ["IfcSpace"],
87
+ * edges: true
88
+ * });
89
+ *
90
+ * model.on("loaded", () => {
91
+ *
92
+ * //----------------------------------------------------------------------------------------------------------
93
+ * // 1. Find metadata on the bottom storey
94
+ * // 2. X-ray all the objects except for the bottom storey
95
+ * // 3. Fit the bottom storey in view
96
+ * //----------------------------------------------------------------------------------------------------------
97
+ *
98
+ * // 1
99
+ * const metaModel = viewer.metaScene.metaModels["myModel"]; // MetaModel with ID "myModel"
100
+ * const metaObject
101
+ * = viewer.metaScene.metaObjects["1xS3BCk291UvhgP2dvNsgp"]; // MetaObject with ID "1xS3BCk291UvhgP2dvNsgp"
102
+ *
103
+ * const name = metaObject.name; // "01 eerste verdieping"
104
+ * const type = metaObject.type; // "IfcBuildingStorey"
105
+ * const parent = metaObject.parent; // MetaObject with type "IfcBuilding"
106
+ * const children = metaObject.children; // Array of child MetaObjects
107
+ * const objectId = metaObject.id; // "1xS3BCk291UvhgP2dvNsgp"
108
+ * const objectIds = viewer.metaScene.getObjectIDsInSubtree(objectId); // IDs of leaf sub-objects
109
+ * const aabb = viewer.scene.getAABB(objectIds); // Axis-aligned boundary of the leaf sub-objects
110
+ *
111
+ * // 2
112
+ * viewer.scene.setObjectsXRayed(viewer.scene.objectIds, true);
113
+ * viewer.scene.setObjectsXRayed(objectIds, false);
114
+ *
115
+ * // 3
116
+ * viewer.cameraFlight.flyTo(aabb);
117
+ *
118
+ * // Find the model Entity by ID
119
+ * model = viewer.scene.models["myModel"];
120
+ *
121
+ * // Destroy the model
122
+ * model.destroy();
123
+ * });
124
+ * ````
125
+ *
126
+ * ## Configuring a custom data source
127
+ *
128
+ * By default, IFCOpenShellLoaderPlugin will load IFC files over HTTP.
129
+ *
130
+ * In the example below, we'll customize the way IFCOpenShellLoaderPlugin loads the files by configuring it with our own data source
131
+ * object. For simplicity, our custom data source example also uses HTTP, using a couple of xeokit utility functions.
132
+ *
133
+ * ````javascript
134
+ * import {utils} from "xeokit-sdk.es.js";
135
+ *
136
+ * class MyDataSource {
137
+ *
138
+ * constructor() {
139
+ * }
140
+ *
141
+ * // Gets the contents of the given IFC file in an arraybuffer
142
+ * getIFC(src, ok, error) {
143
+ * console.log("MyDataSource#getIFC(" + IFCSrc + ", ... )");
144
+ * utils.loadArraybuffer(src,
145
+ * (arraybuffer) => {
146
+ * ok(arraybuffer);
147
+ * },
148
+ * function (errMsg) {
149
+ * error(errMsg);
150
+ * });
151
+ * }
152
+ * }
153
+ *
154
+ * const ifcLoader2 = new IFCOpenShellLoaderPlugin(viewer, {
155
+ * dataSource: new MyDataSource()
156
+ * });
157
+ *
158
+ * const model5 = ifcLoader2.load({
159
+ * id: "myModel5",
160
+ * src: "../assets/models/ifc/Duplex.ifc"
161
+ * });
162
+ * ````
163
+ *
164
+ * ## Loading multiple copies of a model, without object ID clashes
165
+ *
166
+ * Sometimes we need to load two or more instances of the same model, without having clashes
167
+ * between the IDs of the equivalent objects in the model instances.
168
+ *
169
+ * As shown in the example below, we do this by setting {@link IFCOpenShellLoaderPlugin#globalizeObjectIds} ````true```` before we load our models.
170
+ *
171
+ * ````javascript
172
+ * ifcLoader.globalizeObjectIds = true;
173
+ *
174
+ * const model = ifcLoader.load({
175
+ * id: "model1",
176
+ * src: "../assets/models/ifc/Duplex.ifc"
177
+ * });
178
+ *
179
+ * const model2 = ifcLoader.load({
180
+ * id: "model2",
181
+ * src: "../assets/models/ifc/Duplex.ifc"
182
+ * });
183
+ * ````
184
+ *
185
+ * For each {@link Entity} loaded by these two calls, {@link Entity#id} and {@link MetaObject#id} will get prefixed by
186
+ * the ID of their model, in order to avoid ID clashes between the two models.
187
+ *
188
+ * An Entity belonging to the first model will get an ID like this:
189
+ *
190
+ * ````
191
+ * myModel1#0BTBFw6f90Nfh9rP1dlXrb
192
+ * ````
193
+ *
194
+ * The equivalent Entity in the second model will get an ID like this:
195
+ *
196
+ * ````
197
+ * myModel2#0BTBFw6f90Nfh9rP1dlXrb
198
+ * ````
199
+ *
200
+ * Now, to update the visibility of both of those Entities collectively, using {@link Scene#setObjectsVisible}, we can
201
+ * supply just the IFC product ID part to that method:
202
+ *
203
+ * ````javascript
204
+ * myViewer.scene.setObjectVisibilities("0BTBFw6f90Nfh9rP1dlXrb", true);
205
+ * ````
206
+ *
207
+ * The method, along with {@link Scene#setObjectsXRayed}, {@link Scene#setObjectsHighlighted} etc, will internally expand
208
+ * the given ID to refer to the instances of that Entity in both models.
209
+ *
210
+ * We can also, of course, reference each Entity directly, using its globalized ID:
211
+ *
212
+ * ````javascript
213
+ * myViewer.scene.setObjectVisibilities("myModel1#0BTBFw6f90Nfh9rP1dlXrb", true);
214
+ *````
215
+ *
216
+ * @class IFCOpenShellLoaderPlugin
217
+ * @since 2.6.90
218
+ */
219
+ export class IFCOpenShellLoaderPlugin extends Plugin {
220
+
221
+ /**
222
+ * @param {Viewer} viewer The {@link Viewer} that will own this plugin.
223
+ * @param {Object} cfg Plugin configuration.
224
+ * @param {String} [cfg.id="IFCOpenShellLoader"] Optional ID for this plugin instance.
225
+ * @param {Object} [cfg.dataSource] Custom data source (defaults to {@link IFCOpenShellDefaultDataSource}).
226
+ * @param {Object} cfg.ifcopenshell IfcOpenShell API object.
227
+ * @param {Object} cfg.ifcopenshell_geom IfcOpenShell geometry API object.
228
+ */
229
+ constructor(viewer, cfg) {
230
+
231
+ super("IFCOpenShellLoader", viewer, cfg);
232
+
233
+ if (!cfg) {
234
+ throw new Error("IFCOpenShellLoaderPlugin: No configuration given");
235
+ }
236
+
237
+ if (!cfg.ifcopenshell) {
238
+ throw new Error("IFCOpenShellLoaderPlugin: No ifcopenshell given");
239
+ }
240
+
241
+ if (!cfg.ifcopenshell_geom) {
242
+ throw new Error("IFCOpenShellLoaderPlugin: No ifcopenshell_geom given");
243
+ }
244
+
245
+ this.ifcopenshell = cfg.ifcopenshell;
246
+ this.ifcopenshell_geom = cfg.ifcopenshell_geom;
247
+
248
+ this.dataSource = cfg.dataSource;
249
+ }
250
+
251
+ /**
252
+ * Sets a custom data source for IFC files.
253
+ * @param value
254
+ */
255
+ set dataSource(value) {
256
+ this._dataSource = value || new IFCOpenShellDefaultDataSource();
257
+ }
258
+
259
+ /**
260
+ * Gets the data source for IFC files.
261
+ * @returns {*|IFCOpenShellDefaultDataSource}
262
+ */
263
+ get dataSource() {
264
+ return this._dataSource;
265
+ }
266
+
267
+ /**
268
+ * Gets whether IFCOpenShellLoaderPlugin globalizes each {@link Entity#id} and {@link MetaObject#id} as it loads a model.
269
+ *
270
+ * Default value is ````false````.
271
+ *
272
+ * @type {Boolean}
273
+ */
274
+ get globalizeObjectIds() {
275
+ return this._globalizeObjectIds;
276
+ }
277
+
278
+ /**
279
+ * Sets whether IFCOpenShellLoaderPlugin globalizes each {@link Entity#id} and {@link MetaObject#id} as it loads a model.
280
+ *
281
+ * Set this ````true```` when you need to load multiple instances of the same model, to avoid ID clashes
282
+ * between the objects in the different instances.
283
+ *
284
+ * When we load a model with this set ````true````, then each {@link Entity#id} and {@link MetaObject#id} will be
285
+ * prefixed by the ID of the model, ie. ````<modelId>#<objectId>````.
286
+ *
287
+ * {@link Entity#originalSystemId} and {@link MetaObject#originalSystemId} will always hold the original, un-prefixed, ID values.
288
+ *
289
+ * Default value is ````false````.
290
+ *
291
+ * See the main {@link IFCOpenShellLoaderPlugin} class documentation for usage info.
292
+ *
293
+ * @type {Boolean}
294
+ */
295
+ set globalizeObjectIds(value) {
296
+ this._globalizeObjectIds = !!value;
297
+ }
298
+
299
+ /**
300
+ * Loads an IFC model from a file or text into the {@link Viewer}.
301
+ *
302
+ * @param {Object} params
303
+ * @param {String} [params.id] Optional root Entity ID.
304
+ * @param {String} [params.src] IFC file path (alternative to `text`).
305
+ * @param {String} [params.text] IFC text (alternative to `src`).
306
+ * @param {{String:Object}} [params.objectDefaults]
307
+ * @param {String[]} [params.excludeTypes] Array of IFC types to exclude.
308
+ * @param {Number[]} [params.origin=[0,0,0]] Optional World-coordinate origin to apply to the model.
309
+ * @param {Number[]} [params.position=[0,0,0]] Optional position offset to apply to the model.
310
+ * @param {Number[]} [params.rotation=[0,0,0]] Optional XYZ Euler rotation (degrees) to apply to the model.
311
+ * @param {Boolean} [params.backfaces=true] Whether to render backfaces.
312
+ * @param {Boolean} [params.dtxEnabled=true] Whether to enable data texture storage for geometry buffers.
313
+ * @param {Boolean} [params.loadMetadata=true] Whether to load metadata.
314
+ * @param {Boolean} [params.loadMetadataPropertySets=true] Whether to load property sets within the metadata. Only works when `loadMetadata` is true.
315
+ * @param {Boolean} [params.edges=false] Whether to generate edge lines for the model.
316
+ * @param {Boolean} [params.saoEnabled=false] Whether to enable SAO for the model.
317
+ * @param {Boolean} [params.globalizeObjectIds=false] Whether to globalize each {@link Entity#id} and {@link MetaObject#id} as it loads the model.
318
+ * @returns {Entity}
319
+ */
320
+ async load(params = {}) {
321
+
322
+ let {
323
+ id,
324
+ backfaces = true,
325
+ dtxEnabled = true,
326
+ position,
327
+ rotation,
328
+ origin,
329
+ loadMetadata,
330
+ loadMetadataPropertySets,
331
+ edges,
332
+ saoEnabled,
333
+ globalizeObjectIds,
334
+ excludeTypes
335
+ } = params;
336
+
337
+ if (id && this.viewer.scene.components[id]) {
338
+ this.error(`Component with this ID already exists: ${id} - autogenerating SceneModel ID`);
339
+ id = null;
340
+ }
341
+
342
+ const sceneModel = new SceneModel(this.viewer.scene, {
343
+ id,
344
+ isModel: true,
345
+ globalizeObjectIds,
346
+ backfaces,
347
+ dtxEnabled,
348
+ position,
349
+ rotation,
350
+ origin,
351
+ edges,
352
+ saoEnabled
353
+ });
354
+
355
+ const modelId = sceneModel.id;
356
+
357
+ if (!params.src && !params.text) {
358
+ this.error("load() expected 'src' or 'text'");
359
+ return sceneModel; // Return empty model
360
+ }
361
+
362
+ const spinner = this.viewer.scene.canvas.spinner;
363
+ spinner.processes++;
364
+
365
+ const loadIFC = (fileData) => {
366
+ const ifc = this.ifcopenshell.file.from_string(fileData);
367
+ const ctx = {
368
+ loadMetadataPropertySets: (loadMetadataPropertySets !== false),
369
+ globalizeObjectIds: globalizeObjectIds || this._globalizeObjectIds,
370
+ geometryCache: new Map(),
371
+ ifc,
372
+ sceneModel
373
+ };
374
+ if (excludeTypes) {
375
+ ctx.excludeTypes = excludeTypes;
376
+ }
377
+ this._loadIFCGeometry(ctx);
378
+ if (loadMetadata !== false) {
379
+ const metaModelData = this._loadIFCMetaModel(ctx, ifc);
380
+ this.viewer.metaScene.createMetaModel(modelId, metaModelData);
381
+ }
382
+ this.viewer.scene.canvas.spinner.processes--;
383
+ }
384
+
385
+ if (params.src) {
386
+ this.viewer.scene.canvas.spinner.processes++;
387
+ this._dataSource.getIFC(
388
+ params.src,
389
+ (fileData) => {
390
+ loadIFC(fileData);
391
+ this.viewer.scene.canvas.spinner.processes--;
392
+ },
393
+ (err) => {
394
+ this.viewer.scene.canvas.spinner.processes--;
395
+ this.error(err);
396
+ }
397
+ );
398
+ } else {
399
+ loadIFC(params.text);
400
+ }
401
+
402
+ sceneModel.once("destroyed", () => {
403
+ this.viewer.metaScene.destroyMetaModel(modelId);
404
+ });
405
+
406
+ return sceneModel;
407
+ }
408
+
409
+ _loadIFCGeometry(ctx) {
410
+ const {ifc, sceneModel} = ctx;
411
+ const {ifcopenshell_geom} = this;
412
+
413
+ const settings = ifcopenshell_geom.settings();
414
+ settings.set(settings.WELD_VERTICES, false);
415
+
416
+ const iterator = ifcopenshell_geom.iterator.callKwargs({
417
+ settings,
418
+ file_or_filename: ifc,
419
+ exclude: ctx.excludeTypes,
420
+ geometry_library: "hybrid-cgal-simple-opencascade"
421
+ });
422
+
423
+ if (iterator.initialize()) {
424
+ do {
425
+ const obj = iterator.get();
426
+ if (obj) {
427
+ const entity = ifc.by_id(obj.id)
428
+ this._parseIFCEntity(ctx, obj, entity);
429
+ }
430
+ } while (iterator.next());
431
+ }
432
+
433
+ sceneModel.finalize();
434
+
435
+ sceneModel.scene.once("tick", () => {
436
+ if (!sceneModel.destroyed) {
437
+ sceneModel.scene.fire("modelLoaded", sceneModel.id);
438
+ sceneModel.fire("loaded", true, false);
439
+ }
440
+ });
441
+ }
442
+
443
+ _parseIFCEntity(ctx, obj, ifcEntity) {
444
+ const {sceneModel, geometryCache} = ctx;
445
+ const geometry_id = obj.geometry.id;
446
+
447
+ const M = obj.transformation.data().components.toJs();
448
+ const {origin, matrix} = extractRTCTransform(M);
449
+
450
+ if (!geometryCache.get(geometry_id)) {
451
+
452
+ const srcMaterials = obj.geometry.materials.toJs();
453
+ const materials = srcMaterials.map((m) => ({
454
+ diffuse: m.diffuse.components.toJs(),
455
+ transparency: (m.transparency
456
+ && !isNaN(m.transparency)) ? m.transparency : 0.0,
457
+ }));
458
+
459
+ const materialIds = new Int32Array(obj.geometry.material_ids.toJs());
460
+
461
+ // Build mapping: materialIndex -> [faceIdx...]
462
+ const mapping = buildMaterialMapping(materialIds);
463
+
464
+ // Create sub-geometry per materialIndex, once
465
+ const subGeoms = new Map();
466
+
467
+ for (const [matIndexStr, faceList] of Object.entries(mapping)) {
468
+
469
+ if (!faceList || faceList.length === 0) {
470
+ continue;
471
+ }
472
+
473
+ // xeokit auto-generates normals on the GPU side
474
+
475
+ const positions = new Float32Array(obj.geometry.verts.toJs());
476
+ const edgeIndices = new Uint32Array(obj.geometry.edges.toJs());
477
+ const faces = new Uint32Array(obj.geometry.faces.toJs());
478
+ const matIndex = Number(matIndexStr);
479
+ const indices = buildIndicesForFaces(faceList, faces);
480
+ const sceneGeometryId = makeSubGeometryId(geometry_id, matIndex); // deterministic
481
+
482
+ sceneModel.createGeometry({
483
+ id: sceneGeometryId,
484
+ primitive: "triangles",
485
+ positions,
486
+ indices,
487
+ edgeIndices
488
+ });
489
+
490
+ subGeoms.set(matIndex, sceneGeometryId);
491
+ }
492
+
493
+ geometryCache.set(geometry_id, {
494
+ subGeoms,
495
+ materials,
496
+ // store mapping as Map<number, Uint32Array> to avoid recomputing
497
+ mapping: new Map(Object.entries(mapping).map(
498
+ ([k, v]) => [Number(k), new Uint32Array(v)]
499
+ ))
500
+ });
501
+ }
502
+
503
+ // Reuse cached sub-geometries to create per-object meshes
504
+
505
+ const cached = geometryCache.get(geometry_id);
506
+ const meshIds = [];
507
+
508
+ for (const [matIndex, sceneGeometryId] of cached.subGeoms.entries()) {
509
+ const material = cached.materials[matIndex] || {diffuse: [0.6, 0.6, 0.6], transparency: 0.0};
510
+ const meshId = generateUUID();
511
+ const diffuse = material.diffuse;
512
+ sceneModel.createMesh({
513
+ id: meshId,
514
+ geometryId: sceneGeometryId,
515
+ origin,
516
+ matrix,
517
+ color: [diffuse[0], diffuse[1], diffuse[2]],
518
+ opacity: 1.0 - material.transparency
519
+ });
520
+ meshIds.push(meshId);
521
+ }
522
+
523
+ sceneModel.createEntity({
524
+ id: ctx.globalizeObjectIds ? math.globalizeObjectId(ctx.sceneModel.id, ifcEntity.GlobalId) : ifcEntity.GlobalId,
525
+ isObject: true,
526
+ meshIds
527
+ });
528
+ }
529
+
530
+ _loadIFCMetaModel(ctx, ifc) {
531
+
532
+ const visited = new Set();
533
+ const metaObjects = [];
534
+ const propertySets = [];
535
+ const metaObjectPropertySetIds = new Map();
536
+
537
+ // ---- helpers -----------------------------------------------------------
538
+ const toStr = (v) => (v === undefined || v === null) ? "" : String(v);
539
+
540
+ function getGlobalId(entity) {
541
+ try {
542
+ return String(entity.GlobalId);
543
+ } catch {
544
+ return null;
545
+ }
546
+ }
547
+
548
+ function addNode(entity, parent) {
549
+ const id = getGlobalId(entity);
550
+ if (!id || visited.has(id)) return false;
551
+ visited.add(id);
552
+ const globalizeObjectIds = ctx.globalizeObjectIds;
553
+ const modelId = ctx.sceneModel.id;
554
+ const propertySetIds = [];
555
+ if (ctx.loadMetadataPropertySets) {
556
+ // Try all possible association fields
557
+ const associationFields = ["HasAssociations", "IsDefinedBy", "IsDecomposedBy", "ContainsElements"];
558
+ for (const field of associationFields) {
559
+ const associations = entity[field];
560
+ if (associations && associations.length > 0) {
561
+ for (let j = 0; j < associations.length; j++) {
562
+ const rel = associations.get(j);
563
+ if (rel.is_a && rel.is_a() === "IfcRelDefinesByProperties") {
564
+ const propSet = rel.RelatingPropertyDefinition;
565
+ if (propSet && propSet.is_a) {
566
+ // Accept both IfcPropertySet and IfcElementQuantity
567
+ if (["IfcPropertySet", "IfcElementQuantity"].includes(propSet.is_a())) {
568
+ const propSetId = propSet.GlobalId ? String(propSet.GlobalId) : null;
569
+ const propSetName = propSet.Name ? String(propSet.Name) : "";
570
+ const propSetType = propSet.is_a ? String(propSet.is_a()) : "";
571
+ const properties = [];
572
+ const props = propSet.HasProperties || propSet.Quantities;
573
+ if (props && props.length > 0) {
574
+ for (let k = 0; k < props.length; k++) {
575
+ const p = props.get(k);
576
+ const propName = p.Name ? String(p.Name) : "";
577
+ let propValue = "";
578
+ let propType = p.is_a ? String(p.is_a()) : "";
579
+ if (p.is_a && p.is_a() === "IfcPropertySingleValue") {
580
+ try {
581
+ propValue = p.NominalValue ? String(p.NominalValue.wrappedValue) : "";
582
+ } catch {
583
+ propValue = "";
584
+ }
585
+ } else if (p.is_a && p.is_a() === "IfcPropertyEnumeratedValue") {
586
+ try {
587
+ const values = p.EnumerationValues;
588
+ if (values && values.length > 0) {
589
+ const arr = [];
590
+ for (let vi = 0; vi < values.length; vi++) {
591
+ arr.push(String(values.get(vi).wrappedValue));
592
+ }
593
+ propValue = arr.join(", ");
594
+ }
595
+ } catch {
596
+ propValue = "";
597
+ }
598
+ } else if (p.is_a && p.is_a() === "IfcQuantityArea") {
599
+ propValue = p.AreaValue ? String(p.AreaValue) : "";
600
+ } else if (p.is_a && p.is_a() === "IfcQuantityLength") {
601
+ propValue = p.LengthValue ? String(p.LengthValue) : "";
602
+ } else if (p.is_a && p.is_a() === "IfcQuantityVolume") {
603
+ propValue = p.VolumeValue ? String(p.VolumeValue) : "";
604
+ } else {
605
+ try {
606
+ propValue = p.NominalValue ? String(p.NominalValue) : "";
607
+ } catch {
608
+ propValue = "";
609
+ }
610
+ }
611
+ properties.push({
612
+ name: propName,
613
+ value: propValue,
614
+ type: propType
615
+ });
616
+ p.destroy?.();
617
+ }
618
+ props.destroy?.();
619
+ }
620
+ propertySets.push({
621
+ id: propSetId,
622
+ // objectId: ctx.globalizeObjectIds ? math.globalizeObjectId(ctx.sceneModel.id, objectId) : objectId,
623
+ name: propSetName,
624
+ type: propSetType,
625
+ properties
626
+ });
627
+ propertySetIds.push(propSetId);
628
+ propSet.destroy?.();
629
+ }
630
+ }
631
+ rel.destroy?.();
632
+ }
633
+ }
634
+ associations.destroy?.();
635
+ }
636
+ }
637
+ }
638
+ metaObjects.push({
639
+ id: globalizeObjectIds ? math.globalizeObjectId(modelId, id) : id,
640
+ type: String(entity.is_a()),
641
+ parent: parent
642
+ ? (globalizeObjectIds
643
+ ? math.globalizeObjectId(modelId, getGlobalId(parent))
644
+ : getGlobalId(parent))
645
+ : null,
646
+ propertySetIds
647
+ });
648
+ return true;
649
+ }
650
+
651
+ function walk(entity, parent = null) {
652
+ if (!entity) return;
653
+
654
+ // add current node (skip children if already visited)
655
+ if (!addNode(entity, parent)) {
656
+ entity.destroy?.();
657
+ return;
658
+ }
659
+
660
+ // 1) Decomposition (IfcRelAggregates / IfcRelNests)
661
+ const decos = entity.IsDecomposedBy;
662
+ if (decos) {
663
+ for (let i = 0; i < decos.length; i++) {
664
+ const rel = decos.get(i);
665
+ const children = rel.RelatedObjects;
666
+ if (children) {
667
+ for (let j = 0; j < children.length; j++) {
668
+ const child = children.get(j);
669
+ walk(child, entity);
670
+ child.destroy?.();
671
+ }
672
+ children.destroy?.();
673
+ }
674
+ rel.destroy?.();
675
+ }
676
+ }
677
+
678
+ // 2) Spatial containment (IfcRelContainedInSpatialStructure)
679
+ const contains = entity.ContainsElements;
680
+ if (contains) {
681
+ for (let i = 0; i < contains.length; i++) {
682
+ const rel = contains.get(i);
683
+ const elems = rel.RelatedElements;
684
+ if (elems) {
685
+ for (let j = 0; j < elems.length; j++) {
686
+ const elem = elems.get(j);
687
+ walk(elem, entity);
688
+ elem.destroy?.();
689
+ }
690
+ elems.destroy?.();
691
+ }
692
+ rel.destroy?.();
693
+ }
694
+ }
695
+
696
+ entity.destroy?.();
697
+ }
698
+
699
+ // ---- metadata extraction ----------------------------------------------
700
+ let projectId = "";
701
+ let author = "";
702
+ let createdAt = ""; // ISO string
703
+ let schema = "";
704
+ let creatingApplication = "";
705
+
706
+ // Schema (primary)
707
+ try {
708
+ // ifcopenshell.file usually exposes a `schema` string property
709
+ schema = toStr(ifc.schema);
710
+ } catch {
711
+ }
712
+ if (!schema) {
713
+ // Fallback to STEP header
714
+ try {
715
+ const ids = ifc.wrapped_data.header.file_schema.schema_identifiers;
716
+ if (ids && ids.length > 0) schema = toStr(ids.get(0));
717
+ } catch {
718
+ }
719
+ }
720
+
721
+ // Project (also gives us OwnerHistory on many files)
722
+ const projects = ifc.by_type("IfcProject");
723
+ if (projects && projects.length > 0) {
724
+ const project = projects.get(0);
725
+ projectId = toStr(project.GlobalId);
726
+
727
+ // OwnerHistory path (preferred when present)
728
+ try {
729
+ const oh = project.OwnerHistory; // deprecated in newer IFC4.x, but present in many files
730
+ if (oh) {
731
+ // Author: IfcPersonAndOrganization → ThePerson (GivenName/FamilyName) and TheOrganization.Name
732
+ try {
733
+ const user = oh.OwningUser;
734
+ const person = user?.ThePerson;
735
+ const org = user?.TheOrganization;
736
+ const gn = person?.GivenName ? toStr(person.GivenName) : "";
737
+ const fn = person?.FamilyName ? toStr(person.FamilyName) : "";
738
+ const personName = (gn || fn) ? [gn, fn].filter(Boolean).join(" ") : "";
739
+ const orgName = org?.Name ? toStr(org.Name) : "";
740
+ author = [personName, orgName].filter(Boolean).join(" / ");
741
+ } catch {
742
+ }
743
+
744
+ // Creation time (UNIX seconds)
745
+ try {
746
+ const ts = oh?.CreationDate;
747
+ if (typeof ts === "number" && isFinite(ts) && ts > 0) {
748
+ createdAt = new Date(ts * 1000).toISOString();
749
+ }
750
+ } catch {
751
+ }
752
+
753
+ // Creating application
754
+ try {
755
+ const app = oh?.OwningApplication;
756
+ const appName =
757
+ app?.ApplicationFullName ? toStr(app.ApplicationFullName) :
758
+ app?.ApplicationIdentifier ? toStr(app.ApplicationIdentifier) : "";
759
+ const appVer = app?.Version ? toStr(app.Version) : "";
760
+ creatingApplication = [appName, appVer].filter(Boolean).join(" ");
761
+ } catch {
762
+ }
763
+ }
764
+ } catch {
765
+ }
766
+
767
+ // Clean first project (we’ll traverse below with a fresh pointer anyway)
768
+ project.destroy?.();
769
+ }
770
+
771
+ // Fallbacks via STEP header if OwnerHistory wasn’t there / incomplete
772
+ try {
773
+ const fileName = ifc.wrapped_data.header.file_name;
774
+ if (!author) {
775
+ try {
776
+ const authors = fileName.author;
777
+ if (authors && authors.length > 0) {
778
+ // `author` is a LIST in the STEP header; join if multiple
779
+ const parts = [];
780
+ for (let i = 0; i < authors.length; i++) parts.push(toStr(authors.get(i)));
781
+ author = parts.filter(Boolean).join(", ");
782
+ }
783
+ } catch {
784
+ }
785
+ }
786
+ if (!createdAt) {
787
+ const ts = toStr(fileName.time_stamp); // already a string like "2023-08-10T12:34:56"
788
+ if (ts) {
789
+ // normalize to ISO if possible
790
+ const maybe = new Date(ts);
791
+ if (!isNaN(maybe.getTime())) createdAt = maybe.toISOString();
792
+ }
793
+ }
794
+ if (!creatingApplication) {
795
+ // STEP header carries "originating_system" and "preprocessor_version"
796
+ const orig = toStr(fileName.originating_system);
797
+ const prep = toStr(fileName.preprocessor_version);
798
+ creatingApplication = [orig, prep].filter(Boolean).join(" / ");
799
+ }
800
+ } catch {
801
+ }
802
+
803
+ // If createdAt still missing, sweep for earliest OwnerHistory timestamp across roots
804
+ if (!createdAt) {
805
+ try {
806
+ let minTs = Infinity;
807
+ const roots = ifc.by_type("IfcRoot");
808
+ for (let i = 0; i < roots.length; i++) {
809
+ const r = roots.get(i);
810
+ const oh = r?.OwnerHistory;
811
+ const ts = oh?.CreationDate;
812
+ if (typeof ts === "number" && isFinite(ts) && ts > 0 && ts < minTs) {
813
+ minTs = ts;
814
+ }
815
+ r.destroy?.();
816
+ }
817
+ roots.destroy?.();
818
+ if (isFinite(minTs)) createdAt = new Date(minTs * 1000).toISOString();
819
+ } catch {
820
+ }
821
+ }
822
+
823
+ // ---- hierarchy walk ----------------------------------------------------
824
+ // Re-query projects since we destroyed the first pointer above
825
+ const projects2 = ifc.by_type("IfcProject");
826
+ for (let i = 0; i < projects2.length; i++) {
827
+ const project = projects2.get(i);
828
+ walk(project, null);
829
+ project.destroy?.();
830
+ }
831
+ projects2.destroy?.();
832
+
833
+ return {
834
+ id: "",
835
+ projectId,
836
+ author,
837
+ createdAt,
838
+ schema,
839
+ creatingApplication,
840
+ metaObjects,
841
+ propertySets
842
+ };
843
+ }
844
+
845
+ /**
846
+ * Destroys this IFCOpenShellLoaderPlugin instance.
847
+ */
848
+ destroy() {
849
+ super.destroy();
850
+ }
851
+ }
852
+
853
+ function makeSubGeometryId(geometry_id, matIndex) {
854
+ return `${geometry_id}:${matIndex}#geom`;
855
+ }
856
+
857
+ function buildMaterialMapping(materialIds) {
858
+ const mapping = {};
859
+ for (let faceIdx = 0; faceIdx < materialIds.length; faceIdx++) {
860
+ const materialId = materialIds[faceIdx];
861
+ if (materialId == null || materialId < 0) continue; // skip invalid / missing
862
+ (mapping[materialId] ||= []).push(faceIdx);
863
+ }
864
+ return mapping;
865
+ }
866
+
867
+ function buildIndicesForFaces(faceList, faceIndices) {
868
+ const indices = new Uint32Array(faceList.length * 3);
869
+ let k = 0;
870
+ for (const faceIdx of faceList) {
871
+ const base = faceIdx * 3;
872
+ indices[k++] = faceIndices[base + 0];
873
+ indices[k++] = faceIndices[base + 1];
874
+ indices[k++] = faceIndices[base + 2];
875
+ }
876
+ return indices;
877
+ }
878
+
879
+ function generateUUID() {
880
+ return Math.random().toString(36).substr(2, 9);
881
+ }
882
+
883
+ function extractRTCTransform(transform) {
884
+ const matrix = flattenMatrixArray(transform);
885
+ const origin = [];
886
+ const worldOrigin = matrix.slice(12, 15); // translation xyz
887
+ worldToRTCPositions(worldOrigin, worldOrigin, origin);
888
+ matrix.set(worldOrigin, 12);
889
+ return {origin, matrix};
890
+ }
891
+
892
+ function flattenMatrixArray(m) {
893
+ return new Float64Array([
894
+ m[0][0], m[2][0], -m[1][0], m[3][0],
895
+ m[0][1], m[2][1], -m[1][1], m[3][1],
896
+ m[0][2], m[2][2], -m[1][2], m[3][2],
897
+ m[0][3], m[2][3], -m[1][3], m[3][3]
898
+ ]);
899
+ }
900
+