brepjs-bim 0.23.0 → 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.
@@ -206,7 +206,7 @@ function fromBrepError(inner, code, message) {
206
206
  };
207
207
  }
208
208
  //#endregion
209
- //#region \0@oxc-project+runtime@0.143.0/helpers/esm/usingCtx.js
209
+ //#region \0@oxc-project+runtime@0.146.0/helpers/esm/usingCtx.js
210
210
  function _usingCtx() {
211
211
  var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) {
212
212
  var n = Error();
@@ -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;
@@ -8779,7 +8779,7 @@ function writeWallGeometry(w, spec, geomSubContextId, parentPlacementId) {
8779
8779
  value: "AREA"
8780
8780
  },
8781
8781
  ProfileName: null,
8782
- Position: w.ref(writeAxis2Placement2D$1(w)),
8782
+ Position: w.ref(writeAxis2Placement2D$1(w, [thicknessM / 2, heightM / 2])),
8783
8783
  XDim: w.mkType(web_ifc.IFCPOSITIVELENGTHMEASURE, thicknessM),
8784
8784
  YDim: w.mkType(web_ifc.IFCPOSITIVELENGTHMEASURE, heightM)
8785
8785
  });
@@ -9000,12 +9000,12 @@ function writeRoofGeometry(w, spec, solid, geomSubContextId, parentPlacementId)
9000
9000
  usedFallback: false
9001
9001
  };
9002
9002
  }
