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.
@@ -535,7 +535,7 @@ var propertyKeyTypes = /* @__PURE__*/ new Set([
535
535
  function escapeRegex(str) {
536
536
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
537
537
  }
538
- function clone$1(inst, def, params) {
538
+ function clone$2(inst, def, params) {
539
539
  const cl = new inst._zod.constr(def ?? inst._zod.def);
540
540
  if (!def || params?.parent) cl._zod.parent = inst;
541
541
  return cl;
@@ -571,7 +571,7 @@ function pick(schema, mask) {
571
571
  const currDef = schema._zod.def;
572
572
  const checks = currDef.checks;
573
573
  if (checks && checks.length > 0) throw new Error(".pick() cannot be used on object schemas containing refinements");
574
- return clone$1(schema, mergeDefs(schema._zod.def, {
574
+ return clone$2(schema, mergeDefs(schema._zod.def, {
575
575
  get shape() {
576
576
  const newShape = {};
577
577
  for (const key in mask) {
@@ -589,7 +589,7 @@ function omit(schema, mask) {
589
589
  const currDef = schema._zod.def;
590
590
  const checks = currDef.checks;
591
591
  if (checks && checks.length > 0) throw new Error(".omit() cannot be used on object schemas containing refinements");
592
- return clone$1(schema, mergeDefs(schema._zod.def, {
592
+ return clone$2(schema, mergeDefs(schema._zod.def, {
593
593
  get shape() {
594
594
  const newShape = { ...schema._zod.def.shape };
595
595
  for (const key in mask) {
@@ -610,7 +610,7 @@ function extend(schema, shape) {
610
610
  const existingShape = schema._zod.def.shape;
611
611
  for (const key in shape) if (Object.getOwnPropertyDescriptor(existingShape, key) !== void 0) throw new Error("Cannot overwrite keys on object schemas containing refinements. Use `.safeExtend()` instead.");
612
612
  }
613
- return clone$1(schema, mergeDefs(schema._zod.def, { get shape() {
613
+ return clone$2(schema, mergeDefs(schema._zod.def, { get shape() {
614
614
  const _shape = {
615
615
  ...schema._zod.def.shape,
616
616
  ...shape
@@ -621,7 +621,7 @@ function extend(schema, shape) {
621
621
  }
622
622
  function safeExtend(schema, shape) {
623
623
  if (!isPlainObject(shape)) throw new Error("Invalid input to safeExtend: expected a plain object");
624
- return clone$1(schema, mergeDefs(schema._zod.def, { get shape() {
624
+ return clone$2(schema, mergeDefs(schema._zod.def, { get shape() {
625
625
  const _shape = {
626
626
  ...schema._zod.def.shape,
627
627
  ...shape
@@ -632,7 +632,7 @@ function safeExtend(schema, shape) {
632
632
  }
633
633
  function merge(a, b) {
634
634
  if (a._zod.def.checks?.length) throw new Error(".merge() cannot be used on object schemas containing refinements. Use .safeExtend() instead.");
635
- return clone$1(a, mergeDefs(a._zod.def, {
635
+ return clone$2(a, mergeDefs(a._zod.def, {
636
636
  get shape() {
637
637
  const _shape = {
638
638
  ...a._zod.def.shape,
@@ -650,7 +650,7 @@ function merge(a, b) {
650
650
  function partial(Class, schema, mask) {
651
651
  const checks = schema._zod.def.checks;
652
652
  if (checks && checks.length > 0) throw new Error(".partial() cannot be used on object schemas containing refinements");
653
- return clone$1(schema, mergeDefs(schema._zod.def, {
653
+ return clone$2(schema, mergeDefs(schema._zod.def, {
654
654
  get shape() {
655
655
  const oldShape = schema._zod.def.shape;
656
656
  const shape = { ...oldShape };
@@ -673,7 +673,7 @@ function partial(Class, schema, mask) {
673
673
  }));
674
674
  }
675
675
  function required(Class, schema, mask) {
676
- return clone$1(schema, mergeDefs(schema._zod.def, { get shape() {
676
+ return clone$2(schema, mergeDefs(schema._zod.def, { get shape() {
677
677
  const oldShape = schema._zod.def.shape;
678
678
  const shape = { ...oldShape };
679
679
  if (mask) for (const key in mask) {
@@ -4042,7 +4042,7 @@ var ZodType = /*@__PURE__*/ $constructor("ZodType", (inst, def) => {
4042
4042
  return this.check(...chks);
4043
4043
  },
4044
4044
  clone(def, params) {
4045
- return clone$1(this, def, params);
4045
+ return clone$2(this, def, params);
4046
4046
  },
4047
4047
  brand() {
4048
4048
  return this;
@@ -14088,6 +14088,7 @@ function buildElementContainmentMap(reader) {
14088
14088
  //#endregion
14089
14089
  //#region src/import/geometryRead.ts
14090
14090
  var NONE = { kind: "NONE" };
14091
+ var testHooks$1 = null;
14091
14092
  /**
14092
14093
  * Reconstructs the `Body` (SweptSolid) representation of a product into a brepjs
14093
14094
  * solid, falling back to tessellated mesh import when no parametric path exists.
@@ -14098,27 +14099,66 @@ var NONE = { kind: "NONE" };
14098
14099
  * single bad element never aborts a model import.
14099
14100
  */
14100
14101
  function readBodyGeometry(reader, productExpressId, scale, diagnostics) {
14102
+ const body = readBodyItems(reader, productExpressId, scale, diagnostics);
14103
+ const selected = body.items.find((item) => item.kind !== "NONE") ?? NONE;
14104
+ for (const item of body.items) if (item !== selected && item.kind === "SOLID") item.solid[Symbol.dispose]();
14105
+ return selected;
14106
+ }
14107
+ /** Reconstructs each supported IFC Body item independently in representation order. */
14108
+ function readBodyItems(reader, productExpressId, scale, diagnostics) {
14101
14109
  const product = reader.getLine(productExpressId);
14102
- if (product === null) return NONE;
14110
+ if (product === null) return {
14111
+ hasBody: false,
14112
+ itemCount: 0,
14113
+ items: []
14114
+ };
14103
14115
  const representationRef = asRef(product["Representation"]);
14104
- if (representationRef === void 0) return NONE;
14116
+ if (representationRef === void 0) return {
14117
+ hasBody: false,
14118
+ itemCount: 0,
14119
+ items: []
14120
+ };
14105
14121
  const productShape = reader.getLine(representationRef.value);
14106
14122
  const representations = productShape === null ? void 0 : asRefArray(productShape["Representations"]);
14107
- if (representations === void 0) return NONE;
14108
- const bodyItemId = findBodyItem(reader, representations);
14109
- if (bodyItemId === void 0) return NONE;
14123
+ if (representations === void 0) return {
14124
+ hasBody: false,
14125
+ itemCount: 0,
14126
+ items: []
14127
+ };
14128
+ const bodyItemIds = findBodyItems(reader, representations);
14129
+ if (bodyItemIds === null) return {
14130
+ hasBody: false,
14131
+ itemCount: 0,
14132
+ items: []
14133
+ };
14110
14134
  const worldTransform = readWorldTransform(reader, product, scale);
14111
- const itemType = reader.getLineType(bodyItemId);
14112
- try {
14113
- if (itemType === web_ifc.IFCEXTRUDEDAREASOLID) return reconstructExtrusion(reader, bodyItemId, scale, worldTransform, diagnostics);
14114
- if (itemType === web_ifc.IFCREVOLVEDAREASOLID) return reconstructRevolution(reader, bodyItemId, scale, worldTransform, diagnostics);
14115
- if (isTessellatedType(itemType)) return reconstructTessellated(reader, productExpressId, diagnostics);
14116
- } catch (e) {
14117
- diagnostics.push(issue("warning", "GEOMETRY_RECONSTRUCTION_FAILED", `Body geometry reconstruction threw: ${errMsg$1(e)}`, productExpressId));
14118
- return NONE;
14135
+ const itemTypes = new Map(bodyItemIds.map((id) => [id, reader.getLineType(id)]));
14136
+ const streamedItemIds = new Set(bodyItemIds.filter((id) => isStreamedTessellatedType(itemTypes.get(id))));
14137
+ let streamedMeshes = /* @__PURE__ */ new Map();
14138
+ if (streamedItemIds.size > 0) try {
14139
+ streamedMeshes = collectItemMeshes(reader, productExpressId, streamedItemIds);
14140
+ } catch (cause) {
14141
+ diagnostics.push(issue("warning", "GEOMETRY_RECONSTRUCTION_FAILED", `Body tessellation stream failed: ${errMsg$1(cause)}`, productExpressId));
14119
14142
  }
14120
- diagnostics.push(issue("warning", "UNSUPPORTED_REPRESENTATION_ITEM", `Unsupported body representation item type ${itemType}; geometry skipped`, productExpressId));
14121
- return NONE;
14143
+ const items = bodyItemIds.map((bodyItemId) => {
14144
+ const itemType = itemTypes.get(bodyItemId);
14145
+ try {
14146
+ if (itemType === web_ifc.IFCEXTRUDEDAREASOLID) return reconstructExtrusion(reader, bodyItemId, scale, worldTransform, diagnostics);
14147
+ if (itemType === web_ifc.IFCREVOLVEDAREASOLID) return reconstructRevolution(reader, bodyItemId, scale, worldTransform, diagnostics);
14148
+ if (itemType === web_ifc.IFCTRIANGULATEDFACESET) return reconstructTriangulatedItem(reader, bodyItemId, scale, worldTransform, diagnostics);
14149
+ if (isStreamedTessellatedType(itemType)) return reconstructStreamedTessellatedItem(streamedMeshes.get(bodyItemId), bodyItemId, diagnostics);
14150
+ } catch (cause) {
14151
+ diagnostics.push(issue("warning", "GEOMETRY_RECONSTRUCTION_FAILED", `Body item ${bodyItemId} reconstruction threw: ${errMsg$1(cause)}`, bodyItemId, { productExpressId }));
14152
+ return NONE;
14153
+ }
14154
+ diagnostics.push(issue("warning", "UNSUPPORTED_REPRESENTATION_ITEM", `Unsupported Body item ${bodyItemId} type ${itemType}; geometry skipped`, bodyItemId, { productExpressId }));
14155
+ return NONE;
14156
+ });
14157
+ return {
14158
+ hasBody: true,
14159
+ itemCount: bodyItemIds.length,
14160
+ items
14161
+ };
14122
14162
  }
14123
14163
  function reconstructExtrusion(reader, extrusionId, scale, worldTransform, diagnostics) {
14124
14164
  const ext = reader.getLine(extrusionId);
@@ -14250,22 +14290,61 @@ function reconstructRevolution(reader, revolutionId, scale, worldTransform, diag
14250
14290
  }
14251
14291
  return finalizeSolid(placed.value, revolutionId, diagnostics);
14252
14292
  }
14253
- function reconstructTessellated(reader, productExpressId, diagnostics) {
14254
- const mesh = collectMesh(reader, productExpressId);
14293
+ function reconstructTriangulatedItem(reader, itemExpressId, scale, worldTransform, diagnostics) {
14294
+ const faceSet = reader.getLine(itemExpressId);
14295
+ const coordinatesRef = faceSet === null ? void 0 : asRef(faceSet["Coordinates"]);
14296
+ const rawPoints = (coordinatesRef === void 0 ? null : reader.getLine(coordinatesRef.value))?.["CoordList"];
14297
+ const rawTriangles = faceSet?.["CoordIndex"];
14298
+ return reconstructTessellatedMesh(readTriangulatedMesh(rawPoints, rawTriangles, scale, worldTransform), 1, itemExpressId, diagnostics);
14299
+ }
14300
+ function reconstructStreamedTessellatedItem(mesh, itemExpressId, diagnostics) {
14301
+ return reconstructTessellatedMesh(mesh ?? null, 1e3, itemExpressId, diagnostics);
14302
+ }
14303
+ function reconstructTessellatedMesh(mesh, scaleToMm, itemExpressId, diagnostics) {
14255
14304
  if (mesh === null || mesh.indices.length === 0) {
14256
- diagnostics.push(issue("warning", "GEOMETRY_RECONSTRUCTION_FAILED", "Tessellated geometry yielded no triangles", productExpressId));
14305
+ diagnostics.push(issue("warning", "GEOMETRY_RECONSTRUCTION_FAILED", "Tessellated geometry yielded no triangles", itemExpressId));
14257
14306
  return NONE;
14258
14307
  }
14259
- const solid = sewMeshToSolid(mesh, 1e3, productExpressId, diagnostics);
14308
+ const stl = packBinaryStl(mesh.vertices, mesh.indices, scaleToMm);
14309
+ let solid = sewMeshToSolid(mesh, scaleToMm, itemExpressId, diagnostics);
14310
+ try {
14311
+ if (solid === null) {
14312
+ const cast = castFreshResult((0, brepjs.getKernel)().importSTL(new Uint8Array(stl).buffer));
14313
+ if ((0, brepjs.isSolid)(cast)) {
14314
+ const valid = (0, brepjs.validSolid)(cast);
14315
+ if (valid.ok) solid = valid.value;
14316
+ else cast[Symbol.dispose]();
14317
+ } else {
14318
+ const solids = (0, brepjs.getSolids)(cast);
14319
+ if (solids.length === 1 && solids[0] !== void 0) {
14320
+ const copied = (0, brepjs.clone)(solids[0]);
14321
+ if (copied.ok) {
14322
+ const valid = (0, brepjs.validSolid)(copied.value);
14323
+ if (valid.ok) solid = valid.value;
14324
+ else copied.value[Symbol.dispose]();
14325
+ }
14326
+ }
14327
+ cast[Symbol.dispose]();
14328
+ }
14329
+ }
14330
+ } catch (e) {
14331
+ diagnostics.push(issue("info", "TESSELLATION_NOT_MANIFOLD", `STL round-trip failed: ${errMsg$1(e)}`, itemExpressId));
14332
+ }
14260
14333
  if (solid !== null) {
14261
- diagnostics.push(issue("info", "TESSELLATED_MANIFOLD", "Tessellated mesh recovered as a closed solid by sewing its triangles", productExpressId));
14334
+ try {
14335
+ testHooks$1?.afterItemSolid?.(itemExpressId, solid);
14336
+ } catch (cause) {
14337
+ solid[Symbol.dispose]();
14338
+ throw cause;
14339
+ }
14340
+ diagnostics.push(issue("info", "TESSELLATED_MANIFOLD", "Tessellated mesh recovered as a closed solid", itemExpressId));
14262
14341
  return {
14263
14342
  kind: "SOLID",
14264
14343
  solid,
14265
14344
  lossy: true
14266
14345
  };
14267
14346
  }
14268
- diagnostics.push(issue("info", "TESSELLATION_NOT_MANIFOLD", "Tessellated mesh is not closed/manifold; returning raw triangle data (lossy)", productExpressId));
14347
+ diagnostics.push(issue("info", "TESSELLATION_NOT_MANIFOLD", "Tessellated mesh is not closed/manifold; returning raw triangle data (lossy)", itemExpressId));
14269
14348
  return {
14270
14349
  kind: "MESH",
14271
14350
  vertices: mesh.vertices,
@@ -14273,12 +14352,11 @@ function reconstructTessellated(reader, productExpressId, diagnostics) {
14273
14352
  diagnostic: "TESSELLATED_LOSSY"
14274
14353
  };
14275
14354
  }
14276
- /** Loose enough to weld float32 vertices web-ifc emits per face, in mm. */
14355
+ /** Loose enough to weld float32 vertices emitted per face, expressed in millimetres. */
14277
14356
  var MESH_SEW_TOLERANCE_MM = .001;
14278
14357
  /**
14279
14358
  * Brands a fresh kernel result and releases its pre-downcast arena slot when
14280
- * the cast moved to a new one (occt-wasm); in-place kernels share the slot and
14281
- * are left alone. Mirrors brepjs's internal castResultShape.
14359
+ * the cast moved to a new one. In-place kernels share the slot.
14282
14360
  */
14283
14361
  function castFreshResult(raw) {
14284
14362
  const cast = (0, brepjs.castShape)(raw);
@@ -14287,12 +14365,7 @@ function castFreshResult(raw) {
14287
14365
  if (!(rawId !== void 0 && castId !== void 0 ? rawId === castId : cast.wrapped === raw)) (0, brepjs.getKernel)().dispose(raw);
14288
14366
  return cast;
14289
14367
  }
14290
- /**
14291
- * Sews the streamed triangles into a closed solid through the kernel's mesh
14292
- * builder, the same path brepjs's mesh importers use. Null when the mesh is
14293
- * open or the kernel rejects it.
14294
- */
14295
- function sewMeshToSolid(mesh, scaleToMm, productExpressId, diagnostics) {
14368
+ function sewMeshToSolid(mesh, scaleToMm, itemExpressId, diagnostics) {
14296
14369
  const kernel = (0, brepjs.getKernel)();
14297
14370
  const { vertices, indices } = mesh;
14298
14371
  const point = (index) => [
@@ -14301,8 +14374,8 @@ function sewMeshToSolid(mesh, scaleToMm, productExpressId, diagnostics) {
14301
14374
  (vertices[index * 3 + 2] ?? 0) * scaleToMm
14302
14375
  ];
14303
14376
  const triangles = [];
14304
- for (let t = 0; t + 2 < indices.length; t += 3) {
14305
- const face = kernel.buildTriFace(point(indices[t] ?? 0), point(indices[t + 1] ?? 0), point(indices[t + 2] ?? 0));
14377
+ for (let index = 0; index + 2 < indices.length; index += 3) {
14378
+ const face = kernel.buildTriFace(point(indices[index] ?? 0), point(indices[index + 1] ?? 0), point(indices[index + 2] ?? 0));
14306
14379
  if (face !== null) triangles.push(face);
14307
14380
  }
14308
14381
  if (triangles.length === 0) return null;
@@ -14312,38 +14385,45 @@ function sewMeshToSolid(mesh, scaleToMm, productExpressId, diagnostics) {
14312
14385
  if (valid !== null && valid.ok) return valid.value;
14313
14386
  cast[Symbol.dispose]();
14314
14387
  return null;
14315
- } catch (e) {
14316
- diagnostics.push(issue("info", "TESSELLATION_NOT_MANIFOLD", `Mesh sewing failed: ${errMsg$1(e)}`, productExpressId));
14388
+ } catch (cause) {
14389
+ diagnostics.push(issue("info", "TESSELLATION_NOT_MANIFOLD", `Mesh sewing failed: ${errMsg$1(cause)}`, itemExpressId));
14317
14390
  return null;
14318
14391
  } finally {
14319
14392
  for (const face of triangles) kernel.dispose(face);
14320
14393
  }
14321
14394
  }
14322
- function collectMesh(reader, productExpressId) {
14323
- const vertices = [];
14324
- const indices = [];
14325
- let base = 0;
14395
+ /** Streams a product once, then separates web-ifc geometry by Body item express ID. */
14396
+ function collectItemMeshes(reader, productExpressId, itemExpressIds) {
14397
+ const byItem = /* @__PURE__ */ new Map();
14398
+ const soleItemExpressId = itemExpressIds.size === 1 ? [...itemExpressIds][0] : void 0;
14326
14399
  reader.streamMeshes([productExpressId], (flatMesh) => {
14327
14400
  try {
14328
14401
  const geometries = flatMesh.geometries;
14329
- for (let g = 0; g < geometries.size(); g++) {
14330
- const placed = geometries.get(g);
14331
- const geom = reader.getGeometry(placed.geometryExpressID);
14402
+ for (let geometryIndex = 0; geometryIndex < geometries.size(); geometryIndex++) {
14403
+ const placed = geometries.get(geometryIndex);
14404
+ const itemExpressId = itemExpressIds.has(placed.geometryExpressID) ? placed.geometryExpressID : soleItemExpressId;
14405
+ if (itemExpressId === void 0) continue;
14406
+ const accumulator = byItem.get(itemExpressId) ?? {
14407
+ vertices: [],
14408
+ indices: []
14409
+ };
14410
+ byItem.set(itemExpressId, accumulator);
14411
+ const geometry = reader.getGeometry(placed.geometryExpressID);
14332
14412
  try {
14333
- const verts = reader.getVertexArray(geom.GetVertexData(), geom.GetVertexDataSize());
14334
- const idx = reader.getIndexArray(geom.GetIndexData(), geom.GetIndexDataSize());
14335
- const m = placed.flatTransformation;
14336
- const vertexCount = verts.length / 6;
14337
- for (let v = 0; v < vertexCount; v++) {
14338
- const x = verts[v * 6] ?? 0;
14339
- const y = verts[v * 6 + 1] ?? 0;
14340
- const z = verts[v * 6 + 2] ?? 0;
14341
- 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));
14413
+ const vertices = reader.getVertexArray(geometry.GetVertexData(), geometry.GetVertexDataSize());
14414
+ const indices = reader.getIndexArray(geometry.GetIndexData(), geometry.GetIndexDataSize());
14415
+ const transform = placed.flatTransformation;
14416
+ const base = accumulator.vertices.length / 3;
14417
+ const vertexCount = vertices.length / 6;
14418
+ for (let vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) {
14419
+ const x = vertices[vertexIndex * 6] ?? 0;
14420
+ const y = vertices[vertexIndex * 6 + 1] ?? 0;
14421
+ const z = vertices[vertexIndex * 6 + 2] ?? 0;
14422
+ 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));
14342
14423
  }
14343
- for (let i = 0; i < idx.length; i++) indices.push(base + (idx[i] ?? 0));
14344
- base += vertexCount;
14424
+ for (let index = 0; index < indices.length; index++) accumulator.indices.push(base + (indices[index] ?? 0));
14345
14425
  } finally {
14346
- geom.delete();
14426
+ geometry.delete();
14347
14427
  }
14348
14428
  }
14349
14429
  } finally {
@@ -14351,12 +14431,62 @@ function collectMesh(reader, productExpressId) {
14351
14431
  releaseEmbind(flatMesh);
14352
14432
  }
14353
14433
  });
14354
- if (vertices.length === 0) return null;
14434
+ return new Map([...byItem].map(([itemExpressId, mesh]) => [itemExpressId, {
14435
+ vertices: new Float32Array(mesh.vertices),
14436
+ indices: new Uint32Array(mesh.indices)
14437
+ }]));
14438
+ }
14439
+ function readTriangulatedMesh(rawPoints, rawTriangles, scale, worldTransform) {
14440
+ if (!Array.isArray(rawPoints) || !Array.isArray(rawTriangles)) return null;
14441
+ const vertices = [];
14442
+ const indices = [];
14443
+ const lengthFactor = scale * 1e3;
14444
+ for (const rawPoint of rawPoints) {
14445
+ if (!Array.isArray(rawPoint) || rawPoint.length < 3) return null;
14446
+ const placed = transformPoint([
14447
+ (readMeasure(rawPoint[0]) ?? 0) * lengthFactor,
14448
+ (readMeasure(rawPoint[1]) ?? 0) * lengthFactor,
14449
+ (readMeasure(rawPoint[2]) ?? 0) * lengthFactor
14450
+ ], worldTransform);
14451
+ vertices.push(placed[0], placed[1], placed[2]);
14452
+ }
14453
+ for (const rawTriangle of rawTriangles) {
14454
+ if (!Array.isArray(rawTriangle) || rawTriangle.length < 3) return null;
14455
+ indices.push((readMeasure(rawTriangle[0]) ?? 1) - 1, (readMeasure(rawTriangle[1]) ?? 1) - 1, (readMeasure(rawTriangle[2]) ?? 1) - 1);
14456
+ }
14355
14457
  return {
14356
14458
  vertices: new Float32Array(vertices),
14357
14459
  indices: new Uint32Array(indices)
14358
14460
  };
14359
14461
  }
14462
+ function transformPoint(point, transform) {
14463
+ if (transform === null) return point;
14464
+ const m = transform.linear;
14465
+ return [
14466
+ (m[0] ?? 1) * point[0] + (m[1] ?? 0) * point[1] + (m[2] ?? 0) * point[2] + transform.translation[0],
14467
+ (m[3] ?? 0) * point[0] + (m[4] ?? 1) * point[1] + (m[5] ?? 0) * point[2] + transform.translation[1],
14468
+ (m[6] ?? 0) * point[0] + (m[7] ?? 0) * point[1] + (m[8] ?? 1) * point[2] + transform.translation[2]
14469
+ ];
14470
+ }
14471
+ function packBinaryStl(vertices, indices, scaleToMm) {
14472
+ const triCount = Math.floor(indices.length / 3);
14473
+ const buffer = /* @__PURE__ */ new ArrayBuffer(84 + triCount * 50);
14474
+ const view = new DataView(buffer);
14475
+ view.setUint32(80, triCount, true);
14476
+ let offset = 84;
14477
+ for (let t = 0; t < triCount; t++) {
14478
+ offset += 12;
14479
+ for (let c = 0; c < 3; c++) {
14480
+ const vi = (indices[t * 3 + c] ?? 0) * 3;
14481
+ view.setFloat32(offset, (vertices[vi] ?? 0) * scaleToMm, true);
14482
+ view.setFloat32(offset + 4, (vertices[vi + 1] ?? 0) * scaleToMm, true);
14483
+ view.setFloat32(offset + 8, (vertices[vi + 2] ?? 0) * scaleToMm, true);
14484
+ offset += 12;
14485
+ }
14486
+ offset += 2;
14487
+ }
14488
+ return new Uint8Array(buffer);
14489
+ }
14360
14490
  var PARAMETRIC_PROFILE_BUILDERS = /* @__PURE__ */ new Map([
14361
14491
  [web_ifc.IFCRECTANGLEPROFILEDEF, (f) => ({
14362
14492
  kind: "RECTANGULAR",
@@ -14709,15 +14839,16 @@ function isIdentity(t) {
14709
14839
  for (let i = 0; i < 9; i++) if (Math.abs((t.linear[i] ?? 0) - (expected[i] ?? 0)) > 1e-9) return false;
14710
14840
  return Math.abs(t.translation[0]) < 1e-9 && Math.abs(t.translation[1]) < 1e-9 && Math.abs(t.translation[2]) < 1e-9;
14711
14841
  }
14712
- function findBodyItem(reader, representations) {
14842
+ function findBodyItems(reader, representations) {
14713
14843
  for (const repRef of representations) {
14714
14844
  const rep = reader.getLine(repRef.value);
14715
14845
  if (rep === null) continue;
14716
14846
  if (readLabel(rep["RepresentationIdentifier"]) !== "Body") continue;
14717
14847
  const items = asRefArray(rep["Items"]);
14718
- if (items === void 0 || items.length === 0) continue;
14719
- return items[0]?.value;
14848
+ if (items === void 0) return [];
14849
+ return items.map((item) => item.value);
14720
14850
  }
14851
+ return null;
14721
14852
  }
14722
14853
  function readPolylinePoints(reader, ref, scale) {
14723
14854
  const curveRef = asRef(ref);
@@ -14822,8 +14953,8 @@ function readLabel(value) {
14822
14953
  if (typeof v === "string") return v;
14823
14954
  }
14824
14955
  }
14825
- function isTessellatedType(type) {
14826
- return type === web_ifc.IFCTRIANGULATEDFACESET || type === web_ifc.IFCPOLYGONALFACESET || type === web_ifc.IFCFACEBASEDSURFACEMODEL;
14956
+ function isStreamedTessellatedType(type) {
14957
+ return type === web_ifc.IFCPOLYGONALFACESET || type === web_ifc.IFCFACEBASEDSURFACEMODEL;
14827
14958
  }
14828
14959
  function normalize(v) {
14829
14960
  const len = Math.hypot(v[0], v[1], v[2]);
@@ -15162,6 +15293,7 @@ function readOwnerHistory(reader, ownerHistoryExpressId) {
15162
15293
  }
15163
15294
  //#endregion
15164
15295
  //#region src/import/fromIfc.ts
15296
+ var testHooks = null;
15165
15297
  /**
15166
15298
  * Each enumerated physical-element IFC type and the {@link ImportedElementCategory}
15167
15299
  * it maps to. Curtain-wall sub-components (IfcPlate/IfcMember) and stair/ramp
@@ -15210,6 +15342,7 @@ async function fromIfc(bytes, options = {}) {
15210
15342
  const readerResult = await SpfReader.create(bytes, settings);
15211
15343
  if (!readerResult.ok) return (0, brepjs.err)(readerResult.error);
15212
15344
  const reader = readerResult.value;
15345
+ const elements = [];
15213
15346
  try {
15214
15347
  reader.buildGuidMap();
15215
15348
  const diagnostics = [];
@@ -15218,13 +15351,13 @@ async function fromIfc(bytes, options = {}) {
15218
15351
  const spatialRoot = buildSpatialTree(reader, scale);
15219
15352
  const containment = buildElementContainmentMap(reader);
15220
15353
  const typeEnums = buildTypePredefinedMap(reader);
15221
- const elements = [];
15222
15354
  const byExpressId = /* @__PURE__ */ new Map();
15223
15355
  for (const [type, category] of ELEMENT_TYPES) for (const expressId of reader.getLinesOfType(type)) {
15224
15356
  const element = readElement(reader, expressId, category, scale, containment, typeEnums, options.skipGeometry ?? false, diagnostics);
15225
15357
  if (element === null) continue;
15226
15358
  elements.push(element);
15227
15359
  byExpressId.set(element.expressId, element);
15360
+ testHooks?.afterElement?.(element, elements.length);
15228
15361
  }
15229
15362
  const report = appendIssues(emptyReport(), diagnostics);
15230
15363
  const applicationName = readApplicationName(reader);
@@ -15238,6 +15371,7 @@ async function fromIfc(bytes, options = {}) {
15238
15371
  };
15239
15372
  return (0, brepjs.ok)(model);
15240
15373
  } catch (e) {
15374
+ disposeElements(elements);
15241
15375
  return (0, brepjs.err)(importError("IMPORT_FAILED", "Unexpected failure during IFC import", e));
15242
15376
  } finally {
15243
15377
  reader.close();
@@ -15249,6 +15383,7 @@ async function fromIfc(bytes, options = {}) {
15249
15383
  * sub-read failure is isolated by its own reader and never aborts the element.
15250
15384
  */
15251
15385
  function readElement(reader, expressId, category, scale, containment, typeEnums, skipGeometry, diagnostics) {
15386
+ let geometry = null;
15252
15387
  try {
15253
15388
  const line = reader.getLine(expressId);
15254
15389
  if (line === null) {
@@ -15260,10 +15395,15 @@ function readElement(reader, expressId, category, scale, containment, typeEnums,
15260
15395
  const predefinedType = readPredefinedType(line) ?? typeEnums.get(expressId);
15261
15396
  const voidedBy = readVoids(reader, expressId).map((v) => v.openingExpressId);
15262
15397
  const fills = findFills(reader, expressId);
15263
- const geometry = skipGeometry ? {
15398
+ geometry = skipGeometry ? {
15264
15399
  fidelity: "NONE",
15265
- solid: null
15400
+ completeness: "NONE",
15401
+ solids: [],
15402
+ solid: null,
15403
+ bounds: null,
15404
+ volumeMm3: null
15266
15405
  } : reconstructGeometry(reader, expressId, scale, voidedBy, diagnostics);
15406
+ testHooks?.afterGeometry?.(expressId, geometry);
15267
15407
  const psets = readPsets(reader, expressId).map(toImportedPset);
15268
15408
  const material = readMaterial(reader, expressId, scale);
15269
15409
  const classification = readClassification(reader, expressId);
@@ -15286,6 +15426,7 @@ function readElement(reader, expressId, category, scale, containment, typeEnums,
15286
15426
  ...fills !== void 0 ? { fills } : {}
15287
15427
  };
15288
15428
  } catch (e) {
15429
+ if (geometry !== null) disposeGeometry(geometry);
15289
15430
  diagnostics.push(issue("error", "ELEMENT_READ_FAILED", `Element ${expressId} reconstruction threw: ${errMsg(e)}`, expressId));
15290
15431
  return null;
15291
15432
  }
@@ -15307,42 +15448,134 @@ function findFills(reader, elementExpressId) {
15307
15448
  * model. Falls back to the uncut solid (with a diagnostic) if a cut fails.
15308
15449
  */
15309
15450
  function reconstructGeometry(reader, expressId, scale, voidedBy, diagnostics) {
15310
- const base = toImportedGeometry(readBodyGeometry(reader, expressId, scale, diagnostics));
15311
- if (base.fidelity !== "PARAMETRIC" || base.solid === null || voidedBy.length === 0) return base;
15312
- let host = base.solid;
15451
+ const base = toImportedGeometry(reader, expressId, scale, diagnostics);
15452
+ if (base.fidelity !== "PARAMETRIC" || base.completeness !== "COMPLETE" || voidedBy.length === 0) return base;
15453
+ const firstHost = base.solids[0];
15454
+ if (firstHost === void 0) return base;
15455
+ const hosts = [firstHost, ...base.solids.slice(1)];
15313
15456
  for (const openingId of voidedBy) {
15314
15457
  const opening = readBodyGeometry(reader, openingId, scale, diagnostics);
15315
15458
  if (opening.kind !== "SOLID") continue;
15316
- const cutResult = (0, brepjs.cut)(host, opening.solid);
15317
- opening.solid[Symbol.dispose]();
15318
- if (!cutResult.ok) {
15319
- diagnostics.push(issue("warning", "VOID_SUBTRACTION_FAILED", `Opening ${openingId} could not be subtracted from element ${expressId}: ${cutResult.error.message}`, expressId));
15320
- continue;
15321
- }
15322
- host[Symbol.dispose]();
15323
- host = cutResult.value;
15324
- }
15459
+ try {
15460
+ for (let hostIndex = 0; hostIndex < hosts.length; hostIndex++) {
15461
+ const host = hosts[hostIndex];
15462
+ if (host === void 0) continue;
15463
+ const cutResult = (0, brepjs.cut)(host, opening.solid);
15464
+ if (!cutResult.ok) {
15465
+ diagnostics.push(issue("warning", "VOID_SUBTRACTION_FAILED", `Opening ${openingId} could not be subtracted from element ${expressId}: ${cutResult.error.message}`, expressId));
15466
+ continue;
15467
+ }
15468
+ host[Symbol.dispose]();
15469
+ hosts[hostIndex] = cutResult.value;
15470
+ }
15471
+ } finally {
15472
+ opening.solid[Symbol.dispose]();
15473
+ }
15474
+ }
15475
+ return completeImportedGeometry("PARAMETRIC", hosts, expressId, diagnostics);
15476
+ }
15477
+ function toImportedGeometry(reader, expressId, scale, diagnostics) {
15478
+ const body = readBodyItems(reader, expressId, scale, diagnostics);
15479
+ const solids = [];
15480
+ let hasTessellatedSolid = false;
15481
+ let lossyMesh = null;
15482
+ for (const item of body.items) if (item.kind === "SOLID") {
15483
+ solids.push(item.solid);
15484
+ hasTessellatedSolid ||= item.lossy;
15485
+ } else if (item.kind === "MESH") {
15486
+ const meshAccumulator = lossyMesh ?? {
15487
+ vertices: [],
15488
+ indices: []
15489
+ };
15490
+ lossyMesh = meshAccumulator;
15491
+ const vertexOffset = meshAccumulator.vertices.length / 3;
15492
+ for (const vertex of item.vertices) meshAccumulator.vertices.push(vertex);
15493
+ for (const index of item.indices) meshAccumulator.indices.push(vertexOffset + index);
15494
+ }
15495
+ const completeness = body.hasBody && body.itemCount > 0 && solids.length === body.itemCount ? "COMPLETE" : solids.length > 0 ? "PARTIAL" : "NONE";
15496
+ if (completeness === "PARTIAL") diagnostics.push(issue("warning", "PARTIAL_BODY_RECONSTRUCTION", `Reconstructed ${solids.length} of ${body.itemCount} Body items`, expressId, {
15497
+ reconstructedItems: solids.length,
15498
+ bodyItems: body.itemCount
15499
+ }));
15500
+ 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 }));
15501
+ const fidelity = lossyMesh !== null ? "TESSELLATED_LOSSY" : hasTessellatedSolid ? "TESSELLATED_MANIFOLD" : solids.length > 0 ? "PARAMETRIC" : "NONE";
15502
+ const aggregate = completeness === "COMPLETE" ? measureCompleteBody(solids, expressId, diagnostics) : {
15503
+ bounds: null,
15504
+ volumeMm3: null
15505
+ };
15325
15506
  return {
15326
- fidelity: "PARAMETRIC",
15327
- solid: host
15507
+ fidelity,
15508
+ completeness,
15509
+ solids,
15510
+ solid: completeness === "COMPLETE" && solids.length === 1 ? solids[0] ?? null : null,
15511
+ ...aggregate,
15512
+ ...lossyMesh !== null ? {
15513
+ meshVertices: new Float32Array(lossyMesh.vertices),
15514
+ meshIndices: new Uint32Array(lossyMesh.indices)
15515
+ } : {}
15328
15516
  };
15329
15517
  }
15330
- function toImportedGeometry(result) {
15331
- if (result.kind === "SOLID") return {
15332
- fidelity: result.lossy ? "TESSELLATED_MANIFOLD" : "PARAMETRIC",
15333
- solid: result.solid
15334
- };
15335
- if (result.kind === "MESH") return {
15336
- fidelity: "TESSELLATED_LOSSY",
15337
- solid: null,
15338
- meshVertices: result.vertices,
15339
- meshIndices: result.indices
15340
- };
15518
+ function completeImportedGeometry(fidelity, solids, expressId, diagnostics) {
15341
15519
  return {
15342
- fidelity: "NONE",
15343
- solid: null
15520
+ fidelity,
15521
+ completeness: "COMPLETE",
15522
+ solids,
15523
+ solid: solids.length === 1 ? solids[0] : null,
15524
+ ...measureCompleteBody(solids, expressId, diagnostics)
15344
15525
  };
15345
15526
  }
15527
+ function measureCompleteBody(solids, expressId, diagnostics) {
15528
+ try {
15529
+ const first = solids[0];
15530
+ if (first === void 0) return {
15531
+ bounds: null,
15532
+ volumeMm3: null
15533
+ };
15534
+ const initial = (0, brepjs.getBounds)(first);
15535
+ let xMin = initial.xMin;
15536
+ let xMax = initial.xMax;
15537
+ let yMin = initial.yMin;
15538
+ let yMax = initial.yMax;
15539
+ let zMin = initial.zMin;
15540
+ let zMax = initial.zMax;
15541
+ let volumeMm3 = 0;
15542
+ for (const solid of solids) {
15543
+ const measured = (0, brepjs.measureVolume)(solid);
15544
+ if (!measured.ok) throw new Error(measured.error.message);
15545
+ volumeMm3 += measured.value;
15546
+ const itemBounds = (0, brepjs.getBounds)(solid);
15547
+ xMin = Math.min(xMin, itemBounds.xMin);
15548
+ xMax = Math.max(xMax, itemBounds.xMax);
15549
+ yMin = Math.min(yMin, itemBounds.yMin);
15550
+ yMax = Math.max(yMax, itemBounds.yMax);
15551
+ zMin = Math.min(zMin, itemBounds.zMin);
15552
+ zMax = Math.max(zMax, itemBounds.zMax);
15553
+ }
15554
+ return {
15555
+ bounds: {
15556
+ xMin,
15557
+ xMax,
15558
+ yMin,
15559
+ yMax,
15560
+ zMin,
15561
+ zMax
15562
+ },
15563
+ volumeMm3
15564
+ };
15565
+ } catch (cause) {
15566
+ diagnostics.push(issue("warning", "BODY_AGGREGATE_MEASUREMENT_FAILED", `Complete Body aggregate measurement failed: ${errMsg(cause)}`, expressId));
15567
+ return {
15568
+ bounds: null,
15569
+ volumeMm3: null
15570
+ };
15571
+ }
15572
+ }
15573
+ function disposeGeometry(geometry) {
15574
+ for (const solid of geometry.solids) solid[Symbol.dispose]();
15575
+ }
15576
+ function disposeElements(elements) {
15577
+ for (const element of elements) disposeGeometry(element.geometry);
15578
+ }
15346
15579
  function toImportedPset(pset) {
15347
15580
  return {
15348
15581
  name: pset.name,
@@ -15420,7 +15653,7 @@ function errMsg(e) {
15420
15653
  * with the model (it is not reclaimed automatically when the model is GC'd).
15421
15654
  */
15422
15655
  function disposeImportedModel(model) {
15423
- for (const el of model.elements) el.geometry.solid?.[Symbol.dispose]();
15656
+ for (const el of model.elements) for (const solid of el.geometry.solids) solid[Symbol.dispose]();
15424
15657
  }
15425
15658
  //#endregion
15426
15659
  //#region src/specs/materialSpec.ts