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.
@@ -206,7 +206,7 @@ function fromBrepError(inner, code, message) {
206
206
  };
207
207
  }
208
208
  //#endregion
209
- //#region \0@oxc-project+runtime@0.143.0/helpers/esm/usingCtx.js
209
+ //#region \0@oxc-project+runtime@0.146.0/helpers/esm/usingCtx.js
210
210
  function _usingCtx() {
211
211
  var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) {
212
212
  var n = Error();
@@ -7205,7 +7205,7 @@ function placementToMatrix(f) {
7205
7205
  f.axisX[2] - dot * z[2]
7206
7206
  ];
7207
7207
  const x = lengthSq(projX) < 1e-12 ? normalize$1(orthogonal$2(z)) : normalize$1(projX);
7208
- const y = cross$1(z, x);
7208
+ const y = cross$2(z, x);
7209
7209
  return {
7210
7210
  linear: [
7211
7211
  x[0],
@@ -7225,7 +7225,27 @@ function placementToMatrix(f) {
7225
7225
  ]
7226
7226
  };
7227
7227
  }
7228
- function cross$1(a, b) {
7228
+ /** Decomposes a column-major matrix into origin (mm) + IFC axes (Z, X). */
7229
+ function decomposePlacement(m) {
7230
+ return {
7231
+ axisX: normalize$1([
7232
+ m[0],
7233
+ m[1],
7234
+ m[2]
7235
+ ]),
7236
+ axisZ: normalize$1([
7237
+ m[8],
7238
+ m[9],
7239
+ m[10]
7240
+ ]),
7241
+ origin: [
7242
+ m[12],
7243
+ m[13],
7244
+ m[14]
7245
+ ]
7246
+ };
7247
+ }
7248
+ function cross$2(a, b) {
7229
7249
  return [
7230
7250
  a[1] * b[2] - a[2] * b[1],
7231
7251
  a[2] * b[0] - a[0] * b[2],
@@ -7249,11 +7269,11 @@ function normalize$1(v) {
7249
7269
  ];
7250
7270
  }
7251
7271
  function orthogonal$2(v) {
7252
- return Math.abs(v[0]) < .9 ? cross$1(v, [
7272
+ return Math.abs(v[0]) < .9 ? cross$2(v, [
7253
7273
  1,
7254
7274
  0,
7255
7275
  0
7256
- ]) : cross$1(v, [
7276
+ ]) : cross$2(v, [
7257
7277
  0,
7258
7278
  1,
7259
7279
  0
@@ -8759,7 +8779,7 @@ function writeWallGeometry(w, spec, geomSubContextId, parentPlacementId) {
8759
8779
  value: "AREA"
8760
8780
  },
8761
8781
  ProfileName: null,
8762
- Position: w.ref(writeAxis2Placement2D$1(w)),
8782
+ Position: w.ref(writeAxis2Placement2D$1(w, [thicknessM / 2, heightM / 2])),
8763
8783
  XDim: w.mkType(web_ifc.IFCPOSITIVELENGTHMEASURE, thicknessM),
8764
8784
  YDim: w.mkType(web_ifc.IFCPOSITIVELENGTHMEASURE, heightM)
8765
8785
  });
@@ -8980,12 +9000,12 @@ function writeRoofGeometry(w, spec, solid, geomSubContextId, parentPlacementId)
8980
9000
  usedFallback: false
8981
9001
  };
8982
9002
  }
8983
- function writeAxis2Placement2D$1(w) {
9003
+ function writeAxis2Placement2D$1(w, location = [0, 0]) {
8984
9004
  const originId = w.nextId();
8985
9005
  w.writeLine({
8986
9006
  expressID: originId,
8987
9007
  type: web_ifc.IFCCARTESIANPOINT,
8988
- Coordinates: [w.mkType(web_ifc.IFCLENGTHMEASURE, 0), w.mkType(web_ifc.IFCLENGTHMEASURE, 0)]
9008
+ Coordinates: [w.mkType(web_ifc.IFCLENGTHMEASURE, location[0]), w.mkType(web_ifc.IFCLENGTHMEASURE, location[1])]
8989
9009
  });
8990
9010
  const id = w.nextId();
8991
9011
  w.writeLine({
@@ -11490,11 +11510,10 @@ function writeOpeningGeometry(w, guid, openingSpec, wallSpec, wallPlacementId, g
11490
11510
  const offsetAlongWallM = toIfcLengthM(openingSpec.offsetAlongWall);
11491
11511
  const offsetFromFloorM = toIfcLengthM(openingSpec.offsetFromFloor);
11492
11512
  const thicknessM = toIfcLengthM(wallSpec.thickness);
11493
- const wallHeightM = toIfcLengthM(wallSpec.height);
11494
11513
  const placement3DId = writeAxis2Placement3D(w, [
11495
11514
  offsetAlongWallM + widthM / 2,
11496
- thicknessM / 2,
11497
- -wallHeightM / 2 + offsetFromFloorM + heightM / 2
11515
+ thicknessM,
11516
+ offsetFromFloorM + heightM / 2
11498
11517
  ], [
11499
11518
  0,
11500
11519
  -1,
@@ -12370,6 +12389,24 @@ var SUPPORTED_SCHEMAS = [
12370
12389
  * MUST call {@link SpfReader.close} (it always issues `CloseModel`, preventing
12371
12390
  * WASM handle leaks). The handle is `using`-friendly via `Symbol.dispose`.
12372
12391
  */
12392
+ var IFC_Z_UP_TRANSFORMATION = [
12393
+ 1,
12394
+ 0,
12395
+ 0,
12396
+ 0,
12397
+ 0,
12398
+ 0,
12399
+ 1,
12400
+ 0,
12401
+ 0,
12402
+ -1,
12403
+ 0,
12404
+ 0,
12405
+ 0,
12406
+ 0,
12407
+ 0,
12408
+ 1
12409
+ ];
12373
12410
  function deleteVector(vec) {
12374
12411
  vec.delete?.();
12375
12412
  }
@@ -12402,6 +12439,7 @@ var SpfReader = class SpfReader {
12402
12439
  if (modelId >= 0) api.CloseModel(modelId);
12403
12440
  return (0, brepjs.err)(importError("OPEN_MODEL_FAILED", "web-ifc OpenModel returned an invalid model id"));
12404
12441
  }
12442
+ api.SetGeometryTransformation(modelId, IFC_Z_UP_TRANSFORMATION);
12405
12443
  const schemaRaw = api.GetModelSchema(modelId);
12406
12444
  if (!SUPPORTED_SCHEMAS.includes(schemaRaw)) {
12407
12445
  api.CloseModel(modelId);
@@ -14101,12 +14139,14 @@ function reconstructExtrusion(reader, extrusionId, scale, worldTransform, diagno
14101
14139
  diagnostics.push(issue("warning", faceResult.error.code, faceResult.error.message, extrusionId));
14102
14140
  return NONE;
14103
14141
  }
14142
+ const profileFrame = readProfilePosition(reader, sweptArea.value, scale);
14104
14143
  const depthMm = depth * scale * 1e3;
14105
- const extrudeDir = readDirection(reader, ext["ExtrudedDirection"]) ?? [
14144
+ const sweptDir = readDirection(reader, ext["ExtrudedDirection"]) ?? [
14106
14145
  0,
14107
14146
  0,
14108
14147
  1
14109
14148
  ];
14149
+ const extrudeDir = profileFrame === null ? sweptDir : rotateByTranspose(profileFrame.linear, sweptDir);
14110
14150
  const extrudeVec = [
14111
14151
  extrudeDir[0] * depthMm,
14112
14152
  extrudeDir[1] * depthMm,
@@ -14128,7 +14168,11 @@ function reconstructExtrusion(reader, extrusionId, scale, worldTransform, diagno
14128
14168
  return NONE;
14129
14169
  }
14130
14170
  const localFrame = readAxis2Placement3D(reader, ext["Position"], scale);
14131
- const placed = placeSolid(solidResult.value, localFrame, worldTransform);
14171
+ const placed = placeSolid(solidResult.value, [
14172
+ worldTransform,
14173
+ localFrame,
14174
+ profileFrame
14175
+ ]);
14132
14176
  if (!placed.ok) {
14133
14177
  diagnostics.push(issue("warning", placed.error.code, placed.error.message, extrusionId));
14134
14178
  return NONE;
@@ -14156,16 +14200,19 @@ function reconstructRevolution(reader, revolutionId, scale, worldTransform, diag
14156
14200
  return NONE;
14157
14201
  }
14158
14202
  const axis1 = reader.getLine(axisRef.value);
14159
- const center = (axis1 === null ? void 0 : readPoint(reader, axis1["Location"], scale)) ?? [
14203
+ const sweptCenter = (axis1 === null ? void 0 : readPoint(reader, axis1["Location"], scale)) ?? [
14160
14204
  0,
14161
14205
  0,
14162
14206
  0
14163
14207
  ];
14164
- const direction = (axis1 === null ? void 0 : readDirection(reader, axis1["Axis"])) ?? [
14208
+ const sweptDirection = (axis1 === null ? void 0 : readDirection(reader, axis1["Axis"])) ?? [
14165
14209
  0,
14166
14210
  0,
14167
14211
  1
14168
14212
  ];
14213
+ const profileFrame = readProfilePosition(reader, sweptArea.value, scale);
14214
+ const center = profileFrame === null ? sweptCenter : inverseRigidPoint(profileFrame, sweptCenter);
14215
+ const direction = profileFrame === null ? sweptDirection : rotateByTranspose(profileFrame.linear, sweptDirection);
14169
14216
  const revolved = (() => {
14170
14217
  try {
14171
14218
  var _usingCtx3 = _usingCtx();
@@ -14192,7 +14239,11 @@ function reconstructRevolution(reader, revolutionId, scale, worldTransform, diag
14192
14239
  return NONE;
14193
14240
  }
14194
14241
  const localFrame = readAxis2Placement3D(reader, rev["Position"], scale);
14195
- const placed = placeSolid(revolved.value, localFrame, worldTransform);
14242
+ const placed = placeSolid(revolved.value, [
14243
+ worldTransform,
14244
+ localFrame,
14245
+ profileFrame
14246
+ ]);
14196
14247
  if (!placed.ok) {
14197
14248
  diagnostics.push(issue("warning", placed.error.code, placed.error.message, revolutionId));
14198
14249
  return NONE;
@@ -14205,20 +14256,9 @@ function reconstructTessellated(reader, productExpressId, diagnostics) {
14205
14256
  diagnostics.push(issue("warning", "GEOMETRY_RECONSTRUCTION_FAILED", "Tessellated geometry yielded no triangles", productExpressId));
14206
14257
  return NONE;
14207
14258
  }
14208
- const stl = packBinaryStl(mesh.vertices, mesh.indices, 1e3);
14209
- let solid = null;
14210
- try {
14211
- const cast = (0, brepjs.castShape)((0, brepjs.getKernel)().importSTL(stl.buffer));
14212
- if ((0, brepjs.isSolid)(cast)) {
14213
- const valid = (0, brepjs.validSolid)(cast);
14214
- if (valid.ok) solid = valid.value;
14215
- else cast[Symbol.dispose]();
14216
- } else cast[Symbol.dispose]();
14217
- } catch (e) {
14218
- diagnostics.push(issue("info", "TESSELLATION_NOT_MANIFOLD", `STL round-trip failed: ${errMsg$1(e)}`, productExpressId));
14219
- }
14259
+ const solid = sewMeshToSolid(mesh, 1e3, productExpressId, diagnostics);
14220
14260
  if (solid !== null) {
14221
- diagnostics.push(issue("info", "TESSELLATED_MANIFOLD", "Tessellated mesh recovered as a closed solid via STL round-trip", productExpressId));
14261
+ diagnostics.push(issue("info", "TESSELLATED_MANIFOLD", "Tessellated mesh recovered as a closed solid by sewing its triangles", productExpressId));
14222
14262
  return {
14223
14263
  kind: "SOLID",
14224
14264
  solid,
@@ -14233,6 +14273,52 @@ function reconstructTessellated(reader, productExpressId, diagnostics) {
14233
14273
  diagnostic: "TESSELLATED_LOSSY"
14234
14274
  };
14235
14275
  }
14276
+ /** Loose enough to weld float32 vertices web-ifc emits per face, in mm. */
14277
+ var MESH_SEW_TOLERANCE_MM = .001;
14278
+ /**
14279
+ * Brands a fresh kernel result and releases its pre-downcast arena slot when
14280
+ * the cast moved to a new one (occt-wasm); in-place kernels share the slot and
14281
+ * are left alone. Mirrors brepjs's internal castResultShape.
14282
+ */
14283
+ function castFreshResult(raw) {
14284
+ const cast = (0, brepjs.castShape)(raw);
14285
+ const rawId = raw.id;
14286
+ const castId = cast.wrapped.id;
14287
+ if (!(rawId !== void 0 && castId !== void 0 ? rawId === castId : cast.wrapped === raw)) (0, brepjs.getKernel)().dispose(raw);
14288
+ return cast;
14289
+ }
14290
+ /**
14291
+ * Sews the streamed triangles into a closed solid through the kernel's mesh
14292
+ * builder, the same path brepjs's mesh importers use. Null when the mesh is
14293
+ * open or the kernel rejects it.
14294
+ */
14295
+ function sewMeshToSolid(mesh, scaleToMm, productExpressId, diagnostics) {
14296
+ const kernel = (0, brepjs.getKernel)();
14297
+ const { vertices, indices } = mesh;
14298
+ const point = (index) => [
14299
+ (vertices[index * 3] ?? 0) * scaleToMm,
14300
+ (vertices[index * 3 + 1] ?? 0) * scaleToMm,
14301
+ (vertices[index * 3 + 2] ?? 0) * scaleToMm
14302
+ ];
14303
+ const triangles = [];
14304
+ for (let t = 0; t + 2 < indices.length; t += 3) {
14305
+ const face = kernel.buildTriFace(point(indices[t] ?? 0), point(indices[t + 1] ?? 0), point(indices[t + 2] ?? 0));
14306
+ if (face !== null) triangles.push(face);
14307
+ }
14308
+ if (triangles.length === 0) return null;
14309
+ try {
14310
+ const cast = castFreshResult(kernel.sewAndSolidify(triangles, MESH_SEW_TOLERANCE_MM));
14311
+ const valid = (0, brepjs.isSolid)(cast) ? (0, brepjs.validSolid)(cast) : null;
14312
+ if (valid !== null && valid.ok) return valid.value;
14313
+ cast[Symbol.dispose]();
14314
+ return null;
14315
+ } catch (e) {
14316
+ diagnostics.push(issue("info", "TESSELLATION_NOT_MANIFOLD", `Mesh sewing failed: ${errMsg$1(e)}`, productExpressId));
14317
+ return null;
14318
+ } finally {
14319
+ for (const face of triangles) kernel.dispose(face);
14320
+ }
14321
+ }
14236
14322
  function collectMesh(reader, productExpressId) {
14237
14323
  const vertices = [];
14238
14324
  const indices = [];
@@ -14261,7 +14347,8 @@ function collectMesh(reader, productExpressId) {
14261
14347
  }
14262
14348
  }
14263
14349
  } finally {
14264
- flatMesh.delete();
14350
+ releaseEmbind(flatMesh.geometries);
14351
+ releaseEmbind(flatMesh);
14265
14352
  }
14266
14353
  });
14267
14354
  if (vertices.length === 0) return null;
@@ -14270,25 +14357,6 @@ function collectMesh(reader, productExpressId) {
14270
14357
  indices: new Uint32Array(indices)
14271
14358
  };
14272
14359
  }
14273
- function packBinaryStl(vertices, indices, scaleToMm) {
14274
- const triCount = Math.floor(indices.length / 3);
14275
- const buffer = /* @__PURE__ */ new ArrayBuffer(84 + triCount * 50);
14276
- const view = new DataView(buffer);
14277
- view.setUint32(80, triCount, true);
14278
- let offset = 84;
14279
- for (let t = 0; t < triCount; t++) {
14280
- offset += 12;
14281
- for (let c = 0; c < 3; c++) {
14282
- const vi = (indices[t * 3 + c] ?? 0) * 3;
14283
- view.setFloat32(offset, (vertices[vi] ?? 0) * scaleToMm, true);
14284
- view.setFloat32(offset + 4, (vertices[vi + 1] ?? 0) * scaleToMm, true);
14285
- view.setFloat32(offset + 8, (vertices[vi + 2] ?? 0) * scaleToMm, true);
14286
- offset += 12;
14287
- }
14288
- offset += 2;
14289
- }
14290
- return new Uint8Array(buffer);
14291
- }
14292
14360
  var PARAMETRIC_PROFILE_BUILDERS = /* @__PURE__ */ new Map([
14293
14361
  [web_ifc.IFCRECTANGLEPROFILEDEF, (f) => ({
14294
14362
  kind: "RECTANGULAR",
@@ -14432,19 +14500,99 @@ function profileToFace(profile) {
14432
14500
  if (!face.ok) return (0, brepjs.err)(importError("GEOMETRY_RECONSTRUCTION_FAILED", `Profile face build failed: ${face.error.message}`));
14433
14501
  return (0, brepjs.ok)(face.value);
14434
14502
  }
14435
- function placeSolid(solid, localFrame, worldTransform) {
14436
- let current = solid;
14437
- for (const transform of [localFrame, worldTransform]) {
14438
- if (transform === null || isIdentity(transform)) continue;
14439
- const applied = (0, brepjs.applyMatrix)(current, transform);
14440
- if (!applied.ok) {
14441
- current[Symbol.dispose]();
14442
- return (0, brepjs.err)(importError("PLACEMENT_READ_FAILED", `Placement transform failed: ${applied.error.message}`));
14443
- }
14444
- current[Symbol.dispose]();
14445
- current = applied.value;
14446
- }
14447
- return (0, brepjs.ok)(current);
14503
+ /**
14504
+ * IfcParameterizedProfileDef.Position places the profile inside the swept
14505
+ * solid's XY plane; the writers rely on it to corner-anchor centred rectangle
14506
+ * profiles. Absent on arbitrary (polyline) profiles.
14507
+ */
14508
+ function readProfilePosition(reader, profileExpressId, scale) {
14509
+ const def = reader.getLine(profileExpressId);
14510
+ const positionRef = def === null ? void 0 : asRef(def["Position"]);
14511
+ if (positionRef === void 0) return null;
14512
+ const position = reader.getLine(positionRef.value);
14513
+ if (position === null) return null;
14514
+ const [x, y] = readPoint2D(reader, position["Location"], scale) ?? [0, 0];
14515
+ const [dx, dy] = readDirection2D(reader, position["RefDirection"]) ?? [1, 0];
14516
+ const len = Math.hypot(dx, dy);
14517
+ const c = len < 1e-12 ? 1 : dx / len;
14518
+ const s = len < 1e-12 ? 0 : dy / len;
14519
+ return {
14520
+ linear: [
14521
+ c,
14522
+ -s,
14523
+ 0,
14524
+ s,
14525
+ c,
14526
+ 0,
14527
+ 0,
14528
+ 0,
14529
+ 1
14530
+ ],
14531
+ translation: [
14532
+ x,
14533
+ y,
14534
+ 0
14535
+ ]
14536
+ };
14537
+ }
14538
+ /**
14539
+ * Applies the placement chain (outermost first) as one composed rigid motion.
14540
+ * A second applyMatrix on an already-transformed solid can fail BRepCheck on
14541
+ * occt-wasm, so the frames are never applied one after another.
14542
+ */
14543
+ function placeSolid(solid, frames) {
14544
+ let composed = null;
14545
+ for (const frame of frames) {
14546
+ if (frame === null) continue;
14547
+ composed = composed === null ? frame : composeRigid(composed, frame);
14548
+ }
14549
+ if (composed === null || isIdentity(composed)) return (0, brepjs.ok)(solid);
14550
+ const applied = (0, brepjs.applyMatrix)(solid, composed);
14551
+ solid[Symbol.dispose]();
14552
+ if (!applied.ok) return (0, brepjs.err)(importError("PLACEMENT_READ_FAILED", `Placement transform failed: ${applied.error.message}`));
14553
+ return (0, brepjs.ok)(applied.value);
14554
+ }
14555
+ /** `outer ∘ inner`: the motion that applies `inner` first, then `outer`. */
14556
+ function composeRigid(outer, inner) {
14557
+ const [a00, a01, a02, a10, a11, a12, a20, a21, a22] = outer.linear;
14558
+ const [b00, b01, b02, b10, b11, b12, b20, b21, b22] = inner.linear;
14559
+ const [tx, ty, tz] = inner.translation;
14560
+ const [ux, uy, uz] = outer.translation;
14561
+ return {
14562
+ linear: [
14563
+ a00 * b00 + a01 * b10 + a02 * b20,
14564
+ a00 * b01 + a01 * b11 + a02 * b21,
14565
+ a00 * b02 + a01 * b12 + a02 * b22,
14566
+ a10 * b00 + a11 * b10 + a12 * b20,
14567
+ a10 * b01 + a11 * b11 + a12 * b21,
14568
+ a10 * b02 + a11 * b12 + a12 * b22,
14569
+ a20 * b00 + a21 * b10 + a22 * b20,
14570
+ a20 * b01 + a21 * b11 + a22 * b21,
14571
+ a20 * b02 + a21 * b12 + a22 * b22
14572
+ ],
14573
+ translation: [
14574
+ a00 * tx + a01 * ty + a02 * tz + ux,
14575
+ a10 * tx + a11 * ty + a12 * tz + uy,
14576
+ a20 * tx + a21 * ty + a22 * tz + uz
14577
+ ]
14578
+ };
14579
+ }
14580
+ /** Rᵀ·v for a rigid frame's rotation part. */
14581
+ function rotateByTranspose(linear, v) {
14582
+ return [
14583
+ linear[0] * v[0] + linear[3] * v[1] + linear[6] * v[2],
14584
+ linear[1] * v[0] + linear[4] * v[1] + linear[7] * v[2],
14585
+ linear[2] * v[0] + linear[5] * v[1] + linear[8] * v[2]
14586
+ ];
14587
+ }
14588
+ /** Maps a point from the frame's parent space into the frame: Rᵀ·(p − t). */
14589
+ function inverseRigidPoint(frame, p) {
14590
+ const t = frame.translation;
14591
+ return rotateByTranspose(frame.linear, [
14592
+ p[0] - t[0],
14593
+ p[1] - t[1],
14594
+ p[2] - t[2]
14595
+ ]);
14448
14596
  }
14449
14597
  function finalizeSolid(solid, entity, diagnostics) {
14450
14598
  if (!(0, brepjs.isSolid)(solid)) {
@@ -14499,17 +14647,17 @@ function readAxis2Placement3D(reader, ref, scale) {
14499
14647
  refDir[2] - dotXZ * zN[2]
14500
14648
  ];
14501
14649
  const xRawLenSq = xRaw[0] * xRaw[0] + xRaw[1] * xRaw[1] + xRaw[2] * xRaw[2];
14502
- const orthoZ = Math.abs(zN[0]) < .9 ? cross(zN, [
14650
+ const orthoZ = Math.abs(zN[0]) < .9 ? cross$1(zN, [
14503
14651
  1,
14504
14652
  0,
14505
14653
  0
14506
- ]) : cross(zN, [
14654
+ ]) : cross$1(zN, [
14507
14655
  0,
14508
14656
  1,
14509
14657
  0
14510
14658
  ]);
14511
14659
  const xN = xRawLenSq < 1e-12 ? normalize(orthoZ) : normalize(xRaw);
14512
- const yN = cross(zN, xN);
14660
+ const yN = cross$1(zN, xN);
14513
14661
  return {
14514
14662
  linear: [
14515
14663
  xN[0],
@@ -14593,6 +14741,24 @@ function readPolylinePoints(reader, ref, scale) {
14593
14741
  }
14594
14742
  return out.length >= 3 ? out : void 0;
14595
14743
  }
14744
+ function readPoint2D(reader, ref, scale) {
14745
+ const pointRef = asRef(ref);
14746
+ if (pointRef === void 0) return void 0;
14747
+ const pt = reader.getLine(pointRef.value);
14748
+ if (pt === null) return void 0;
14749
+ const coords = asMeasureArray(pt["Coordinates"]);
14750
+ if (coords.length < 2) return void 0;
14751
+ return [(coords[0] ?? 0) * scale * 1e3, (coords[1] ?? 0) * scale * 1e3];
14752
+ }
14753
+ function readDirection2D(reader, ref) {
14754
+ const dirRef = asRef(ref);
14755
+ if (dirRef === void 0) return void 0;
14756
+ const dir = reader.getLine(dirRef.value);
14757
+ if (dir === null) return void 0;
14758
+ const ratios = asMeasureArray(dir["DirectionRatios"]);
14759
+ if (ratios.length < 2) return void 0;
14760
+ return [ratios[0] ?? 0, ratios[1] ?? 0];
14761
+ }
14596
14762
  function readPoint(reader, ref, scale) {
14597
14763
  const pointRef = asRef(ref);
14598
14764
  if (pointRef === void 0) return void 0;
@@ -14619,6 +14785,9 @@ function readDirection(reader, ref) {
14619
14785
  ratios[2] ?? 0
14620
14786
  ];
14621
14787
  }
14788
+ function releaseEmbind(value) {
14789
+ value.delete?.();
14790
+ }
14622
14791
  function asRef(value) {
14623
14792
  if (value !== null && typeof value === "object" && "value" in value) {
14624
14793
  const v = value.value;
@@ -14669,7 +14838,7 @@ function normalize(v) {
14669
14838
  v[2] / len
14670
14839
  ];
14671
14840
  }
14672
- function cross(a, b) {
14841
+ function cross$1(a, b) {
14673
14842
  return [
14674
14843
  a[1] * b[2] - a[2] * b[1],
14675
14844
  a[2] * b[0] - a[0] * b[2],
@@ -27931,6 +28100,217 @@ function assignNum(target, key, value) {
27931
28100
  if (value !== void 0) target[key] = Number(value);
27932
28101
  }
27933
28102
  //#endregion
28103
+ //#region src/placementFrame.ts
28104
+ var IDENTITY_FRAME = [
28105
+ 1,
28106
+ 0,
28107
+ 0,
28108
+ 0,
28109
+ 0,
28110
+ 1,
28111
+ 0,
28112
+ 0,
28113
+ 0,
28114
+ 0,
28115
+ 1,
28116
+ 0,
28117
+ 0,
28118
+ 0,
28119
+ 0,
28120
+ 1
28121
+ ];
28122
+ var DEFAULT_AXIS = [
28123
+ 0,
28124
+ 0,
28125
+ 1
28126
+ ];
28127
+ var ORIGIN = [
28128
+ 0,
28129
+ 0,
28130
+ 0
28131
+ ];
28132
+ /** Column-major 4x4 multiply: `a . b` (apply b first, then a). */
28133
+ function frameMul(a, b) {
28134
+ const out = new Array(16).fill(0);
28135
+ for (let col = 0; col < 4; col++) for (let row = 0; row < 4; row++) {
28136
+ let sum = 0;
28137
+ for (let k = 0; k < 4; k++) sum += (a[k * 4 + row] ?? 0) * (b[col * 4 + k] ?? 0);
28138
+ out[col * 4 + row] = sum;
28139
+ }
28140
+ return out;
28141
+ }
28142
+ function translationFrame(v) {
28143
+ return [
28144
+ 1,
28145
+ 0,
28146
+ 0,
28147
+ 0,
28148
+ 0,
28149
+ 1,
28150
+ 0,
28151
+ 0,
28152
+ 0,
28153
+ 0,
28154
+ 1,
28155
+ 0,
28156
+ v[0],
28157
+ v[1],
28158
+ v[2],
28159
+ 1
28160
+ ];
28161
+ }
28162
+ function normalizeAxis(axis) {
28163
+ const len = Math.sqrt(axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]);
28164
+ if (len < 1e-12) return DEFAULT_AXIS;
28165
+ return [
28166
+ axis[0] / len,
28167
+ axis[1] / len,
28168
+ axis[2] / len
28169
+ ];
28170
+ }
28171
+ function cross(a, b) {
28172
+ return [
28173
+ a[1] * b[2] - a[2] * b[1],
28174
+ a[2] * b[0] - a[0] * b[2],
28175
+ a[0] * b[1] - a[1] * b[0]
28176
+ ];
28177
+ }
28178
+ /**
28179
+ * Builds a frame from an authored `(origin, axisX, axisZ)` placement, using the
28180
+ * same IFC orthonormalization as `placementToMatrix`/`readAxis2Placement3D`:
28181
+ * z = normalize(axisZ); x = normalize(axisX projected perpendicular to z);
28182
+ * y = z x. Lets a civil node authored with explicit axis props enter the same
28183
+ * frame pipeline as a `tRotate` chain.
28184
+ */
28185
+ function frameFromPlacement(f) {
28186
+ const z = normalizeAxis(f.axisZ);
28187
+ const dot = z[0] * f.axisX[0] + z[1] * f.axisX[1] + z[2] * f.axisX[2];
28188
+ const x = normalizeAxis([
28189
+ f.axisX[0] - dot * z[0],
28190
+ f.axisX[1] - dot * z[1],
28191
+ f.axisX[2] - dot * z[2]
28192
+ ]);
28193
+ const y = cross(z, x);
28194
+ return [
28195
+ x[0],
28196
+ x[1],
28197
+ x[2],
28198
+ 0,
28199
+ y[0],
28200
+ y[1],
28201
+ y[2],
28202
+ 0,
28203
+ z[0],
28204
+ z[1],
28205
+ z[2],
28206
+ 0,
28207
+ f.origin[0],
28208
+ f.origin[1],
28209
+ f.origin[2],
28210
+ 1
28211
+ ];
28212
+ }
28213
+ /**
28214
+ * Rotation by `angleDeg` about `axis` (default +Z) through pivot `at` (default
28215
+ * origin), matching `csg.rotate` (Rodrigues; right-handed about the axis). With
28216
+ * a pivot the motion is `T(at) . R . T(-at)`, i.e. `p -> at + R(p - at)`.
28217
+ */
28218
+ function rotationFrame(angleDeg, axis = DEFAULT_AXIS, at = ORIGIN) {
28219
+ const [x, y, z] = normalizeAxis(axis);
28220
+ const t = angleDeg * Math.PI / 180;
28221
+ const c = Math.cos(t);
28222
+ const s = Math.sin(t);
28223
+ const C = 1 - c;
28224
+ const r00 = c + x * x * C;
28225
+ const r01 = x * y * C - z * s;
28226
+ const r02 = x * z * C + y * s;
28227
+ const r10 = y * x * C + z * s;
28228
+ const r11 = c + y * y * C;
28229
+ const r12 = y * z * C - x * s;
28230
+ const r20 = z * x * C - y * s;
28231
+ const r21 = z * y * C + x * s;
28232
+ const r22 = c + z * z * C;
28233
+ const rat0 = r00 * at[0] + r01 * at[1] + r02 * at[2];
28234
+ const rat1 = r10 * at[0] + r11 * at[1] + r12 * at[2];
28235
+ const rat2 = r20 * at[0] + r21 * at[1] + r22 * at[2];
28236
+ return [
28237
+ r00,
28238
+ r10,
28239
+ r20,
28240
+ 0,
28241
+ r01,
28242
+ r11,
28243
+ r21,
28244
+ 0,
28245
+ r02,
28246
+ r12,
28247
+ r22,
28248
+ 0,
28249
+ at[0] - rat0,
28250
+ at[1] - rat1,
28251
+ at[2] - rat2,
28252
+ 1
28253
+ ];
28254
+ }
28255
+ function opFrame(op) {
28256
+ if (op.op === "translate") return translationFrame(op.v);
28257
+ return rotationFrame(op.angleDeg, op.axis ?? DEFAULT_AXIS, op.at ?? ORIGIN);
28258
+ }
28259
+ /**
28260
+ * Composes an authored `TransformOp` chain into one frame, matching families'
28261
+ * `applyOps`: `ops[0]` is innermost (applied first). For `[A, B]` the composed
28262
+ * motion is `B . A`, so a point transforms as `B(A(p))`.
28263
+ */
28264
+ function frameFromOps(ops) {
28265
+ let m = IDENTITY_FRAME;
28266
+ for (const op of ops) m = frameMul(opFrame(op), m);
28267
+ return m;
28268
+ }
28269
+ /** Rigid inverse of `[R | t]`: `[R^T | -R^T t]`. */
28270
+ function frameInverse(f) {
28271
+ const t0 = f[12] ?? 0;
28272
+ const t1 = f[13] ?? 0;
28273
+ const t2 = f[14] ?? 0;
28274
+ const inv0 = -(f[0] * t0 + f[1] * t1 + f[2] * t2);
28275
+ const inv1 = -(f[4] * t0 + f[5] * t1 + f[6] * t2);
28276
+ const inv2 = -(f[8] * t0 + f[9] * t1 + f[10] * t2);
28277
+ return [
28278
+ f[0],
28279
+ f[4],
28280
+ f[8],
28281
+ 0,
28282
+ f[1],
28283
+ f[5],
28284
+ f[9],
28285
+ 0,
28286
+ f[2],
28287
+ f[6],
28288
+ f[10],
28289
+ 0,
28290
+ inv0,
28291
+ inv1,
28292
+ inv2,
28293
+ 1
28294
+ ];
28295
+ }
28296
+ /** Decomposes a frame into origin (mm) + IFC axes (axisX, axisZ). */
28297
+ function decomposeFrame(f) {
28298
+ return decomposePlacement(f);
28299
+ }
28300
+ /** Translation column of a frame. */
28301
+ function frameOrigin(f) {
28302
+ return [
28303
+ f[12] ?? 0,
28304
+ f[13] ?? 0,
28305
+ f[14] ?? 0
28306
+ ];
28307
+ }
28308
+ /** True when the rotation part is the identity (within tolerance): a pure
28309
+ * translation, so the existing translation-only placement path suffices. */
28310
+ function isPureTranslation(f, eps = 1e-9) {
28311
+ 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;
28312
+ }
28313
+ //#endregion
27934
28314
  //#region src/familiesAdapter.ts
27935
28315
  /**
27936
28316
  * brepjs-families -> BimModel adapter. Consumes a resolved element tree and
@@ -28152,6 +28532,36 @@ function centreOrigin(base, shift) {
28152
28532
  origin[2] + shift[2]
28153
28533
  ];
28154
28534
  }
28535
+ function synthesizedProfile(el) {
28536
+ if (el.semantics?.kind !== "product" || el.props["profile"] !== void 0) return void 0;
28537
+ const length = semanticDimension(el, "length");
28538
+ const width = semanticDimension(el, "width");
28539
+ const height = semanticDimension(el, "height");
28540
+ if (el.semantics.category === "beam" && width !== void 0 && height !== void 0) return {
28541
+ profile: {
28542
+ kind: "RECTANGULAR",
28543
+ width,
28544
+ height
28545
+ },
28546
+ shift: [
28547
+ 0,
28548
+ width / 2,
28549
+ height / 2
28550
+ ]
28551
+ };
28552
+ if (el.semantics.category === "column" && length !== void 0 && width !== void 0) return {
28553
+ profile: {
28554
+ kind: "RECTANGULAR",
28555
+ width: length,
28556
+ height: width
28557
+ },
28558
+ shift: [
28559
+ length / 2,
28560
+ width / 2,
28561
+ 0
28562
+ ]
28563
+ };
28564
+ }
28155
28565
  /**
28156
28566
  * Adapts target-independent civil envelope dimensions onto existing typed BIM
28157
28567
  * spec inputs. Reference infrastructure Families intentionally author domain
@@ -28166,18 +28576,7 @@ function civilProductSpecInput(el) {
28166
28576
  const height = semanticDimension(el, "height");
28167
28577
  switch (el.semantics.category) {
28168
28578
  case "beam": {
28169
- const synthesized = base["profile"] === void 0 && width !== void 0 && height !== void 0 ? {
28170
- profile: {
28171
- kind: "RECTANGULAR",
28172
- width,
28173
- height
28174
- },
28175
- shift: [
28176
- 0,
28177
- width / 2,
28178
- height / 2
28179
- ]
28180
- } : void 0;
28579
+ const synthesized = synthesizedProfile(el);
28181
28580
  return {
28182
28581
  ...base,
28183
28582
  ...synthesized !== void 0 ? {
@@ -28189,18 +28588,7 @@ function civilProductSpecInput(el) {
28189
28588
  };
28190
28589
  }
28191
28590
  case "column": {
28192
- const synthesized = base["profile"] === void 0 && length !== void 0 && width !== void 0 ? {
28193
- profile: {
28194
- kind: "RECTANGULAR",
28195
- width: length,
28196
- height: width
28197
- },
28198
- shift: [
28199
- length / 2,
28200
- width / 2,
28201
- 0
28202
- ]
28203
- } : void 0;
28591
+ const synthesized = synthesizedProfile(el);
28204
28592
  return {
28205
28593
  ...base,
28206
28594
  ...synthesized !== void 0 ? {
@@ -28267,12 +28655,12 @@ function peelTranslates(node) {
28267
28655
  moved
28268
28656
  };
28269
28657
  }
28270
- /** True when the element's rendered local transform carries a rotation. The spec path
28271
- * folds only translations into IfcLocalPlacement (walls orient via `axisX`),
28272
- * so a tRotate placement would export un-rotated while the viewport shows it
28273
- * rotated reject instead of diverging. Rotations a family render bakes
28274
- * into its own body geometry (e.g. a circular beam oriented along axisX) are
28275
- * fine: the spec rebuilds that body parametrically from props. */
28658
+ /** True when the element's rendered local transform carries a rotation, which
28659
+ * switches the walk to the rigid-frame placement path (the composed rotation
28660
+ * folds into origin + axisX + axisZ). Only authored `transform` rotations
28661
+ * count: a rotation a family render bakes into its own body geometry (e.g. a
28662
+ * circular beam oriented along axisX) stays in the body, which the spec
28663
+ * rebuilds parametrically from props. */
28276
28664
  function hasRotateOp(el) {
28277
28665
  return el.localTransforms.some((op) => op.op === "rotate");
28278
28666
  }
@@ -28387,7 +28775,7 @@ function materializeOwnedSolid(el, evaluator, errors) {
28387
28775
  /** Materialize an unrouted element's IR and add it as a proxy. The body is
28388
28776
  * authoritative for a proxy (no parametric spec to diverge from), so baked
28389
28777
  * transforms — rotations included — are fine here. */
28390
- function addProxyElement(model, el, evaluator, projectedSpatialTranslation) {
28778
+ function addProxyElement(model, el, evaluator, spatialFrame) {
28391
28779
  const body = materializeOwnedSolid(el, evaluator, {
28392
28780
  evalCode: "FAMILIES_PROXY_EVAL_FAILED",
28393
28781
  notSolidCode: "FAMILIES_PROXY_NOT_SOLID",
@@ -28395,7 +28783,7 @@ function addProxyElement(model, el, evaluator, projectedSpatialTranslation) {
28395
28783
  routeName: "proxy"
28396
28784
  });
28397
28785
  if (!body.ok) return body;
28398
- const localized = localizeOwnedSolid(el, body.value, projectedSpatialTranslation, "FAMILIES_PROXY_LOCALIZE_FAILED", "spatial parent");
28786
+ const localized = localizeBodyToFrame(el, body.value, spatialFrame, "FAMILIES_PROXY_LOCALIZE_FAILED", "spatial parent");
28399
28787
  if (!localized.ok) return localized;
28400
28788
  const nameAttr = el.attributes["name"];
28401
28789
  const materialProp = el.props["materialName"];
@@ -28558,8 +28946,9 @@ var DEFAULT_AXIS_Z = [
28558
28946
  function axisEquals(value, axis) {
28559
28947
  return Array.isArray(value) && value.length === 3 && value.every((component, i) => typeof component === "number" && component === axis[i]);
28560
28948
  }
28561
- /** Civil descendant relativization is translation-only, so a civil frame with
28562
- * non-default axes would rotate the exported hierarchy away from the IR. */
28949
+ /** True when a civil node authors non-default `axisX`/`axisZ` props: like a
28950
+ * tRotate, this puts the node (and its subtree) on the rigid-frame placement
28951
+ * path so descendants relativize against the rotated parent frame. */
28563
28952
  function hasRotatedAxes(el) {
28564
28953
  const axisX = el.props["axisX"];
28565
28954
  const axisZ = el.props["axisZ"];
@@ -28598,6 +28987,131 @@ function authoredTranslation(el) {
28598
28987
  }
28599
28988
  return total;
28600
28989
  }
28990
+ function authoredAxisX(el) {
28991
+ const v = el.props["axisX"];
28992
+ return Array.isArray(v) && v.length === 3 ? v : DEFAULT_AXIS_X;
28993
+ }
28994
+ function authoredAxisZ(el) {
28995
+ const v = el.props["axisZ"];
28996
+ return Array.isArray(v) && v.length === 3 ? v : DEFAULT_AXIS_Z;
28997
+ }
28998
+ /** The placement shift that centres a synthesized beam/column cross-section on
28999
+ * its axis, in the element's own (body) frame. Zero for every other route. */
29000
+ function specLocalShift(el) {
29001
+ return synthesizedProfile(el)?.shift ?? [
29002
+ 0,
29003
+ 0,
29004
+ 0
29005
+ ];
29006
+ }
29007
+ /** Skip the resolver occurrence wrappers already owned by `cumulativeFrame`,
29008
+ * then recover only the Body's outer literal Datum translations. An unexpected
29009
+ * wrapper shape returns zero rather than risking an occurrence transform twice. */
29010
+ function bodyLocalTranslation(el, occurrenceTransformDepth) {
29011
+ let body = el.geometry;
29012
+ for (let i = 0; i < occurrenceTransformDepth; i++) {
29013
+ if (body.kind !== "Translate" && body.kind !== "Rotate") return ZERO_TRANSLATION;
29014
+ body = body.target;
29015
+ }
29016
+ return peelTranslates(body).total;
29017
+ }
29018
+ /** World frame of an element's own body: the walk's cumulative frame (all
29019
+ * authored transforms, ancestors + own) composed with the body's authored axes
29020
+ * (`axisX`/`axisZ` props) and its local origin (`origin` prop + literal Body
29021
+ * Datum + centring shift). Under no rotation this reduces to `composedOrigin`
29022
+ * + shift. */
29023
+ function elementBodyFrame(el, cumulativeFrame, occurrenceTransformDepth) {
29024
+ return frameMul(frameMul(cumulativeFrame, frameFromPlacement({
29025
+ origin: addTranslation(authoredSpecOrigin(el), bodyLocalTranslation(el, occurrenceTransformDepth)),
29026
+ axisX: authoredAxisX(el),
29027
+ axisZ: authoredAxisZ(el)
29028
+ })), translationFrame(specLocalShift(el)));
29029
+ }
29030
+ /** World frame of a civil spatial node: cumulative transforms composed with any
29031
+ * authored `origin`/`axisX`/`axisZ` props (identity when default). */
29032
+ function civilNodeFrame(el, cumulativeFrame) {
29033
+ return frameMul(cumulativeFrame, frameFromPlacement({
29034
+ origin: authoredSpecOrigin(el),
29035
+ axisX: authoredAxisX(el),
29036
+ axisZ: authoredAxisZ(el)
29037
+ }));
29038
+ }
29039
+ /** Places a routed element's spec input through the composed placement frame:
29040
+ * the flat spec input keeps its dimensions/profile/psets, but `origin`/`axisX`/
29041
+ * `axisZ` are recomputed from the element's world frame relative to its spatial
29042
+ * container. Stair/ramp flights, which carry their own per-flight frame, are
29043
+ * each re-placed the same way. */
29044
+ function rotatedRoutedInput(flatInput, el, cumulativeFrame, occurrenceTransformDepth, spatialFrame) {
29045
+ const toSpatial = frameInverse(spatialFrame);
29046
+ if (Array.isArray(flatInput["flights"])) {
29047
+ const elementFrame = elementBodyFrame(el, cumulativeFrame, occurrenceTransformDepth);
29048
+ const flights = flatInput["flights"];
29049
+ return {
29050
+ ...flatInput,
29051
+ origin: [
29052
+ 0,
29053
+ 0,
29054
+ 0
29055
+ ],
29056
+ axisX: [
29057
+ 1,
29058
+ 0,
29059
+ 0
29060
+ ],
29061
+ axisZ: [
29062
+ 0,
29063
+ 0,
29064
+ 1
29065
+ ],
29066
+ flights: flights.map((flight) => {
29067
+ if (typeof flight !== "object" || flight === null) return flight;
29068
+ const f = flight;
29069
+ const world = frameMul(elementFrame, frameFromPlacement({
29070
+ origin: f["origin"] ?? [
29071
+ 0,
29072
+ 0,
29073
+ 0
29074
+ ],
29075
+ axisX: f["axisX"] ?? DEFAULT_AXIS_X,
29076
+ axisZ: f["axisZ"] ?? DEFAULT_AXIS_Z
29077
+ }));
29078
+ const placed = decomposeFrame(frameMul(toSpatial, world));
29079
+ return {
29080
+ ...f,
29081
+ origin: placed.origin,
29082
+ axisX: placed.axisX,
29083
+ axisZ: placed.axisZ
29084
+ };
29085
+ })
29086
+ };
29087
+ }
29088
+ const placed = decomposeFrame(frameMul(toSpatial, elementBodyFrame(el, cumulativeFrame, occurrenceTransformDepth)));
29089
+ return {
29090
+ ...flatInput,
29091
+ origin: placed.origin,
29092
+ axisX: placed.axisX,
29093
+ axisZ: placed.axisZ
29094
+ };
29095
+ }
29096
+ /** Moves an owned world-baked body into its spatial container's local frame.
29097
+ * A pure translation keeps the fast `translate` path (identical to the prior
29098
+ * behaviour); a rotated container applies the inverse frame via `applyMatrix`. */
29099
+ function localizeBodyToFrame(el, body, spatialFrame, errorCode, frameName) {
29100
+ if (isPureTranslation(spatialFrame)) return localizeOwnedSolid(el, body, frameOrigin(spatialFrame), errorCode, frameName);
29101
+ const inverse = decomposeFrame(frameInverse(spatialFrame));
29102
+ try {
29103
+ const localized = (0, brepjs.applyMatrix)(body, placementToMatrix(inverse));
29104
+ if (!localized.ok) {
29105
+ body[Symbol.dispose]();
29106
+ return (0, brepjs.err)(specError(errorCode, `familiesToBim: '${el.keyPath}' could not move its body into the ${frameName} frame`, localized.error));
29107
+ }
29108
+ body[Symbol.dispose]();
29109
+ return (0, brepjs.ok)(localized.value);
29110
+ } catch (cause) {
29111
+ body[Symbol.dispose]();
29112
+ return (0, brepjs.err)(specError(errorCode, `familiesToBim: '${el.keyPath}' could not move its body into the ${frameName} frame`, cause));
29113
+ }
29114
+ }
28601
29115
  function civilSpatialInput(el, localTranslation) {
28602
29116
  const semantics = el.semantics;
28603
29117
  const composition = semantics !== void 0 && "composition" in semantics ? CIVIL_COMPOSITION[semantics.composition] : void 0;
@@ -28610,22 +29124,36 @@ function civilSpatialInput(el, localTranslation) {
28610
29124
  ...composition !== void 0 ? { compositionType: composition } : {}
28611
29125
  };
28612
29126
  }
29127
+ /** Relativizes a civil node's world frame against its parent civil frame into
29128
+ * the origin/axisX/axisZ the spatial specs consume. */
29129
+ function civilSpatialFrameInput(el, nodeFrame, parentFrame) {
29130
+ const semantics = el.semantics;
29131
+ const composition = semantics !== void 0 && "composition" in semantics ? CIVIL_COMPOSITION[semantics.composition] : void 0;
29132
+ const local = decomposeFrame(frameMul(frameInverse(parentFrame), nodeFrame));
29133
+ return {
29134
+ name: semanticName(el),
29135
+ origin: local.origin,
29136
+ axisX: local.axisX,
29137
+ axisZ: local.axisZ,
29138
+ ...composition !== void 0 ? { compositionType: composition } : {}
29139
+ };
29140
+ }
28613
29141
  function civilParentAccepts(kind, parent) {
28614
29142
  if (kind === "site") return parent === "project";
28615
29143
  if (kind === "bridge") return parent === "site";
28616
29144
  return parent === "bridge" || parent === "bridge-part";
28617
29145
  }
28618
- function addCivilSpatialOccurrence(model, el, kind, localTranslation) {
29146
+ function addCivilSpatialOccurrence(model, el, kind, input) {
28619
29147
  if (kind === "site") {
28620
29148
  if (el.semantics?.role !== "transport-site") return unsupportedCivilRole(el, "Site");
28621
- const parsed = parseSiteSpec(civilSpatialInput(el, localTranslation));
29149
+ const parsed = parseSiteSpec(input);
28622
29150
  return parsed.ok ? model.addSite(parsed.value, { stableKey: el.keyPath }) : parsed;
28623
29151
  }
28624
29152
  if (kind === "bridge") {
28625
29153
  const predefinedType = lookup(BRIDGE_ROLE, el.semantics?.role ?? "");
28626
29154
  if (predefinedType === void 0) return unsupportedCivilRole(el, "Bridge");
28627
29155
  const parsed = parseBridgeSpec({
28628
- ...civilSpatialInput(el, localTranslation),
29156
+ ...input,
28629
29157
  predefinedType
28630
29158
  });
28631
29159
  return parsed.ok ? model.addBridge(parsed.value, { stableKey: el.keyPath }) : parsed;
@@ -28634,7 +29162,7 @@ function addCivilSpatialOccurrence(model, el, kind, localTranslation) {
28634
29162
  const predefinedType = lookup(BRIDGE_PART_ROLE, el.semantics?.role ?? "");
28635
29163
  if (predefinedType === void 0) return unsupportedCivilRole(el, "Bridge Part");
28636
29164
  const parsed = parseBridgePartSpec({
28637
- ...civilSpatialInput(el, localTranslation),
29165
+ ...input,
28638
29166
  predefinedType,
28639
29167
  usageType: subdivision !== void 0 ? CIVIL_USAGE[subdivision] : "NOTDEFINED"
28640
29168
  });
@@ -28661,7 +29189,7 @@ function relativeSpecInput(input, projectedSpatialTranslation) {
28661
29189
  origin: relativeOrigin(input["origin"])
28662
29190
  };
28663
29191
  }
28664
- function addEarthworksFillElement(model, el, evaluator, projectedSpatialTranslation) {
29192
+ function addEarthworksFillElement(model, el, evaluator, spatialFrame) {
28665
29193
  if (el.semantics?.kind !== "product") return (0, brepjs.err)(specError("FAMILIES_UNSUPPORTED_CIVIL_SEMANTICS", `familiesToBim: '${el.keyPath}' is not authored as a civil Product`));
28666
29194
  const predefinedType = lookup(EARTHWORKS_FILL_ROLE, el.semantics.role);
28667
29195
  if (predefinedType === void 0) return unsupportedCivilRole(el, "Earthworks Fill");
@@ -28672,7 +29200,7 @@ function addEarthworksFillElement(model, el, evaluator, projectedSpatialTranslat
28672
29200
  routeName: "Earthworks Fill"
28673
29201
  });
28674
29202
  if (!body.ok) return body;
28675
- const localized = localizeOwnedSolid(el, body.value, projectedSpatialTranslation, "FAMILIES_EARTHWORKS_LOCALIZE_FAILED", "Bridge Part");
29203
+ const localized = localizeBodyToFrame(el, body.value, spatialFrame, "FAMILIES_EARTHWORKS_LOCALIZE_FAILED", "Bridge Part");
28676
29204
  if (!localized.ok) return localized;
28677
29205
  const specProps = collectSpecProps(el);
28678
29206
  const authoredMaterial = el.props["materialName"];
@@ -28722,10 +29250,15 @@ function familiesToBim(root, options) {
28722
29250
  const walk = (el, state) => {
28723
29251
  const rotatedHere = state.rotated || hasRotateOp(el);
28724
29252
  const cumulativeTranslationHere = addTranslation(state.cumulativeTranslation, authoredTranslation(el));
29253
+ const occurrenceTransformDepthHere = state.occurrenceTransformDepth + el.localTransforms.length;
29254
+ const cumulativeFrameHere = frameMul(state.cumulativeFrame, frameFromOps(el.localTransforms));
28725
29255
  let proxiedHere = false;
29256
+ let nextRotated = rotatedHere;
28726
29257
  let nextSpatialStructureId = state.spatialStructureId;
28727
29258
  let nextCivilParent = state.civilParent;
28728
29259
  let nextProjectedSpatialTranslation = state.projectedSpatialTranslation;
29260
+ let nextCumulativeFrame = cumulativeFrameHere;
29261
+ let nextSpatialFrame = state.spatialFrame;
28729
29262
  const archetype = archetypeFor(el);
28730
29263
  const effectiveArchetype = el.semantics?.kind === "product" ? civilProductArchetype(el) : archetype;
28731
29264
  const route = specRoute(effectiveArchetype);
@@ -28734,15 +29267,19 @@ function familiesToBim(root, options) {
28734
29267
  if (!usesAuthoredCivilHierarchy || !civilParentAccepts(civilKind, state.civilParent) || state.spatialStructureId === null) return (0, brepjs.err)(specError("FAMILIES_INVALID_CIVIL_HIERARCHY", `familiesToBim: civil '${civilKind}' at '${el.keyPath}' cannot occur under '${state.civilParent}'`));
28735
29268
  const keyed = requireKeyed(el);
28736
29269
  if (!keyed.ok) return keyed;
28737
- if (rotatedHere || hasRotatedAxes(el)) return (0, brepjs.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`));
28738
29270
  if (el.geometry.kind !== "Empty") return (0, brepjs.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)`));
28739
- const localTranslation = subtractTranslation(cumulativeTranslationHere, state.projectedSpatialTranslation);
28740
- const added = addCivilSpatialOccurrence(model, el, civilKind, localTranslation);
29271
+ const nodeFrame = civilNodeFrame(el, cumulativeFrameHere);
29272
+ const rotatedFrame = rotatedHere || hasRotatedAxes(el);
29273
+ const input = rotatedFrame ? civilSpatialFrameInput(el, nodeFrame, state.spatialFrame) : civilSpatialInput(el, subtractTranslation(cumulativeTranslationHere, state.projectedSpatialTranslation));
29274
+ const added = addCivilSpatialOccurrence(model, el, civilKind, input);
28741
29275
  if (!added.ok) return added;
28742
29276
  model.aggregate(state.spatialStructureId, added.value);
28743
29277
  idByKeyPath.set(el.keyPath, added.value);
28744
29278
  nextSpatialStructureId = added.value;
28745
29279
  nextCivilParent = civilKind;
29280
+ nextRotated = rotatedFrame;
29281
+ nextCumulativeFrame = nodeFrame;
29282
+ nextSpatialFrame = nodeFrame;
28746
29283
  nextProjectedSpatialTranslation = addTranslation(cumulativeTranslationHere, authoredSpecOrigin(el));
28747
29284
  } else if (isCivilSpatialIntent(el)) return (0, brepjs.err)(specError("FAMILIES_UNSUPPORTED_CIVIL_SEMANTICS", `familiesToBim: unsupported civil '${el.semantics?.kind ?? "unknown"}' category '${el.semantics?.category ?? ""}' at '${el.keyPath}'`));
28748
29285
  else if (archetype === "storey") {
@@ -28763,21 +29300,22 @@ function familiesToBim(root, options) {
28763
29300
  if (bodyEvaluator === void 0) return (0, brepjs.err)(specError("FAMILIES_EARTHWORKS_EVALUATOR_REQUIRED", `familiesToBim: Earthworks Fill '${el.keyPath}' needs bodyEvaluator to materialize its exact Product Body`));
28764
29301
  const keyed = requireKeyed(el);
28765
29302
  if (!keyed.ok) return keyed;
28766
- const added = addEarthworksFillElement(model, el, bodyEvaluator, state.projectedSpatialTranslation);
29303
+ const added = addEarthworksFillElement(model, el, bodyEvaluator, state.spatialFrame);
28767
29304
  if (!added.ok) return added;
28768
29305
  model.placeIn(added.value, nextSpatialStructureId);
28769
29306
  idByKeyPath.set(el.keyPath, added.value);
28770
29307
  } else if (route !== void 0) {
28771
29308
  const keyed = requireKeyed(el);
28772
29309
  if (!keyed.ok) return keyed;
28773
- if (rotatedHere) return (0, brepjs.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`));
28774
29310
  const voids = el.props["voids"];
28775
29311
  if (Array.isArray(voids)) {
28776
29312
  const openings = el.children.filter((c) => c.type === "Opening").length;
28777
29313
  if (voids.length > openings) return (0, brepjs.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`));
28778
29314
  }
28779
29315
  const routedInput = el.semantics?.kind === "product" ? civilProductSpecInput(el) : ("input" in route ? route.input : specInput)(el);
28780
- const parsed = route.parse(usesAuthoredCivilHierarchy ? relativeSpecInput(routedInput, state.projectedSpatialTranslation) : routedInput);
29316
+ const rotatedBase = Array.isArray(routedInput["flights"]) ? specInput(el) : routedInput;
29317
+ const placedInput = rotatedHere ? rotatedRoutedInput(rotatedBase, el, cumulativeFrameHere, occurrenceTransformDepthHere, state.spatialFrame) : usesAuthoredCivilHierarchy ? relativeSpecInput(routedInput, state.projectedSpatialTranslation) : routedInput;
29318
+ const parsed = route.parse(placedInput);
28781
29319
  if (!parsed.ok) return parsed;
28782
29320
  const added = route.add(model, parsed.value, el.keyPath);
28783
29321
  if (!added.ok) return added;
@@ -28794,7 +29332,7 @@ function familiesToBim(root, options) {
28794
29332
  const keyed = requireKeyed(el);
28795
29333
  if (!keyed.ok) return keyed;
28796
29334
  if (nextSpatialStructureId === null || usesAuthoredCivilHierarchy && state.civilParent !== "bridge-part") return (0, brepjs.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`));
28797
- const added = addProxyElement(model, el, options.proxyEvaluator, state.projectedSpatialTranslation);
29335
+ const added = addProxyElement(model, el, options.proxyEvaluator, state.spatialFrame);
28798
29336
  if (!added.ok) return added;
28799
29337
  model.placeIn(added.value, nextSpatialStructureId);
28800
29338
  idByKeyPath.set(el.keyPath, added.value);
@@ -28810,9 +29348,12 @@ function familiesToBim(root, options) {
28810
29348
  const r = walk(child, {
28811
29349
  spatialStructureId: nextSpatialStructureId,
28812
29350
  civilParent: nextCivilParent,
28813
- rotated: rotatedHere,
29351
+ rotated: nextRotated,
28814
29352
  cumulativeTranslation: cumulativeTranslationHere,
28815
- projectedSpatialTranslation: nextProjectedSpatialTranslation
29353
+ projectedSpatialTranslation: nextProjectedSpatialTranslation,
29354
+ occurrenceTransformDepth: occurrenceTransformDepthHere,
29355
+ cumulativeFrame: nextCumulativeFrame,
29356
+ spatialFrame: nextSpatialFrame
28816
29357
  });
28817
29358
  if (!r.ok) return r;
28818
29359
  }
@@ -28823,7 +29364,10 @@ function familiesToBim(root, options) {
28823
29364
  civilParent: "project",
28824
29365
  rotated: false,
28825
29366
  cumulativeTranslation: ZERO_TRANSLATION,
28826
- projectedSpatialTranslation: ZERO_TRANSLATION
29367
+ projectedSpatialTranslation: ZERO_TRANSLATION,
29368
+ occurrenceTransformDepth: 0,
29369
+ cumulativeFrame: IDENTITY_FRAME,
29370
+ spatialFrame: IDENTITY_FRAME
28827
29371
  });
28828
29372
  if (!walked.ok) {
28829
29373
  model[Symbol.dispose]();