brepjs-bim 0.22.0 → 0.23.0

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.
@@ -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
@@ -14476,17 +14496,17 @@ function readAxis2Placement3D(reader, ref, scale) {
14476
14496
  refDir[2] - dotXZ * zN[2]
14477
14497
  ];
14478
14498
  const xRawLenSq = xRaw[0] * xRaw[0] + xRaw[1] * xRaw[1] + xRaw[2] * xRaw[2];
14479
- const orthoZ = Math.abs(zN[0]) < .9 ? cross(zN, [
14499
+ const orthoZ = Math.abs(zN[0]) < .9 ? cross$1(zN, [
14480
14500
  1,
14481
14501
  0,
14482
14502
  0
14483
- ]) : cross(zN, [
14503
+ ]) : cross$1(zN, [
14484
14504
  0,
14485
14505
  1,
14486
14506
  0
14487
14507
  ]);
14488
14508
  const xN = xRawLenSq < 1e-12 ? normalize(orthoZ) : normalize(xRaw);
14489
- const yN = cross(zN, xN);
14509
+ const yN = cross$1(zN, xN);
14490
14510
  return {
14491
14511
  linear: [
14492
14512
  xN[0],
@@ -14646,7 +14666,7 @@ function normalize(v) {
14646
14666
  v[2] / len
14647
14667
  ];
14648
14668
  }
14649
- function cross(a, b) {
14669
+ function cross$1(a, b) {
14650
14670
  return [
14651
14671
  a[1] * b[2] - a[2] * b[1],
14652
14672
  a[2] * b[0] - a[0] * b[2],
@@ -27908,6 +27928,217 @@ function assignNum(target, key, value) {
27908
27928
  if (value !== void 0) target[key] = Number(value);
27909
27929
  }
27910
27930
  //#endregion
27931
+ //#region src/placementFrame.ts
27932
+ var IDENTITY_FRAME = [
27933
+ 1,
27934
+ 0,
27935
+ 0,
27936
+ 0,
27937
+ 0,
27938
+ 1,
27939
+ 0,
27940
+ 0,
27941
+ 0,
27942
+ 0,
27943
+ 1,
27944
+ 0,
27945
+ 0,
27946
+ 0,
27947
+ 0,
27948
+ 1
27949
+ ];
27950
+ var DEFAULT_AXIS = [
27951
+ 0,
27952
+ 0,
27953
+ 1
27954
+ ];
27955
+ var ORIGIN = [
27956
+ 0,
27957
+ 0,
27958
+ 0
27959
+ ];
27960
+ /** Column-major 4x4 multiply: `a . b` (apply b first, then a). */
27961
+ function frameMul(a, b) {
27962
+ const out = new Array(16).fill(0);
27963
+ for (let col = 0; col < 4; col++) for (let row = 0; row < 4; row++) {
27964
+ let sum = 0;
27965
+ for (let k = 0; k < 4; k++) sum += (a[k * 4 + row] ?? 0) * (b[col * 4 + k] ?? 0);
27966
+ out[col * 4 + row] = sum;
27967
+ }
27968
+ return out;
27969
+ }
27970
+ function translationFrame(v) {
27971
+ return [
27972
+ 1,
27973
+ 0,
27974
+ 0,
27975
+ 0,
27976
+ 0,
27977
+ 1,
27978
+ 0,
27979
+ 0,
27980
+ 0,
27981
+ 0,
27982
+ 1,
27983
+ 0,
27984
+ v[0],
27985
+ v[1],
27986
+ v[2],
27987
+ 1
27988
+ ];
27989
+ }
27990
+ function normalizeAxis(axis) {
27991
+ const len = Math.sqrt(axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]);
27992
+ if (len < 1e-12) return DEFAULT_AXIS;
27993
+ return [
27994
+ axis[0] / len,
27995
+ axis[1] / len,
27996
+ axis[2] / len
27997
+ ];
27998
+ }
27999
+ function cross(a, b) {
28000
+ return [
28001
+ a[1] * b[2] - a[2] * b[1],
28002
+ a[2] * b[0] - a[0] * b[2],
28003
+ a[0] * b[1] - a[1] * b[0]
28004
+ ];
28005
+ }
28006
+ /**
28007
+ * Builds a frame from an authored `(origin, axisX, axisZ)` placement, using the
28008
+ * same IFC orthonormalization as `placementToMatrix`/`readAxis2Placement3D`:
28009
+ * z = normalize(axisZ); x = normalize(axisX projected perpendicular to z);
28010
+ * y = z x. Lets a civil node authored with explicit axis props enter the same
28011
+ * frame pipeline as a `tRotate` chain.
28012
+ */
28013
+ function frameFromPlacement(f) {
28014
+ const z = normalizeAxis(f.axisZ);
28015
+ const dot = z[0] * f.axisX[0] + z[1] * f.axisX[1] + z[2] * f.axisX[2];
28016
+ const x = normalizeAxis([
28017
+ f.axisX[0] - dot * z[0],
28018
+ f.axisX[1] - dot * z[1],
28019
+ f.axisX[2] - dot * z[2]
28020
+ ]);
28021
+ const y = cross(z, x);
28022
+ return [
28023
+ x[0],
28024
+ x[1],
28025
+ x[2],
28026
+ 0,
28027
+ y[0],
28028
+ y[1],
28029
+ y[2],
28030
+ 0,
28031
+ z[0],
28032
+ z[1],
28033
+ z[2],
28034
+ 0,
28035
+ f.origin[0],
28036
+ f.origin[1],
28037
+ f.origin[2],
28038
+ 1
28039
+ ];
28040
+ }
28041
+ /**
28042
+ * Rotation by `angleDeg` about `axis` (default +Z) through pivot `at` (default
28043
+ * origin), matching `csg.rotate` (Rodrigues; right-handed about the axis). With
28044
+ * a pivot the motion is `T(at) . R . T(-at)`, i.e. `p -> at + R(p - at)`.
28045
+ */
28046
+ function rotationFrame(angleDeg, axis = DEFAULT_AXIS, at = ORIGIN) {
28047
+ const [x, y, z] = normalizeAxis(axis);
28048
+ const t = angleDeg * Math.PI / 180;
28049
+ const c = Math.cos(t);
28050
+ const s = Math.sin(t);
28051
+ const C = 1 - c;
28052
+ const r00 = c + x * x * C;
28053
+ const r01 = x * y * C - z * s;
28054
+ const r02 = x * z * C + y * s;
28055
+ const r10 = y * x * C + z * s;
28056
+ const r11 = c + y * y * C;
28057
+ const r12 = y * z * C - x * s;
28058
+ const r20 = z * x * C - y * s;
28059
+ const r21 = z * y * C + x * s;
28060
+ const r22 = c + z * z * C;
28061
+ const rat0 = r00 * at[0] + r01 * at[1] + r02 * at[2];
28062
+ const rat1 = r10 * at[0] + r11 * at[1] + r12 * at[2];
28063
+ const rat2 = r20 * at[0] + r21 * at[1] + r22 * at[2];
28064
+ return [
28065
+ r00,
28066
+ r10,
28067
+ r20,
28068
+ 0,
28069
+ r01,
28070
+ r11,
28071
+ r21,
28072
+ 0,
28073
+ r02,
28074
+ r12,
28075
+ r22,
28076
+ 0,
28077
+ at[0] - rat0,
28078
+ at[1] - rat1,
28079
+ at[2] - rat2,
28080
+ 1
28081
+ ];
28082
+ }
28083
+ function opFrame(op) {
28084
+ if (op.op === "translate") return translationFrame(op.v);
28085
+ return rotationFrame(op.angleDeg, op.axis ?? DEFAULT_AXIS, op.at ?? ORIGIN);
28086
+ }
28087
+ /**
28088
+ * Composes an authored `TransformOp` chain into one frame, matching families'
28089
+ * `applyOps`: `ops[0]` is innermost (applied first). For `[A, B]` the composed
28090
+ * motion is `B . A`, so a point transforms as `B(A(p))`.
28091
+ */
28092
+ function frameFromOps(ops) {
28093
+ let m = IDENTITY_FRAME;
28094
+ for (const op of ops) m = frameMul(opFrame(op), m);
28095
+ return m;
28096
+ }
28097
+ /** Rigid inverse of `[R | t]`: `[R^T | -R^T t]`. */
28098
+ function frameInverse(f) {
28099
+ const t0 = f[12] ?? 0;
28100
+ const t1 = f[13] ?? 0;
28101
+ const t2 = f[14] ?? 0;
28102
+ const inv0 = -(f[0] * t0 + f[1] * t1 + f[2] * t2);
28103
+ const inv1 = -(f[4] * t0 + f[5] * t1 + f[6] * t2);
28104
+ const inv2 = -(f[8] * t0 + f[9] * t1 + f[10] * t2);
28105
+ return [
28106
+ f[0],
28107
+ f[4],
28108
+ f[8],
28109
+ 0,
28110
+ f[1],
28111
+ f[5],
28112
+ f[9],
28113
+ 0,
28114
+ f[2],
28115
+ f[6],
28116
+ f[10],
28117
+ 0,
28118
+ inv0,
28119
+ inv1,
28120
+ inv2,
28121
+ 1
28122
+ ];
28123
+ }
28124
+ /** Decomposes a frame into origin (mm) + IFC axes (axisX, axisZ). */
28125
+ function decomposeFrame(f) {
28126
+ return decomposePlacement(f);
28127
+ }
28128
+ /** Translation column of a frame. */
28129
+ function frameOrigin(f) {
28130
+ return [
28131
+ f[12] ?? 0,
28132
+ f[13] ?? 0,
28133
+ f[14] ?? 0
28134
+ ];
28135
+ }
28136
+ /** True when the rotation part is the identity (within tolerance): a pure
28137
+ * translation, so the existing translation-only placement path suffices. */
28138
+ function isPureTranslation(f, eps = 1e-9) {
28139
+ 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;
28140
+ }
28141
+ //#endregion
27911
28142
  //#region src/familiesAdapter.ts
27912
28143
  /**
27913
28144
  * brepjs-families -> BimModel adapter. Consumes a resolved element tree and
@@ -28129,6 +28360,36 @@ function centreOrigin(base, shift) {
28129
28360
  origin[2] + shift[2]
28130
28361
  ];
28131
28362
  }
28363
+ function synthesizedProfile(el) {
28364
+ if (el.semantics?.kind !== "product" || el.props["profile"] !== void 0) return void 0;
28365
+ const length = semanticDimension(el, "length");
28366
+ const width = semanticDimension(el, "width");
28367
+ const height = semanticDimension(el, "height");
28368
+ if (el.semantics.category === "beam" && width !== void 0 && height !== void 0) return {
28369
+ profile: {
28370
+ kind: "RECTANGULAR",
28371
+ width,
28372
+ height
28373
+ },
28374
+ shift: [
28375
+ 0,
28376
+ width / 2,
28377
+ height / 2
28378
+ ]
28379
+ };
28380
+ if (el.semantics.category === "column" && length !== void 0 && width !== void 0) return {
28381
+ profile: {
28382
+ kind: "RECTANGULAR",
28383
+ width: length,
28384
+ height: width
28385
+ },
28386
+ shift: [
28387
+ length / 2,
28388
+ width / 2,
28389
+ 0
28390
+ ]
28391
+ };
28392
+ }
28132
28393
  /**
28133
28394
  * Adapts target-independent civil envelope dimensions onto existing typed BIM
28134
28395
  * spec inputs. Reference infrastructure Families intentionally author domain
@@ -28143,18 +28404,7 @@ function civilProductSpecInput(el) {
28143
28404
  const height = semanticDimension(el, "height");
28144
28405
  switch (el.semantics.category) {
28145
28406
  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;
28407
+ const synthesized = synthesizedProfile(el);
28158
28408
  return {
28159
28409
  ...base,
28160
28410
  ...synthesized !== void 0 ? {
@@ -28166,18 +28416,7 @@ function civilProductSpecInput(el) {
28166
28416
  };
28167
28417
  }
28168
28418
  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;
28419
+ const synthesized = synthesizedProfile(el);
28181
28420
  return {
28182
28421
  ...base,
28183
28422
  ...synthesized !== void 0 ? {
@@ -28244,12 +28483,12 @@ function peelTranslates(node) {
28244
28483
  moved
28245
28484
  };
28246
28485
  }
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. */
28486
+ /** True when the element's rendered local transform carries a rotation, which
28487
+ * switches the walk to the rigid-frame placement path (the composed rotation
28488
+ * folds into origin + axisX + axisZ). Only authored `transform` rotations
28489
+ * count: a rotation a family render bakes into its own body geometry (e.g. a
28490
+ * circular beam oriented along axisX) stays in the body, which the spec
28491
+ * rebuilds parametrically from props. */
28253
28492
  function hasRotateOp(el) {
28254
28493
  return el.localTransforms.some((op) => op.op === "rotate");
28255
28494
  }
@@ -28364,7 +28603,7 @@ function materializeOwnedSolid(el, evaluator, errors) {
28364
28603
  /** Materialize an unrouted element's IR and add it as a proxy. The body is
28365
28604
  * authoritative for a proxy (no parametric spec to diverge from), so baked
28366
28605
  * transforms — rotations included — are fine here. */
28367
- function addProxyElement(model, el, evaluator, projectedSpatialTranslation) {
28606
+ function addProxyElement(model, el, evaluator, spatialFrame) {
28368
28607
  const body = materializeOwnedSolid(el, evaluator, {
28369
28608
  evalCode: "FAMILIES_PROXY_EVAL_FAILED",
28370
28609
  notSolidCode: "FAMILIES_PROXY_NOT_SOLID",
@@ -28372,7 +28611,7 @@ function addProxyElement(model, el, evaluator, projectedSpatialTranslation) {
28372
28611
  routeName: "proxy"
28373
28612
  });
28374
28613
  if (!body.ok) return body;
28375
- const localized = localizeOwnedSolid(el, body.value, projectedSpatialTranslation, "FAMILIES_PROXY_LOCALIZE_FAILED", "spatial parent");
28614
+ const localized = localizeBodyToFrame(el, body.value, spatialFrame, "FAMILIES_PROXY_LOCALIZE_FAILED", "spatial parent");
28376
28615
  if (!localized.ok) return localized;
28377
28616
  const nameAttr = el.attributes["name"];
28378
28617
  const materialProp = el.props["materialName"];
@@ -28535,8 +28774,9 @@ var DEFAULT_AXIS_Z = [
28535
28774
  function axisEquals(value, axis) {
28536
28775
  return Array.isArray(value) && value.length === 3 && value.every((component, i) => typeof component === "number" && component === axis[i]);
28537
28776
  }
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. */
28777
+ /** True when a civil node authors non-default `axisX`/`axisZ` props: like a
28778
+ * tRotate, this puts the node (and its subtree) on the rigid-frame placement
28779
+ * path so descendants relativize against the rotated parent frame. */
28540
28780
  function hasRotatedAxes(el) {
28541
28781
  const axisX = el.props["axisX"];
28542
28782
  const axisZ = el.props["axisZ"];
@@ -28575,6 +28815,119 @@ function authoredTranslation(el) {
28575
28815
  }
28576
28816
  return total;
28577
28817
  }
28818
+ function authoredAxisX(el) {
28819
+ const v = el.props["axisX"];
28820
+ return Array.isArray(v) && v.length === 3 ? v : DEFAULT_AXIS_X;
28821
+ }
28822
+ function authoredAxisZ(el) {
28823
+ const v = el.props["axisZ"];
28824
+ return Array.isArray(v) && v.length === 3 ? v : DEFAULT_AXIS_Z;
28825
+ }
28826
+ /** The placement shift that centres a synthesized beam/column cross-section on
28827
+ * its axis, in the element's own (body) frame. Zero for every other route. */
28828
+ function specLocalShift(el) {
28829
+ return synthesizedProfile(el)?.shift ?? [
28830
+ 0,
28831
+ 0,
28832
+ 0
28833
+ ];
28834
+ }
28835
+ /** World frame of an element's own body: the walk's cumulative frame (all
28836
+ * authored transforms, ancestors + own) composed with the body's authored axes
28837
+ * (`axisX`/`axisZ` props) and its local origin (`origin` prop + centring
28838
+ * shift). Under no rotation this reduces to `composedOrigin` + shift. */
28839
+ function elementBodyFrame(el, cumulativeFrame) {
28840
+ return frameMul(frameMul(cumulativeFrame, frameFromPlacement({
28841
+ origin: authoredSpecOrigin(el),
28842
+ axisX: authoredAxisX(el),
28843
+ axisZ: authoredAxisZ(el)
28844
+ })), translationFrame(specLocalShift(el)));
28845
+ }
28846
+ /** World frame of a civil spatial node: cumulative transforms composed with any
28847
+ * authored `origin`/`axisX`/`axisZ` props (identity when default). */
28848
+ function civilNodeFrame(el, cumulativeFrame) {
28849
+ return frameMul(cumulativeFrame, frameFromPlacement({
28850
+ origin: authoredSpecOrigin(el),
28851
+ axisX: authoredAxisX(el),
28852
+ axisZ: authoredAxisZ(el)
28853
+ }));
28854
+ }
28855
+ /** Places a routed element's spec input through the composed placement frame:
28856
+ * the flat spec input keeps its dimensions/profile/psets, but `origin`/`axisX`/
28857
+ * `axisZ` are recomputed from the element's world frame relative to its spatial
28858
+ * container. Stair/ramp flights, which carry their own per-flight frame, are
28859
+ * each re-placed the same way. */
28860
+ function rotatedRoutedInput(flatInput, el, cumulativeFrame, spatialFrame) {
28861
+ const toSpatial = frameInverse(spatialFrame);
28862
+ if (Array.isArray(flatInput["flights"])) {
28863
+ const elementFrame = elementBodyFrame(el, cumulativeFrame);
28864
+ const flights = flatInput["flights"];
28865
+ return {
28866
+ ...flatInput,
28867
+ origin: [
28868
+ 0,
28869
+ 0,
28870
+ 0
28871
+ ],
28872
+ axisX: [
28873
+ 1,
28874
+ 0,
28875
+ 0
28876
+ ],
28877
+ axisZ: [
28878
+ 0,
28879
+ 0,
28880
+ 1
28881
+ ],
28882
+ flights: flights.map((flight) => {
28883
+ if (typeof flight !== "object" || flight === null) return flight;
28884
+ const f = flight;
28885
+ const world = frameMul(elementFrame, frameFromPlacement({
28886
+ origin: f["origin"] ?? [
28887
+ 0,
28888
+ 0,
28889
+ 0
28890
+ ],
28891
+ axisX: f["axisX"] ?? DEFAULT_AXIS_X,
28892
+ axisZ: f["axisZ"] ?? DEFAULT_AXIS_Z
28893
+ }));
28894
+ const placed = decomposeFrame(frameMul(toSpatial, world));
28895
+ return {
28896
+ ...f,
28897
+ origin: placed.origin,
28898
+ axisX: placed.axisX,
28899
+ axisZ: placed.axisZ
28900
+ };
28901
+ })
28902
+ };
28903
+ }
28904
+ const placed = decomposeFrame(frameMul(toSpatial, elementBodyFrame(el, cumulativeFrame)));
28905
+ return {
28906
+ ...flatInput,
28907
+ origin: placed.origin,
28908
+ axisX: placed.axisX,
28909
+ axisZ: placed.axisZ
28910
+ };
28911
+ }
28912
+ /** Moves an owned world-baked body into its spatial container's local frame.
28913
+ * A pure translation keeps the fast `translate` path (identical to the prior
28914
+ * behaviour); a rotated container applies the inverse frame via `applyMatrix`. */
28915
+ function localizeBodyToFrame(el, body, spatialFrame, errorCode, frameName) {
28916
+ if (isPureTranslation(spatialFrame)) return localizeOwnedSolid(el, body, frameOrigin(spatialFrame), errorCode, frameName);
28917
+ const inverse = decomposeFrame(frameInverse(spatialFrame));
28918
+ try {
28919
+ const localized = applyMatrix(body, placementToMatrix(inverse));
28920
+ if (!localized.ok) {
28921
+ body[Symbol.dispose]();
28922
+ return err(specError(errorCode, `familiesToBim: '${el.keyPath}' could not move its body into the ${frameName} frame`, localized.error));
28923
+ }
28924
+ body[Symbol.dispose]();
28925
+ return ok(localized.value);
28926
+ } catch (cause) {
28927
+ body[Symbol.dispose]();
28928
+ return err(specError(errorCode, `familiesToBim: '${el.keyPath}' could not move its body into the ${frameName} frame`, cause));
28929
+ }
28930
+ }
28578
28931
  function civilSpatialInput(el, localTranslation) {
28579
28932
  const semantics = el.semantics;
28580
28933
  const composition = semantics !== void 0 && "composition" in semantics ? CIVIL_COMPOSITION[semantics.composition] : void 0;
@@ -28587,22 +28940,36 @@ function civilSpatialInput(el, localTranslation) {
28587
28940
  ...composition !== void 0 ? { compositionType: composition } : {}
28588
28941
  };
28589
28942
  }
28943
+ /** Relativizes a civil node's world frame against its parent civil frame into
28944
+ * the origin/axisX/axisZ the spatial specs consume. */
28945
+ function civilSpatialFrameInput(el, nodeFrame, parentFrame) {
28946
+ const semantics = el.semantics;
28947
+ const composition = semantics !== void 0 && "composition" in semantics ? CIVIL_COMPOSITION[semantics.composition] : void 0;
28948
+ const local = decomposeFrame(frameMul(frameInverse(parentFrame), nodeFrame));
28949
+ return {
28950
+ name: semanticName(el),
28951
+ origin: local.origin,
28952
+ axisX: local.axisX,
28953
+ axisZ: local.axisZ,
28954
+ ...composition !== void 0 ? { compositionType: composition } : {}
28955
+ };
28956
+ }
28590
28957
  function civilParentAccepts(kind, parent) {
28591
28958
  if (kind === "site") return parent === "project";
28592
28959
  if (kind === "bridge") return parent === "site";
28593
28960
  return parent === "bridge" || parent === "bridge-part";
28594
28961
  }
28595
- function addCivilSpatialOccurrence(model, el, kind, localTranslation) {
28962
+ function addCivilSpatialOccurrence(model, el, kind, input) {
28596
28963
  if (kind === "site") {
28597
28964
  if (el.semantics?.role !== "transport-site") return unsupportedCivilRole(el, "Site");
28598
- const parsed = parseSiteSpec(civilSpatialInput(el, localTranslation));
28965
+ const parsed = parseSiteSpec(input);
28599
28966
  return parsed.ok ? model.addSite(parsed.value, { stableKey: el.keyPath }) : parsed;
28600
28967
  }
28601
28968
  if (kind === "bridge") {
28602
28969
  const predefinedType = lookup(BRIDGE_ROLE, el.semantics?.role ?? "");
28603
28970
  if (predefinedType === void 0) return unsupportedCivilRole(el, "Bridge");
28604
28971
  const parsed = parseBridgeSpec({
28605
- ...civilSpatialInput(el, localTranslation),
28972
+ ...input,
28606
28973
  predefinedType
28607
28974
  });
28608
28975
  return parsed.ok ? model.addBridge(parsed.value, { stableKey: el.keyPath }) : parsed;
@@ -28611,7 +28978,7 @@ function addCivilSpatialOccurrence(model, el, kind, localTranslation) {
28611
28978
  const predefinedType = lookup(BRIDGE_PART_ROLE, el.semantics?.role ?? "");
28612
28979
  if (predefinedType === void 0) return unsupportedCivilRole(el, "Bridge Part");
28613
28980
  const parsed = parseBridgePartSpec({
28614
- ...civilSpatialInput(el, localTranslation),
28981
+ ...input,
28615
28982
  predefinedType,
28616
28983
  usageType: subdivision !== void 0 ? CIVIL_USAGE[subdivision] : "NOTDEFINED"
28617
28984
  });
@@ -28638,7 +29005,7 @@ function relativeSpecInput(input, projectedSpatialTranslation) {
28638
29005
  origin: relativeOrigin(input["origin"])
28639
29006
  };
28640
29007
  }
28641
- function addEarthworksFillElement(model, el, evaluator, projectedSpatialTranslation) {
29008
+ function addEarthworksFillElement(model, el, evaluator, spatialFrame) {
28642
29009
  if (el.semantics?.kind !== "product") return err(specError("FAMILIES_UNSUPPORTED_CIVIL_SEMANTICS", `familiesToBim: '${el.keyPath}' is not authored as a civil Product`));
28643
29010
  const predefinedType = lookup(EARTHWORKS_FILL_ROLE, el.semantics.role);
28644
29011
  if (predefinedType === void 0) return unsupportedCivilRole(el, "Earthworks Fill");
@@ -28649,7 +29016,7 @@ function addEarthworksFillElement(model, el, evaluator, projectedSpatialTranslat
28649
29016
  routeName: "Earthworks Fill"
28650
29017
  });
28651
29018
  if (!body.ok) return body;
28652
- const localized = localizeOwnedSolid(el, body.value, projectedSpatialTranslation, "FAMILIES_EARTHWORKS_LOCALIZE_FAILED", "Bridge Part");
29019
+ const localized = localizeBodyToFrame(el, body.value, spatialFrame, "FAMILIES_EARTHWORKS_LOCALIZE_FAILED", "Bridge Part");
28653
29020
  if (!localized.ok) return localized;
28654
29021
  const specProps = collectSpecProps(el);
28655
29022
  const authoredMaterial = el.props["materialName"];
@@ -28699,10 +29066,14 @@ function familiesToBim(root, options) {
28699
29066
  const walk = (el, state) => {
28700
29067
  const rotatedHere = state.rotated || hasRotateOp(el);
28701
29068
  const cumulativeTranslationHere = addTranslation(state.cumulativeTranslation, authoredTranslation(el));
29069
+ const cumulativeFrameHere = frameMul(state.cumulativeFrame, frameFromOps(el.localTransforms));
28702
29070
  let proxiedHere = false;
29071
+ let nextRotated = rotatedHere;
28703
29072
  let nextSpatialStructureId = state.spatialStructureId;
28704
29073
  let nextCivilParent = state.civilParent;
28705
29074
  let nextProjectedSpatialTranslation = state.projectedSpatialTranslation;
29075
+ let nextCumulativeFrame = cumulativeFrameHere;
29076
+ let nextSpatialFrame = state.spatialFrame;
28706
29077
  const archetype = archetypeFor(el);
28707
29078
  const effectiveArchetype = el.semantics?.kind === "product" ? civilProductArchetype(el) : archetype;
28708
29079
  const route = specRoute(effectiveArchetype);
@@ -28711,15 +29082,19 @@ function familiesToBim(root, options) {
28711
29082
  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
29083
  const keyed = requireKeyed(el);
28713
29084
  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
29085
  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);
29086
+ const nodeFrame = civilNodeFrame(el, cumulativeFrameHere);
29087
+ const rotatedFrame = rotatedHere || hasRotatedAxes(el);
29088
+ const input = rotatedFrame ? civilSpatialFrameInput(el, nodeFrame, state.spatialFrame) : civilSpatialInput(el, subtractTranslation(cumulativeTranslationHere, state.projectedSpatialTranslation));
29089
+ const added = addCivilSpatialOccurrence(model, el, civilKind, input);
28718
29090
  if (!added.ok) return added;
28719
29091
  model.aggregate(state.spatialStructureId, added.value);
28720
29092
  idByKeyPath.set(el.keyPath, added.value);
28721
29093
  nextSpatialStructureId = added.value;
28722
29094
  nextCivilParent = civilKind;
29095
+ nextRotated = rotatedFrame;
29096
+ nextCumulativeFrame = nodeFrame;
29097
+ nextSpatialFrame = nodeFrame;
28723
29098
  nextProjectedSpatialTranslation = addTranslation(cumulativeTranslationHere, authoredSpecOrigin(el));
28724
29099
  } 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
29100
  else if (archetype === "storey") {
@@ -28740,21 +29115,22 @@ function familiesToBim(root, options) {
28740
29115
  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
29116
  const keyed = requireKeyed(el);
28742
29117
  if (!keyed.ok) return keyed;
28743
- const added = addEarthworksFillElement(model, el, bodyEvaluator, state.projectedSpatialTranslation);
29118
+ const added = addEarthworksFillElement(model, el, bodyEvaluator, state.spatialFrame);
28744
29119
  if (!added.ok) return added;
28745
29120
  model.placeIn(added.value, nextSpatialStructureId);
28746
29121
  idByKeyPath.set(el.keyPath, added.value);
28747
29122
  } else if (route !== void 0) {
28748
29123
  const keyed = requireKeyed(el);
28749
29124
  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
29125
  const voids = el.props["voids"];
28752
29126
  if (Array.isArray(voids)) {
28753
29127
  const openings = el.children.filter((c) => c.type === "Opening").length;
28754
29128
  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
29129
  }
28756
29130
  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);
29131
+ const rotatedBase = Array.isArray(routedInput["flights"]) ? specInput(el) : routedInput;
29132
+ const placedInput = rotatedHere ? rotatedRoutedInput(rotatedBase, el, cumulativeFrameHere, state.spatialFrame) : usesAuthoredCivilHierarchy ? relativeSpecInput(routedInput, state.projectedSpatialTranslation) : routedInput;
29133
+ const parsed = route.parse(placedInput);
28758
29134
  if (!parsed.ok) return parsed;
28759
29135
  const added = route.add(model, parsed.value, el.keyPath);
28760
29136
  if (!added.ok) return added;
@@ -28771,7 +29147,7 @@ function familiesToBim(root, options) {
28771
29147
  const keyed = requireKeyed(el);
28772
29148
  if (!keyed.ok) return keyed;
28773
29149
  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);
29150
+ const added = addProxyElement(model, el, options.proxyEvaluator, state.spatialFrame);
28775
29151
  if (!added.ok) return added;
28776
29152
  model.placeIn(added.value, nextSpatialStructureId);
28777
29153
  idByKeyPath.set(el.keyPath, added.value);
@@ -28787,9 +29163,11 @@ function familiesToBim(root, options) {
28787
29163
  const r = walk(child, {
28788
29164
  spatialStructureId: nextSpatialStructureId,
28789
29165
  civilParent: nextCivilParent,
28790
- rotated: rotatedHere,
29166
+ rotated: nextRotated,
28791
29167
  cumulativeTranslation: cumulativeTranslationHere,
28792
- projectedSpatialTranslation: nextProjectedSpatialTranslation
29168
+ projectedSpatialTranslation: nextProjectedSpatialTranslation,
29169
+ cumulativeFrame: nextCumulativeFrame,
29170
+ spatialFrame: nextSpatialFrame
28793
29171
  });
28794
29172
  if (!r.ok) return r;
28795
29173
  }
@@ -28800,7 +29178,9 @@ function familiesToBim(root, options) {
28800
29178
  civilParent: "project",
28801
29179
  rotated: false,
28802
29180
  cumulativeTranslation: ZERO_TRANSLATION,
28803
- projectedSpatialTranslation: ZERO_TRANSLATION
29181
+ projectedSpatialTranslation: ZERO_TRANSLATION,
29182
+ cumulativeFrame: IDENTITY_FRAME,
29183
+ spatialFrame: IDENTITY_FRAME
28804
29184
  });
28805
29185
  if (!walked.ok) {
28806
29186
  model[Symbol.dispose]();