brepjs-bim 0.23.1 → 0.23.2

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.
@@ -1,4 +1,4 @@
1
- import { addHoles, applyMatrix, autoHeal, box, castShape, clone, convexHull, cut, err, extrude, fuse, getKernel, getSolids, isClosedWire, isOk, isPlanarWire, isSolid, isValid, isValidSolid, measureVolume, mesh, ok, outerWire, polygon, revolve, rotate, translate, validSolid } from "brepjs";
1
+ import { addHoles, applyMatrix, autoHeal, box, castShape, clone, convexHull, cut, err, extrude, fuse, getBounds, getKernel, getSolids, isClosedWire, isOk, isPlanarWire, isSolid, isValid, isValidSolid, measureVolume, mesh, ok, outerWire, polygon, revolve, rotate, translate, validSolid } from "brepjs";
2
2
  import * as WebIFC from "web-ifc";
3
3
  import { Handle, IfcAPI } from "web-ifc";
4
4
  //#region src/identity/ifcGuid.ts
@@ -14065,6 +14065,7 @@ function buildElementContainmentMap(reader) {
14065
14065
  //#endregion
14066
14066
  //#region src/import/geometryRead.ts
14067
14067
  var NONE = { kind: "NONE" };
14068
+ var testHooks$1 = null;
14068
14069
  /**
14069
14070
  * Reconstructs the `Body` (SweptSolid) representation of a product into a brepjs
14070
14071
  * solid, falling back to tessellated mesh import when no parametric path exists.
@@ -14075,27 +14076,66 @@ var NONE = { kind: "NONE" };
14075
14076
  * single bad element never aborts a model import.
14076
14077
  */
14077
14078
  function readBodyGeometry(reader, productExpressId, scale, diagnostics) {
14079
+ const body = readBodyItems(reader, productExpressId, scale, diagnostics);
14080
+ const selected = body.items.find((item) => item.kind !== "NONE") ?? NONE;
14081
+ for (const item of body.items) if (item !== selected && item.kind === "SOLID") item.solid[Symbol.dispose]();
14082
+ return selected;
14083
+ }
14084
+ /** Reconstructs each supported IFC Body item independently in representation order. */
14085
+ function readBodyItems(reader, productExpressId, scale, diagnostics) {
14078
14086
  const product = reader.getLine(productExpressId);
14079
- if (product === null) return NONE;
14087
+ if (product === null) return {
14088
+ hasBody: false,
14089
+ itemCount: 0,
14090
+ items: []
14091
+ };
14080
14092
  const representationRef = asRef(product["Representation"]);
14081
- if (representationRef === void 0) return NONE;
14093
+ if (representationRef === void 0) return {
14094
+ hasBody: false,
14095
+ itemCount: 0,
14096
+ items: []
14097
+ };
14082
14098
  const productShape = reader.getLine(representationRef.value);
14083
14099
  const representations = productShape === null ? void 0 : asRefArray(productShape["Representations"]);
14084
- if (representations === void 0) return NONE;
14085
- const bodyItemId = findBodyItem(reader, representations);
14086
- if (bodyItemId === void 0) return NONE;
14100
+ if (representations === void 0) return {
14101
+ hasBody: false,
14102
+ itemCount: 0,
14103
+ items: []
14104
+ };
14105
+ const bodyItemIds = findBodyItems(reader, representations);
14106
+ if (bodyItemIds === null) return {
14107
+ hasBody: false,
14108
+ itemCount: 0,
14109
+ items: []
14110
+ };
14087
14111
  const worldTransform = readWorldTransform(reader, product, scale);
14088
- const itemType = reader.getLineType(bodyItemId);
14089
- try {
14090
- if (itemType === WebIFC.IFCEXTRUDEDAREASOLID) return reconstructExtrusion(reader, bodyItemId, scale, worldTransform, diagnostics);
14091
- if (itemType === WebIFC.IFCREVOLVEDAREASOLID) return reconstructRevolution(reader, bodyItemId, scale, worldTransform, diagnostics);
14092
- if (isTessellatedType(itemType)) return reconstructTessellated(reader, productExpressId, diagnostics);
14093
- } catch (e) {
14094
- diagnostics.push(issue("warning", "GEOMETRY_RECONSTRUCTION_FAILED", `Body geometry reconstruction threw: ${errMsg$1(e)}`, productExpressId));
14095
- return NONE;
14112
+ const itemTypes = new Map(bodyItemIds.map((id) => [id, reader.getLineType(id)]));
14113
+ const streamedItemIds = new Set(bodyItemIds.filter((id) => isStreamedTessellatedType(itemTypes.get(id))));
14114
+ let streamedMeshes = /* @__PURE__ */ new Map();
14115
+ if (streamedItemIds.size > 0) try {
14116
+ streamedMeshes = collectItemMeshes(reader, productExpressId, streamedItemIds);
14117
+ } catch (cause) {
14118
+ diagnostics.push(issue("warning", "GEOMETRY_RECONSTRUCTION_FAILED", `Body tessellation stream failed: ${errMsg$1(cause)}`, productExpressId));
14096
14119
  }
14097
- diagnostics.push(issue("warning", "UNSUPPORTED_REPRESENTATION_ITEM", `Unsupported body representation item type ${itemType}; geometry skipped`, productExpressId));
14098
- return NONE;
14120
+ const items = bodyItemIds.map((bodyItemId) => {
14121
+ const itemType = itemTypes.get(bodyItemId);
14122
+ try {
14123
+ if (itemType === WebIFC.IFCEXTRUDEDAREASOLID) return reconstructExtrusion(reader, bodyItemId, scale, worldTransform, diagnostics);
14124
+ if (itemType === WebIFC.IFCREVOLVEDAREASOLID) return reconstructRevolution(reader, bodyItemId, scale, worldTransform, diagnostics);
14125
+ if (itemType === WebIFC.IFCTRIANGULATEDFACESET) return reconstructTriangulatedItem(reader, bodyItemId, scale, worldTransform, diagnostics);
14126
+ if (isStreamedTessellatedType(itemType)) return reconstructStreamedTessellatedItem(streamedMeshes.get(bodyItemId), bodyItemId, diagnostics);
14127
+ } catch (cause) {
14128
+ diagnostics.push(issue("warning", "GEOMETRY_RECONSTRUCTION_FAILED", `Body item ${bodyItemId} reconstruction threw: ${errMsg$1(cause)}`, bodyItemId, { productExpressId }));
14129
+ return NONE;
14130
+ }
14131
+ diagnostics.push(issue("warning", "UNSUPPORTED_REPRESENTATION_ITEM", `Unsupported Body item ${bodyItemId} type ${itemType}; geometry skipped`, bodyItemId, { productExpressId }));
14132
+ return NONE;
14133
+ });
14134
+ return {
14135
+ hasBody: true,
14136
+ itemCount: bodyItemIds.length,
14137
+ items
14138
+ };
14099
14139
  }
14100
14140
  function reconstructExtrusion(reader, extrusionId, scale, worldTransform, diagnostics) {
14101
14141
  const ext = reader.getLine(extrusionId);
@@ -14227,22 +14267,61 @@ function reconstructRevolution(reader, revolutionId, scale, worldTransform, diag
14227
14267
  }
14228
14268
  return finalizeSolid(placed.value, revolutionId, diagnostics);
14229
14269
  }
14230
- function reconstructTessellated(reader, productExpressId, diagnostics) {
14231
- const mesh = collectMesh(reader, productExpressId);
14270
+ function reconstructTriangulatedItem(reader, itemExpressId, scale, worldTransform, diagnostics) {
14271
+ const faceSet = reader.getLine(itemExpressId);
14272
+ const coordinatesRef = faceSet === null ? void 0 : asRef(faceSet["Coordinates"]);
14273
+ const rawPoints = (coordinatesRef === void 0 ? null : reader.getLine(coordinatesRef.value))?.["CoordList"];
14274
+ const rawTriangles = faceSet?.["CoordIndex"];
14275
+ return reconstructTessellatedMesh(readTriangulatedMesh(rawPoints, rawTriangles, scale, worldTransform), 1, itemExpressId, diagnostics);
14276
+ }
14277
+ function reconstructStreamedTessellatedItem(mesh, itemExpressId, diagnostics) {
14278
+ return reconstructTessellatedMesh(mesh ?? null, 1e3, itemExpressId, diagnostics);
14279
+ }
14280
+ function reconstructTessellatedMesh(mesh, scaleToMm, itemExpressId, diagnostics) {
14232
14281
  if (mesh === null || mesh.indices.length === 0) {
14233
- diagnostics.push(issue("warning", "GEOMETRY_RECONSTRUCTION_FAILED", "Tessellated geometry yielded no triangles", productExpressId));
14282
+ diagnostics.push(issue("warning", "GEOMETRY_RECONSTRUCTION_FAILED", "Tessellated geometry yielded no triangles", itemExpressId));
14234
14283
  return NONE;
14235
14284
  }
14236
- const solid = sewMeshToSolid(mesh, 1e3, productExpressId, diagnostics);
14285
+ const stl = packBinaryStl(mesh.vertices, mesh.indices, scaleToMm);
14286
+ let solid = sewMeshToSolid(mesh, scaleToMm, itemExpressId, diagnostics);
14287
+ try {
14288
+ if (solid === null) {
14289
+ const cast = castFreshResult(getKernel().importSTL(new Uint8Array(stl).buffer));
14290
+ if (isSolid(cast)) {
14291
+ const valid = validSolid(cast);
14292
+ if (valid.ok) solid = valid.value;
14293
+ else cast[Symbol.dispose]();
14294
+ } else {
14295
+ const solids = getSolids(cast);
14296
+ if (solids.length === 1 && solids[0] !== void 0) {
14297
+ const copied = clone(solids[0]);
14298
+ if (copied.ok) {
14299
+ const valid = validSolid(copied.value);
14300
+ if (valid.ok) solid = valid.value;
14301
+ else copied.value[Symbol.dispose]();
14302
+ }
14303
+ }
14304
+ cast[Symbol.dispose]();
14305
+ }
14306
+ }
14307
+ } catch (e) {
14308
+ diagnostics.push(issue("info", "TESSELLATION_NOT_MANIFOLD", `STL round-trip failed: ${errMsg$1(e)}`, itemExpressId));
14309
+ }
14237
14310
  if (solid !== null) {
14238
- diagnostics.push(issue("info", "TESSELLATED_MANIFOLD", "Tessellated mesh recovered as a closed solid by sewing its triangles", productExpressId));
14311
+ try {
14312
+ testHooks$1?.afterItemSolid?.(itemExpressId, solid);
14313
+ } catch (cause) {
14314
+ solid[Symbol.dispose]();
14315
+ throw cause;
14316
+ }
14317
+ diagnostics.push(issue("info", "TESSELLATED_MANIFOLD", "Tessellated mesh recovered as a closed solid", itemExpressId));
14239
14318
  return {
14240
14319
  kind: "SOLID",
14241
14320
  solid,
14242
14321
  lossy: true
14243
14322
  };
14244
14323
  }
14245
- diagnostics.push(issue("info", "TESSELLATION_NOT_MANIFOLD", "Tessellated mesh is not closed/manifold; returning raw triangle data (lossy)", productExpressId));
14324
+ diagnostics.push(issue("info", "TESSELLATION_NOT_MANIFOLD", "Tessellated mesh is not closed/manifold; returning raw triangle data (lossy)", itemExpressId));
14246
14325
  return {
14247
14326
  kind: "MESH",
14248
14327
  vertices: mesh.vertices,
@@ -14250,12 +14329,11 @@ function reconstructTessellated(reader, productExpressId, diagnostics) {
14250
14329
  diagnostic: "TESSELLATED_LOSSY"
14251
14330
  };
14252
14331
  }
14253
- /** Loose enough to weld float32 vertices web-ifc emits per face, in mm. */
14332
+ /** Loose enough to weld float32 vertices emitted per face, expressed in millimetres. */
14254
14333
  var MESH_SEW_TOLERANCE_MM = .001;
14255
14334
  /**
14256
14335
  * Brands a fresh kernel result and releases its pre-downcast arena slot when
14257
- * the cast moved to a new one (occt-wasm); in-place kernels share the slot and
14258
- * are left alone. Mirrors brepjs's internal castResultShape.
14336
+ * the cast moved to a new one. In-place kernels share the slot.
14259
14337
  */
14260
14338
  function castFreshResult(raw) {
14261
14339
  const cast = castShape(raw);
@@ -14264,12 +14342,7 @@ function castFreshResult(raw) {
14264
14342
  if (!(rawId !== void 0 && castId !== void 0 ? rawId === castId : cast.wrapped === raw)) getKernel().dispose(raw);
14265
14343
  return cast;
14266
14344
  }
14267
- /**
14268
- * Sews the streamed triangles into a closed solid through the kernel's mesh
14269
- * builder, the same path brepjs's mesh importers use. Null when the mesh is
14270
- * open or the kernel rejects it.
14271
- */
14272
- function sewMeshToSolid(mesh, scaleToMm, productExpressId, diagnostics) {
14345
+ function sewMeshToSolid(mesh, scaleToMm, itemExpressId, diagnostics) {
14273
14346
  const kernel = getKernel();
14274
14347
  const { vertices, indices } = mesh;
14275
14348
  const point = (index) => [
@@ -14278,8 +14351,8 @@ function sewMeshToSolid(mesh, scaleToMm, productExpressId, diagnostics) {
14278
14351
  (vertices[index * 3 + 2] ?? 0) * scaleToMm
14279
14352
  ];
14280
14353
  const triangles = [];
14281
- for (let t = 0; t + 2 < indices.length; t += 3) {
14282
- const face = kernel.buildTriFace(point(indices[t] ?? 0), point(indices[t + 1] ?? 0), point(indices[t + 2] ?? 0));
14354
+ for (let index = 0; index + 2 < indices.length; index += 3) {
14355
+ const face = kernel.buildTriFace(point(indices[index] ?? 0), point(indices[index + 1] ?? 0), point(indices[index + 2] ?? 0));
14283
14356
  if (face !== null) triangles.push(face);
14284
14357
  }
14285
14358
  if (triangles.length === 0) return null;
@@ -14289,38 +14362,45 @@ function sewMeshToSolid(mesh, scaleToMm, productExpressId, diagnostics) {
14289
14362
  if (valid !== null && valid.ok) return valid.value;
14290
14363
  cast[Symbol.dispose]();
14291
14364
  return null;
14292
- } catch (e) {
14293
- diagnostics.push(issue("info", "TESSELLATION_NOT_MANIFOLD", `Mesh sewing failed: ${errMsg$1(e)}`, productExpressId));
14365
+ } catch (cause) {
14366
+ diagnostics.push(issue("info", "TESSELLATION_NOT_MANIFOLD", `Mesh sewing failed: ${errMsg$1(cause)}`, itemExpressId));
14294
14367
  return null;
14295
14368
  } finally {
14296
14369
  for (const face of triangles) kernel.dispose(face);
14297
14370
  }
14298
14371
  }
14299
- function collectMesh(reader, productExpressId) {
14300
- const vertices = [];
14301
- const indices = [];
14302
- let base = 0;
14372
+ /** Streams a product once, then separates web-ifc geometry by Body item express ID. */
14373
+ function collectItemMeshes(reader, productExpressId, itemExpressIds) {
14374
+ const byItem = /* @__PURE__ */ new Map();
14375
+ const soleItemExpressId = itemExpressIds.size === 1 ? [...itemExpressIds][0] : void 0;
14303
14376
  reader.streamMeshes([productExpressId], (flatMesh) => {
14304
14377
  try {
14305
14378
  const geometries = flatMesh.geometries;
14306
- for (let g = 0; g < geometries.size(); g++) {
14307
- const placed = geometries.get(g);
14308
- const geom = reader.getGeometry(placed.geometryExpressID);
14379
+ for (let geometryIndex = 0; geometryIndex < geometries.size(); geometryIndex++) {
14380
+ const placed = geometries.get(geometryIndex);
14381
+ const itemExpressId = itemExpressIds.has(placed.geometryExpressID) ? placed.geometryExpressID : soleItemExpressId;
14382
+ if (itemExpressId === void 0) continue;
14383
+ const accumulator = byItem.get(itemExpressId) ?? {
14384
+ vertices: [],
14385
+ indices: []
14386
+ };
14387
+ byItem.set(itemExpressId, accumulator);
14388
+ const geometry = reader.getGeometry(placed.geometryExpressID);
14309
14389
  try {
14310
- const verts = reader.getVertexArray(geom.GetVertexData(), geom.GetVertexDataSize());
14311
- const idx = reader.getIndexArray(geom.GetIndexData(), geom.GetIndexDataSize());
14312
- const m = placed.flatTransformation;
14313
- const vertexCount = verts.length / 6;
14314
- for (let v = 0; v < vertexCount; v++) {
14315
- const x = verts[v * 6] ?? 0;
14316
- const y = verts[v * 6 + 1] ?? 0;
14317
- const z = verts[v * 6 + 2] ?? 0;
14318
- vertices.push((m[0] ?? 1) * x + (m[4] ?? 0) * y + (m[8] ?? 0) * z + (m[12] ?? 0), (m[1] ?? 0) * x + (m[5] ?? 1) * y + (m[9] ?? 0) * z + (m[13] ?? 0), (m[2] ?? 0) * x + (m[6] ?? 0) * y + (m[10] ?? 1) * z + (m[14] ?? 0));
14390
+ const vertices = reader.getVertexArray(geometry.GetVertexData(), geometry.GetVertexDataSize());
14391
+ const indices = reader.getIndexArray(geometry.GetIndexData(), geometry.GetIndexDataSize());
14392
+ const transform = placed.flatTransformation;
14393
+ const base = accumulator.vertices.length / 3;
14394
+ const vertexCount = vertices.length / 6;
14395
+ for (let vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) {
14396
+ const x = vertices[vertexIndex * 6] ?? 0;
14397
+ const y = vertices[vertexIndex * 6 + 1] ?? 0;
14398
+ const z = vertices[vertexIndex * 6 + 2] ?? 0;
14399
+ accumulator.vertices.push((transform[0] ?? 1) * x + (transform[4] ?? 0) * y + (transform[8] ?? 0) * z + (transform[12] ?? 0), (transform[1] ?? 0) * x + (transform[5] ?? 1) * y + (transform[9] ?? 0) * z + (transform[13] ?? 0), (transform[2] ?? 0) * x + (transform[6] ?? 0) * y + (transform[10] ?? 1) * z + (transform[14] ?? 0));
14319
14400
  }
14320
- for (let i = 0; i < idx.length; i++) indices.push(base + (idx[i] ?? 0));
14321
- base += vertexCount;
14401
+ for (let index = 0; index < indices.length; index++) accumulator.indices.push(base + (indices[index] ?? 0));
14322
14402
  } finally {
14323
- geom.delete();
14403
+ geometry.delete();
14324
14404
  }
14325
14405
  }
14326
14406
  } finally {
@@ -14328,12 +14408,62 @@ function collectMesh(reader, productExpressId) {
14328
14408
  releaseEmbind(flatMesh);
14329
14409
  }
14330
14410
  });
14331
- if (vertices.length === 0) return null;
14411
+ return new Map([...byItem].map(([itemExpressId, mesh]) => [itemExpressId, {
14412
+ vertices: new Float32Array(mesh.vertices),
14413
+ indices: new Uint32Array(mesh.indices)
14414
+ }]));
14415
+ }
14416
+ function readTriangulatedMesh(rawPoints, rawTriangles, scale, worldTransform) {
14417
+ if (!Array.isArray(rawPoints) || !Array.isArray(rawTriangles)) return null;
14418
+ const vertices = [];
14419
+ const indices = [];
14420
+ const lengthFactor = scale * 1e3;
14421
+ for (const rawPoint of rawPoints) {
14422
+ if (!Array.isArray(rawPoint) || rawPoint.length < 3) return null;
14423
+ const placed = transformPoint([
14424
+ (readMeasure(rawPoint[0]) ?? 0) * lengthFactor,
14425
+ (readMeasure(rawPoint[1]) ?? 0) * lengthFactor,
14426
+ (readMeasure(rawPoint[2]) ?? 0) * lengthFactor
14427
+ ], worldTransform);
14428
+ vertices.push(placed[0], placed[1], placed[2]);
14429
+ }
14430
+ for (const rawTriangle of rawTriangles) {
14431
+ if (!Array.isArray(rawTriangle) || rawTriangle.length < 3) return null;
14432
+ indices.push((readMeasure(rawTriangle[0]) ?? 1) - 1, (readMeasure(rawTriangle[1]) ?? 1) - 1, (readMeasure(rawTriangle[2]) ?? 1) - 1);
14433
+ }
14332
14434
  return {
14333
14435
  vertices: new Float32Array(vertices),
14334
14436
  indices: new Uint32Array(indices)
14335
14437
  };
14336
14438
  }
14439
+ function transformPoint(point, transform) {
14440
+ if (transform === null) return point;
14441
+ const m = transform.linear;
14442
+ return [
14443
+ (m[0] ?? 1) * point[0] + (m[1] ?? 0) * point[1] + (m[2] ?? 0) * point[2] + transform.translation[0],
14444
+ (m[3] ?? 0) * point[0] + (m[4] ?? 1) * point[1] + (m[5] ?? 0) * point[2] + transform.translation[1],
14445
+ (m[6] ?? 0) * point[0] + (m[7] ?? 0) * point[1] + (m[8] ?? 1) * point[2] + transform.translation[2]
14446
+ ];
14447
+ }
14448
+ function packBinaryStl(vertices, indices, scaleToMm) {
14449
+ const triCount = Math.floor(indices.length / 3);
14450
+ const buffer = /* @__PURE__ */ new ArrayBuffer(84 + triCount * 50);
14451
+ const view = new DataView(buffer);
14452
+ view.setUint32(80, triCount, true);
14453
+ let offset = 84;
14454
+ for (let t = 0; t < triCount; t++) {
14455
+ offset += 12;
14456
+ for (let c = 0; c < 3; c++) {
14457
+ const vi = (indices[t * 3 + c] ?? 0) * 3;
14458
+ view.setFloat32(offset, (vertices[vi] ?? 0) * scaleToMm, true);
14459
+ view.setFloat32(offset + 4, (vertices[vi + 1] ?? 0) * scaleToMm, true);
14460
+ view.setFloat32(offset + 8, (vertices[vi + 2] ?? 0) * scaleToMm, true);
14461
+ offset += 12;
14462
+ }
14463
+ offset += 2;
14464
+ }
14465
+ return new Uint8Array(buffer);
14466
+ }
14337
14467
  var PARAMETRIC_PROFILE_BUILDERS = /* @__PURE__ */ new Map([
14338
14468
  [WebIFC.IFCRECTANGLEPROFILEDEF, (f) => ({
14339
14469
  kind: "RECTANGULAR",
@@ -14686,15 +14816,16 @@ function isIdentity(t) {
14686
14816
  for (let i = 0; i < 9; i++) if (Math.abs((t.linear[i] ?? 0) - (expected[i] ?? 0)) > 1e-9) return false;
14687
14817
  return Math.abs(t.translation[0]) < 1e-9 && Math.abs(t.translation[1]) < 1e-9 && Math.abs(t.translation[2]) < 1e-9;
14688
14818
  }
14689
- function findBodyItem(reader, representations) {
14819
+ function findBodyItems(reader, representations) {
14690
14820
  for (const repRef of representations) {
14691
14821
  const rep = reader.getLine(repRef.value);
14692
14822
  if (rep === null) continue;
14693
14823
  if (readLabel(rep["RepresentationIdentifier"]) !== "Body") continue;
14694
14824
  const items = asRefArray(rep["Items"]);
14695
- if (items === void 0 || items.length === 0) continue;
14696
- return items[0]?.value;
14825
+ if (items === void 0) return [];
14826
+ return items.map((item) => item.value);
14697
14827
  }
14828
+ return null;
14698
14829
  }
14699
14830
  function readPolylinePoints(reader, ref, scale) {
14700
14831
  const curveRef = asRef(ref);
@@ -14799,8 +14930,8 @@ function readLabel(value) {
14799
14930
  if (typeof v === "string") return v;
14800
14931
  }
14801
14932
  }
14802
- function isTessellatedType(type) {
14803
- return type === WebIFC.IFCTRIANGULATEDFACESET || type === WebIFC.IFCPOLYGONALFACESET || type === WebIFC.IFCFACEBASEDSURFACEMODEL;
14933
+ function isStreamedTessellatedType(type) {
14934
+ return type === WebIFC.IFCPOLYGONALFACESET || type === WebIFC.IFCFACEBASEDSURFACEMODEL;
14804
14935
  }
14805
14936
  function normalize(v) {
14806
14937
  const len = Math.hypot(v[0], v[1], v[2]);
@@ -15139,6 +15270,7 @@ function readOwnerHistory(reader, ownerHistoryExpressId) {
15139
15270
  }
15140
15271
  //#endregion
15141
15272
  //#region src/import/fromIfc.ts
15273
+ var testHooks = null;
15142
15274
  /**
15143
15275
  * Each enumerated physical-element IFC type and the {@link ImportedElementCategory}
15144
15276
  * it maps to. Curtain-wall sub-components (IfcPlate/IfcMember) and stair/ramp
@@ -15187,6 +15319,7 @@ async function fromIfc(bytes, options = {}) {
15187
15319
  const readerResult = await SpfReader.create(bytes, settings);
15188
15320
  if (!readerResult.ok) return err(readerResult.error);
15189
15321
  const reader = readerResult.value;
15322
+ const elements = [];
15190
15323
  try {
15191
15324
  reader.buildGuidMap();
15192
15325
  const diagnostics = [];
@@ -15195,13 +15328,13 @@ async function fromIfc(bytes, options = {}) {
15195
15328
  const spatialRoot = buildSpatialTree(reader, scale);
15196
15329
  const containment = buildElementContainmentMap(reader);
15197
15330
  const typeEnums = buildTypePredefinedMap(reader);
15198
- const elements = [];
15199
15331
  const byExpressId = /* @__PURE__ */ new Map();
15200
15332
  for (const [type, category] of ELEMENT_TYPES) for (const expressId of reader.getLinesOfType(type)) {
15201
15333
  const element = readElement(reader, expressId, category, scale, containment, typeEnums, options.skipGeometry ?? false, diagnostics);
15202
15334
  if (element === null) continue;
15203
15335
  elements.push(element);
15204
15336
  byExpressId.set(element.expressId, element);
15337
+ testHooks?.afterElement?.(element, elements.length);
15205
15338
  }
15206
15339
  const report = appendIssues(emptyReport(), diagnostics);
15207
15340
  const applicationName = readApplicationName(reader);
@@ -15215,6 +15348,7 @@ async function fromIfc(bytes, options = {}) {
15215
15348
  };
15216
15349
  return ok(model);
15217
15350
  } catch (e) {
15351
+ disposeElements(elements);
15218
15352
  return err(importError("IMPORT_FAILED", "Unexpected failure during IFC import", e));
15219
15353
  } finally {
15220
15354
  reader.close();
@@ -15226,6 +15360,7 @@ async function fromIfc(bytes, options = {}) {
15226
15360
  * sub-read failure is isolated by its own reader and never aborts the element.
15227
15361
  */
15228
15362
  function readElement(reader, expressId, category, scale, containment, typeEnums, skipGeometry, diagnostics) {
15363
+ let geometry = null;
15229
15364
  try {
15230
15365
  const line = reader.getLine(expressId);
15231
15366
  if (line === null) {
@@ -15237,10 +15372,15 @@ function readElement(reader, expressId, category, scale, containment, typeEnums,
15237
15372
  const predefinedType = readPredefinedType(line) ?? typeEnums.get(expressId);
15238
15373
  const voidedBy = readVoids(reader, expressId).map((v) => v.openingExpressId);
15239
15374
  const fills = findFills(reader, expressId);
15240
- const geometry = skipGeometry ? {
15375
+ geometry = skipGeometry ? {
15241
15376
  fidelity: "NONE",
15242
- solid: null
15377
+ completeness: "NONE",
15378
+ solids: [],
15379
+ solid: null,
15380
+ bounds: null,
15381
+ volumeMm3: null
15243
15382
  } : reconstructGeometry(reader, expressId, scale, voidedBy, diagnostics);
15383
+ testHooks?.afterGeometry?.(expressId, geometry);
15244
15384
  const psets = readPsets(reader, expressId).map(toImportedPset);
15245
15385
  const material = readMaterial(reader, expressId, scale);
15246
15386
  const classification = readClassification(reader, expressId);
@@ -15263,6 +15403,7 @@ function readElement(reader, expressId, category, scale, containment, typeEnums,
15263
15403
  ...fills !== void 0 ? { fills } : {}
15264
15404
  };
15265
15405
  } catch (e) {
15406
+ if (geometry !== null) disposeGeometry(geometry);
15266
15407
  diagnostics.push(issue("error", "ELEMENT_READ_FAILED", `Element ${expressId} reconstruction threw: ${errMsg(e)}`, expressId));
15267
15408
  return null;
15268
15409
  }
@@ -15284,42 +15425,134 @@ function findFills(reader, elementExpressId) {
15284
15425
  * model. Falls back to the uncut solid (with a diagnostic) if a cut fails.
15285
15426
  */
15286
15427
  function reconstructGeometry(reader, expressId, scale, voidedBy, diagnostics) {
15287
- const base = toImportedGeometry(readBodyGeometry(reader, expressId, scale, diagnostics));
15288
- if (base.fidelity !== "PARAMETRIC" || base.solid === null || voidedBy.length === 0) return base;
15289
- let host = base.solid;
15428
+ const base = toImportedGeometry(reader, expressId, scale, diagnostics);
15429
+ if (base.fidelity !== "PARAMETRIC" || base.completeness !== "COMPLETE" || voidedBy.length === 0) return base;
15430
+ const firstHost = base.solids[0];
15431
+ if (firstHost === void 0) return base;
15432
+ const hosts = [firstHost, ...base.solids.slice(1)];
15290
15433
  for (const openingId of voidedBy) {
15291
15434
  const opening = readBodyGeometry(reader, openingId, scale, diagnostics);
15292
15435
  if (opening.kind !== "SOLID") continue;
15293
- const cutResult = cut(host, opening.solid);
15294
- opening.solid[Symbol.dispose]();
15295
- if (!cutResult.ok) {
15296
- diagnostics.push(issue("warning", "VOID_SUBTRACTION_FAILED", `Opening ${openingId} could not be subtracted from element ${expressId}: ${cutResult.error.message}`, expressId));
15297
- continue;
15298
- }
15299
- host[Symbol.dispose]();
15300
- host = cutResult.value;
15301
- }
15436
+ try {
15437
+ for (let hostIndex = 0; hostIndex < hosts.length; hostIndex++) {
15438
+ const host = hosts[hostIndex];
15439
+ if (host === void 0) continue;
15440
+ const cutResult = cut(host, opening.solid);
15441
+ if (!cutResult.ok) {
15442
+ diagnostics.push(issue("warning", "VOID_SUBTRACTION_FAILED", `Opening ${openingId} could not be subtracted from element ${expressId}: ${cutResult.error.message}`, expressId));
15443
+ continue;
15444
+ }
15445
+ host[Symbol.dispose]();
15446
+ hosts[hostIndex] = cutResult.value;
15447
+ }
15448
+ } finally {
15449
+ opening.solid[Symbol.dispose]();
15450
+ }
15451
+ }
15452
+ return completeImportedGeometry("PARAMETRIC", hosts, expressId, diagnostics);
15453
+ }
15454
+ function toImportedGeometry(reader, expressId, scale, diagnostics) {
15455
+ const body = readBodyItems(reader, expressId, scale, diagnostics);
15456
+ const solids = [];
15457
+ let hasTessellatedSolid = false;
15458
+ let lossyMesh = null;
15459
+ for (const item of body.items) if (item.kind === "SOLID") {
15460
+ solids.push(item.solid);
15461
+ hasTessellatedSolid ||= item.lossy;
15462
+ } else if (item.kind === "MESH") {
15463
+ const meshAccumulator = lossyMesh ?? {
15464
+ vertices: [],
15465
+ indices: []
15466
+ };
15467
+ lossyMesh = meshAccumulator;
15468
+ const vertexOffset = meshAccumulator.vertices.length / 3;
15469
+ for (const vertex of item.vertices) meshAccumulator.vertices.push(vertex);
15470
+ for (const index of item.indices) meshAccumulator.indices.push(vertexOffset + index);
15471
+ }
15472
+ const completeness = body.hasBody && body.itemCount > 0 && solids.length === body.itemCount ? "COMPLETE" : solids.length > 0 ? "PARTIAL" : "NONE";
15473
+ if (completeness === "PARTIAL") diagnostics.push(issue("warning", "PARTIAL_BODY_RECONSTRUCTION", `Reconstructed ${solids.length} of ${body.itemCount} Body items`, expressId, {
15474
+ reconstructedItems: solids.length,
15475
+ bodyItems: body.itemCount
15476
+ }));
15477
+ else if (completeness === "NONE" && body.hasBody) diagnostics.push(issue("warning", "BODY_RECONSTRUCTION_NONE", `The Product has ${body.itemCount} Body item(s), but none reconstructed as solids`, expressId, { bodyItems: body.itemCount }));
15478
+ const fidelity = lossyMesh !== null ? "TESSELLATED_LOSSY" : hasTessellatedSolid ? "TESSELLATED_MANIFOLD" : solids.length > 0 ? "PARAMETRIC" : "NONE";
15479
+ const aggregate = completeness === "COMPLETE" ? measureCompleteBody(solids, expressId, diagnostics) : {
15480
+ bounds: null,
15481
+ volumeMm3: null
15482
+ };
15302
15483
  return {
15303
- fidelity: "PARAMETRIC",
15304
- solid: host
15484
+ fidelity,
15485
+ completeness,
15486
+ solids,
15487
+ solid: completeness === "COMPLETE" && solids.length === 1 ? solids[0] ?? null : null,
15488
+ ...aggregate,
15489
+ ...lossyMesh !== null ? {
15490
+ meshVertices: new Float32Array(lossyMesh.vertices),
15491
+ meshIndices: new Uint32Array(lossyMesh.indices)
15492
+ } : {}
15305
15493
  };
15306
15494
  }
15307
- function toImportedGeometry(result) {
15308
- if (result.kind === "SOLID") return {
15309
- fidelity: result.lossy ? "TESSELLATED_MANIFOLD" : "PARAMETRIC",
15310
- solid: result.solid
15311
- };
15312
- if (result.kind === "MESH") return {
15313
- fidelity: "TESSELLATED_LOSSY",
15314
- solid: null,
15315
- meshVertices: result.vertices,
15316
- meshIndices: result.indices
15317
- };
15495
+ function completeImportedGeometry(fidelity, solids, expressId, diagnostics) {
15318
15496
  return {
15319
- fidelity: "NONE",
15320
- solid: null
15497
+ fidelity,
15498
+ completeness: "COMPLETE",
15499
+ solids,
15500
+ solid: solids.length === 1 ? solids[0] : null,
15501
+ ...measureCompleteBody(solids, expressId, diagnostics)
15321
15502
  };
15322
15503
  }
15504
+ function measureCompleteBody(solids, expressId, diagnostics) {
15505
+ try {
15506
+ const first = solids[0];
15507
+ if (first === void 0) return {
15508
+ bounds: null,
15509
+ volumeMm3: null
15510
+ };
15511
+ const initial = getBounds(first);
15512
+ let xMin = initial.xMin;
15513
+ let xMax = initial.xMax;
15514
+ let yMin = initial.yMin;
15515
+ let yMax = initial.yMax;
15516
+ let zMin = initial.zMin;
15517
+ let zMax = initial.zMax;
15518
+ let volumeMm3 = 0;
15519
+ for (const solid of solids) {
15520
+ const measured = measureVolume(solid);
15521
+ if (!measured.ok) throw new Error(measured.error.message);
15522
+ volumeMm3 += measured.value;
15523
+ const itemBounds = getBounds(solid);
15524
+ xMin = Math.min(xMin, itemBounds.xMin);
15525
+ xMax = Math.max(xMax, itemBounds.xMax);
15526
+ yMin = Math.min(yMin, itemBounds.yMin);
15527
+ yMax = Math.max(yMax, itemBounds.yMax);
15528
+ zMin = Math.min(zMin, itemBounds.zMin);
15529
+ zMax = Math.max(zMax, itemBounds.zMax);
15530
+ }
15531
+ return {
15532
+ bounds: {
15533
+ xMin,
15534
+ xMax,
15535
+ yMin,
15536
+ yMax,
15537
+ zMin,
15538
+ zMax
15539
+ },
15540
+ volumeMm3
15541
+ };
15542
+ } catch (cause) {
15543
+ diagnostics.push(issue("warning", "BODY_AGGREGATE_MEASUREMENT_FAILED", `Complete Body aggregate measurement failed: ${errMsg(cause)}`, expressId));
15544
+ return {
15545
+ bounds: null,
15546
+ volumeMm3: null
15547
+ };
15548
+ }
15549
+ }
15550
+ function disposeGeometry(geometry) {
15551
+ for (const solid of geometry.solids) solid[Symbol.dispose]();
15552
+ }
15553
+ function disposeElements(elements) {
15554
+ for (const element of elements) disposeGeometry(element.geometry);
15555
+ }
15323
15556
  function toImportedPset(pset) {
15324
15557
  return {
15325
15558
  name: pset.name,
@@ -15397,7 +15630,7 @@ function errMsg(e) {
15397
15630
  * with the model (it is not reclaimed automatically when the model is GC'd).
15398
15631
  */
15399
15632
  function disposeImportedModel(model) {
15400
- for (const el of model.elements) el.geometry.solid?.[Symbol.dispose]();
15633
+ for (const el of model.elements) for (const solid of el.geometry.solids) solid[Symbol.dispose]();
15401
15634
  }
15402
15635
  //#endregion
15403
15636
  //#region src/specs/materialSpec.ts
@@ -1,6 +1,6 @@
1
1
  import { Result } from 'brepjs';
2
2
  import { BimError } from '../errors/bimError.js';
3
- import { ImportedModel } from './importedModel.js';
3
+ import { ImportedModel, ImportedElement, ImportedGeometry } from './importedModel.js';
4
4
  export type { ImportedModel } from './importedModel.js';
5
5
  export interface FromIfcOptions {
6
6
  /** Activate web-ifc's large-coordinate recentering on open. Default false. */
@@ -8,6 +8,12 @@ export interface FromIfcOptions {
8
8
  /** Skip body-geometry reconstruction for fast metadata-only reads. Default false. */
9
9
  readonly skipGeometry?: boolean | undefined;
10
10
  }
11
+ export interface FromIfcTestHooks {
12
+ readonly afterGeometry?: ((expressId: number, geometry: ImportedGeometry) => void) | undefined;
13
+ readonly afterElement?: ((element: ImportedElement, accumulatedCount: number) => void) | undefined;
14
+ }
15
+ /** Package-internal deterministic failure seam for import ownership tests. */
16
+ export declare function setFromIfcTestHooksForTesting(hooks: FromIfcTestHooks | null): void;
11
17
  /**
12
18
  * Reads an IFC STEP-SPF byte buffer into an {@link ImportedModel}: schema, unit
13
19
  * scale, spatial tree, and one {@link ImportedElement} per physical product
@@ -7,7 +7,7 @@ import { SpfReader } from './spfReader.js';
7
7
  * Outcome of reconstructing a single product's body geometry.
8
8
  *
9
9
  * - `SOLID` — a brepjs ValidSolid was rebuilt (parametrically from a swept
10
- * solid, or by a manifold STL round-trip of a tessellated mesh).
10
+ * solid, or by sewing a manifold tessellated mesh).
11
11
  * - `MESH` — geometry exists but could not be turned into a closed solid;
12
12
  * raw triangle data is returned and flagged lossy via `diagnostic`.
13
13
  * - `NONE` — the product carries no recognised body representation.
@@ -24,6 +24,16 @@ export type GeometryResult = {
24
24
  } | {
25
25
  readonly kind: 'NONE';
26
26
  };
27
+ export interface BodyGeometryItems {
28
+ readonly hasBody: boolean;
29
+ readonly itemCount: number;
30
+ readonly items: readonly GeometryResult[];
31
+ }
32
+ export interface GeometryReadTestHooks {
33
+ readonly afterItemSolid?: ((itemExpressId: number, solid: ValidSolid) => void) | undefined;
34
+ }
35
+ /** Package-internal deterministic failure seam for import ownership tests. */
36
+ export declare function setGeometryReadTestHooksForTesting(hooks: GeometryReadTestHooks | null): void;
27
37
  /**
28
38
  * Reconstructs the `Body` (SweptSolid) representation of a product into a brepjs
29
39
  * solid, falling back to tessellated mesh import when no parametric path exists.
@@ -34,6 +44,8 @@ export type GeometryResult = {
34
44
  * single bad element never aborts a model import.
35
45
  */
36
46
  export declare function readBodyGeometry(reader: SpfReader, productExpressId: number, scale: number, diagnostics: ValidationIssue[]): GeometryResult;
47
+ /** Reconstructs each supported IFC Body item independently in representation order. */
48
+ export declare function readBodyItems(reader: SpfReader, productExpressId: number, scale: number, diagnostics: ValidationIssue[]): BodyGeometryItems;
37
49
  /**
38
50
  * Reads an IfcProfileDef into a brepjs Profile (mm). Mirrors the writer's
39
51
  * profile-def emission (geometryWriter.writeProfile / profileDefWriter).