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.
@@ -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
@@ -183,7 +183,7 @@ function fromBrepError(inner, code, message) {
183
183
  };
184
184
  }
185
185
  //#endregion
186
- //#region \0@oxc-project+runtime@0.143.0/helpers/esm/usingCtx.js
186
+ //#region \0@oxc-project+runtime@0.146.0/helpers/esm/usingCtx.js
187
187
  function _usingCtx() {
188
188
  var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) {
189
189
  var n = Error();
@@ -8756,7 +8756,7 @@ function writeWallGeometry(w, spec, geomSubContextId, parentPlacementId) {
8756
8756
  value: "AREA"
8757
8757
  },
8758
8758
  ProfileName: null,
8759
- Position: w.ref(writeAxis2Placement2D$1(w)),
8759
+ Position: w.ref(writeAxis2Placement2D$1(w, [thicknessM / 2, heightM / 2])),
8760
8760
  XDim: w.mkType(WebIFC.IFCPOSITIVELENGTHMEASURE, thicknessM),
8761
8761
  YDim: w.mkType(WebIFC.IFCPOSITIVELENGTHMEASURE, heightM)
8762
8762
  });
@@ -8977,12 +8977,12 @@ function writeRoofGeometry(w, spec, solid, geomSubContextId, parentPlacementId)
8977
8977
  usedFallback: false
8978
8978
  };
8979
8979
  }
