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.
@@ -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
@@ -14499,17 +14519,17 @@ function readAxis2Placement3D(reader, ref, scale) {
14499
14519
  refDir[2] - dotXZ * zN[2]
14500
14520
  ];
14501
14521
  const xRawLenSq = xRaw[0] * xRaw[0] + xRaw[1] * xRaw[1] + xRaw[2] * xRaw[2];
14502
- const orthoZ = Math.abs(zN[0]) < .9 ? cross(zN, [
14522
+ const orthoZ = Math.abs(zN[0]) < .9 ? cross$1(zN, [
14503
14523
  1,
14504
14524
  0,
14505
14525
  0
14506
- ]) : cross(zN, [
14526
+ ]) : cross$1(zN, [
14507
14527
  0,
14508
14528
  1,
14509
14529
  0
14510
14530
  ]);
14511
14531
  const xN = xRawLenSq < 1e-12 ? normalize(orthoZ) : normalize(xRaw);
14512
- const yN = cross(zN, xN);
14532
+ const yN = cross$1(zN, xN);
14513
14533
  return {
14514
14534
  linear: [
14515
14535
  xN[0],
@@ -14669,7 +14689,7 @@ function normalize(v) {
14669
14689
  v[2] / len
14670
14690
  ];
14671
14691
  }
14672
- function cross(a, b) {
14692
+ function cross$1(a, b) {
14673
14693
  return [
14674
14694
  a[1] * b[2] - a[2] * b[1],
14675
14695
  a[2] * b[0] - a[0] * b[2],
@@ -27931,6 +27951,217 @@ function assignNum(target, key, value) {
27931
27951
  if (value !== void 0) target[key] = Number(value);
27932
27952
  }
27933
27953
  //#endregion
27954
+ //#region src/placementFrame.ts
27955
+ var IDENTITY_FRAME = [
27956
+ 1,
27957
+ 0,
27958
+ 0,
27959
+ 0,
27960
+ 0,
27961
+ 1,
27962
+ 0,
27963
+ 0,
27964
+ 0,
27965
+ 0,
27966
+ 1,
27967
+ 0,
27968
+ 0,
27969
+ 0,
27970
+ 0,
27971
+ 1
27972
+ ];
27973
+ var DEFAULT_AXIS = [
27974
+ 0,
27975
+ 0,
27976
+ 1
27977
+ ];
27978
+ var ORIGIN = [
27979
+ 0,
27980
+ 0,
27981
+ 0
27982
+ ];
27983
+ /** Column-major 4x4 multiply: `a . b` (apply b first, then a). */
27984
+ function frameMul(a, b) {
27985
+ const out = new Array(16).fill(0);
27986
+ for (let col = 0; col < 4; col++) for (let row = 0; row < 4; row++) {
27987
+ let sum = 0;
27988
+ for (let k = 0; k < 4; k++) sum += (a[k * 4 + row] ?? 0) * (b[col * 4 + k] ?? 0);
27989
+ out[col * 4 + row] = sum;
27990
+ }
27991
+ return out;
27992
+ }
27993
+ function translationFrame(v) {
27994
+ return [
27995
+ 1,
27996
+ 0,
27997
+ 0,
27998
+ 0,
27999
+ 0,
28000
+ 1,
28001
+ 0,
28002
+ 0,
28003
+ 0,
28004
+ 0,
28005
+ 1,
28006
+ 0,
28007
+ v[0],
28008
+ v[1],
28009
+ v[2],
28010
+ 1
28011
+ ];
28012
+ }
28013
+ function normalizeAxis(axis) {
28014
+ const len = Math.sqrt(axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]);
28015
+ if (len < 1e-12) return DEFAULT_AXIS;
28016
+ return [
28017
+ axis[0] / len,
28018
+ axis[1] / len,
28019
+ axis[2] / len
28020
+ ];
28021
+ }
28022
+ function cross(a, b) {
28023
+ return [
28024
+ a[1] * b[2] - a[2] * b[1],
28025
+ a[2] * b[0] - a[0] * b[2],
28026
+ a[0] * b[1] - a[1] * b[0]
28027
+ ];
28028
+ }
28029
+ /**
28030
+ * Builds a frame from an authored `(origin, axisX, axisZ)` placement, using the
28031
+ * same IFC orthonormalization as `placementToMatrix`/`readAxis2Placement3D`:
28032
+ * z = normalize(axisZ); x = normalize(axisX projected perpendicular to z);
28033
+ * y = z x. Lets a civil node authored with explicit axis props enter the same
28034
+ * frame pipeline as a `tRotate` chain.
28035
+ */
28036
+ function frameFromPlacement(f) {
28037
+ const z = normalizeAxis(f.axisZ);
28038
+ const dot = z[0] * f.axisX[0] + z[1] * f.axisX[1] + z[2] * f.axisX[2];
28039
+ const x = normalizeAxis([
28040
+ f.axisX[0] - dot * z[0],
28041
+ f.axisX[1] - dot * z[1],
28042
+ f.axisX[2] - dot * z[2]
28043
+ ]);
28044
+ const y = cross(z, x);
28045
+ return [
28046
+ x[0],
28047
+ x[1],
28048
+ x[2],
28049
+ 0,
28050
+ y[0],
28051
+ y[1],
28052
+ y[2],
28053
+ 0,
28054
+ z[0],
28055
+ z[1],
28056
+ z[2],
28057
+ 0,
28058
+ f.origin[0],
28059
+ f.origin[1],
28060
+ f.origin[2],
28061
+ 1
28062
+ ];
28063
+ }
28064
+ /**
28065
+ * Rotation by `angleDeg` about `axis` (default +Z) through pivot `at` (default
28066
+ * origin), matching `csg.rotate` (Rodrigues; right-handed about the axis). With
28067
+ * a pivot the motion is `T(at) . R . T(-at)`, i.e. `p -> at + R(p - at)`.
28068
+ */
28069
+ function rotationFrame(angleDeg, axis = DEFAULT_AXIS, at = ORIGIN) {
28070
+ const [x, y, z] = normalizeAxis(axis);
28071
+ const t = angleDeg * Math.PI / 180;
28072
+ const c = Math.cos(t);
28073
+ const s = Math.sin(t);
28074
+ const C = 1 - c;
28075
+ const r00 = c + x * x * C;
28076
+ const r01 = x * y * C - z * s;
28077
+ const r02 = x * z * C + y * s;
28078
+ const r10 = y * x * C + z * s;
28079
+ const r11 = c + y * y * C;
28080
+ const r12 = y * z * C - x * s;
28081
+ const r20 = z * x * C - y * s;
28082
+ const r21 = z * y * C + x * s;
28083
+ const r22 = c + z * z * C;
28084
+ const rat0 = r00 * at[0] + r01 * at[1] + r02 * at[2];
28085
+ const rat1 = r10 * at[0] + r11 * at[1] + r12 * at[2];
28086
+ const rat2 = r20 * at[0] + r21 * at[1] + r22 * at[2];
28087
+ return [
28088
+ r00,
28089
+ r10,
28090
+ r20,
28091
+ 0,
28092
+ r01,
28093
+ r11,
28094
+ r21,
28095
+ 0,
28096
+ r02,
28097
+ r12,
28098
+ r22,
28099
+ 0,
28100
+ at[0] - rat0,
28101
+ at[1] - rat1,
28102
+ at[2] - rat2,
28103
+ 1
28104
+ ];
28105
+ }
28106
+ function opFrame(op) {
28107
+ if (op.op === "translate") return translationFrame(op.v);
28108
+ return rotationFrame(op.angleDeg, op.axis ?? DEFAULT_AXIS, op.at ?? ORIGIN);
28109
+ }
28110
+ /**
28111
+ * Composes an authored `TransformOp` chain into one frame, matching families'
28112
+ * `applyOps`: `ops[0]` is innermost (applied first). For `[A, B]` the composed
28113
+ * motion is `B . A`, so a point transforms as `B(A(p))`.
28114
+ */
28115
+ function frameFromOps(ops) {
28116
+ let m = IDENTITY_FRAME;
28117
+ for (const op of ops) m = frameMul(opFrame(op), m);
28118
+ return m;
28119
+ }
28120
+ /** Rigid inverse of `[R | t]`: `[R^T | -R^T t]`. */
28121
+ function frameInverse(f) {
28122
+ const t0 = f[12] ?? 0;
28123
+ const t1 = f[13] ?? 0;
28124
+ const t2 = f[14] ?? 0;
28125
+ const inv0 = -(f[0] * t0 + f[1] * t1 + f[2] * t2);
28126
+ const inv1 = -(f[4] * t0 + f[5] * t1 + f[6] * t2);
28127
+ const inv2 = -(f[8] * t0 + f[9] * t1 + f[10] * t2);
28128
+ return [
28129
+ f[0],
28130
+ f[4],
28131
+ f[8],
28132
+ 0,
28133
+ f[1],
28134
+ f[5],
28135
+ f[9],
28136
+ 0,
28137
+ f[2],
28138
+ f[6],
28139
+ f[10],
28140
+ 0,
28141
+ inv0,
28142
+ inv1,
28143
+ inv2,
28144
+ 1
28145
+ ];
28146
+ }
28147
+ /** Decomposes a frame into origin (mm) + IFC axes (axisX, axisZ). */
28148
+ function decomposeFrame(f) {
28149
+ return decomposePlacement(f);
28150
+ }
28151
+ /** Translation column of a frame. */
28152
+ function frameOrigin(f) {
28153
+ return [
28154
+ f[12] ?? 0,
28155
+ f[13] ?? 0,
28156
+ f[14] ?? 0
28157
+ ];
28158
+ }
28159
+ /** True when the rotation part is the identity (within tolerance): a pure
28160
+ * translation, so the existing translation-only placement path suffices. */
28161
+ function isPureTranslation(f, eps = 1e-9) {
28162
+ 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;
28163
+ }
28164
+ //#endregion
27934
28165
  //#region src/familiesAdapter.ts
27935
28166
  /**
27936
28167
  * brepjs-families -> BimModel adapter. Consumes a resolved element tree and
@@ -28152,6 +28383,36 @@ function centreOrigin(base, shift) {
28152
28383
  origin[2] + shift[2]
28153
28384
  ];
28154
28385
  }
28386
+ function synthesizedProfile(el) {
28387
+ if (el.semantics?.kind !== "product" || el.props["profile"] !== void 0) return void 0;
28388
+ const length = semanticDimension(el, "length");
28389
+ const width = semanticDimension(el, "width");
28390
+ const height = semanticDimension(el, "height");
28391
+ if (el.semantics.category === "beam" && width !== void 0 && height !== void 0) return {
28392
+ profile: {
28393
+ kind: "RECTANGULAR",
28394
+ width,
28395
+ height
28396
+ },
28397
+ shift: [
28398
+ 0,
28399
+ width / 2,
28400
+ height / 2
28401
+ ]
28402
+ };
28403
+ if (el.semantics.category === "column" && length !== void 0 && width !== void 0) return {
28404
+ profile: {
28405
+ kind: "RECTANGULAR",
28406
+ width: length,
28407
+ height: width
28408
+ },
28409
+ shift: [
28410
+ length / 2,
28411
+ width / 2,
28412
+ 0
28413
+ ]
28414
+ };
28415
+ }
28155
28416
  /**
28156
28417
  * Adapts target-independent civil envelope dimensions onto existing typed BIM
28157
28418
  * spec inputs. Reference infrastructure Families intentionally author domain
@@ -28166,18 +28427,7 @@ function civilProductSpecInput(el) {
28166
28427
  const height = semanticDimension(el, "height");
28167
28428
  switch (el.semantics.category) {
28168
28429
  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;
28430
+ const synthesized = synthesizedProfile(el);
28181
28431
  return {
28182
28432
  ...base,
28183
28433
  ...synthesized !== void 0 ? {
@@ -28189,18 +28439,7 @@ function civilProductSpecInput(el) {
28189
28439
  };
28190
28440
  }
28191
28441
  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;
28442
+ const synthesized = synthesizedProfile(el);
28204
28443
  return {
28205
28444
  ...base,
28206
28445
  ...synthesized !== void 0 ? {
@@ -28267,12 +28506,12 @@ function peelTranslates(node) {
28267
28506
  moved
28268
28507
  };
28269
28508
  }
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. */
28509
+ /** True when the element's rendered local transform carries a rotation, which
28510
+ * switches the walk to the rigid-frame placement path (the composed rotation
28511
+ * folds into origin + axisX + axisZ). Only authored `transform` rotations
28512
+ * count: a rotation a family render bakes into its own body geometry (e.g. a
28513
+ * circular beam oriented along axisX) stays in the body, which the spec
28514
+ * rebuilds parametrically from props. */
28276
28515
  function hasRotateOp(el) {
28277
28516
  return el.localTransforms.some((op) => op.op === "rotate");
28278
28517
  }
@@ -28387,7 +28626,7 @@ function materializeOwnedSolid(el, evaluator, errors) {
28387
28626
  /** Materialize an unrouted element's IR and add it as a proxy. The body is
28388
28627
  * authoritative for a proxy (no parametric spec to diverge from), so baked
28389
28628
  * transforms — rotations included — are fine here. */
28390
- function addProxyElement(model, el, evaluator, projectedSpatialTranslation) {
28629
+ function addProxyElement(model, el, evaluator, spatialFrame) {
28391
28630
  const body = materializeOwnedSolid(el, evaluator, {
28392
28631
  evalCode: "FAMILIES_PROXY_EVAL_FAILED",
28393
28632
  notSolidCode: "FAMILIES_PROXY_NOT_SOLID",
@@ -28395,7 +28634,7 @@ function addProxyElement(model, el, evaluator, projectedSpatialTranslation) {
28395
28634
  routeName: "proxy"
28396
28635
  });
28397
28636
  if (!body.ok) return body;
28398
- const localized = localizeOwnedSolid(el, body.value, projectedSpatialTranslation, "FAMILIES_PROXY_LOCALIZE_FAILED", "spatial parent");
28637
+ const localized = localizeBodyToFrame(el, body.value, spatialFrame, "FAMILIES_PROXY_LOCALIZE_FAILED", "spatial parent");
28399
28638
  if (!localized.ok) return localized;
28400
28639
  const nameAttr = el.attributes["name"];
28401
28640
  const materialProp = el.props["materialName"];
@@ -28558,8 +28797,9 @@ var DEFAULT_AXIS_Z = [
28558
28797
  function axisEquals(value, axis) {
28559
28798
  return Array.isArray(value) && value.length === 3 && value.every((component, i) => typeof component === "number" && component === axis[i]);
28560
28799
  }
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. */
28800
+ /** True when a civil node authors non-default `axisX`/`axisZ` props: like a
28801
+ * tRotate, this puts the node (and its subtree) on the rigid-frame placement
28802
+ * path so descendants relativize against the rotated parent frame. */
28563
28803
  function hasRotatedAxes(el) {
28564
28804
  const axisX = el.props["axisX"];
28565
28805
  const axisZ = el.props["axisZ"];
@@ -28598,6 +28838,119 @@ function authoredTranslation(el) {
28598
28838
  }
28599
28839
  return total;
28600
28840
  }
28841
+ function authoredAxisX(el) {
28842
+ const v = el.props["axisX"];
28843
+ return Array.isArray(v) && v.length === 3 ? v : DEFAULT_AXIS_X;
28844
+ }
28845
+ function authoredAxisZ(el) {
28846
+ const v = el.props["axisZ"];
28847
+ return Array.isArray(v) && v.length === 3 ? v : DEFAULT_AXIS_Z;
28848
+ }
28849
+ /** The placement shift that centres a synthesized beam/column cross-section on
28850
+ * its axis, in the element's own (body) frame. Zero for every other route. */
28851
+ function specLocalShift(el) {
28852
+ return synthesizedProfile(el)?.shift ?? [
28853
+ 0,
28854
+ 0,
28855
+ 0
28856
+ ];
28857
+ }
28858
+ /** World frame of an element's own body: the walk's cumulative frame (all
28859
+ * authored transforms, ancestors + own) composed with the body's authored axes
28860
+ * (`axisX`/`axisZ` props) and its local origin (`origin` prop + centring
28861
+ * shift). Under no rotation this reduces to `composedOrigin` + shift. */
28862
+ function elementBodyFrame(el, cumulativeFrame) {
28863
+ return frameMul(frameMul(cumulativeFrame, frameFromPlacement({
28864
+ origin: authoredSpecOrigin(el),
28865
+ axisX: authoredAxisX(el),
28866
+ axisZ: authoredAxisZ(el)
28867
+ })), translationFrame(specLocalShift(el)));
28868
+ }
28869
+ /** World frame of a civil spatial node: cumulative transforms composed with any
28870
+ * authored `origin`/`axisX`/`axisZ` props (identity when default). */
28871
+ function civilNodeFrame(el, cumulativeFrame) {
28872
+ return frameMul(cumulativeFrame, frameFromPlacement({
28873
+ origin: authoredSpecOrigin(el),
28874
+ axisX: authoredAxisX(el),
28875
+ axisZ: authoredAxisZ(el)
28876
+ }));
28877
+ }
28878
+ /** Places a routed element's spec input through the composed placement frame:
28879
+ * the flat spec input keeps its dimensions/profile/psets, but `origin`/`axisX`/
28880
+ * `axisZ` are recomputed from the element's world frame relative to its spatial
28881
+ * container. Stair/ramp flights, which carry their own per-flight frame, are
28882
+ * each re-placed the same way. */
28883
+ function rotatedRoutedInput(flatInput, el, cumulativeFrame, spatialFrame) {
28884
+ const toSpatial = frameInverse(spatialFrame);
28885
+ if (Array.isArray(flatInput["flights"])) {
28886
+ const elementFrame = elementBodyFrame(el, cumulativeFrame);
28887
+ const flights = flatInput["flights"];
28888
+ return {
28889
+ ...flatInput,
28890
+ origin: [
28891
+ 0,
28892
+ 0,
28893
+ 0
28894
+ ],
28895
+ axisX: [
28896
+ 1,
28897
+ 0,
28898
+ 0
28899
+ ],
28900
+ axisZ: [
28901
+ 0,
28902
+ 0,
28903
+ 1
28904
+ ],
28905
+ flights: flights.map((flight) => {
28906
+ if (typeof flight !== "object" || flight === null) return flight;
28907
+ const f = flight;
28908
+ const world = frameMul(elementFrame, frameFromPlacement({
28909
+ origin: f["origin"] ?? [
28910
+ 0,
28911
+ 0,
28912
+ 0
28913
+ ],
28914
+ axisX: f["axisX"] ?? DEFAULT_AXIS_X,
28915
+ axisZ: f["axisZ"] ?? DEFAULT_AXIS_Z
28916
+ }));
28917
+ const placed = decomposeFrame(frameMul(toSpatial, world));
28918
+ return {
28919
+ ...f,
28920
+ origin: placed.origin,
28921
+ axisX: placed.axisX,
28922
+ axisZ: placed.axisZ
28923
+ };
28924
+ })
28925
+ };
28926
+ }
28927
+ const placed = decomposeFrame(frameMul(toSpatial, elementBodyFrame(el, cumulativeFrame)));
28928
+ return {
28929
+ ...flatInput,
28930
+ origin: placed.origin,
28931
+ axisX: placed.axisX,
28932
+ axisZ: placed.axisZ
28933
+ };
28934
+ }
28935
+ /** Moves an owned world-baked body into its spatial container's local frame.
28936
+ * A pure translation keeps the fast `translate` path (identical to the prior
28937
+ * behaviour); a rotated container applies the inverse frame via `applyMatrix`. */
28938
+ function localizeBodyToFrame(el, body, spatialFrame, errorCode, frameName) {
28939
+ if (isPureTranslation(spatialFrame)) return localizeOwnedSolid(el, body, frameOrigin(spatialFrame), errorCode, frameName);
28940
+ const inverse = decomposeFrame(frameInverse(spatialFrame));
28941
+ try {
28942
+ const localized = (0, brepjs.applyMatrix)(body, placementToMatrix(inverse));
28943
+ if (!localized.ok) {
28944
+ body[Symbol.dispose]();
28945
+ return (0, brepjs.err)(specError(errorCode, `familiesToBim: '${el.keyPath}' could not move its body into the ${frameName} frame`, localized.error));
28946
+ }
28947
+ body[Symbol.dispose]();
28948
+ return (0, brepjs.ok)(localized.value);
28949
+ } catch (cause) {
28950
+ body[Symbol.dispose]();
28951
+ return (0, brepjs.err)(specError(errorCode, `familiesToBim: '${el.keyPath}' could not move its body into the ${frameName} frame`, cause));
28952
+ }
28953
+ }
28601
28954
  function civilSpatialInput(el, localTranslation) {
28602
28955
  const semantics = el.semantics;
28603
28956
  const composition = semantics !== void 0 && "composition" in semantics ? CIVIL_COMPOSITION[semantics.composition] : void 0;
@@ -28610,22 +28963,36 @@ function civilSpatialInput(el, localTranslation) {
28610
28963
  ...composition !== void 0 ? { compositionType: composition } : {}
28611
28964
  };
28612
28965
  }
28966
+ /** Relativizes a civil node's world frame against its parent civil frame into
28967
+ * the origin/axisX/axisZ the spatial specs consume. */
28968
+ function civilSpatialFrameInput(el, nodeFrame, parentFrame) {
28969
+ const semantics = el.semantics;
28970
+ const composition = semantics !== void 0 && "composition" in semantics ? CIVIL_COMPOSITION[semantics.composition] : void 0;
28971
+ const local = decomposeFrame(frameMul(frameInverse(parentFrame), nodeFrame));
28972
+ return {
28973
+ name: semanticName(el),
28974
+ origin: local.origin,
28975
+ axisX: local.axisX,
28976
+ axisZ: local.axisZ,
28977
+ ...composition !== void 0 ? { compositionType: composition } : {}
28978
+ };
28979
+ }
28613
28980
  function civilParentAccepts(kind, parent) {
28614
28981
  if (kind === "site") return parent === "project";
28615
28982
  if (kind === "bridge") return parent === "site";
28616
28983
  return parent === "bridge" || parent === "bridge-part";
28617
28984
  }
28618
- function addCivilSpatialOccurrence(model, el, kind, localTranslation) {
28985
+ function addCivilSpatialOccurrence(model, el, kind, input) {
28619
28986
  if (kind === "site") {
28620
28987
  if (el.semantics?.role !== "transport-site") return unsupportedCivilRole(el, "Site");
28621
- const parsed = parseSiteSpec(civilSpatialInput(el, localTranslation));
28988
+ const parsed = parseSiteSpec(input);
28622
28989
  return parsed.ok ? model.addSite(parsed.value, { stableKey: el.keyPath }) : parsed;
28623
28990
  }
28624
28991
  if (kind === "bridge") {
28625
28992
  const predefinedType = lookup(BRIDGE_ROLE, el.semantics?.role ?? "");
28626
28993
  if (predefinedType === void 0) return unsupportedCivilRole(el, "Bridge");
28627
28994
  const parsed = parseBridgeSpec({
28628
- ...civilSpatialInput(el, localTranslation),
28995
+ ...input,
28629
28996
  predefinedType
28630
28997
  });
28631
28998
  return parsed.ok ? model.addBridge(parsed.value, { stableKey: el.keyPath }) : parsed;
@@ -28634,7 +29001,7 @@ function addCivilSpatialOccurrence(model, el, kind, localTranslation) {
28634
29001
  const predefinedType = lookup(BRIDGE_PART_ROLE, el.semantics?.role ?? "");
28635
29002
  if (predefinedType === void 0) return unsupportedCivilRole(el, "Bridge Part");
28636
29003
  const parsed = parseBridgePartSpec({
28637
- ...civilSpatialInput(el, localTranslation),
29004
+ ...input,
28638
29005
  predefinedType,
28639
29006
  usageType: subdivision !== void 0 ? CIVIL_USAGE[subdivision] : "NOTDEFINED"
28640
29007
  });
@@ -28661,7 +29028,7 @@ function relativeSpecInput(input, projectedSpatialTranslation) {
28661
29028
  origin: relativeOrigin(input["origin"])
28662
29029
  };
28663
29030
  }
28664
- function addEarthworksFillElement(model, el, evaluator, projectedSpatialTranslation) {
29031
+ function addEarthworksFillElement(model, el, evaluator, spatialFrame) {
28665
29032
  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
29033
  const predefinedType = lookup(EARTHWORKS_FILL_ROLE, el.semantics.role);
28667
29034
  if (predefinedType === void 0) return unsupportedCivilRole(el, "Earthworks Fill");
@@ -28672,7 +29039,7 @@ function addEarthworksFillElement(model, el, evaluator, projectedSpatialTranslat
28672
29039
  routeName: "Earthworks Fill"
28673
29040
  });
28674
29041
  if (!body.ok) return body;
28675
- const localized = localizeOwnedSolid(el, body.value, projectedSpatialTranslation, "FAMILIES_EARTHWORKS_LOCALIZE_FAILED", "Bridge Part");
29042
+ const localized = localizeBodyToFrame(el, body.value, spatialFrame, "FAMILIES_EARTHWORKS_LOCALIZE_FAILED", "Bridge Part");
28676
29043
  if (!localized.ok) return localized;
28677
29044
  const specProps = collectSpecProps(el);
28678
29045
  const authoredMaterial = el.props["materialName"];
@@ -28722,10 +29089,14 @@ function familiesToBim(root, options) {
28722
29089
  const walk = (el, state) => {
28723
29090
  const rotatedHere = state.rotated || hasRotateOp(el);
28724
29091
  const cumulativeTranslationHere = addTranslation(state.cumulativeTranslation, authoredTranslation(el));
29092
+ const cumulativeFrameHere = frameMul(state.cumulativeFrame, frameFromOps(el.localTransforms));
28725
29093
  let proxiedHere = false;
29094
+ let nextRotated = rotatedHere;
28726
29095
  let nextSpatialStructureId = state.spatialStructureId;
28727
29096
  let nextCivilParent = state.civilParent;
28728
29097
  let nextProjectedSpatialTranslation = state.projectedSpatialTranslation;
29098
+ let nextCumulativeFrame = cumulativeFrameHere;
29099
+ let nextSpatialFrame = state.spatialFrame;
28729
29100
  const archetype = archetypeFor(el);
28730
29101
  const effectiveArchetype = el.semantics?.kind === "product" ? civilProductArchetype(el) : archetype;
28731
29102
  const route = specRoute(effectiveArchetype);
@@ -28734,15 +29105,19 @@ function familiesToBim(root, options) {
28734
29105
  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
29106
  const keyed = requireKeyed(el);
28736
29107
  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
29108
  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);
29109
+ const nodeFrame = civilNodeFrame(el, cumulativeFrameHere);
29110
+ const rotatedFrame = rotatedHere || hasRotatedAxes(el);
29111
+ const input = rotatedFrame ? civilSpatialFrameInput(el, nodeFrame, state.spatialFrame) : civilSpatialInput(el, subtractTranslation(cumulativeTranslationHere, state.projectedSpatialTranslation));
29112
+ const added = addCivilSpatialOccurrence(model, el, civilKind, input);
28741
29113
  if (!added.ok) return added;
28742
29114
  model.aggregate(state.spatialStructureId, added.value);
28743
29115
  idByKeyPath.set(el.keyPath, added.value);
28744
29116
  nextSpatialStructureId = added.value;
28745
29117
  nextCivilParent = civilKind;
29118
+ nextRotated = rotatedFrame;
29119
+ nextCumulativeFrame = nodeFrame;
29120
+ nextSpatialFrame = nodeFrame;
28746
29121
  nextProjectedSpatialTranslation = addTranslation(cumulativeTranslationHere, authoredSpecOrigin(el));
28747
29122
  } 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
29123
  else if (archetype === "storey") {
@@ -28763,21 +29138,22 @@ function familiesToBim(root, options) {
28763
29138
  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
29139
  const keyed = requireKeyed(el);
28765
29140
  if (!keyed.ok) return keyed;
28766
- const added = addEarthworksFillElement(model, el, bodyEvaluator, state.projectedSpatialTranslation);
29141
+ const added = addEarthworksFillElement(model, el, bodyEvaluator, state.spatialFrame);
28767
29142
  if (!added.ok) return added;
28768
29143
  model.placeIn(added.value, nextSpatialStructureId);
28769
29144
  idByKeyPath.set(el.keyPath, added.value);
28770
29145
  } else if (route !== void 0) {
28771
29146
  const keyed = requireKeyed(el);
28772
29147
  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
29148
  const voids = el.props["voids"];
28775
29149
  if (Array.isArray(voids)) {
28776
29150
  const openings = el.children.filter((c) => c.type === "Opening").length;
28777
29151
  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
29152
  }
28779
29153
  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);
29154
+ const rotatedBase = Array.isArray(routedInput["flights"]) ? specInput(el) : routedInput;
29155
+ const placedInput = rotatedHere ? rotatedRoutedInput(rotatedBase, el, cumulativeFrameHere, state.spatialFrame) : usesAuthoredCivilHierarchy ? relativeSpecInput(routedInput, state.projectedSpatialTranslation) : routedInput;
29156
+ const parsed = route.parse(placedInput);
28781
29157
  if (!parsed.ok) return parsed;
28782
29158
  const added = route.add(model, parsed.value, el.keyPath);
28783
29159
  if (!added.ok) return added;
@@ -28794,7 +29170,7 @@ function familiesToBim(root, options) {
28794
29170
  const keyed = requireKeyed(el);
28795
29171
  if (!keyed.ok) return keyed;
28796
29172
  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);
29173
+ const added = addProxyElement(model, el, options.proxyEvaluator, state.spatialFrame);
28798
29174
  if (!added.ok) return added;
28799
29175
  model.placeIn(added.value, nextSpatialStructureId);
28800
29176
  idByKeyPath.set(el.keyPath, added.value);
@@ -28810,9 +29186,11 @@ function familiesToBim(root, options) {
28810
29186
  const r = walk(child, {
28811
29187
  spatialStructureId: nextSpatialStructureId,
28812
29188
  civilParent: nextCivilParent,
28813
- rotated: rotatedHere,
29189
+ rotated: nextRotated,
28814
29190
  cumulativeTranslation: cumulativeTranslationHere,
28815
- projectedSpatialTranslation: nextProjectedSpatialTranslation
29191
+ projectedSpatialTranslation: nextProjectedSpatialTranslation,
29192
+ cumulativeFrame: nextCumulativeFrame,
29193
+ spatialFrame: nextSpatialFrame
28816
29194
  });
28817
29195
  if (!r.ok) return r;
28818
29196
  }
@@ -28823,7 +29201,9 @@ function familiesToBim(root, options) {
28823
29201
  civilParent: "project",
28824
29202
  rotated: false,
28825
29203
  cumulativeTranslation: ZERO_TRANSLATION,
28826
- projectedSpatialTranslation: ZERO_TRANSLATION
29204
+ projectedSpatialTranslation: ZERO_TRANSLATION,
29205
+ cumulativeFrame: IDENTITY_FRAME,
29206
+ spatialFrame: IDENTITY_FRAME
28827
29207
  });
28828
29208
  if (!walked.ok) {
28829
29209
  model[Symbol.dispose]();