9003
- function writeAxis2Placement2D$1(w) {
9003
+ function writeAxis2Placement2D$1(w, location = [0, 0]) {
9004
9004
  const originId = w.nextId();
9005
9005
  w.writeLine({
9006
9006
  expressID: originId,
9007
9007
  type: web_ifc.IFCCARTESIANPOINT,
9008
- Coordinates: [w.mkType(web_ifc.IFCLENGTHMEASURE, 0), w.mkType(web_ifc.IFCLENGTHMEASURE, 0)]
9008
+ Coordinates: [w.mkType(web_ifc.IFCLENGTHMEASURE, location[0]), w.mkType(web_ifc.IFCLENGTHMEASURE, location[1])]
9009
9009
  });
9010
9010
  const id = w.nextId();
9011
9011
  w.writeLine({
@@ -11510,11 +11510,10 @@ function writeOpeningGeometry(w, guid, openingSpec, wallSpec, wallPlacementId, g
11510
11510
  const offsetAlongWallM = toIfcLengthM(openingSpec.offsetAlongWall);
11511
11511
  const offsetFromFloorM = toIfcLengthM(openingSpec.offsetFromFloor);
11512
11512
  const thicknessM = toIfcLengthM(wallSpec.thickness);
11513
- const wallHeightM = toIfcLengthM(wallSpec.height);
11514
11513
  const placement3DId = writeAxis2Placement3D(w, [
11515
11514
  offsetAlongWallM + widthM / 2,
11516
- thicknessM / 2,
11517
- -wallHeightM / 2 + offsetFromFloorM + heightM / 2
11515
+ thicknessM,
11516
+ offsetFromFloorM + heightM / 2
11518
11517
  ], [
11519
11518
  0,
11520
11519
  -1,
@@ -12390,6 +12389,24 @@ var SUPPORTED_SCHEMAS = [
12390
12389
  * MUST call {@link SpfReader.close} (it always issues `CloseModel`, preventing
12391
12390
  * WASM handle leaks). The handle is `using`-friendly via `Symbol.dispose`.
12392
12391
  */
12392
+ var IFC_Z_UP_TRANSFORMATION = [
12393
+ 1,
12394
+ 0,
12395
+ 0,
12396
+ 0,
12397
+ 0,
12398
+ 0,
12399
+ 1,
12400
+ 0,
12401
+ 0,
12402
+ -1,
12403
+ 0,
12404
+ 0,
12405
+ 0,
12406
+ 0,
12407
+ 0,
12408
+ 1
12409
+ ];
12393
12410
  function deleteVector(vec) {
12394
12411
  vec.delete?.();
12395
12412
  }
@@ -12422,6 +12439,7 @@ var SpfReader = class SpfReader {
12422
12439
  if (modelId >= 0) api.CloseModel(modelId);
12423
12440
  return (0, brepjs.err)(importError("OPEN_MODEL_FAILED", "web-ifc OpenModel returned an invalid model id"));
12424
12441
  }
12442
+ api.SetGeometryTransformation(modelId, IFC_Z_UP_TRANSFORMATION);
12425
12443
  const schemaRaw = api.GetModelSchema(modelId);
12426
12444
  if (!SUPPORTED_SCHEMAS.includes(schemaRaw)) {
12427
12445
  api.CloseModel(modelId);
@@ -14070,6 +14088,7 @@ function buildElementContainmentMap(reader) {
14070
14088
  //#endregion
14071
14089
  //#region src/import/geometryRead.ts
14072
14090
  var NONE = { kind: "NONE" };
14091
+ var testHooks$1 = null;
14073
14092
  /**
14074
14093
  * Reconstructs the `Body` (SweptSolid) representation of a product into a brepjs
14075
14094
  * solid, falling back to tessellated mesh import when no parametric path exists.
@@ -14080,27 +14099,66 @@ var NONE = { kind: "NONE" };
14080
14099
  * single bad element never aborts a model import.
14081
14100
  */
14082
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) {
14083
14109
  const product = reader.getLine(productExpressId);
14084
- if (product === null) return NONE;
14110
+ if (product === null) return {
14111
+ hasBody: false,
14112
+ itemCount: 0,
14113
+ items: []
14114
+ };
14085
14115
  const representationRef = asRef(product["Representation"]);
14086
- if (representationRef === void 0) return NONE;
14116
+ if (representationRef === void 0) return {
14117
+ hasBody: false,
14118
+ itemCount: 0,
14119
+ items: []
14120
+ };
14087
14121
  const productShape = reader.getLine(representationRef.value);
14088
14122
  const representations = productShape === null ? void 0 : asRefArray(productShape["Representations"]);
14089
- if (representations === void 0) return NONE;
14090
- const bodyItemId = findBodyItem(reader, representations);
14091
- 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
+ };
14092
14134
  const worldTransform = readWorldTransform(reader, product, scale);
14093
- const itemType = reader.getLineType(bodyItemId);
14094
- try {
14095
- if (itemType === web_ifc.IFCEXTRUDEDAREASOLID) return reconstructExtrusion(reader, bodyItemId, scale, worldTransform, diagnostics);
14096
- if (itemType === web_ifc.IFCREVOLVEDAREASOLID) return reconstructRevolution(reader, bodyItemId, scale, worldTransform, diagnostics);
14097
- if (isTessellatedType(itemType)) return reconstructTessellated(reader, productExpressId, diagnostics);
14098
- } catch (e) {
14099
- diagnostics.push(issue("warning", "GEOMETRY_RECONSTRUCTION_FAILED", `Body geometry reconstruction threw: ${errMsg$1(e)}`, productExpressId));
14100
- 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));
14101
14142
  }
14102
- diagnostics.push(issue("warning", "UNSUPPORTED_REPRESENTATION_ITEM", `Unsupported body representation item type ${itemType}; geometry skipped`, productExpressId));
14103
- 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
+ };
14104
14162
  }
14105
14163
  function reconstructExtrusion(reader, extrusionId, scale, worldTransform, diagnostics) {
14106
14164
  const ext = reader.getLine(extrusionId);
@@ -14121,12 +14179,14 @@ function reconstructExtrusion(reader, extrusionId, scale, worldTransform, diagno
14121
14179
  diagnostics.push(issue("warning", faceResult.error.code, faceResult.error.message, extrusionId));
14122
14180
  return NONE;
14123
14181
  }
14182
+ const profileFrame = readProfilePosition(reader, sweptArea.value, scale);
14124
14183
  const depthMm = depth * scale * 1e3;
14125
- const extrudeDir = readDirection(reader, ext["ExtrudedDirection"]) ?? [
14184
+ const sweptDir = readDirection(reader, ext["ExtrudedDirection"]) ?? [
14126
14185
  0,
14127
14186
  0,
14128
14187
  1
14129
14188
  ];
14189
+ const extrudeDir = profileFrame === null ? sweptDir : rotateByTranspose(profileFrame.linear, sweptDir);
14130
14190
  const extrudeVec = [
14131
14191
  extrudeDir[0] * depthMm,
14132
14192
  extrudeDir[1] * depthMm,
@@ -14148,7 +14208,11 @@ function reconstructExtrusion(reader, extrusionId, scale, worldTransform, diagno
14148
14208
  return NONE;
14149
14209
  }
14150
14210
  const localFrame = readAxis2Placement3D(reader, ext["Position"], scale);
14151
- const placed = placeSolid(solidResult.value, localFrame, worldTransform);
14211
+ const placed = placeSolid(solidResult.value, [
14212
+ worldTransform,
14213
+ localFrame,
14214
+ profileFrame
14215
+ ]);
14152
14216
  if (!placed.ok) {
14153
14217
  diagnostics.push(issue("warning", placed.error.code, placed.error.message, extrusionId));
14154
14218
  return NONE;
@@ -14176,16 +14240,19 @@ function reconstructRevolution(reader, revolutionId, scale, worldTransform, diag
14176
14240
  return NONE;
14177
14241
  }
14178
14242
  const axis1 = reader.getLine(axisRef.value);
14179
- const center = (axis1 === null ? void 0 : readPoint(reader, axis1["Location"], scale)) ?? [
14243
+ const sweptCenter = (axis1 === null ? void 0 : readPoint(reader, axis1["Location"], scale)) ?? [
14180
14244
  0,
14181
14245
  0,
14182
14246
  0
14183
14247
  ];
14184
- const direction = (axis1 === null ? void 0 : readDirection(reader, axis1["Axis"])) ?? [
14248
+ const sweptDirection = (axis1 === null ? void 0 : readDirection(reader, axis1["Axis"])) ?? [
14185
14249
  0,
14186
14250
  0,
14187
14251
  1
14188
14252
  ];
14253
+ const profileFrame = readProfilePosition(reader, sweptArea.value, scale);
14254
+ const center = profileFrame === null ? sweptCenter : inverseRigidPoint(profileFrame, sweptCenter);
14255
+ const direction = profileFrame === null ? sweptDirection : rotateByTranspose(profileFrame.linear, sweptDirection);
14189
14256
  const revolved = (() => {
14190
14257
  try {
14191
14258
  var _usingCtx3 = _usingCtx();
@@ -14212,40 +14279,72 @@ function reconstructRevolution(reader, revolutionId, scale, worldTransform, diag
14212
14279
  return NONE;
14213
14280
  }
14214
14281
  const localFrame = readAxis2Placement3D(reader, rev["Position"], scale);
14215
- const placed = placeSolid(revolved.value, localFrame, worldTransform);
14282
+ const placed = placeSolid(revolved.value, [
14283
+ worldTransform,
14284
+ localFrame,
14285
+ profileFrame
14286
+ ]);
14216
14287
  if (!placed.ok) {
14217
14288
  diagnostics.push(issue("warning", placed.error.code, placed.error.message, revolutionId));
14218
14289
  return NONE;
14219
14290
  }
14220
14291
  return finalizeSolid(placed.value, revolutionId, diagnostics);
14221
14292
  }
14222
- function reconstructTessellated(reader, productExpressId, diagnostics) {
14223
- 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) {
14224
14304
  if (mesh === null || mesh.indices.length === 0) {
14225
- 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));
14226
14306
  return NONE;
14227
14307
  }
14228
- const stl = packBinaryStl(mesh.vertices, mesh.indices, 1e3);
14229
- let solid = null;
14308
+ const stl = packBinaryStl(mesh.vertices, mesh.indices, scaleToMm);
14309
+ let solid = sewMeshToSolid(mesh, scaleToMm, itemExpressId, diagnostics);
14230
14310
  try {
14231
- const cast = (0, brepjs.castShape)((0, brepjs.getKernel)().importSTL(stl.buffer));
14232
- if ((0, brepjs.isSolid)(cast)) {
14233
- const valid = (0, brepjs.validSolid)(cast);
14234
- if (valid.ok) solid = valid.value;
14235
- else cast[Symbol.dispose]();
14236
- } else cast[Symbol.dispose]();
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
+ }
14237
14330
  } catch (e) {
14238
- diagnostics.push(issue("info", "TESSELLATION_NOT_MANIFOLD", `STL round-trip failed: ${errMsg$1(e)}`, productExpressId));
14331
+ diagnostics.push(issue("info", "TESSELLATION_NOT_MANIFOLD", `STL round-trip failed: ${errMsg$1(e)}`, itemExpressId));
14239
14332
  }
14240
14333
  if (solid !== null) {
14241
- diagnostics.push(issue("info", "TESSELLATED_MANIFOLD", "Tessellated mesh recovered as a closed solid via STL round-trip", 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));
14242
14341
  return {
14243
14342
  kind: "SOLID",
14244
14343
  solid,
14245
14344
  lossy: true
14246
14345
  };
14247
14346
  }
14248
- 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));
14249
14348
  return {
14250
14349
  kind: "MESH",
14251
14350
  vertices: mesh.vertices,
@@ -14253,43 +14352,122 @@ function reconstructTessellated(reader, productExpressId, diagnostics) {
14253
14352
  diagnostic: "TESSELLATED_LOSSY"
14254
14353
  };
14255
14354
  }
14256
- function collectMesh(reader, productExpressId) {
14257
- const vertices = [];
14258
- const indices = [];
14259
- let base = 0;
14355
+ /** Loose enough to weld float32 vertices emitted per face, expressed in millimetres. */
14356
+ var MESH_SEW_TOLERANCE_MM = .001;
14357
+ /**
14358
+ * Brands a fresh kernel result and releases its pre-downcast arena slot when
14359
+ * the cast moved to a new one. In-place kernels share the slot.
14360
+ */
14361
+ function castFreshResult(raw) {
14362
+ const cast = (0, brepjs.castShape)(raw);
14363
+ const rawId = raw.id;
14364
+ const castId = cast.wrapped.id;
14365
+ if (!(rawId !== void 0 && castId !== void 0 ? rawId === castId : cast.wrapped === raw)) (0, brepjs.getKernel)().dispose(raw);
14366
+ return cast;
14367
+ }
14368
+ function sewMeshToSolid(mesh, scaleToMm, itemExpressId, diagnostics) {
14369
+ const kernel = (0, brepjs.getKernel)();
14370
+ const { vertices, indices } = mesh;
14371
+ const point = (index) => [
14372
+ (vertices[index * 3] ?? 0) * scaleToMm,
14373
+ (vertices[index * 3 + 1] ?? 0) * scaleToMm,
14374
+ (vertices[index * 3 + 2] ?? 0) * scaleToMm
14375
+ ];
14376
+ const triangles = [];
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));
14379
+ if (face !== null) triangles.push(face);
14380
+ }
14381
+ if (triangles.length === 0) return null;
14382
+ try {
14383
+ const cast = castFreshResult(kernel.sewAndSolidify(triangles, MESH_SEW_TOLERANCE_MM));
14384
+ const valid = (0, brepjs.isSolid)(cast) ? (0, brepjs.validSolid)(cast) : null;
14385
+ if (valid !== null && valid.ok) return valid.value;
14386
+ cast[Symbol.dispose]();
14387
+ return null;
14388
+ } catch (cause) {
14389
+ diagnostics.push(issue("info", "TESSELLATION_NOT_MANIFOLD", `Mesh sewing failed: ${errMsg$1(cause)}`, itemExpressId));
14390
+ return null;
14391
+ } finally {
14392
+ for (const face of triangles) kernel.dispose(face);
14393
+ }
14394
+ }
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;
14260
14399
  reader.streamMeshes([productExpressId], (flatMesh) => {
14261
14400
  try {
14262
14401
  const geometries = flatMesh.geometries;
14263
- for (let g = 0; g < geometries.size(); g++) {
14264
- const placed = geometries.get(g);
14265
- 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);
14266
14412
  try {
14267
- const verts = reader.getVertexArray(geom.GetVertexData(), geom.GetVertexDataSize());
14268
- const idx = reader.getIndexArray(geom.GetIndexData(), geom.GetIndexDataSize());
14269
- const m = placed.flatTransformation;
14270
- const vertexCount = verts.length / 6;
14271
- for (let v = 0; v < vertexCount; v++) {
14272
- const x = verts[v * 6] ?? 0;
14273
- const y = verts[v * 6 + 1] ?? 0;
14274
- const z = verts[v * 6 + 2] ?? 0;
14275
- 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));
14276
14423
  }
14277
- for (let i = 0; i < idx.length; i++) indices.push(base + (idx[i] ?? 0));
14278
- base += vertexCount;
14424
+ for (let index = 0; index < indices.length; index++) accumulator.indices.push(base + (indices[index] ?? 0));
14279
14425
  } finally {
14280
- geom.delete();
14426
+ geometry.delete();
14281
14427
  }
14282
14428
  }
14283
14429
  } finally {
14284
- flatMesh.delete();
14430
+ releaseEmbind(flatMesh.geometries);
14431
+ releaseEmbind(flatMesh);
14285
14432
  }
14286
14433
  });
14287
- 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
+ }
14288
14457
  return {
14289
14458
  vertices: new Float32Array(vertices),
14290
14459
  indices: new Uint32Array(indices)
14291
14460
  };
14292
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
+ }
14293
14471
  function packBinaryStl(vertices, indices, scaleToMm) {
14294
14472
  const triCount = Math.floor(indices.length / 3);
14295
14473
  const buffer = /* @__PURE__ */ new ArrayBuffer(84 + triCount * 50);
@@ -14452,19 +14630,99 @@ function profileToFace(profile) {
14452
14630
  if (!face.ok) return (0, brepjs.err)(importError("GEOMETRY_RECONSTRUCTION_FAILED", `Profile face build failed: ${face.error.message}`));
14453
14631
  return (0, brepjs.ok)(face.value);
14454
14632
  }
14455
- function placeSolid(solid, localFrame, worldTransform) {
14456
- let current = solid;
14457
- for (const transform of [localFrame, worldTransform]) {
14458
- if (transform === null || isIdentity(transform)) continue;
14459
- const applied = (0, brepjs.applyMatrix)(current, transform);
14460
- if (!applied.ok) {
14461
- current[Symbol.dispose]();
14462
- return (0, brepjs.err)(importError("PLACEMENT_READ_FAILED", `Placement transform failed: ${applied.error.message}`));
14463
- }
14464
- current[Symbol.dispose]();
14465
- current = applied.value;
14466
- }
14467
- return (0, brepjs.ok)(current);
14633
+ /**
14634
+ * IfcParameterizedProfileDef.Position places the profile inside the swept
14635
+ * solid's XY plane; the writers rely on it to corner-anchor centred rectangle
14636
+ * profiles. Absent on arbitrary (polyline) profiles.
14637
+ */
14638
+ function readProfilePosition(reader, profileExpressId, scale) {
14639
+ const def = reader.getLine(profileExpressId);
14640
+ const positionRef = def === null ? void 0 : asRef(def["Position"]);
14641
+ if (positionRef === void 0) return null;
14642
+ const position = reader.getLine(positionRef.value);
14643
+ if (position === null) return null;
14644
+ const [x, y] = readPoint2D(reader, position["Location"], scale) ?? [0, 0];
14645
+ const [dx, dy] = readDirection2D(reader, position["RefDirection"]) ?? [1, 0];
14646
+ const len = Math.hypot(dx, dy);
14647
+ const c = len < 1e-12 ? 1 : dx / len;
14648
+ const s = len < 1e-12 ? 0 : dy / len;
14649
+ return {
14650
+ linear: [
14651
+ c,
14652
+ -s,
14653
+ 0,
14654
+ s,
14655
+ c,
14656
+ 0,
14657
+ 0,
14658
+ 0,
14659
+ 1
14660
+ ],
14661
+ translation: [
14662
+ x,
14663
+ y,
14664
+ 0
14665
+ ]
14666
+ };
14667
+ }
14668
+ /**
14669
+ * Applies the placement chain (outermost first) as one composed rigid motion.
14670
+ * A second applyMatrix on an already-transformed solid can fail BRepCheck on
14671
+ * occt-wasm, so the frames are never applied one after another.
14672
+ */
14673
+ function placeSolid(solid, frames) {
14674
+ let composed = null;
14675
+ for (const frame of frames) {
14676
+ if (frame === null) continue;
14677
+ composed = composed === null ? frame : composeRigid(composed, frame);
14678
+ }
14679
+ if (composed === null || isIdentity(composed)) return (0, brepjs.ok)(solid);
14680
+ const applied = (0, brepjs.applyMatrix)(solid, composed);
14681
+ solid[Symbol.dispose]();
14682
+ if (!applied.ok) return (0, brepjs.err)(importError("PLACEMENT_READ_FAILED", `Placement transform failed: ${applied.error.message}`));
14683
+ return (0, brepjs.ok)(applied.value);
14684
+ }
14685
+ /** `outer ∘ inner`: the motion that applies `inner` first, then `outer`. */
14686
+ function composeRigid(outer, inner) {
14687
+ const [a00, a01, a02, a10, a11, a12, a20, a21, a22] = outer.linear;
14688
+ const [b00, b01, b02, b10, b11, b12, b20, b21, b22] = inner.linear;
14689
+ const [tx, ty, tz] = inner.translation;
14690
+ const [ux, uy, uz] = outer.translation;
14691
+ return {
14692
+ linear: [
14693
+ a00 * b00 + a01 * b10 + a02 * b20,
14694
+ a00 * b01 + a01 * b11 + a02 * b21,
14695
+ a00 * b02 + a01 * b12 + a02 * b22,
14696
+ a10 * b00 + a11 * b10 + a12 * b20,
14697
+ a10 * b01 + a11 * b11 + a12 * b21,
14698
+ a10 * b02 + a11 * b12 + a12 * b22,
14699
+ a20 * b00 + a21 * b10 + a22 * b20,
14700
+ a20 * b01 + a21 * b11 + a22 * b21,
14701
+ a20 * b02 + a21 * b12 + a22 * b22
14702
+ ],
14703
+ translation: [
14704
+ a00 * tx + a01 * ty + a02 * tz + ux,
14705
+ a10 * tx + a11 * ty + a12 * tz + uy,
14706
+ a20 * tx + a21 * ty + a22 * tz + uz
14707
+ ]
14708
+ };
14709
+ }
14710
+ /** Rᵀ·v for a rigid frame's rotation part. */
14711
+ function rotateByTranspose(linear, v) {
14712
+ return [
14713
+ linear[0] * v[0] + linear[3] * v[1] + linear[6] * v[2],
14714
+ linear[1] * v[0] + linear[4] * v[1] + linear[7] * v[2],
14715
+ linear[2] * v[0] + linear[5] * v[1] + linear[8] * v[2]
14716
+ ];
14717
+ }
14718
+ /** Maps a point from the frame's parent space into the frame: Rᵀ·(p − t). */
14719
+ function inverseRigidPoint(frame, p) {
14720
+ const t = frame.translation;
14721
+ return rotateByTranspose(frame.linear, [
14722
+ p[0] - t[0],
14723
+ p[1] - t[1],
14724
+ p[2] - t[2]
14725
+ ]);
14468
14726
  }
14469
14727
  function finalizeSolid(solid, entity, diagnostics) {
14470
14728
  if (!(0, brepjs.isSolid)(solid)) {
@@ -14581,15 +14839,16 @@ function isIdentity(t) {
14581
14839
  for (let i = 0; i < 9; i++) if (Math.abs((t.linear[i] ?? 0) - (expected[i] ?? 0)) > 1e-9) return false;
14582
14840
  return Math.abs(t.translation[0]) < 1e-9 && Math.abs(t.translation[1]) < 1e-9 && Math.abs(t.translation[2]) < 1e-9;
14583
14841
  }
14584
- function findBodyItem(reader, representations) {
14842
+ function findBodyItems(reader, representations) {
14585
14843
  for (const repRef of representations) {
14586
14844
  const rep = reader.getLine(repRef.value);
14587
14845
  if (rep === null) continue;
14588
14846
  if (readLabel(rep["RepresentationIdentifier"]) !== "Body") continue;
14589
14847
  const items = asRefArray(rep["Items"]);
14590
- if (items === void 0 || items.length === 0) continue;
14591
- return items[0]?.value;
14848
+ if (items === void 0) return [];
14849
+ return items.map((item) => item.value);
14592
14850
  }
14851
+ return null;
14593
14852
  }
14594
14853
  function readPolylinePoints(reader, ref, scale) {
14595
14854
  const curveRef = asRef(ref);
@@ -14613,6 +14872,24 @@ function readPolylinePoints(reader, ref, scale) {
14613
14872
  }
14614
14873
  return out.length >= 3 ? out : void 0;
14615
14874
  }
14875
+ function readPoint2D(reader, ref, scale) {
14876
+ const pointRef = asRef(ref);
14877
+ if (pointRef === void 0) return void 0;
14878
+ const pt = reader.getLine(pointRef.value);
14879
+ if (pt === null) return void 0;
14880
+ const coords = asMeasureArray(pt["Coordinates"]);
14881
+ if (coords.length < 2) return void 0;
14882
+ return [(coords[0] ?? 0) * scale * 1e3, (coords[1] ?? 0) * scale * 1e3];
14883
+ }
14884
+ function readDirection2D(reader, ref) {
14885
+ const dirRef = asRef(ref);
14886
+ if (dirRef === void 0) return void 0;
14887
+ const dir = reader.getLine(dirRef.value);
14888
+ if (dir === null) return void 0;
14889
+ const ratios = asMeasureArray(dir["DirectionRatios"]);
14890
+ if (ratios.length < 2) return void 0;
14891
+ return [ratios[0] ?? 0, ratios[1] ?? 0];
14892
+ }
14616
14893
  function readPoint(reader, ref, scale) {
14617
14894
  const pointRef = asRef(ref);
14618
14895
  if (pointRef === void 0) return void 0;
@@ -14639,6 +14916,9 @@ function readDirection(reader, ref) {
14639
14916
  ratios[2] ?? 0
14640
14917
  ];
14641
14918
  }
14919
+ function releaseEmbind(value) {
14920
+ value.delete?.();
14921
+ }
14642
14922
  function asRef(value) {
14643
14923
  if (value !== null && typeof value === "object" && "value" in value) {
14644
14924
  const v = value.value;
@@ -14673,8 +14953,8 @@ function readLabel(value) {
14673
14953
  if (typeof v === "string") return v;
14674
14954
  }
14675
14955
  }
14676
- function isTessellatedType(type) {
14677
- 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;
14678
14958
  }
14679
14959
  function normalize(v) {
14680
14960
  const len = Math.hypot(v[0], v[1], v[2]);
@@ -15013,6 +15293,7 @@ function readOwnerHistory(reader, ownerHistoryExpressId) {
15013
15293
  }
15014
15294
  //#endregion
15015
15295
  //#region src/import/fromIfc.ts
15296
+ var testHooks = null;
15016
15297
  /**
15017
15298
  * Each enumerated physical-element IFC type and the {@link ImportedElementCategory}
15018
15299
  * it maps to. Curtain-wall sub-components (IfcPlate/IfcMember) and stair/ramp
@@ -15061,6 +15342,7 @@ async function fromIfc(bytes, options = {}) {
15061
15342
  const readerResult = await SpfReader.create(bytes, settings);
15062
15343
  if (!readerResult.ok) return (0, brepjs.err)(readerResult.error);
15063
15344
  const reader = readerResult.value;
15345
+ const elements = [];
15064
15346
  try {
15065
15347
  reader.buildGuidMap();
15066
15348
  const diagnostics = [];
@@ -15069,13 +15351,13 @@ async function fromIfc(bytes, options = {}) {
15069
15351
  const spatialRoot = buildSpatialTree(reader, scale);
15070
15352
  const containment = buildElementContainmentMap(reader);
15071
15353
  const typeEnums = buildTypePredefinedMap(reader);
15072
- const elements = [];
15073
15354
  const byExpressId = /* @__PURE__ */ new Map();
15074
15355
  for (const [type, category] of ELEMENT_TYPES) for (const expressId of reader.getLinesOfType(type)) {
15075
15356
  const element = readElement(reader, expressId, category, scale, containment, typeEnums, options.skipGeometry ?? false, diagnostics);
15076
15357
  if (element === null) continue;
15077
15358
  elements.push(element);
15078
15359
  byExpressId.set(element.expressId, element);
15360
+ testHooks?.afterElement?.(element, elements.length);
15079
15361
  }
15080
15362
  const report = appendIssues(emptyReport(), diagnostics);
15081
15363
  const applicationName = readApplicationName(reader);
@@ -15089,6 +15371,7 @@ async function fromIfc(bytes, options = {}) {
15089
15371
  };
15090
15372
  return (0, brepjs.ok)(model);
15091
15373
  } catch (e) {
15374
+ disposeElements(elements);
15092
15375
  return (0, brepjs.err)(importError("IMPORT_FAILED", "Unexpected failure during IFC import", e));
15093
15376
  } finally {
15094
15377
  reader.close();
@@ -15100,6 +15383,7 @@ async function fromIfc(bytes, options = {}) {
15100
15383
  * sub-read failure is isolated by its own reader and never aborts the element.
15101
15384
  */
15102
15385
  function readElement(reader, expressId, category, scale, containment, typeEnums, skipGeometry, diagnostics) {
15386
+ let geometry = null;
15103
15387
  try {
15104
15388
  const line = reader.getLine(expressId);
15105
15389
  if (line === null) {
@@ -15111,10 +15395,15 @@ function readElement(reader, expressId, category, scale, containment, typeEnums,
15111
15395
  const predefinedType = readPredefinedType(line) ?? typeEnums.get(expressId);
15112
15396
  const voidedBy = readVoids(reader, expressId).map((v) => v.openingExpressId);
15113
15397
  const fills = findFills(reader, expressId);
15114
- const geometry = skipGeometry ? {
15398
+ geometry = skipGeometry ? {
15115
15399
  fidelity: "NONE",
15116
- solid: null
15400
+ completeness: "NONE",
15401
+ solids: [],
15402
+ solid: null,
15403
+ bounds: null,
15404
+ volumeMm3: null
15117
15405
  } : reconstructGeometry(reader, expressId, scale, voidedBy, diagnostics);
15406
+ testHooks?.afterGeometry?.(expressId, geometry);
15118
15407
  const psets = readPsets(reader, expressId).map(toImportedPset);
15119
15408
  const material = readMaterial(reader, expressId, scale);
15120
15409
  const classification = readClassification(reader, expressId);
@@ -15137,6 +15426,7 @@ function readElement(reader, expressId, category, scale, containment, typeEnums,
15137
15426
  ...fills !== void 0 ? { fills } : {}
15138
15427
  };
15139
15428
  } catch (e) {
15429
+ if (geometry !== null) disposeGeometry(geometry);
15140
15430
  diagnostics.push(issue("error", "ELEMENT_READ_FAILED", `Element ${expressId} reconstruction threw: ${errMsg(e)}`, expressId));
15141
15431
  return null;
15142
15432
  }
@@ -15158,42 +15448,134 @@ function findFills(reader, elementExpressId) {
15158
15448
  * model. Falls back to the uncut solid (with a diagnostic) if a cut fails.
15159
15449
  */
15160
15450
  function reconstructGeometry(reader, expressId, scale, voidedBy, diagnostics) {
15161
- const base = toImportedGeometry(readBodyGeometry(reader, expressId, scale, diagnostics));
15162
- if (base.fidelity !== "PARAMETRIC" || base.solid === null || voidedBy.length === 0) return base;
15163
- 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)];
15164
15456
  for (const openingId of voidedBy) {
15165
15457
  const opening = readBodyGeometry(reader, openingId, scale, diagnostics);
15166
15458
  if (opening.kind !== "SOLID") continue;
15167
- const cutResult = (0, brepjs.cut)(host, opening.solid);
15168
- opening.solid[Symbol.dispose]();
15169
- if (!cutResult.ok) {
15170
- diagnostics.push(issue("warning", "VOID_SUBTRACTION_FAILED", `Opening ${openingId} could not be subtracted from element ${expressId}: ${cutResult.error.message}`, expressId));
15171
- continue;
15172
- }
15173
- host[Symbol.dispose]();
15174
- host = cutResult.value;
15175
- }
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
+ };
15176
15506
  return {
15177
- fidelity: "PARAMETRIC",
15178
- 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
+ } : {}
15179
15516
  };
15180
15517
  }
15181
- function toImportedGeometry(result) {
15182
- if (result.kind === "SOLID") return {
15183
- fidelity: result.lossy ? "TESSELLATED_MANIFOLD" : "PARAMETRIC",
15184
- solid: result.solid
15185
- };
15186
- if (result.kind === "MESH") return {
15187
- fidelity: "TESSELLATED_LOSSY",
15188
- solid: null,
15189
- meshVertices: result.vertices,
15190
- meshIndices: result.indices
15191
- };
15518
+ function completeImportedGeometry(fidelity, solids, expressId, diagnostics) {
15192
15519
  return {
15193
- fidelity: "NONE",
15194
- solid: null
15520
+ fidelity,
15521
+ completeness: "COMPLETE",
15522
+ solids,
15523
+ solid: solids.length === 1 ? solids[0] : null,
15524
+ ...measureCompleteBody(solids, expressId, diagnostics)
15195
15525
  };
15196
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
+ }
15197
15579
  function toImportedPset(pset) {
15198
15580
  return {
15199
15581
  name: pset.name,
@@ -15271,7 +15653,7 @@ function errMsg(e) {
15271
15653
  * with the model (it is not reclaimed automatically when the model is GC'd).
15272
15654
  */
15273
15655
  function disposeImportedModel(model) {
15274
- 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]();
15275
15657
  }
15276
15658
  //#endregion
15277
15659
  //#region src/specs/materialSpec.ts
@@ -28855,13 +29237,25 @@ function specLocalShift(el) {
28855
29237
  0
28856
29238
  ];
28857
29239
  }
29240
+ /** Skip the resolver occurrence wrappers already owned by `cumulativeFrame`,
29241
+ * then recover only the Body's outer literal Datum translations. An unexpected
29242
+ * wrapper shape returns zero rather than risking an occurrence transform twice. */
29243
+ function bodyLocalTranslation(el, occurrenceTransformDepth) {
29244
+ let body = el.geometry;
29245
+ for (let i = 0; i < occurrenceTransformDepth; i++) {
29246
+ if (body.kind !== "Translate" && body.kind !== "Rotate") return ZERO_TRANSLATION;
29247
+ body = body.target;
29248
+ }
29249
+ return peelTranslates(body).total;
29250
+ }
28858
29251
  /** World frame of an element's own body: the walk's cumulative frame (all
28859
29252
  * authored transforms, ancestors + own) composed with the body's authored axes
28860
- * (`axisX`/`axisZ` props) and its local origin (`origin` prop + centring
28861
- * shift). Under no rotation this reduces to `composedOrigin` + shift. */
28862
- function elementBodyFrame(el, cumulativeFrame) {
29253
+ * (`axisX`/`axisZ` props) and its local origin (`origin` prop + literal Body
29254
+ * Datum + centring shift). Under no rotation this reduces to `composedOrigin`
29255
+ * + shift. */
29256
+ function elementBodyFrame(el, cumulativeFrame, occurrenceTransformDepth) {
28863
29257
  return frameMul(frameMul(cumulativeFrame, frameFromPlacement({
28864
- origin: authoredSpecOrigin(el),
29258
+ origin: addTranslation(authoredSpecOrigin(el), bodyLocalTranslation(el, occurrenceTransformDepth)),
28865
29259
  axisX: authoredAxisX(el),
28866
29260
  axisZ: authoredAxisZ(el)
28867
29261
  })), translationFrame(specLocalShift(el)));
@@ -28880,10 +29274,10 @@ function civilNodeFrame(el, cumulativeFrame) {
28880
29274
  * `axisZ` are recomputed from the element's world frame relative to its spatial
28881
29275
  * container. Stair/ramp flights, which carry their own per-flight frame, are
28882
29276
  * each re-placed the same way. */
28883
- function rotatedRoutedInput(flatInput, el, cumulativeFrame, spatialFrame) {
29277
+ function rotatedRoutedInput(flatInput, el, cumulativeFrame, occurrenceTransformDepth, spatialFrame) {
28884
29278
  const toSpatial = frameInverse(spatialFrame);
28885
29279
  if (Array.isArray(flatInput["flights"])) {
28886
- const elementFrame = elementBodyFrame(el, cumulativeFrame);
29280
+ const elementFrame = elementBodyFrame(el, cumulativeFrame, occurrenceTransformDepth);
28887
29281
  const flights = flatInput["flights"];
28888
29282
  return {
28889
29283
  ...flatInput,
@@ -28924,7 +29318,7 @@ function rotatedRoutedInput(flatInput, el, cumulativeFrame, spatialFrame) {
28924
29318
  })
28925
29319
  };
28926
29320
  }
28927
- const placed = decomposeFrame(frameMul(toSpatial, elementBodyFrame(el, cumulativeFrame)));
29321
+ const placed = decomposeFrame(frameMul(toSpatial, elementBodyFrame(el, cumulativeFrame, occurrenceTransformDepth)));
28928
29322
  return {
28929
29323
  ...flatInput,
28930
29324
  origin: placed.origin,
@@ -29089,6 +29483,7 @@ function familiesToBim(root, options) {
29089
29483
  const walk = (el, state) => {
29090
29484
  const rotatedHere = state.rotated || hasRotateOp(el);
29091
29485
  const cumulativeTranslationHere = addTranslation(state.cumulativeTranslation, authoredTranslation(el));
29486
+ const occurrenceTransformDepthHere = state.occurrenceTransformDepth + el.localTransforms.length;
29092
29487
  const cumulativeFrameHere = frameMul(state.cumulativeFrame, frameFromOps(el.localTransforms));
29093
29488
  let proxiedHere = false;
29094
29489
  let nextRotated = rotatedHere;
@@ -29152,7 +29547,7 @@ function familiesToBim(root, options) {
29152
29547
  }
29153
29548
  const routedInput = el.semantics?.kind === "product" ? civilProductSpecInput(el) : ("input" in route ? route.input : specInput)(el);
29154
29549
  const rotatedBase = Array.isArray(routedInput["flights"]) ? specInput(el) : routedInput;
29155
- const placedInput = rotatedHere ? rotatedRoutedInput(rotatedBase, el, cumulativeFrameHere, state.spatialFrame) : usesAuthoredCivilHierarchy ? relativeSpecInput(routedInput, state.projectedSpatialTranslation) : routedInput;
29550
+ const placedInput = rotatedHere ? rotatedRoutedInput(rotatedBase, el, cumulativeFrameHere, occurrenceTransformDepthHere, state.spatialFrame) : usesAuthoredCivilHierarchy ? relativeSpecInput(routedInput, state.projectedSpatialTranslation) : routedInput;
29156
29551
  const parsed = route.parse(placedInput);
29157
29552
  if (!parsed.ok) return parsed;
29158
29553
  const added = route.add(model, parsed.value, el.keyPath);
@@ -29189,6 +29584,7 @@ function familiesToBim(root, options) {
29189
29584
  rotated: nextRotated,
29190
29585
  cumulativeTranslation: cumulativeTranslationHere,
29191
29586
  projectedSpatialTranslation: nextProjectedSpatialTranslation,
29587
+ occurrenceTransformDepth: occurrenceTransformDepthHere,
29192
29588
  cumulativeFrame: nextCumulativeFrame,
29193
29589
  spatialFrame: nextSpatialFrame
29194
29590
  });
@@ -29202,6 +29598,7 @@ function familiesToBim(root, options) {
29202
29598
  rotated: false,
29203
29599
  cumulativeTranslation: ZERO_TRANSLATION,
29204
29600
  projectedSpatialTranslation: ZERO_TRANSLATION,
29601
+ occurrenceTransformDepth: 0,
29205
29602
  cumulativeFrame: IDENTITY_FRAME,
29206
29603
  spatialFrame: IDENTITY_FRAME
29207
29604
  });