8980
- function writeAxis2Placement2D$1(w) {
8980
+ function writeAxis2Placement2D$1(w, location = [0, 0]) {
8981
8981
  const originId = w.nextId();
8982
8982
  w.writeLine({
8983
8983
  expressID: originId,
8984
8984
  type: WebIFC.IFCCARTESIANPOINT,
8985
- Coordinates: [w.mkType(WebIFC.IFCLENGTHMEASURE, 0), w.mkType(WebIFC.IFCLENGTHMEASURE, 0)]
8985
+ Coordinates: [w.mkType(WebIFC.IFCLENGTHMEASURE, location[0]), w.mkType(WebIFC.IFCLENGTHMEASURE, location[1])]
8986
8986
  });
8987
8987
  const id = w.nextId();
8988
8988
  w.writeLine({
@@ -11487,11 +11487,10 @@ function writeOpeningGeometry(w, guid, openingSpec, wallSpec, wallPlacementId, g
11487
11487
  const offsetAlongWallM = toIfcLengthM(openingSpec.offsetAlongWall);
11488
11488
  const offsetFromFloorM = toIfcLengthM(openingSpec.offsetFromFloor);
11489
11489
  const thicknessM = toIfcLengthM(wallSpec.thickness);
11490
- const wallHeightM = toIfcLengthM(wallSpec.height);
11491
11490
  const placement3DId = writeAxis2Placement3D(w, [
11492
11491
  offsetAlongWallM + widthM / 2,
11493
- thicknessM / 2,
11494
- -wallHeightM / 2 + offsetFromFloorM + heightM / 2
11492
+ thicknessM,
11493
+ offsetFromFloorM + heightM / 2
11495
11494
  ], [
11496
11495
  0,
11497
11496
  -1,
@@ -12367,6 +12366,24 @@ var SUPPORTED_SCHEMAS = [
12367
12366
  * MUST call {@link SpfReader.close} (it always issues `CloseModel`, preventing
12368
12367
  * WASM handle leaks). The handle is `using`-friendly via `Symbol.dispose`.
12369
12368
  */
12369
+ var IFC_Z_UP_TRANSFORMATION = [
12370
+ 1,
12371
+ 0,
12372
+ 0,
12373
+ 0,
12374
+ 0,
12375
+ 0,
12376
+ 1,
12377
+ 0,
12378
+ 0,
12379
+ -1,
12380
+ 0,
12381
+ 0,
12382
+ 0,
12383
+ 0,
12384
+ 0,
12385
+ 1
12386
+ ];
12370
12387
  function deleteVector(vec) {
12371
12388
  vec.delete?.();
12372
12389
  }
@@ -12399,6 +12416,7 @@ var SpfReader = class SpfReader {
12399
12416
  if (modelId >= 0) api.CloseModel(modelId);
12400
12417
  return err(importError("OPEN_MODEL_FAILED", "web-ifc OpenModel returned an invalid model id"));
12401
12418
  }
12419
+ api.SetGeometryTransformation(modelId, IFC_Z_UP_TRANSFORMATION);
12402
12420
  const schemaRaw = api.GetModelSchema(modelId);
12403
12421
  if (!SUPPORTED_SCHEMAS.includes(schemaRaw)) {
12404
12422
  api.CloseModel(modelId);
@@ -14047,6 +14065,7 @@ function buildElementContainmentMap(reader) {
14047
14065
  //#endregion
14048
14066
  //#region src/import/geometryRead.ts
14049
14067
  var NONE = { kind: "NONE" };
14068
+ var testHooks$1 = null;
14050
14069
  /**
14051
14070
  * Reconstructs the `Body` (SweptSolid) representation of a product into a brepjs
14052
14071
  * solid, falling back to tessellated mesh import when no parametric path exists.
@@ -14057,27 +14076,66 @@ var NONE = { kind: "NONE" };
14057
14076
  * single bad element never aborts a model import.
14058
14077
  */
14059
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) {
14060
14086
  const product = reader.getLine(productExpressId);
14061
- if (product === null) return NONE;
14087
+ if (product === null) return {
14088
+ hasBody: false,
14089
+ itemCount: 0,
14090
+ items: []
14091
+ };
14062
14092
  const representationRef = asRef(product["Representation"]);
14063
- if (representationRef === void 0) return NONE;
14093
+ if (representationRef === void 0) return {
14094
+ hasBody: false,
14095
+ itemCount: 0,
14096
+ items: []
14097
+ };
14064
14098
  const productShape = reader.getLine(representationRef.value);
14065
14099
  const representations = productShape === null ? void 0 : asRefArray(productShape["Representations"]);
14066
- if (representations === void 0) return NONE;
14067
- const bodyItemId = findBodyItem(reader, representations);
14068
- 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
+ };
14069
14111
  const worldTransform = readWorldTransform(reader, product, scale);
14070
- const itemType = reader.getLineType(bodyItemId);
14071
- try {
14072
- if (itemType === WebIFC.IFCEXTRUDEDAREASOLID) return reconstructExtrusion(reader, bodyItemId, scale, worldTransform, diagnostics);
14073
- if (itemType === WebIFC.IFCREVOLVEDAREASOLID) return reconstructRevolution(reader, bodyItemId, scale, worldTransform, diagnostics);
14074
- if (isTessellatedType(itemType)) return reconstructTessellated(reader, productExpressId, diagnostics);
14075
- } catch (e) {
14076
- diagnostics.push(issue("warning", "GEOMETRY_RECONSTRUCTION_FAILED", `Body geometry reconstruction threw: ${errMsg$1(e)}`, productExpressId));
14077
- 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));
14078
14119
  }
14079
- diagnostics.push(issue("warning", "UNSUPPORTED_REPRESENTATION_ITEM", `Unsupported body representation item type ${itemType}; geometry skipped`, productExpressId));
14080
- 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
+ };
14081
14139
  }
14082
14140
  function reconstructExtrusion(reader, extrusionId, scale, worldTransform, diagnostics) {
14083
14141
  const ext = reader.getLine(extrusionId);
@@ -14098,12 +14156,14 @@ function reconstructExtrusion(reader, extrusionId, scale, worldTransform, diagno
14098
14156
  diagnostics.push(issue("warning", faceResult.error.code, faceResult.error.message, extrusionId));
14099
14157
  return NONE;
14100
14158
  }
14159
+ const profileFrame = readProfilePosition(reader, sweptArea.value, scale);
14101
14160
  const depthMm = depth * scale * 1e3;
14102
- const extrudeDir = readDirection(reader, ext["ExtrudedDirection"]) ?? [
14161
+ const sweptDir = readDirection(reader, ext["ExtrudedDirection"]) ?? [
14103
14162
  0,
14104
14163
  0,
14105
14164
  1
14106
14165
  ];
14166
+ const extrudeDir = profileFrame === null ? sweptDir : rotateByTranspose(profileFrame.linear, sweptDir);
14107
14167
  const extrudeVec = [
14108
14168
  extrudeDir[0] * depthMm,
14109
14169
  extrudeDir[1] * depthMm,
@@ -14125,7 +14185,11 @@ function reconstructExtrusion(reader, extrusionId, scale, worldTransform, diagno
14125
14185
  return NONE;
14126
14186
  }
14127
14187
  const localFrame = readAxis2Placement3D(reader, ext["Position"], scale);
14128
- const placed = placeSolid(solidResult.value, localFrame, worldTransform);
14188
+ const placed = placeSolid(solidResult.value, [
14189
+ worldTransform,
14190
+ localFrame,
14191
+ profileFrame
14192
+ ]);
14129
14193
  if (!placed.ok) {
14130
14194
  diagnostics.push(issue("warning", placed.error.code, placed.error.message, extrusionId));
14131
14195
  return NONE;
@@ -14153,16 +14217,19 @@ function reconstructRevolution(reader, revolutionId, scale, worldTransform, diag
14153
14217
  return NONE;
14154
14218
  }
14155
14219
  const axis1 = reader.getLine(axisRef.value);
14156
- const center = (axis1 === null ? void 0 : readPoint(reader, axis1["Location"], scale)) ?? [
14220
+ const sweptCenter = (axis1 === null ? void 0 : readPoint(reader, axis1["Location"], scale)) ?? [
14157
14221
  0,
14158
14222
  0,
14159
14223
  0
14160
14224
  ];
14161
- const direction = (axis1 === null ? void 0 : readDirection(reader, axis1["Axis"])) ?? [
14225
+ const sweptDirection = (axis1 === null ? void 0 : readDirection(reader, axis1["Axis"])) ?? [
14162
14226
  0,
14163
14227
  0,
14164
14228
  1
14165
14229
  ];
14230
+ const profileFrame = readProfilePosition(reader, sweptArea.value, scale);
14231
+ const center = profileFrame === null ? sweptCenter : inverseRigidPoint(profileFrame, sweptCenter);
14232
+ const direction = profileFrame === null ? sweptDirection : rotateByTranspose(profileFrame.linear, sweptDirection);
14166
14233
  const revolved = (() => {
14167
14234
  try {
14168
14235
  var _usingCtx3 = _usingCtx();
@@ -14189,40 +14256,72 @@ function reconstructRevolution(reader, revolutionId, scale, worldTransform, diag
14189
14256
  return NONE;
14190
14257
  }
14191
14258
  const localFrame = readAxis2Placement3D(reader, rev["Position"], scale);
14192
- const placed = placeSolid(revolved.value, localFrame, worldTransform);
14259
+ const placed = placeSolid(revolved.value, [
14260
+ worldTransform,
14261
+ localFrame,
14262
+ profileFrame
14263
+ ]);
14193
14264
  if (!placed.ok) {
14194
14265
  diagnostics.push(issue("warning", placed.error.code, placed.error.message, revolutionId));
14195
14266
  return NONE;
14196
14267
  }
14197
14268
  return finalizeSolid(placed.value, revolutionId, diagnostics);
14198
14269
  }
14199
- function reconstructTessellated(reader, productExpressId, diagnostics) {
14200
- 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) {
14201
14281
  if (mesh === null || mesh.indices.length === 0) {
14202
- 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));
14203
14283
  return NONE;
14204
14284
  }
14205
- const stl = packBinaryStl(mesh.vertices, mesh.indices, 1e3);
14206
- let solid = null;
14285
+ const stl = packBinaryStl(mesh.vertices, mesh.indices, scaleToMm);
14286
+ let solid = sewMeshToSolid(mesh, scaleToMm, itemExpressId, diagnostics);
14207
14287
  try {
14208
- const cast = castShape(getKernel().importSTL(stl.buffer));
14209
- if (isSolid(cast)) {
14210
- const valid = validSolid(cast);
14211
- if (valid.ok) solid = valid.value;
14212
- else cast[Symbol.dispose]();
14213
- } else cast[Symbol.dispose]();
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
+ }
14214
14307
  } catch (e) {
14215
- diagnostics.push(issue("info", "TESSELLATION_NOT_MANIFOLD", `STL round-trip failed: ${errMsg$1(e)}`, productExpressId));
14308
+ diagnostics.push(issue("info", "TESSELLATION_NOT_MANIFOLD", `STL round-trip failed: ${errMsg$1(e)}`, itemExpressId));
14216
14309
  }
14217
14310
  if (solid !== null) {
14218
- diagnostics.push(issue("info", "TESSELLATED_MANIFOLD", "Tessellated mesh recovered as a closed solid via STL round-trip", 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));
14219
14318
  return {
14220
14319
  kind: "SOLID",
14221
14320
  solid,
14222
14321
  lossy: true
14223
14322
  };
14224
14323
  }
14225
- 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));
14226
14325
  return {
14227
14326
  kind: "MESH",
14228
14327
  vertices: mesh.vertices,
@@ -14230,43 +14329,122 @@ function reconstructTessellated(reader, productExpressId, diagnostics) {
14230
14329
  diagnostic: "TESSELLATED_LOSSY"
14231
14330
  };
14232
14331
  }
14233
- function collectMesh(reader, productExpressId) {
14234
- const vertices = [];
14235
- const indices = [];
14236
- let base = 0;
14332
+ /** Loose enough to weld float32 vertices emitted per face, expressed in millimetres. */
14333
+ var MESH_SEW_TOLERANCE_MM = .001;
14334
+ /**
14335
+ * Brands a fresh kernel result and releases its pre-downcast arena slot when
14336
+ * the cast moved to a new one. In-place kernels share the slot.
14337
+ */
14338
+ function castFreshResult(raw) {
14339
+ const cast = castShape(raw);
14340
+ const rawId = raw.id;
14341
+ const castId = cast.wrapped.id;
14342
+ if (!(rawId !== void 0 && castId !== void 0 ? rawId === castId : cast.wrapped === raw)) getKernel().dispose(raw);
14343
+ return cast;
14344
+ }
14345
+ function sewMeshToSolid(mesh, scaleToMm, itemExpressId, diagnostics) {
14346
+ const kernel = getKernel();
14347
+ const { vertices, indices } = mesh;
14348
+ const point = (index) => [
14349
+ (vertices[index * 3] ?? 0) * scaleToMm,
14350
+ (vertices[index * 3 + 1] ?? 0) * scaleToMm,
14351
+ (vertices[index * 3 + 2] ?? 0) * scaleToMm
14352
+ ];
14353
+ const triangles = [];
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));
14356
+ if (face !== null) triangles.push(face);
14357
+ }
14358
+ if (triangles.length === 0) return null;
14359
+ try {
14360
+ const cast = castFreshResult(kernel.sewAndSolidify(triangles, MESH_SEW_TOLERANCE_MM));
14361
+ const valid = isSolid(cast) ? validSolid(cast) : null;
14362
+ if (valid !== null && valid.ok) return valid.value;
14363
+ cast[Symbol.dispose]();
14364
+ return null;
14365
+ } catch (cause) {
14366
+ diagnostics.push(issue("info", "TESSELLATION_NOT_MANIFOLD", `Mesh sewing failed: ${errMsg$1(cause)}`, itemExpressId));
14367
+ return null;
14368
+ } finally {
14369
+ for (const face of triangles) kernel.dispose(face);
14370
+ }
14371
+ }
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;
14237
14376
  reader.streamMeshes([productExpressId], (flatMesh) => {
14238
14377
  try {
14239
14378
  const geometries = flatMesh.geometries;
14240
- for (let g = 0; g < geometries.size(); g++) {
14241
- const placed = geometries.get(g);
14242
- 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);
14243
14389
  try {
14244
- const verts = reader.getVertexArray(geom.GetVertexData(), geom.GetVertexDataSize());
14245
- const idx = reader.getIndexArray(geom.GetIndexData(), geom.GetIndexDataSize());
14246
- const m = placed.flatTransformation;
14247
- const vertexCount = verts.length / 6;
14248
- for (let v = 0; v < vertexCount; v++) {
14249
- const x = verts[v * 6] ?? 0;
14250
- const y = verts[v * 6 + 1] ?? 0;
14251
- const z = verts[v * 6 + 2] ?? 0;
14252
- 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));
14253
14400
  }
14254
- for (let i = 0; i < idx.length; i++) indices.push(base + (idx[i] ?? 0));
14255
- base += vertexCount;
14401
+ for (let index = 0; index < indices.length; index++) accumulator.indices.push(base + (indices[index] ?? 0));
14256
14402
  } finally {
14257
- geom.delete();
14403
+ geometry.delete();
14258
14404
  }
14259
14405
  }
14260
14406
  } finally {
14261
- flatMesh.delete();
14407
+ releaseEmbind(flatMesh.geometries);
14408
+ releaseEmbind(flatMesh);
14262
14409
  }
14263
14410
  });
14264
- 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
+ }
14265
14434
  return {
14266
14435
  vertices: new Float32Array(vertices),
14267
14436
  indices: new Uint32Array(indices)
14268
14437
  };
14269
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
+ }
14270
14448
  function packBinaryStl(vertices, indices, scaleToMm) {
14271
14449
  const triCount = Math.floor(indices.length / 3);
14272
14450
  const buffer = /* @__PURE__ */ new ArrayBuffer(84 + triCount * 50);
@@ -14429,19 +14607,99 @@ function profileToFace(profile) {
14429
14607
  if (!face.ok) return err(importError("GEOMETRY_RECONSTRUCTION_FAILED", `Profile face build failed: ${face.error.message}`));
14430
14608
  return ok(face.value);
14431
14609
  }
14432
- function placeSolid(solid, localFrame, worldTransform) {
14433
- let current = solid;
14434
- for (const transform of [localFrame, worldTransform]) {
14435
- if (transform === null || isIdentity(transform)) continue;
14436
- const applied = applyMatrix(current, transform);
14437
- if (!applied.ok) {
14438
- current[Symbol.dispose]();
14439
- return err(importError("PLACEMENT_READ_FAILED", `Placement transform failed: ${applied.error.message}`));
14440
- }
14441
- current[Symbol.dispose]();
14442
- current = applied.value;
14443
- }
14444
- return ok(current);
14610
+ /**
14611
+ * IfcParameterizedProfileDef.Position places the profile inside the swept
14612
+ * solid's XY plane; the writers rely on it to corner-anchor centred rectangle
14613
+ * profiles. Absent on arbitrary (polyline) profiles.
14614
+ */
14615
+ function readProfilePosition(reader, profileExpressId, scale) {
14616
+ const def = reader.getLine(profileExpressId);
14617
+ const positionRef = def === null ? void 0 : asRef(def["Position"]);
14618
+ if (positionRef === void 0) return null;
14619
+ const position = reader.getLine(positionRef.value);
14620
+ if (position === null) return null;
14621
+ const [x, y] = readPoint2D(reader, position["Location"], scale) ?? [0, 0];
14622
+ const [dx, dy] = readDirection2D(reader, position["RefDirection"]) ?? [1, 0];
14623
+ const len = Math.hypot(dx, dy);
14624
+ const c = len < 1e-12 ? 1 : dx / len;
14625
+ const s = len < 1e-12 ? 0 : dy / len;
14626
+ return {
14627
+ linear: [
14628
+ c,
14629
+ -s,
14630
+ 0,
14631
+ s,
14632
+ c,
14633
+ 0,
14634
+ 0,
14635
+ 0,
14636
+ 1
14637
+ ],
14638
+ translation: [
14639
+ x,
14640
+ y,
14641
+ 0
14642
+ ]
14643
+ };
14644
+ }
14645
+ /**
14646
+ * Applies the placement chain (outermost first) as one composed rigid motion.
14647
+ * A second applyMatrix on an already-transformed solid can fail BRepCheck on
14648
+ * occt-wasm, so the frames are never applied one after another.
14649
+ */
14650
+ function placeSolid(solid, frames) {
14651
+ let composed = null;
14652
+ for (const frame of frames) {
14653
+ if (frame === null) continue;
14654
+ composed = composed === null ? frame : composeRigid(composed, frame);
14655
+ }
14656
+ if (composed === null || isIdentity(composed)) return ok(solid);
14657
+ const applied = applyMatrix(solid, composed);
14658
+ solid[Symbol.dispose]();
14659
+ if (!applied.ok) return err(importError("PLACEMENT_READ_FAILED", `Placement transform failed: ${applied.error.message}`));
14660
+ return ok(applied.value);
14661
+ }
14662
+ /** `outer ∘ inner`: the motion that applies `inner` first, then `outer`. */
14663
+ function composeRigid(outer, inner) {
14664
+ const [a00, a01, a02, a10, a11, a12, a20, a21, a22] = outer.linear;
14665
+ const [b00, b01, b02, b10, b11, b12, b20, b21, b22] = inner.linear;
14666
+ const [tx, ty, tz] = inner.translation;
14667
+ const [ux, uy, uz] = outer.translation;
14668
+ return {
14669
+ linear: [
14670
+ a00 * b00 + a01 * b10 + a02 * b20,
14671
+ a00 * b01 + a01 * b11 + a02 * b21,
14672
+ a00 * b02 + a01 * b12 + a02 * b22,
14673
+ a10 * b00 + a11 * b10 + a12 * b20,
14674
+ a10 * b01 + a11 * b11 + a12 * b21,
14675
+ a10 * b02 + a11 * b12 + a12 * b22,
14676
+ a20 * b00 + a21 * b10 + a22 * b20,
14677
+ a20 * b01 + a21 * b11 + a22 * b21,
14678
+ a20 * b02 + a21 * b12 + a22 * b22
14679
+ ],
14680
+ translation: [
14681
+ a00 * tx + a01 * ty + a02 * tz + ux,
14682
+ a10 * tx + a11 * ty + a12 * tz + uy,
14683
+ a20 * tx + a21 * ty + a22 * tz + uz
14684
+ ]
14685
+ };
14686
+ }
14687
+ /** Rᵀ·v for a rigid frame's rotation part. */
14688
+ function rotateByTranspose(linear, v) {
14689
+ return [
14690
+ linear[0] * v[0] + linear[3] * v[1] + linear[6] * v[2],
14691
+ linear[1] * v[0] + linear[4] * v[1] + linear[7] * v[2],
14692
+ linear[2] * v[0] + linear[5] * v[1] + linear[8] * v[2]
14693
+ ];
14694
+ }
14695
+ /** Maps a point from the frame's parent space into the frame: Rᵀ·(p − t). */
14696
+ function inverseRigidPoint(frame, p) {
14697
+ const t = frame.translation;
14698
+ return rotateByTranspose(frame.linear, [
14699
+ p[0] - t[0],
14700
+ p[1] - t[1],
14701
+ p[2] - t[2]
14702
+ ]);
14445
14703
  }
14446
14704
  function finalizeSolid(solid, entity, diagnostics) {
14447
14705
  if (!isSolid(solid)) {
@@ -14558,15 +14816,16 @@ function isIdentity(t) {
14558
14816
  for (let i = 0; i < 9; i++) if (Math.abs((t.linear[i] ?? 0) - (expected[i] ?? 0)) > 1e-9) return false;
14559
14817
  return Math.abs(t.translation[0]) < 1e-9 && Math.abs(t.translation[1]) < 1e-9 && Math.abs(t.translation[2]) < 1e-9;
14560
14818
  }
14561
- function findBodyItem(reader, representations) {
14819
+ function findBodyItems(reader, representations) {
14562
14820
  for (const repRef of representations) {
14563
14821
  const rep = reader.getLine(repRef.value);
14564
14822
  if (rep === null) continue;
14565
14823
  if (readLabel(rep["RepresentationIdentifier"]) !== "Body") continue;
14566
14824
  const items = asRefArray(rep["Items"]);
14567
- if (items === void 0 || items.length === 0) continue;
14568
- return items[0]?.value;
14825
+ if (items === void 0) return [];
14826
+ return items.map((item) => item.value);
14569
14827
  }
14828
+ return null;
14570
14829
  }
14571
14830
  function readPolylinePoints(reader, ref, scale) {
14572
14831
  const curveRef = asRef(ref);
@@ -14590,6 +14849,24 @@ function readPolylinePoints(reader, ref, scale) {
14590
14849
  }
14591
14850
  return out.length >= 3 ? out : void 0;
14592
14851
  }
14852
+ function readPoint2D(reader, ref, scale) {
14853
+ const pointRef = asRef(ref);
14854
+ if (pointRef === void 0) return void 0;
14855
+ const pt = reader.getLine(pointRef.value);
14856
+ if (pt === null) return void 0;
14857
+ const coords = asMeasureArray(pt["Coordinates"]);
14858
+ if (coords.length < 2) return void 0;
14859
+ return [(coords[0] ?? 0) * scale * 1e3, (coords[1] ?? 0) * scale * 1e3];
14860
+ }
14861
+ function readDirection2D(reader, ref) {
14862
+ const dirRef = asRef(ref);
14863
+ if (dirRef === void 0) return void 0;
14864
+ const dir = reader.getLine(dirRef.value);
14865
+ if (dir === null) return void 0;
14866
+ const ratios = asMeasureArray(dir["DirectionRatios"]);
14867
+ if (ratios.length < 2) return void 0;
14868
+ return [ratios[0] ?? 0, ratios[1] ?? 0];
14869
+ }
14593
14870
  function readPoint(reader, ref, scale) {
14594
14871
  const pointRef = asRef(ref);
14595
14872
  if (pointRef === void 0) return void 0;
@@ -14616,6 +14893,9 @@ function readDirection(reader, ref) {
14616
14893
  ratios[2] ?? 0
14617
14894
  ];
14618
14895
  }
14896
+ function releaseEmbind(value) {
14897
+ value.delete?.();
14898
+ }
14619
14899
  function asRef(value) {
14620
14900
  if (value !== null && typeof value === "object" && "value" in value) {
14621
14901
  const v = value.value;
@@ -14650,8 +14930,8 @@ function readLabel(value) {
14650
14930
  if (typeof v === "string") return v;
14651
14931
  }
14652
14932
  }
14653
- function isTessellatedType(type) {
14654
- return type === WebIFC.IFCTRIANGULATEDFACESET || type === WebIFC.IFCPOLYGONALFACESET || type === WebIFC.IFCFACEBASEDSURFACEMODEL;
14933
+ function isStreamedTessellatedType(type) {
14934
+ return type === WebIFC.IFCPOLYGONALFACESET || type === WebIFC.IFCFACEBASEDSURFACEMODEL;
14655
14935
  }
14656
14936
  function normalize(v) {
14657
14937
  const len = Math.hypot(v[0], v[1], v[2]);
@@ -14990,6 +15270,7 @@ function readOwnerHistory(reader, ownerHistoryExpressId) {
14990
15270
  }
14991
15271
  //#endregion
14992
15272
  //#region src/import/fromIfc.ts
15273
+ var testHooks = null;
14993
15274
  /**
14994
15275
  * Each enumerated physical-element IFC type and the {@link ImportedElementCategory}
14995
15276
  * it maps to. Curtain-wall sub-components (IfcPlate/IfcMember) and stair/ramp
@@ -15038,6 +15319,7 @@ async function fromIfc(bytes, options = {}) {
15038
15319
  const readerResult = await SpfReader.create(bytes, settings);
15039
15320
  if (!readerResult.ok) return err(readerResult.error);
15040
15321
  const reader = readerResult.value;
15322
+ const elements = [];
15041
15323
  try {
15042
15324
  reader.buildGuidMap();
15043
15325
  const diagnostics = [];
@@ -15046,13 +15328,13 @@ async function fromIfc(bytes, options = {}) {
15046
15328
  const spatialRoot = buildSpatialTree(reader, scale);
15047
15329
  const containment = buildElementContainmentMap(reader);
15048
15330
  const typeEnums = buildTypePredefinedMap(reader);
15049
- const elements = [];
15050
15331
  const byExpressId = /* @__PURE__ */ new Map();
15051
15332
  for (const [type, category] of ELEMENT_TYPES) for (const expressId of reader.getLinesOfType(type)) {
15052
15333
  const element = readElement(reader, expressId, category, scale, containment, typeEnums, options.skipGeometry ?? false, diagnostics);
15053
15334
  if (element === null) continue;
15054
15335
  elements.push(element);
15055
15336
  byExpressId.set(element.expressId, element);
15337
+ testHooks?.afterElement?.(element, elements.length);
15056
15338
  }
15057
15339
  const report = appendIssues(emptyReport(), diagnostics);
15058
15340
  const applicationName = readApplicationName(reader);
@@ -15066,6 +15348,7 @@ async function fromIfc(bytes, options = {}) {
15066
15348
  };
15067
15349
  return ok(model);
15068
15350
  } catch (e) {
15351
+ disposeElements(elements);
15069
15352
  return err(importError("IMPORT_FAILED", "Unexpected failure during IFC import", e));
15070
15353
  } finally {
15071
15354
  reader.close();
@@ -15077,6 +15360,7 @@ async function fromIfc(bytes, options = {}) {
15077
15360
  * sub-read failure is isolated by its own reader and never aborts the element.
15078
15361
  */
15079
15362
  function readElement(reader, expressId, category, scale, containment, typeEnums, skipGeometry, diagnostics) {
15363
+ let geometry = null;
15080
15364
  try {
15081
15365
  const line = reader.getLine(expressId);
15082
15366
  if (line === null) {
@@ -15088,10 +15372,15 @@ function readElement(reader, expressId, category, scale, containment, typeEnums,
15088
15372
  const predefinedType = readPredefinedType(line) ?? typeEnums.get(expressId);
15089
15373
  const voidedBy = readVoids(reader, expressId).map((v) => v.openingExpressId);
15090
15374
  const fills = findFills(reader, expressId);
15091
- const geometry = skipGeometry ? {
15375
+ geometry = skipGeometry ? {
15092
15376
  fidelity: "NONE",
15093
- solid: null
15377
+ completeness: "NONE",
15378
+ solids: [],
15379
+ solid: null,
15380
+ bounds: null,
15381
+ volumeMm3: null
15094
15382
  } : reconstructGeometry(reader, expressId, scale, voidedBy, diagnostics);
15383
+ testHooks?.afterGeometry?.(expressId, geometry);
15095
15384
  const psets = readPsets(reader, expressId).map(toImportedPset);
15096
15385
  const material = readMaterial(reader, expressId, scale);
15097
15386
  const classification = readClassification(reader, expressId);
@@ -15114,6 +15403,7 @@ function readElement(reader, expressId, category, scale, containment, typeEnums,
15114
15403
  ...fills !== void 0 ? { fills } : {}
15115
15404
  };
15116
15405
  } catch (e) {
15406
+ if (geometry !== null) disposeGeometry(geometry);
15117
15407
  diagnostics.push(issue("error", "ELEMENT_READ_FAILED", `Element ${expressId} reconstruction threw: ${errMsg(e)}`, expressId));
15118
15408
  return null;
15119
15409
  }
@@ -15135,42 +15425,134 @@ function findFills(reader, elementExpressId) {
15135
15425
  * model. Falls back to the uncut solid (with a diagnostic) if a cut fails.
15136
15426
  */
15137
15427
  function reconstructGeometry(reader, expressId, scale, voidedBy, diagnostics) {
15138
- const base = toImportedGeometry(readBodyGeometry(reader, expressId, scale, diagnostics));
15139
- if (base.fidelity !== "PARAMETRIC" || base.solid === null || voidedBy.length === 0) return base;
15140
- 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)];
15141
15433
  for (const openingId of voidedBy) {
15142
15434
  const opening = readBodyGeometry(reader, openingId, scale, diagnostics);
15143
15435
  if (opening.kind !== "SOLID") continue;
15144
- const cutResult = cut(host, opening.solid);
15145
- opening.solid[Symbol.dispose]();
15146
- if (!cutResult.ok) {
15147
- diagnostics.push(issue("warning", "VOID_SUBTRACTION_FAILED", `Opening ${openingId} could not be subtracted from element ${expressId}: ${cutResult.error.message}`, expressId));
15148
- continue;
15149
- }
15150
- host[Symbol.dispose]();
15151
- host = cutResult.value;
15152
- }
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
+ };
15153
15483
  return {
15154
- fidelity: "PARAMETRIC",
15155
- 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
+ } : {}
15156
15493
  };
15157
15494
  }
15158
- function toImportedGeometry(result) {
15159
- if (result.kind === "SOLID") return {
15160
- fidelity: result.lossy ? "TESSELLATED_MANIFOLD" : "PARAMETRIC",
15161
- solid: result.solid
15162
- };
15163
- if (result.kind === "MESH") return {
15164
- fidelity: "TESSELLATED_LOSSY",
15165
- solid: null,
15166
- meshVertices: result.vertices,
15167
- meshIndices: result.indices
15168
- };
15495
+ function completeImportedGeometry(fidelity, solids, expressId, diagnostics) {
15169
15496
  return {
15170
- fidelity: "NONE",
15171
- solid: null
15497
+ fidelity,
15498
+ completeness: "COMPLETE",
15499
+ solids,
15500
+ solid: solids.length === 1 ? solids[0] : null,
15501
+ ...measureCompleteBody(solids, expressId, diagnostics)
15172
15502
  };
15173
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
+ }
15174
15556
  function toImportedPset(pset) {
15175
15557
  return {
15176
15558
  name: pset.name,
@@ -15248,7 +15630,7 @@ function errMsg(e) {
15248
15630
  * with the model (it is not reclaimed automatically when the model is GC'd).
15249
15631
  */
15250
15632
  function disposeImportedModel(model) {
15251
- 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]();
15252
15634
  }
15253
15635
  //#endregion
15254
15636
  //#region src/specs/materialSpec.ts
@@ -28832,13 +29214,25 @@ function specLocalShift(el) {
28832
29214
  0
28833
29215
  ];
28834
29216
  }
29217
+ /** Skip the resolver occurrence wrappers already owned by `cumulativeFrame`,
29218
+ * then recover only the Body's outer literal Datum translations. An unexpected
29219
+ * wrapper shape returns zero rather than risking an occurrence transform twice. */
29220
+ function bodyLocalTranslation(el, occurrenceTransformDepth) {
29221
+ let body = el.geometry;
29222
+ for (let i = 0; i < occurrenceTransformDepth; i++) {
29223
+ if (body.kind !== "Translate" && body.kind !== "Rotate") return ZERO_TRANSLATION;
29224
+ body = body.target;
29225
+ }
29226
+ return peelTranslates(body).total;
29227
+ }
28835
29228
  /** World frame of an element's own body: the walk's cumulative frame (all
28836
29229
  * authored transforms, ancestors + own) composed with the body's authored axes
28837
- * (`axisX`/`axisZ` props) and its local origin (`origin` prop + centring
28838
- * shift). Under no rotation this reduces to `composedOrigin` + shift. */
28839
- function elementBodyFrame(el, cumulativeFrame) {
29230
+ * (`axisX`/`axisZ` props) and its local origin (`origin` prop + literal Body
29231
+ * Datum + centring shift). Under no rotation this reduces to `composedOrigin`
29232
+ * + shift. */
29233
+ function elementBodyFrame(el, cumulativeFrame, occurrenceTransformDepth) {
28840
29234
  return frameMul(frameMul(cumulativeFrame, frameFromPlacement({
28841
- origin: authoredSpecOrigin(el),
29235
+ origin: addTranslation(authoredSpecOrigin(el), bodyLocalTranslation(el, occurrenceTransformDepth)),
28842
29236
  axisX: authoredAxisX(el),
28843
29237
  axisZ: authoredAxisZ(el)
28844
29238
  })), translationFrame(specLocalShift(el)));
@@ -28857,10 +29251,10 @@ function civilNodeFrame(el, cumulativeFrame) {
28857
29251
  * `axisZ` are recomputed from the element's world frame relative to its spatial
28858
29252
  * container. Stair/ramp flights, which carry their own per-flight frame, are
28859
29253
  * each re-placed the same way. */
28860
- function rotatedRoutedInput(flatInput, el, cumulativeFrame, spatialFrame) {
29254
+ function rotatedRoutedInput(flatInput, el, cumulativeFrame, occurrenceTransformDepth, spatialFrame) {
28861
29255
  const toSpatial = frameInverse(spatialFrame);
28862
29256
  if (Array.isArray(flatInput["flights"])) {
28863
- const elementFrame = elementBodyFrame(el, cumulativeFrame);
29257
+ const elementFrame = elementBodyFrame(el, cumulativeFrame, occurrenceTransformDepth);
28864
29258
  const flights = flatInput["flights"];
28865
29259
  return {
28866
29260
  ...flatInput,
@@ -28901,7 +29295,7 @@ function rotatedRoutedInput(flatInput, el, cumulativeFrame, spatialFrame) {
28901
29295
  })
28902
29296
  };
28903
29297
  }
28904
- const placed = decomposeFrame(frameMul(toSpatial, elementBodyFrame(el, cumulativeFrame)));
29298
+ const placed = decomposeFrame(frameMul(toSpatial, elementBodyFrame(el, cumulativeFrame, occurrenceTransformDepth)));
28905
29299
  return {
28906
29300
  ...flatInput,
28907
29301
  origin: placed.origin,
@@ -29066,6 +29460,7 @@ function familiesToBim(root, options) {
29066
29460
  const walk = (el, state) => {
29067
29461
  const rotatedHere = state.rotated || hasRotateOp(el);
29068
29462
  const cumulativeTranslationHere = addTranslation(state.cumulativeTranslation, authoredTranslation(el));
29463
+ const occurrenceTransformDepthHere = state.occurrenceTransformDepth + el.localTransforms.length;
29069
29464
  const cumulativeFrameHere = frameMul(state.cumulativeFrame, frameFromOps(el.localTransforms));
29070
29465
  let proxiedHere = false;
29071
29466
  let nextRotated = rotatedHere;
@@ -29129,7 +29524,7 @@ function familiesToBim(root, options) {
29129
29524
  }
29130
29525
  const routedInput = el.semantics?.kind === "product" ? civilProductSpecInput(el) : ("input" in route ? route.input : specInput)(el);
29131
29526
  const rotatedBase = Array.isArray(routedInput["flights"]) ? specInput(el) : routedInput;
29132
- const placedInput = rotatedHere ? rotatedRoutedInput(rotatedBase, el, cumulativeFrameHere, state.spatialFrame) : usesAuthoredCivilHierarchy ? relativeSpecInput(routedInput, state.projectedSpatialTranslation) : routedInput;
29527
+ const placedInput = rotatedHere ? rotatedRoutedInput(rotatedBase, el, cumulativeFrameHere, occurrenceTransformDepthHere, state.spatialFrame) : usesAuthoredCivilHierarchy ? relativeSpecInput(routedInput, state.projectedSpatialTranslation) : routedInput;
29133
29528
  const parsed = route.parse(placedInput);
29134
29529
  if (!parsed.ok) return parsed;
29135
29530
  const added = route.add(model, parsed.value, el.keyPath);
@@ -29166,6 +29561,7 @@ function familiesToBim(root, options) {
29166
29561
  rotated: nextRotated,
29167
29562
  cumulativeTranslation: cumulativeTranslationHere,
29168
29563
  projectedSpatialTranslation: nextProjectedSpatialTranslation,
29564
+ occurrenceTransformDepth: occurrenceTransformDepthHere,
29169
29565
  cumulativeFrame: nextCumulativeFrame,
29170
29566
  spatialFrame: nextSpatialFrame
29171
29567
  });
@@ -29179,6 +29575,7 @@ function familiesToBim(root, options) {
29179
29575
  rotated: false,
29180
29576
  cumulativeTranslation: ZERO_TRANSLATION,
29181
29577
  projectedSpatialTranslation: ZERO_TRANSLATION,
29578
+ occurrenceTransformDepth: 0,
29182
29579
  cumulativeFrame: IDENTITY_FRAME,
29183
29580
  spatialFrame: IDENTITY_FRAME
29184
29581
  });