brepjs-bim 0.22.0 → 0.23.1

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.
@@ -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();
@@ -7182,7 +7182,7 @@ function placementToMatrix(f) {
7182
7182
  f.axisX[2] - dot * z[2]
7183
7183
  ];
7184
7184
  const x = lengthSq(projX) < 1e-12 ? normalize$1(orthogonal$2(z)) : normalize$1(projX);
7185
- const y = cross$1(z, x);
7185
+ const y = cross$2(z, x);
7186
7186
  return {
7187
7187
  linear: [
7188
7188
  x[0],
@@ -7202,7 +7202,27 @@ function placementToMatrix(f) {
7202
7202
  ]
7203
7203
  };
7204
7204
  }
7205
- function cross$1(a, b) {
7205
+ /** Decomposes a column-major matrix into origin (mm) + IFC axes (Z, X). */
7206
+ function decomposePlacement(m) {
7207
+ return {
7208
+ axisX: normalize$1([
7209
+ m[0],
7210
+ m[1],
7211
+ m[2]
7212
+ ]),
7213
+ axisZ: normalize$1([
7214
+ m[8],
7215
+ m[9],
7216
+ m[10]
7217
+ ]),
7218
+ origin: [
7219
+ m[12],
7220
+ m[13],
7221
+ m[14]
7222
+ ]
7223
+ };
7224
+ }
7225
+ function cross$2(a, b) {
7206
7226
  return [
7207
7227
  a[1] * b[2] - a[2] * b[1],
7208
7228
  a[2] * b[0] - a[0] * b[2],
@@ -7226,11 +7246,11 @@ function normalize$1(v) {
7226
7246
  ];
7227
7247
  }
7228
7248
  function orthogonal$2(v) {
7229
- return Math.abs(v[0]) < .9 ? cross$1(v, [
7249
+ return Math.abs(v[0]) < .9 ? cross$2(v, [
7230
7250
  1,
7231
7251
  0,
7232
7252
  0
7233
- ]) : cross$1(v, [
7253
+ ]) : cross$2(v, [
7234
7254
  0,
7235
7255
  1,
7236
7256
  0
@@ -8736,7 +8756,7 @@ function writeWallGeometry(w, spec, geomSubContextId, parentPlacementId) {
8736
8756
  value: "AREA"
8737
8757
  },
8738
8758
  ProfileName: null,
8739
- Position: w.ref(writeAxis2Placement2D$1(w)),
8759
+ Position: w.ref(writeAxis2Placement2D$1(w, [thicknessM / 2, heightM / 2])),
8740
8760
  XDim: w.mkType(WebIFC.IFCPOSITIVELENGTHMEASURE, thicknessM),
8741
8761
  YDim: w.mkType(WebIFC.IFCPOSITIVELENGTHMEASURE, heightM)
8742
8762
  });
@@ -8957,12 +8977,12 @@ function writeRoofGeometry(w, spec, solid, geomSubContextId, parentPlacementId)
8957
8977
  usedFallback: false
8958
8978
  };
8959
8979
  }
8960
- function writeAxis2Placement2D$1(w) {
8980
+ function writeAxis2Placement2D$1(w, location = [0, 0]) {
8961
8981
  const originId = w.nextId();
8962
8982
  w.writeLine({
8963
8983
  expressID: originId,
8964
8984
  type: WebIFC.IFCCARTESIANPOINT,
8965
- 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])]
8966
8986
  });
8967
8987
  const id = w.nextId();
8968
8988
  w.writeLine({
@@ -11467,11 +11487,10 @@ function writeOpeningGeometry(w, guid, openingSpec, wallSpec, wallPlacementId, g
11467
11487
  const offsetAlongWallM = toIfcLengthM(openingSpec.offsetAlongWall);
11468
11488
  const offsetFromFloorM = toIfcLengthM(openingSpec.offsetFromFloor);
11469
11489
  const thicknessM = toIfcLengthM(wallSpec.thickness);
11470
- const wallHeightM = toIfcLengthM(wallSpec.height);
11471
11490
  const placement3DId = writeAxis2Placement3D(w, [
11472
11491
  offsetAlongWallM + widthM / 2,
11473
- thicknessM / 2,
11474
- -wallHeightM / 2 + offsetFromFloorM + heightM / 2
11492
+ thicknessM,
11493
+ offsetFromFloorM + heightM / 2
11475
11494
  ], [
11476
11495
  0,
11477
11496
  -1,
@@ -12347,6 +12366,24 @@ var SUPPORTED_SCHEMAS = [
12347
12366
  * MUST call {@link SpfReader.close} (it always issues `CloseModel`, preventing
12348
12367
  * WASM handle leaks). The handle is `using`-friendly via `Symbol.dispose`.
12349
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
+ ];
12350
12387
  function deleteVector(vec) {
12351
12388
  vec.delete?.();
12352
12389
  }
@@ -12379,6 +12416,7 @@ var SpfReader = class SpfReader {
12379
12416
  if (modelId >= 0) api.CloseModel(modelId);
12380
12417
  return err(importError("OPEN_MODEL_FAILED", "web-ifc OpenModel returned an invalid model id"));
12381
12418
  }
12419
+ api.SetGeometryTransformation(modelId, IFC_Z_UP_TRANSFORMATION);
12382
12420
  const schemaRaw = api.GetModelSchema(modelId);
12383
12421
  if (!SUPPORTED_SCHEMAS.includes(schemaRaw)) {
12384
12422
  api.CloseModel(modelId);
@@ -14078,12 +14116,14 @@ function reconstructExtrusion(reader, extrusionId, scale, worldTransform, diagno
14078
14116
  diagnostics.push(issue("warning", faceResult.error.code, faceResult.error.message, extrusionId));
14079
14117
  return NONE;
14080
14118
  }
14119
+ const profileFrame = readProfilePosition(reader, sweptArea.value, scale);
14081
14120
  const depthMm = depth * scale * 1e3;
14082
- const extrudeDir = readDirection(reader, ext["ExtrudedDirection"]) ?? [
14121
+ const sweptDir = readDirection(reader, ext["ExtrudedDirection"]) ?? [
14083
14122
  0,
14084
14123
  0,
14085
14124
  1
14086
14125
  ];
14126
+ const extrudeDir = profileFrame === null ? sweptDir : rotateByTranspose(profileFrame.linear, sweptDir);
14087
14127
  const extrudeVec = [
14088
14128
  extrudeDir[0] * depthMm,
14089
14129
  extrudeDir[1] * depthMm,
@@ -14105,7 +14145,11 @@ function reconstructExtrusion(reader, extrusionId, scale, worldTransform, diagno
14105
14145
  return NONE;
14106
14146
  }
14107
14147
  const localFrame = readAxis2Placement3D(reader, ext["Position"], scale);
14108
- const placed = placeSolid(solidResult.value, localFrame, worldTransform);
14148
+ const placed = placeSolid(solidResult.value, [
14149
+ worldTransform,
14150
+ localFrame,
14151
+ profileFrame
14152
+ ]);
14109
14153
  if (!placed.ok) {
14110
14154
  diagnostics.push(issue("warning", placed.error.code, placed.error.message, extrusionId));
14111
14155
  return NONE;
@@ -14133,16 +14177,19 @@ function reconstructRevolution(reader, revolutionId, scale, worldTransform, diag
14133
14177
  return NONE;
14134
14178
  }
14135
14179
  const axis1 = reader.getLine(axisRef.value);
14136
- const center = (axis1 === null ? void 0 : readPoint(reader, axis1["Location"], scale)) ?? [
14180
+ const sweptCenter = (axis1 === null ? void 0 : readPoint(reader, axis1["Location"], scale)) ?? [
14137
14181
  0,
14138
14182
  0,
14139
14183
  0
14140
14184
  ];
14141
- const direction = (axis1 === null ? void 0 : readDirection(reader, axis1["Axis"])) ?? [
14185
+ const sweptDirection = (axis1 === null ? void 0 : readDirection(reader, axis1["Axis"])) ?? [
14142
14186
  0,
14143
14187
  0,
14144
14188
  1
14145
14189
  ];
14190
+ const profileFrame = readProfilePosition(reader, sweptArea.value, scale);
14191
+ const center = profileFrame === null ? sweptCenter : inverseRigidPoint(profileFrame, sweptCenter);
14192
+ const direction = profileFrame === null ? sweptDirection : rotateByTranspose(profileFrame.linear, sweptDirection);
14146
14193
  const revolved = (() => {
14147
14194
  try {
14148
14195
  var _usingCtx3 = _usingCtx();
@@ -14169,7 +14216,11 @@ function reconstructRevolution(reader, revolutionId, scale, worldTransform, diag
14169
14216
  return NONE;
14170
14217
  }
14171
14218
  const localFrame = readAxis2Placement3D(reader, rev["Position"], scale);
14172
- const placed = placeSolid(revolved.value, localFrame, worldTransform);
14219
+ const placed = placeSolid(revolved.value, [
14220
+ worldTransform,
14221
+ localFrame,
14222
+ profileFrame
14223
+ ]);
14173
14224
  if (!placed.ok) {
14174
14225
  diagnostics.push(issue("warning", placed.error.code, placed.error.message, revolutionId));
14175
14226
  return NONE;
@@ -14182,20 +14233,9 @@ function reconstructTessellated(reader, productExpressId, diagnostics) {
14182
14233
  diagnostics.push(issue("warning", "GEOMETRY_RECONSTRUCTION_FAILED", "Tessellated geometry yielded no triangles", productExpressId));
14183
14234
  return NONE;
14184
14235
  }
14185
- const stl = packBinaryStl(mesh.vertices, mesh.indices, 1e3);
14186
- let solid = null;
14187
- try {
14188
- const cast = castShape(getKernel().importSTL(stl.buffer));
14189
- if (isSolid(cast)) {
14190
- const valid = validSolid(cast);
14191
- if (valid.ok) solid = valid.value;
14192
- else cast[Symbol.dispose]();
14193
- } else cast[Symbol.dispose]();
14194
- } catch (e) {
14195
- diagnostics.push(issue("info", "TESSELLATION_NOT_MANIFOLD", `STL round-trip failed: ${errMsg$1(e)}`, productExpressId));
14196
- }
14236
+ const solid = sewMeshToSolid(mesh, 1e3, productExpressId, diagnostics);
14197
14237
  if (solid !== null) {
14198
- diagnostics.push(issue("info", "TESSELLATED_MANIFOLD", "Tessellated mesh recovered as a closed solid via STL round-trip", productExpressId));
14238
+ diagnostics.push(issue("info", "TESSELLATED_MANIFOLD", "Tessellated mesh recovered as a closed solid by sewing its triangles", productExpressId));
14199
14239
  return {
14200
14240
  kind: "SOLID",
14201
14241
  solid,
@@ -14210,6 +14250,52 @@ function reconstructTessellated(reader, productExpressId, diagnostics) {
14210
14250
  diagnostic: "TESSELLATED_LOSSY"
14211
14251
  };
14212
14252
  }
14253
+ /** Loose enough to weld float32 vertices web-ifc emits per face, in mm. */
14254
+ var MESH_SEW_TOLERANCE_MM = .001;
14255
+ /**
14256
+ * Brands a fresh kernel result and releases its pre-downcast arena slot when
14257
+ * the cast moved to a new one (occt-wasm); in-place kernels share the slot and
14258
+ * are left alone. Mirrors brepjs's internal castResultShape.
14259
+ */
14260
+ function castFreshResult(raw) {
14261
+ const cast = castShape(raw);
14262
+ const rawId = raw.id;
14263
+ const castId = cast.wrapped.id;
14264
+ if (!(rawId !== void 0 && castId !== void 0 ? rawId === castId : cast.wrapped === raw)) getKernel().dispose(raw);
14265
+ return cast;
14266
+ }
14267
+ /**
14268
+ * Sews the streamed triangles into a closed solid through the kernel's mesh
14269
+ * builder, the same path brepjs's mesh importers use. Null when the mesh is
14270
+ * open or the kernel rejects it.
14271
+ */
14272
+ function sewMeshToSolid(mesh, scaleToMm, productExpressId, diagnostics) {
14273
+ const kernel = getKernel();
14274
+ const { vertices, indices } = mesh;
14275
+ const point = (index) => [
14276
+ (vertices[index * 3] ?? 0) * scaleToMm,
14277
+ (vertices[index * 3 + 1] ?? 0) * scaleToMm,
14278
+ (vertices[index * 3 + 2] ?? 0) * scaleToMm
14279
+ ];
14280
+ const triangles = [];
14281
+ for (let t = 0; t + 2 < indices.length; t += 3) {
14282
+ const face = kernel.buildTriFace(point(indices[t] ?? 0), point(indices[t + 1] ?? 0), point(indices[t + 2] ?? 0));
14283
+ if (face !== null) triangles.push(face);
14284
+ }
14285
+ if (triangles.length === 0) return null;
14286
+ try {
14287
+ const cast = castFreshResult(kernel.sewAndSolidify(triangles, MESH_SEW_TOLERANCE_MM));
14288
+ const valid = isSolid(cast) ? validSolid(cast) : null;
14289
+ if (valid !== null && valid.ok) return valid.value;
14290
+ cast[Symbol.dispose]();
14291
+ return null;
14292
+ } catch (e) {
14293
+ diagnostics.push(issue("info", "TESSELLATION_NOT_MANIFOLD", `Mesh sewing failed: ${errMsg$1(e)}`, productExpressId));
14294
+ return null;
14295
+ } finally {
14296
+ for (const face of triangles) kernel.dispose(face);
14297
+ }
14298
+ }
14213
14299
  function collectMesh(reader, productExpressId) {
14214
14300
  const vertices = [];
14215
14301
  const indices = [];
@@ -14238,7 +14324,8 @@ function collectMesh(reader, productExpressId) {
14238
14324
  }
14239
14325
  }
14240
14326
  } finally {
14241
- flatMesh.delete();
14327
+ releaseEmbind(flatMesh.geometries);
14328
+ releaseEmbind(flatMesh);
14242
14329
  }
14243
14330
  });
14244
14331
  if (vertices.length === 0) return null;
@@ -14247,25 +14334,6 @@ function collectMesh(reader, productExpressId) {
14247
14334
  indices: new Uint32Array(indices)
14248
14335
  };
14249
14336
  }
14250
- function packBinaryStl(vertices, indices, scaleToMm) {
14251
- const triCount = Math.floor(indices.length / 3);
14252
- const buffer = /* @__PURE__ */ new ArrayBuffer(84 + triCount * 50);
14253
- const view = new DataView(buffer);
14254
- view.setUint32(80, triCount, true);
14255
- let offset = 84;
14256
- for (let t = 0; t < triCount; t++) {
14257
- offset += 12;
14258
- for (let c = 0; c < 3; c++) {
14259
- const vi = (indices[t * 3 + c] ?? 0) * 3;
14260
- view.setFloat32(offset, (vertices[vi] ?? 0) * scaleToMm, true);
14261
- view.setFloat32(offset + 4, (vertices[vi + 1] ?? 0) * scaleToMm, true);
14262
- view.setFloat32(offset + 8, (vertices[vi + 2] ?? 0) * scaleToMm, true);
14263
- offset += 12;
14264
- }
14265
- offset += 2;
14266
- }
14267
- return new Uint8Array(buffer);
14268
- }
14269
14337
  var PARAMETRIC_PROFILE_BUILDERS = /* @__PURE__ */ new Map([
14270
14338
  [WebIFC.IFCRECTANGLEPROFILEDEF, (f) => ({
14271
14339
  kind: "RECTANGULAR",
@@ -14409,19 +14477,99 @@ function profileToFace(profile) {
14409
14477
  if (!face.ok) return err(importError("GEOMETRY_RECONSTRUCTION_FAILED", `Profile face build failed: ${face.error.message}`));
14410
14478
  return ok(face.value);
14411
14479
  }
14412
- function placeSolid(solid, localFrame, worldTransform) {
14413
- let current = solid;
14414
- for (const transform of [localFrame, worldTransform]) {
14415
- if (transform === null || isIdentity(transform)) continue;
14416
- const applied = applyMatrix(current, transform);
14417
- if (!applied.ok) {
14418
- current[Symbol.dispose]();
14419
- return err(importError("PLACEMENT_READ_FAILED", `Placement transform failed: ${applied.error.message}`));
14420
- }
14421
- current[Symbol.dispose]();
14422
- current = applied.value;
14423
- }
14424
- return ok(current);
14480
+ /**
14481
+ * IfcParameterizedProfileDef.Position places the profile inside the swept
14482
+ * solid's XY plane; the writers rely on it to corner-anchor centred rectangle
14483
+ * profiles. Absent on arbitrary (polyline) profiles.
14484
+ */
14485
+ function readProfilePosition(reader, profileExpressId, scale) {
14486
+ const def = reader.getLine(profileExpressId);
14487
+ const positionRef = def === null ? void 0 : asRef(def["Position"]);
14488
+ if (positionRef === void 0) return null;
14489
+ const position = reader.getLine(positionRef.value);
14490
+ if (position === null) return null;
14491
+ const [x, y] = readPoint2D(reader, position["Location"], scale) ?? [0, 0];
14492
+ const [dx, dy] = readDirection2D(reader, position["RefDirection"]) ?? [1, 0];
14493
+ const len = Math.hypot(dx, dy);
14494
+ const c = len < 1e-12 ? 1 : dx / len;
14495
+ const s = len < 1e-12 ? 0 : dy / len;
14496
+ return {
14497
+ linear: [
14498
+ c,
14499
+ -s,
14500
+ 0,
14501
+ s,
14502
+ c,
14503
+ 0,
14504
+ 0,
14505
+ 0,
14506
+ 1
14507
+ ],
14508
+ translation: [
14509
+ x,
14510
+ y,
14511
+ 0
14512
+ ]
14513
+ };
14514
+ }
14515
+ /**
14516
+ * Applies the placement chain (outermost first) as one composed rigid motion.
14517
+ * A second applyMatrix on an already-transformed solid can fail BRepCheck on
14518
+ * occt-wasm, so the frames are never applied one after another.
14519
+ */
14520
+ function placeSolid(solid, frames) {
14521
+ let composed = null;
14522
+ for (const frame of frames) {
14523
+ if (frame === null) continue;
14524
+ composed = composed === null ? frame : composeRigid(composed, frame);
14525
+ }
14526
+ if (composed === null || isIdentity(composed)) return ok(solid);
14527
+ const applied = applyMatrix(solid, composed);
14528
+ solid[Symbol.dispose]();
14529
+ if (!applied.ok) return err(importError("PLACEMENT_READ_FAILED", `Placement transform failed: ${applied.error.message}`));
14530
+ return ok(applied.value);
14531
+ }
14532
+ /** `outer ∘ inner`: the motion that applies `inner` first, then `outer`. */
14533
+ function composeRigid(outer, inner) {
14534
+ const [a00, a01, a02, a10, a11, a12, a20, a21, a22] = outer.linear;
14535
+ const [b00, b01, b02, b10, b11, b12, b20, b21, b22] = inner.linear;
14536
+ const [tx, ty, tz] = inner.translation;
14537
+ const [ux, uy, uz] = outer.translation;
14538
+ return {
14539
+ linear: [
14540
+ a00 * b00 + a01 * b10 + a02 * b20,
14541
+ a00 * b01 + a01 * b11 + a02 * b21,
14542
+ a00 * b02 + a01 * b12 + a02 * b22,
14543
+ a10 * b00 + a11 * b10 + a12 * b20,
14544
+ a10 * b01 + a11 * b11 + a12 * b21,
14545
+ a10 * b02 + a11 * b12 + a12 * b22,
14546
+ a20 * b00 + a21 * b10 + a22 * b20,
14547
+ a20 * b01 + a21 * b11 + a22 * b21,
14548
+ a20 * b02 + a21 * b12 + a22 * b22
14549
+ ],
14550
+ translation: [
14551
+ a00 * tx + a01 * ty + a02 * tz + ux,
14552
+ a10 * tx + a11 * ty + a12 * tz + uy,
14553
+ a20 * tx + a21 * ty + a22 * tz + uz
14554
+ ]
14555
+ };
14556
+ }
14557
+ /** Rᵀ·v for a rigid frame's rotation part. */
14558
+ function rotateByTranspose(linear, v) {
14559
+ return [
14560
+ linear[0] * v[0] + linear[3] * v[1] + linear[6] * v[2],
14561
+ linear[1] * v[0] + linear[4] * v[1] + linear[7] * v[2],
14562
+ linear[2] * v[0] + linear[5] * v[1] + linear[8] * v[2]
14563
+ ];
14564
+ }
14565
+ /** Maps a point from the frame's parent space into the frame: Rᵀ·(p − t). */
14566
+ function inverseRigidPoint(frame, p) {
14567
+ const t = frame.translation;
14568
+ return rotateByTranspose(frame.linear, [
14569
+ p[0] - t[0],
14570
+ p[1] - t[1],
14571
+ p[2] - t[2]
14572
+ ]);
14425
14573
  }
14426
14574
  function finalizeSolid(solid, entity, diagnostics) {
14427
14575
  if (!isSolid(solid)) {
@@ -14476,17 +14624,17 @@ function readAxis2Placement3D(reader, ref, scale) {
14476
14624
  refDir[2] - dotXZ * zN[2]
14477
14625
  ];
14478
14626
  const xRawLenSq = xRaw[0] * xRaw[0] + xRaw[1] * xRaw[1] + xRaw[2] * xRaw[2];
14479
- const orthoZ = Math.abs(zN[0]) < .9 ? cross(zN, [
14627
+ const orthoZ = Math.abs(zN[0]) < .9 ? cross$1(zN, [
14480
14628
  1,
14481
14629
  0,
14482
14630
  0
14483
- ]) : cross(zN, [
14631
+ ]) : cross$1(zN, [
14484
14632
  0,
14485
14633
  1,
14486
14634
  0
14487
14635
  ]);
14488
14636
  const xN = xRawLenSq < 1e-12 ? normalize(orthoZ) : normalize(xRaw);
14489
- const yN = cross(zN, xN);
14637
+ const yN = cross$1(zN, xN);
14490
14638
  return {
14491
14639
  linear: [
14492
14640
  xN[0],
@@ -14570,6 +14718,24 @@ function readPolylinePoints(reader, ref, scale) {
14570
14718
  }
14571
14719
  return out.length >= 3 ? out : void 0;
14572
14720
  }
14721
+ function readPoint2D(reader, ref, scale) {
14722
+ const pointRef = asRef(ref);
14723
+ if (pointRef === void 0) return void 0;
14724
+ const pt = reader.getLine(pointRef.value);
14725
+ if (pt === null) return void 0;
14726
+ const coords = asMeasureArray(pt["Coordinates"]);
14727
+ if (coords.length < 2) return void 0;
14728
+ return [(coords[0] ?? 0) * scale * 1e3, (coords[1] ?? 0) * scale * 1e3];
14729
+ }
14730
+ function readDirection2D(reader, ref) {
14731
+ const dirRef = asRef(ref);
14732
+ if (dirRef === void 0) return void 0;
14733
+ const dir = reader.getLine(dirRef.value);
14734
+ if (dir === null) return void 0;
14735
+ const ratios = asMeasureArray(dir["DirectionRatios"]);
14736
+ if (ratios.length < 2) return void 0;
14737
+ return [ratios[0] ?? 0, ratios[1] ?? 0];
14738
+ }
14573
14739
  function readPoint(reader, ref, scale) {
14574
14740
  const pointRef = asRef(ref);
14575
14741
  if (pointRef === void 0) return void 0;
@@ -14596,6 +14762,9 @@ function readDirection(reader, ref) {
14596
14762
  ratios[2] ?? 0
14597
14763
  ];
14598
14764
  }
14765
+ function releaseEmbind(value) {
14766
+ value.delete?.();
14767
+ }
14599
14768
  function asRef(value) {
14600
14769
  if (value !== null && typeof value === "object" && "value" in value) {
14601
14770
  const v = value.value;
@@ -14646,7 +14815,7 @@ function normalize(v) {
14646
14815
  v[2] / len
14647
14816
  ];
14648
14817
  }
14649
- function cross(a, b) {
14818
+ function cross$1(a, b) {
14650
14819
  return [
14651
14820
  a[1] * b[2] - a[2] * b[1],
14652
14821
  a[2] * b[0] - a[0] * b[2],
@@ -27908,6 +28077,217 @@ function assignNum(target, key, value) {
27908
28077
  if (value !== void 0) target[key] = Number(value);
27909
28078
  }
27910
28079
  //#endregion
28080
+ //#region src/placementFrame.ts
28081
+ var IDENTITY_FRAME = [
28082
+ 1,
28083
+ 0,
28084
+ 0,
28085
+ 0,
28086
+ 0,
28087
+ 1,
28088
+ 0,
28089
+ 0,
28090
+ 0,
28091
+ 0,
28092
+ 1,
28093
+ 0,
28094
+ 0,
28095
+ 0,
28096
+ 0,
28097
+ 1
28098
+ ];
28099
+ var DEFAULT_AXIS = [
28100
+ 0,
28101
+ 0,
28102
+ 1
28103
+ ];
28104
+ var ORIGIN = [
28105
+ 0,
28106
+ 0,
28107
+ 0
28108
+ ];
28109
+ /** Column-major 4x4 multiply: `a . b` (apply b first, then a). */
28110
+ function frameMul(a, b) {
28111
+ const out = new Array(16).fill(0);
28112
+ for (let col = 0; col < 4; col++) for (let row = 0; row < 4; row++) {
28113
+ let sum = 0;
28114
+ for (let k = 0; k < 4; k++) sum += (a[k * 4 + row] ?? 0) * (b[col * 4 + k] ?? 0);
28115
+ out[col * 4 + row] = sum;
28116
+ }
28117
+ return out;
28118
+ }
28119
+ function translationFrame(v) {
28120
+ return [
28121
+ 1,
28122
+ 0,
28123
+ 0,
28124
+ 0,
28125
+ 0,
28126
+ 1,
28127
+ 0,
28128
+ 0,
28129
+ 0,
28130
+ 0,
28131
+ 1,
28132
+ 0,
28133
+ v[0],
28134
+ v[1],
28135
+ v[2],
28136
+ 1
28137
+ ];
28138
+ }
28139
+ function normalizeAxis(axis) {
28140
+ const len = Math.sqrt(axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]);
28141
+ if (len < 1e-12) return DEFAULT_AXIS;
28142
+ return [
28143
+ axis[0] / len,
28144
+ axis[1] / len,
28145
+ axis[2] / len
28146
+ ];
28147
+ }
28148
+ function cross(a, b) {
28149
+ return [
28150
+ a[1] * b[2] - a[2] * b[1],
28151
+ a[2] * b[0] - a[0] * b[2],
28152
+ a[0] * b[1] - a[1] * b[0]
28153
+ ];
28154
+ }
28155
+ /**
28156
+ * Builds a frame from an authored `(origin, axisX, axisZ)` placement, using the
28157
+ * same IFC orthonormalization as `placementToMatrix`/`readAxis2Placement3D`:
28158
+ * z = normalize(axisZ); x = normalize(axisX projected perpendicular to z);
28159
+ * y = z x. Lets a civil node authored with explicit axis props enter the same
28160
+ * frame pipeline as a `tRotate` chain.
28161
+ */
28162
+ function frameFromPlacement(f) {
28163
+ const z = normalizeAxis(f.axisZ);
28164
+ const dot = z[0] * f.axisX[0] + z[1] * f.axisX[1] + z[2] * f.axisX[2];
28165
+ const x = normalizeAxis([
28166
+ f.axisX[0] - dot * z[0],
28167
+ f.axisX[1] - dot * z[1],
28168
+ f.axisX[2] - dot * z[2]
28169
+ ]);
28170
+ const y = cross(z, x);
28171
+ return [
28172
+ x[0],
28173
+ x[1],
28174
+ x[2],
28175
+ 0,
28176
+ y[0],
28177
+ y[1],
28178
+ y[2],
28179
+ 0,
28180
+ z[0],
28181
+ z[1],
28182
+ z[2],
28183
+ 0,
28184
+ f.origin[0],
28185
+ f.origin[1],
28186
+ f.origin[2],
28187
+ 1
28188
+ ];
28189
+ }
28190
+ /**
28191
+ * Rotation by `angleDeg` about `axis` (default +Z) through pivot `at` (default
28192
+ * origin), matching `csg.rotate` (Rodrigues; right-handed about the axis). With
28193
+ * a pivot the motion is `T(at) . R . T(-at)`, i.e. `p -> at + R(p - at)`.
28194
+ */
28195
+ function rotationFrame(angleDeg, axis = DEFAULT_AXIS, at = ORIGIN) {
28196
+ const [x, y, z] = normalizeAxis(axis);
28197
+ const t = angleDeg * Math.PI / 180;
28198
+ const c = Math.cos(t);
28199
+ const s = Math.sin(t);
28200
+ const C = 1 - c;
28201
+ const r00 = c + x * x * C;
28202
+ const r01 = x * y * C - z * s;
28203
+ const r02 = x * z * C + y * s;
28204
+ const r10 = y * x * C + z * s;
28205
+ const r11 = c + y * y * C;
28206
+ const r12 = y * z * C - x * s;
28207
+ const r20 = z * x * C - y * s;
28208
+ const r21 = z * y * C + x * s;
28209
+ const r22 = c + z * z * C;
28210
+ const rat0 = r00 * at[0] + r01 * at[1] + r02 * at[2];
28211
+ const rat1 = r10 * at[0] + r11 * at[1] + r12 * at[2];
28212
+ const rat2 = r20 * at[0] + r21 * at[1] + r22 * at[2];
28213
+ return [
28214
+ r00,
28215
+ r10,
28216
+ r20,
28217
+ 0,
28218
+ r01,
28219
+ r11,
28220
+ r21,
28221
+ 0,
28222
+ r02,
28223
+ r12,
28224
+ r22,
28225
+ 0,
28226
+ at[0] - rat0,
28227
+ at[1] - rat1,
28228
+ at[2] - rat2,
28229
+ 1
28230
+ ];
28231
+ }
28232
+ function opFrame(op) {
28233
+ if (op.op === "translate") return translationFrame(op.v);
28234
+ return rotationFrame(op.angleDeg, op.axis ?? DEFAULT_AXIS, op.at ?? ORIGIN);
28235
+ }
28236
+ /**
28237
+ * Composes an authored `TransformOp` chain into one frame, matching families'
28238
+ * `applyOps`: `ops[0]` is innermost (applied first). For `[A, B]` the composed
28239
+ * motion is `B . A`, so a point transforms as `B(A(p))`.
28240
+ */
28241
+ function frameFromOps(ops) {
28242
+ let m = IDENTITY_FRAME;
28243
+ for (const op of ops) m = frameMul(opFrame(op), m);
28244
+ return m;
28245
+ }
28246
+ /** Rigid inverse of `[R | t]`: `[R^T | -R^T t]`. */
28247
+ function frameInverse(f) {
28248
+ const t0 = f[12] ?? 0;
28249
+ const t1 = f[13] ?? 0;
28250
+ const t2 = f[14] ?? 0;
28251
+ const inv0 = -(f[0] * t0 + f[1] * t1 + f[2] * t2);
28252
+ const inv1 = -(f[4] * t0 + f[5] * t1 + f[6] * t2);
28253
+ const inv2 = -(f[8] * t0 + f[9] * t1 + f[10] * t2);
28254
+ return [
28255
+ f[0],
28256
+ f[4],
28257
+ f[8],
28258
+ 0,
28259
+ f[1],
28260
+ f[5],
28261
+ f[9],
28262
+ 0,
28263
+ f[2],
28264
+ f[6],
28265
+ f[10],
28266
+ 0,
28267
+ inv0,
28268
+ inv1,
28269
+ inv2,
28270
+ 1
28271
+ ];
28272
+ }
28273
+ /** Decomposes a frame into origin (mm) + IFC axes (axisX, axisZ). */
28274
+ function decomposeFrame(f) {
28275
+ return decomposePlacement(f);
28276
+ }
28277
+ /** Translation column of a frame. */
28278
+ function frameOrigin(f) {
28279
+ return [
28280
+ f[12] ?? 0,
28281
+ f[13] ?? 0,
28282
+ f[14] ?? 0
28283
+ ];
28284
+ }
28285
+ /** True when the rotation part is the identity (within tolerance): a pure
28286
+ * translation, so the existing translation-only placement path suffices. */
28287
+ function isPureTranslation(f, eps = 1e-9) {
28288
+ return Math.abs((f[0] ?? 1) - 1) < eps && Math.abs((f[5] ?? 1) - 1) < eps && Math.abs((f[10] ?? 1) - 1) < eps && Math.abs(f[1] ?? 0) < eps && Math.abs(f[2] ?? 0) < eps && Math.abs(f[4] ?? 0) < eps && Math.abs(f[6] ?? 0) < eps && Math.abs(f[8] ?? 0) < eps && Math.abs(f[9] ?? 0) < eps;
28289
+ }
28290
+ //#endregion
27911
28291
  //#region src/familiesAdapter.ts
27912
28292
  /**
27913
28293
  * brepjs-families -> BimModel adapter. Consumes a resolved element tree and
@@ -28129,6 +28509,36 @@ function centreOrigin(base, shift) {
28129
28509
  origin[2] + shift[2]
28130
28510
  ];
28131
28511
  }
28512
+ function synthesizedProfile(el) {
28513
+ if (el.semantics?.kind !== "product" || el.props["profile"] !== void 0) return void 0;
28514
+ const length = semanticDimension(el, "length");
28515
+ const width = semanticDimension(el, "width");
28516
+ const height = semanticDimension(el, "height");
28517
+ if (el.semantics.category === "beam" && width !== void 0 && height !== void 0) return {
28518
+ profile: {
28519
+ kind: "RECTANGULAR",
28520
+ width,
28521
+ height
28522
+ },
28523
+ shift: [
28524
+ 0,
28525
+ width / 2,
28526
+ height / 2
28527
+ ]
28528
+ };
28529
+ if (el.semantics.category === "column" && length !== void 0 && width !== void 0) return {
28530
+ profile: {
28531
+ kind: "RECTANGULAR",
28532
+ width: length,
28533
+ height: width
28534
+ },
28535
+ shift: [
28536
+ length / 2,
28537
+ width / 2,
28538
+ 0
28539
+ ]
28540
+ };
28541
+ }
28132
28542
  /**
28133
28543
  * Adapts target-independent civil envelope dimensions onto existing typed BIM
28134
28544
  * spec inputs. Reference infrastructure Families intentionally author domain
@@ -28143,18 +28553,7 @@ function civilProductSpecInput(el) {
28143
28553
  const height = semanticDimension(el, "height");
28144
28554
  switch (el.semantics.category) {
28145
28555
  case "beam": {
28146
- const synthesized = base["profile"] === void 0 && width !== void 0 && height !== void 0 ? {
28147
- profile: {
28148
- kind: "RECTANGULAR",
28149
- width,
28150
- height
28151
- },
28152
- shift: [
28153
- 0,
28154
- width / 2,
28155
- height / 2
28156
- ]
28157
- } : void 0;
28556
+ const synthesized = synthesizedProfile(el);
28158
28557
  return {
28159
28558
  ...base,
28160
28559
  ...synthesized !== void 0 ? {
@@ -28166,18 +28565,7 @@ function civilProductSpecInput(el) {
28166
28565
  };
28167
28566
  }
28168
28567
  case "column": {
28169
- const synthesized = base["profile"] === void 0 && length !== void 0 && width !== void 0 ? {
28170
- profile: {
28171
- kind: "RECTANGULAR",
28172
- width: length,
28173
- height: width
28174
- },
28175
- shift: [
28176
- length / 2,
28177
- width / 2,
28178
- 0
28179
- ]
28180
- } : void 0;
28568
+ const synthesized = synthesizedProfile(el);
28181
28569
  return {
28182
28570
  ...base,
28183
28571
  ...synthesized !== void 0 ? {
@@ -28244,12 +28632,12 @@ function peelTranslates(node) {
28244
28632
  moved
28245
28633
  };
28246
28634
  }
28247
- /** True when the element's rendered local transform carries a rotation. The spec path
28248
- * folds only translations into IfcLocalPlacement (walls orient via `axisX`),
28249
- * so a tRotate placement would export un-rotated while the viewport shows it
28250
- * rotated — reject instead of diverging. Rotations a family render bakes
28251
- * into its own body geometry (e.g. a circular beam oriented along axisX) are
28252
- * fine: the spec rebuilds that body parametrically from props. */
28635
+ /** True when the element's rendered local transform carries a rotation, which
28636
+ * switches the walk to the rigid-frame placement path (the composed rotation
28637
+ * folds into origin + axisX + axisZ). Only authored `transform` rotations
28638
+ * count: a rotation a family render bakes into its own body geometry (e.g. a
28639
+ * circular beam oriented along axisX) stays in the body, which the spec
28640
+ * rebuilds parametrically from props. */
28253
28641
  function hasRotateOp(el) {
28254
28642
  return el.localTransforms.some((op) => op.op === "rotate");
28255
28643
  }
@@ -28364,7 +28752,7 @@ function materializeOwnedSolid(el, evaluator, errors) {
28364
28752
  /** Materialize an unrouted element's IR and add it as a proxy. The body is
28365
28753
  * authoritative for a proxy (no parametric spec to diverge from), so baked
28366
28754
  * transforms — rotations included — are fine here. */
28367
- function addProxyElement(model, el, evaluator, projectedSpatialTranslation) {
28755
+ function addProxyElement(model, el, evaluator, spatialFrame) {
28368
28756
  const body = materializeOwnedSolid(el, evaluator, {
28369
28757
  evalCode: "FAMILIES_PROXY_EVAL_FAILED",
28370
28758
  notSolidCode: "FAMILIES_PROXY_NOT_SOLID",
@@ -28372,7 +28760,7 @@ function addProxyElement(model, el, evaluator, projectedSpatialTranslation) {
28372
28760
  routeName: "proxy"
28373
28761
  });
28374
28762
  if (!body.ok) return body;
28375
- const localized = localizeOwnedSolid(el, body.value, projectedSpatialTranslation, "FAMILIES_PROXY_LOCALIZE_FAILED", "spatial parent");
28763
+ const localized = localizeBodyToFrame(el, body.value, spatialFrame, "FAMILIES_PROXY_LOCALIZE_FAILED", "spatial parent");
28376
28764
  if (!localized.ok) return localized;
28377
28765
  const nameAttr = el.attributes["name"];
28378
28766
  const materialProp = el.props["materialName"];
@@ -28535,8 +28923,9 @@ var DEFAULT_AXIS_Z = [
28535
28923
  function axisEquals(value, axis) {
28536
28924
  return Array.isArray(value) && value.length === 3 && value.every((component, i) => typeof component === "number" && component === axis[i]);
28537
28925
  }
28538
- /** Civil descendant relativization is translation-only, so a civil frame with
28539
- * non-default axes would rotate the exported hierarchy away from the IR. */
28926
+ /** True when a civil node authors non-default `axisX`/`axisZ` props: like a
28927
+ * tRotate, this puts the node (and its subtree) on the rigid-frame placement
28928
+ * path so descendants relativize against the rotated parent frame. */
28540
28929
  function hasRotatedAxes(el) {
28541
28930
  const axisX = el.props["axisX"];
28542
28931
  const axisZ = el.props["axisZ"];
@@ -28575,6 +28964,131 @@ function authoredTranslation(el) {
28575
28964
  }
28576
28965
  return total;
28577
28966
  }
28967
+ function authoredAxisX(el) {
28968
+ const v = el.props["axisX"];
28969
+ return Array.isArray(v) && v.length === 3 ? v : DEFAULT_AXIS_X;
28970
+ }
28971
+ function authoredAxisZ(el) {
28972
+ const v = el.props["axisZ"];
28973
+ return Array.isArray(v) && v.length === 3 ? v : DEFAULT_AXIS_Z;
28974
+ }
28975
+ /** The placement shift that centres a synthesized beam/column cross-section on
28976
+ * its axis, in the element's own (body) frame. Zero for every other route. */
28977
+ function specLocalShift(el) {
28978
+ return synthesizedProfile(el)?.shift ?? [
28979
+ 0,
28980
+ 0,
28981
+ 0
28982
+ ];
28983
+ }
28984
+ /** Skip the resolver occurrence wrappers already owned by `cumulativeFrame`,
28985
+ * then recover only the Body's outer literal Datum translations. An unexpected
28986
+ * wrapper shape returns zero rather than risking an occurrence transform twice. */
28987
+ function bodyLocalTranslation(el, occurrenceTransformDepth) {
28988
+ let body = el.geometry;
28989
+ for (let i = 0; i < occurrenceTransformDepth; i++) {
28990
+ if (body.kind !== "Translate" && body.kind !== "Rotate") return ZERO_TRANSLATION;
28991
+ body = body.target;
28992
+ }
28993
+ return peelTranslates(body).total;
28994
+ }
28995
+ /** World frame of an element's own body: the walk's cumulative frame (all
28996
+ * authored transforms, ancestors + own) composed with the body's authored axes
28997
+ * (`axisX`/`axisZ` props) and its local origin (`origin` prop + literal Body
28998
+ * Datum + centring shift). Under no rotation this reduces to `composedOrigin`
28999
+ * + shift. */
29000
+ function elementBodyFrame(el, cumulativeFrame, occurrenceTransformDepth) {
29001
+ return frameMul(frameMul(cumulativeFrame, frameFromPlacement({
29002
+ origin: addTranslation(authoredSpecOrigin(el), bodyLocalTranslation(el, occurrenceTransformDepth)),
29003
+ axisX: authoredAxisX(el),
29004
+ axisZ: authoredAxisZ(el)
29005
+ })), translationFrame(specLocalShift(el)));
29006
+ }
29007
+ /** World frame of a civil spatial node: cumulative transforms composed with any
29008
+ * authored `origin`/`axisX`/`axisZ` props (identity when default). */
29009
+ function civilNodeFrame(el, cumulativeFrame) {
29010
+ return frameMul(cumulativeFrame, frameFromPlacement({
29011
+ origin: authoredSpecOrigin(el),
29012
+ axisX: authoredAxisX(el),
29013
+ axisZ: authoredAxisZ(el)
29014
+ }));
29015
+ }
29016
+ /** Places a routed element's spec input through the composed placement frame:
29017
+ * the flat spec input keeps its dimensions/profile/psets, but `origin`/`axisX`/
29018
+ * `axisZ` are recomputed from the element's world frame relative to its spatial
29019
+ * container. Stair/ramp flights, which carry their own per-flight frame, are
29020
+ * each re-placed the same way. */
29021
+ function rotatedRoutedInput(flatInput, el, cumulativeFrame, occurrenceTransformDepth, spatialFrame) {
29022
+ const toSpatial = frameInverse(spatialFrame);
29023
+ if (Array.isArray(flatInput["flights"])) {
29024
+ const elementFrame = elementBodyFrame(el, cumulativeFrame, occurrenceTransformDepth);
29025
+ const flights = flatInput["flights"];
29026
+ return {
29027
+ ...flatInput,
29028
+ origin: [
29029
+ 0,
29030
+ 0,
29031
+ 0
29032
+ ],
29033
+ axisX: [
29034
+ 1,
29035
+ 0,
29036
+ 0
29037
+ ],
29038
+ axisZ: [
29039
+ 0,
29040
+ 0,
29041
+ 1
29042
+ ],
29043
+ flights: flights.map((flight) => {
29044
+ if (typeof flight !== "object" || flight === null) return flight;
29045
+ const f = flight;
29046
+ const world = frameMul(elementFrame, frameFromPlacement({
29047
+ origin: f["origin"] ?? [
29048
+ 0,
29049
+ 0,
29050
+ 0
29051
+ ],
29052
+ axisX: f["axisX"] ?? DEFAULT_AXIS_X,
29053
+ axisZ: f["axisZ"] ?? DEFAULT_AXIS_Z
29054
+ }));
29055
+ const placed = decomposeFrame(frameMul(toSpatial, world));
29056
+ return {
29057
+ ...f,
29058
+ origin: placed.origin,
29059
+ axisX: placed.axisX,
29060
+ axisZ: placed.axisZ
29061
+ };
29062
+ })
29063
+ };
29064
+ }
29065
+ const placed = decomposeFrame(frameMul(toSpatial, elementBodyFrame(el, cumulativeFrame, occurrenceTransformDepth)));
29066
+ return {
29067
+ ...flatInput,
29068
+ origin: placed.origin,
29069
+ axisX: placed.axisX,
29070
+ axisZ: placed.axisZ
29071
+ };
29072
+ }
29073
+ /** Moves an owned world-baked body into its spatial container's local frame.
29074
+ * A pure translation keeps the fast `translate` path (identical to the prior
29075
+ * behaviour); a rotated container applies the inverse frame via `applyMatrix`. */
29076
+ function localizeBodyToFrame(el, body, spatialFrame, errorCode, frameName) {
29077
+ if (isPureTranslation(spatialFrame)) return localizeOwnedSolid(el, body, frameOrigin(spatialFrame), errorCode, frameName);
29078
+ const inverse = decomposeFrame(frameInverse(spatialFrame));
29079
+ try {
29080
+ const localized = applyMatrix(body, placementToMatrix(inverse));
29081
+ if (!localized.ok) {
29082
+ body[Symbol.dispose]();
29083
+ return err(specError(errorCode, `familiesToBim: '${el.keyPath}' could not move its body into the ${frameName} frame`, localized.error));
29084
+ }
29085
+ body[Symbol.dispose]();
29086
+ return ok(localized.value);
29087
+ } catch (cause) {
29088
+ body[Symbol.dispose]();
29089
+ return err(specError(errorCode, `familiesToBim: '${el.keyPath}' could not move its body into the ${frameName} frame`, cause));
29090
+ }
29091
+ }
28578
29092
  function civilSpatialInput(el, localTranslation) {
28579
29093
  const semantics = el.semantics;
28580
29094
  const composition = semantics !== void 0 && "composition" in semantics ? CIVIL_COMPOSITION[semantics.composition] : void 0;
@@ -28587,22 +29101,36 @@ function civilSpatialInput(el, localTranslation) {
28587
29101
  ...composition !== void 0 ? { compositionType: composition } : {}
28588
29102
  };
28589
29103
  }
29104
+ /** Relativizes a civil node's world frame against its parent civil frame into
29105
+ * the origin/axisX/axisZ the spatial specs consume. */
29106
+ function civilSpatialFrameInput(el, nodeFrame, parentFrame) {
29107
+ const semantics = el.semantics;
29108
+ const composition = semantics !== void 0 && "composition" in semantics ? CIVIL_COMPOSITION[semantics.composition] : void 0;
29109
+ const local = decomposeFrame(frameMul(frameInverse(parentFrame), nodeFrame));
29110
+ return {
29111
+ name: semanticName(el),
29112
+ origin: local.origin,
29113
+ axisX: local.axisX,
29114
+ axisZ: local.axisZ,
29115
+ ...composition !== void 0 ? { compositionType: composition } : {}
29116
+ };
29117
+ }
28590
29118
  function civilParentAccepts(kind, parent) {
28591
29119
  if (kind === "site") return parent === "project";
28592
29120
  if (kind === "bridge") return parent === "site";
28593
29121
  return parent === "bridge" || parent === "bridge-part";
28594
29122
  }
28595
- function addCivilSpatialOccurrence(model, el, kind, localTranslation) {
29123
+ function addCivilSpatialOccurrence(model, el, kind, input) {
28596
29124
  if (kind === "site") {
28597
29125
  if (el.semantics?.role !== "transport-site") return unsupportedCivilRole(el, "Site");
28598
- const parsed = parseSiteSpec(civilSpatialInput(el, localTranslation));
29126
+ const parsed = parseSiteSpec(input);
28599
29127
  return parsed.ok ? model.addSite(parsed.value, { stableKey: el.keyPath }) : parsed;
28600
29128
  }
28601
29129
  if (kind === "bridge") {
28602
29130
  const predefinedType = lookup(BRIDGE_ROLE, el.semantics?.role ?? "");
28603
29131
  if (predefinedType === void 0) return unsupportedCivilRole(el, "Bridge");
28604
29132
  const parsed = parseBridgeSpec({
28605
- ...civilSpatialInput(el, localTranslation),
29133
+ ...input,
28606
29134
  predefinedType
28607
29135
  });
28608
29136
  return parsed.ok ? model.addBridge(parsed.value, { stableKey: el.keyPath }) : parsed;
@@ -28611,7 +29139,7 @@ function addCivilSpatialOccurrence(model, el, kind, localTranslation) {
28611
29139
  const predefinedType = lookup(BRIDGE_PART_ROLE, el.semantics?.role ?? "");
28612
29140
  if (predefinedType === void 0) return unsupportedCivilRole(el, "Bridge Part");
28613
29141
  const parsed = parseBridgePartSpec({
28614
- ...civilSpatialInput(el, localTranslation),
29142
+ ...input,
28615
29143
  predefinedType,
28616
29144
  usageType: subdivision !== void 0 ? CIVIL_USAGE[subdivision] : "NOTDEFINED"
28617
29145
  });
@@ -28638,7 +29166,7 @@ function relativeSpecInput(input, projectedSpatialTranslation) {
28638
29166
  origin: relativeOrigin(input["origin"])
28639
29167
  };
28640
29168
  }
28641
- function addEarthworksFillElement(model, el, evaluator, projectedSpatialTranslation) {
29169
+ function addEarthworksFillElement(model, el, evaluator, spatialFrame) {
28642
29170
  if (el.semantics?.kind !== "product") return err(specError("FAMILIES_UNSUPPORTED_CIVIL_SEMANTICS", `familiesToBim: '${el.keyPath}' is not authored as a civil Product`));
28643
29171
  const predefinedType = lookup(EARTHWORKS_FILL_ROLE, el.semantics.role);
28644
29172
  if (predefinedType === void 0) return unsupportedCivilRole(el, "Earthworks Fill");
@@ -28649,7 +29177,7 @@ function addEarthworksFillElement(model, el, evaluator, projectedSpatialTranslat
28649
29177
  routeName: "Earthworks Fill"
28650
29178
  });
28651
29179
  if (!body.ok) return body;
28652
- const localized = localizeOwnedSolid(el, body.value, projectedSpatialTranslation, "FAMILIES_EARTHWORKS_LOCALIZE_FAILED", "Bridge Part");
29180
+ const localized = localizeBodyToFrame(el, body.value, spatialFrame, "FAMILIES_EARTHWORKS_LOCALIZE_FAILED", "Bridge Part");
28653
29181
  if (!localized.ok) return localized;
28654
29182
  const specProps = collectSpecProps(el);
28655
29183
  const authoredMaterial = el.props["materialName"];
@@ -28699,10 +29227,15 @@ function familiesToBim(root, options) {
28699
29227
  const walk = (el, state) => {
28700
29228
  const rotatedHere = state.rotated || hasRotateOp(el);
28701
29229
  const cumulativeTranslationHere = addTranslation(state.cumulativeTranslation, authoredTranslation(el));
29230
+ const occurrenceTransformDepthHere = state.occurrenceTransformDepth + el.localTransforms.length;
29231
+ const cumulativeFrameHere = frameMul(state.cumulativeFrame, frameFromOps(el.localTransforms));
28702
29232
  let proxiedHere = false;
29233
+ let nextRotated = rotatedHere;
28703
29234
  let nextSpatialStructureId = state.spatialStructureId;
28704
29235
  let nextCivilParent = state.civilParent;
28705
29236
  let nextProjectedSpatialTranslation = state.projectedSpatialTranslation;
29237
+ let nextCumulativeFrame = cumulativeFrameHere;
29238
+ let nextSpatialFrame = state.spatialFrame;
28706
29239
  const archetype = archetypeFor(el);
28707
29240
  const effectiveArchetype = el.semantics?.kind === "product" ? civilProductArchetype(el) : archetype;
28708
29241
  const route = specRoute(effectiveArchetype);
@@ -28711,15 +29244,19 @@ function familiesToBim(root, options) {
28711
29244
  if (!usesAuthoredCivilHierarchy || !civilParentAccepts(civilKind, state.civilParent) || state.spatialStructureId === null) return err(specError("FAMILIES_INVALID_CIVIL_HIERARCHY", `familiesToBim: civil '${civilKind}' at '${el.keyPath}' cannot occur under '${state.civilParent}'`));
28712
29245
  const keyed = requireKeyed(el);
28713
29246
  if (!keyed.ok) return keyed;
28714
- if (rotatedHere || hasRotatedAxes(el)) return err(specError("FAMILIES_UNSUPPORTED_TRANSFORM", `familiesToBim: civil spatial element '${el.keyPath}' carries a rotated frame — descendants are relativized by translation only, so bake the orientation into child geometry`));
28715
29247
  if (el.geometry.kind !== "Empty") return err(specError("FAMILIES_UNSUPPORTED_CIVIL_SEMANTICS", `familiesToBim: civil spatial element '${el.keyPath}' carries its own geometry — Site/Bridge/Bridge Part export no body, so author it as a child Product (e.g. Earthworks Fill)`));
28716
- const localTranslation = subtractTranslation(cumulativeTranslationHere, state.projectedSpatialTranslation);
28717
- const added = addCivilSpatialOccurrence(model, el, civilKind, localTranslation);
29248
+ const nodeFrame = civilNodeFrame(el, cumulativeFrameHere);
29249
+ const rotatedFrame = rotatedHere || hasRotatedAxes(el);
29250
+ const input = rotatedFrame ? civilSpatialFrameInput(el, nodeFrame, state.spatialFrame) : civilSpatialInput(el, subtractTranslation(cumulativeTranslationHere, state.projectedSpatialTranslation));
29251
+ const added = addCivilSpatialOccurrence(model, el, civilKind, input);
28718
29252
  if (!added.ok) return added;
28719
29253
  model.aggregate(state.spatialStructureId, added.value);
28720
29254
  idByKeyPath.set(el.keyPath, added.value);
28721
29255
  nextSpatialStructureId = added.value;
28722
29256
  nextCivilParent = civilKind;
29257
+ nextRotated = rotatedFrame;
29258
+ nextCumulativeFrame = nodeFrame;
29259
+ nextSpatialFrame = nodeFrame;
28723
29260
  nextProjectedSpatialTranslation = addTranslation(cumulativeTranslationHere, authoredSpecOrigin(el));
28724
29261
  } else if (isCivilSpatialIntent(el)) return err(specError("FAMILIES_UNSUPPORTED_CIVIL_SEMANTICS", `familiesToBim: unsupported civil '${el.semantics?.kind ?? "unknown"}' category '${el.semantics?.category ?? ""}' at '${el.keyPath}'`));
28725
29262
  else if (archetype === "storey") {
@@ -28740,21 +29277,22 @@ function familiesToBim(root, options) {
28740
29277
  if (bodyEvaluator === void 0) return err(specError("FAMILIES_EARTHWORKS_EVALUATOR_REQUIRED", `familiesToBim: Earthworks Fill '${el.keyPath}' needs bodyEvaluator to materialize its exact Product Body`));
28741
29278
  const keyed = requireKeyed(el);
28742
29279
  if (!keyed.ok) return keyed;
28743
- const added = addEarthworksFillElement(model, el, bodyEvaluator, state.projectedSpatialTranslation);
29280
+ const added = addEarthworksFillElement(model, el, bodyEvaluator, state.spatialFrame);
28744
29281
  if (!added.ok) return added;
28745
29282
  model.placeIn(added.value, nextSpatialStructureId);
28746
29283
  idByKeyPath.set(el.keyPath, added.value);
28747
29284
  } else if (route !== void 0) {
28748
29285
  const keyed = requireKeyed(el);
28749
29286
  if (!keyed.ok) return keyed;
28750
- if (rotatedHere) return err(specError("FAMILIES_UNSUPPORTED_TRANSFORM", `familiesToBim: '${el.keyPath}' carries a rotated placement — the spec path folds only translations into IfcLocalPlacement; orient walls via axisX instead of tRotate`));
28751
29287
  const voids = el.props["voids"];
28752
29288
  if (Array.isArray(voids)) {
28753
29289
  const openings = el.children.filter((c) => c.type === "Opening").length;
28754
29290
  if (voids.length > openings) return err(specError("FAMILIES_ANONYMOUS_VOID", `familiesToBim: '${el.keyPath}' has ${voids.length - openings} anonymous void(s) the IFC body cannot carry — use a fill-role family (Door/Window) for each void`));
28755
29291
  }
28756
29292
  const routedInput = el.semantics?.kind === "product" ? civilProductSpecInput(el) : ("input" in route ? route.input : specInput)(el);
28757
- const parsed = route.parse(usesAuthoredCivilHierarchy ? relativeSpecInput(routedInput, state.projectedSpatialTranslation) : routedInput);
29293
+ const rotatedBase = Array.isArray(routedInput["flights"]) ? specInput(el) : routedInput;
29294
+ const placedInput = rotatedHere ? rotatedRoutedInput(rotatedBase, el, cumulativeFrameHere, occurrenceTransformDepthHere, state.spatialFrame) : usesAuthoredCivilHierarchy ? relativeSpecInput(routedInput, state.projectedSpatialTranslation) : routedInput;
29295
+ const parsed = route.parse(placedInput);
28758
29296
  if (!parsed.ok) return parsed;
28759
29297
  const added = route.add(model, parsed.value, el.keyPath);
28760
29298
  if (!added.ok) return added;
@@ -28771,7 +29309,7 @@ function familiesToBim(root, options) {
28771
29309
  const keyed = requireKeyed(el);
28772
29310
  if (!keyed.ok) return keyed;
28773
29311
  if (nextSpatialStructureId === null || usesAuthoredCivilHierarchy && state.civilParent !== "bridge-part") return err(specError(usesAuthoredCivilHierarchy ? "FAMILIES_INVALID_CIVIL_HIERARCHY" : "FAMILIES_NO_STOREY", usesAuthoredCivilHierarchy ? `familiesToBim: physical product '${el.keyPath}' needs a Bridge Part ancestor` : `familiesToBim: '${el.keyPath}' has no Storey ancestor — IFC elements need spatial containment; a container family needs archetype: 'storey' to be recognised under any name`));
28774
- const added = addProxyElement(model, el, options.proxyEvaluator, state.projectedSpatialTranslation);
29312
+ const added = addProxyElement(model, el, options.proxyEvaluator, state.spatialFrame);
28775
29313
  if (!added.ok) return added;
28776
29314
  model.placeIn(added.value, nextSpatialStructureId);
28777
29315
  idByKeyPath.set(el.keyPath, added.value);
@@ -28787,9 +29325,12 @@ function familiesToBim(root, options) {
28787
29325
  const r = walk(child, {
28788
29326
  spatialStructureId: nextSpatialStructureId,
28789
29327
  civilParent: nextCivilParent,
28790
- rotated: rotatedHere,
29328
+ rotated: nextRotated,
28791
29329
  cumulativeTranslation: cumulativeTranslationHere,
28792
- projectedSpatialTranslation: nextProjectedSpatialTranslation
29330
+ projectedSpatialTranslation: nextProjectedSpatialTranslation,
29331
+ occurrenceTransformDepth: occurrenceTransformDepthHere,
29332
+ cumulativeFrame: nextCumulativeFrame,
29333
+ spatialFrame: nextSpatialFrame
28793
29334
  });
28794
29335
  if (!r.ok) return r;
28795
29336
  }
@@ -28800,7 +29341,10 @@ function familiesToBim(root, options) {
28800
29341
  civilParent: "project",
28801
29342
  rotated: false,
28802
29343
  cumulativeTranslation: ZERO_TRANSLATION,
28803
- projectedSpatialTranslation: ZERO_TRANSLATION
29344
+ projectedSpatialTranslation: ZERO_TRANSLATION,
29345
+ occurrenceTransformDepth: 0,
29346
+ cumulativeFrame: IDENTITY_FRAME,
29347
+ spatialFrame: IDENTITY_FRAME
28804
29348
  });
28805
29349
  if (!walked.ok) {
28806
29350
  model[Symbol.dispose]();