brepjs-bim 0.10.0 → 0.12.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.
@@ -6378,10 +6378,12 @@ var BimModel = class {
6378
6378
  this.#associateClassification(id, spec);
6379
6379
  return (0, brepjs.ok)(id);
6380
6380
  }
6381
- addBeam(spec) {
6381
+ addBeam(spec, options) {
6382
+ const keyCheck = this.#checkStableKey(options);
6383
+ if (!keyCheck.ok) return keyCheck;
6382
6384
  const geomResult = beamToSolid(spec);
6383
6385
  if (!geomResult.ok) return (0, brepjs.err)(geomResult.error);
6384
- const id = this.#makeElement("BEAM", spec, geomResult.value);
6386
+ const id = this.#makeElement("BEAM", spec, geomResult.value, options?.stableKey);
6385
6387
  this.#associateMaterial(id, spec);
6386
6388
  this.#associateClassification(id, spec);
6387
6389
  return (0, brepjs.ok)(id);
@@ -6404,10 +6406,12 @@ var BimModel = class {
6404
6406
  this.#associateClassification(id, spec);
6405
6407
  return (0, brepjs.ok)(id);
6406
6408
  }
6407
- addRoof(spec) {
6409
+ addRoof(spec, options) {
6410
+ const keyCheck = this.#checkStableKey(options);
6411
+ if (!keyCheck.ok) return keyCheck;
6408
6412
  const geomResult = roofToSolid(spec);
6409
6413
  if (!geomResult.ok) return (0, brepjs.err)(geomResult.error);
6410
- const id = this.#makeElement("ROOF", spec, geomResult.value);
6414
+ const id = this.#makeElement("ROOF", spec, geomResult.value, options?.stableKey);
6411
6415
  this.#associateMaterial(id, spec);
6412
6416
  this.#associateClassification(id, spec);
6413
6417
  return (0, brepjs.ok)(id);
@@ -6441,8 +6445,10 @@ var BimModel = class {
6441
6445
  * the IFC layer from `spec.flights`; the STAIR element itself carries no solid
6442
6446
  * (the assembly container's Representation is null, valid per IFC4).
6443
6447
  */
6444
- addStair(spec) {
6445
- const id = this.#makeElement("STAIR", spec, null);
6448
+ addStair(spec, options) {
6449
+ const keyCheck = this.#checkStableKey(options);
6450
+ if (!keyCheck.ok) return keyCheck;
6451
+ const id = this.#makeElement("STAIR", spec, null, options?.stableKey);
6446
6452
  this.#associateMaterial(id, spec);
6447
6453
  this.#associateClassification(id, spec);
6448
6454
  return (0, brepjs.ok)(id);
@@ -6451,8 +6457,10 @@ var BimModel = class {
6451
6457
  * Adds an IfcRamp assembly. Geometry for each flight is built and written by the
6452
6458
  * IFC layer from `spec.flights`; the RAMP element carries no solid of its own.
6453
6459
  */
6454
- addRamp(spec) {
6455
- const id = this.#makeElement("RAMP", spec, null);
6460
+ addRamp(spec, options) {
6461
+ const keyCheck = this.#checkStableKey(options);
6462
+ if (!keyCheck.ok) return keyCheck;
6463
+ const id = this.#makeElement("RAMP", spec, null, options?.stableKey);
6456
6464
  this.#associateMaterial(id, spec);
6457
6465
  this.#associateClassification(id, spec);
6458
6466
  return (0, brepjs.ok)(id);
@@ -9628,13 +9636,15 @@ function writeRampEntity(w, guid, name, predefinedType, ownerHistoryId, localPla
9628
9636
  * per `spec.flights[i]` (each with a tessellated stepped-solid body), and a single
9629
9637
  * IfcRelAggregates linking the flights to the stair. Flight placements are
9630
9638
  * relative to the stair placement, which is relative to `parentPlacementId`
9631
- * (typically the storey). GUIDs are deterministic, keyed on `stairKey`/index so
9639
+ * (typically the storey). The assembly's GlobalId is the element's stored guid
9640
+ * (stableKey-derived when the caller provided one); flight, type, and
9641
+ * relationship GUIDs are deterministic, keyed on `stairKey`/index so
9632
9642
  * re-serializing an identical model is byte-stable.
9633
9643
  *
9634
9644
  * Returns a BimError if any flight solid cannot be built. Stair flights are real
9635
9645
  * stepped solids; tessellation failure is flagged via `geometrySimplified`.
9636
9646
  */
9637
- function writeStairAssembly(w, spec, stairKey, ownerHistoryId, geomSubContextId, parentPlacementId) {
9647
+ function writeStairAssembly(w, spec, stairKey, assemblyGuid, ownerHistoryId, geomSubContextId, parentPlacementId) {
9638
9648
  const predefinedType = spec.predefinedType ?? "NOTDEFINED";
9639
9649
  const stairName = spec.name ?? "Stair";
9640
9650
  if (spec.flights[0] === void 0) return {
@@ -9654,7 +9664,7 @@ function writeStairAssembly(w, spec, stairKey, ownerHistoryId, geomSubContextId,
9654
9664
  0,
9655
9665
  0
9656
9666
  ], parentPlacementId);
9657
- const assemblyExpressId = writeStairEntity(w, deriveIfcGuidSync(`elem:STAIR:${stairKey}`), stairName, predefinedType, ownerHistoryId, stairPlacementId);
9667
+ const assemblyExpressId = writeStairEntity(w, assemblyGuid, stairName, predefinedType, ownerHistoryId, stairPlacementId);
9658
9668
  const flightExpressIds = [];
9659
9669
  let geometrySimplified = false;
9660
9670
  for (const [i, flight] of spec.flights.entries()) try {
@@ -9694,7 +9704,7 @@ function writeStairAssembly(w, spec, stairKey, ownerHistoryId, geomSubContextId,
9694
9704
  * inclined-slab solids, so `geometrySimplified` is always true. GUIDs are
9695
9705
  * deterministic keyed on `rampKey`.
9696
9706
  */
9697
- function writeRampAssembly(w, spec, rampKey, ownerHistoryId, geomSubContextId, parentPlacementId) {
9707
+ function writeRampAssembly(w, spec, rampKey, assemblyGuid, ownerHistoryId, geomSubContextId, parentPlacementId) {
9698
9708
  const predefinedType = spec.predefinedType ?? "NOTDEFINED";
9699
9709
  const rampName = spec.name ?? "Ramp";
9700
9710
  if (spec.flights[0] === void 0) return {
@@ -9714,7 +9724,7 @@ function writeRampAssembly(w, spec, rampKey, ownerHistoryId, geomSubContextId, p
9714
9724
  0,
9715
9725
  0
9716
9726
  ], parentPlacementId);
9717
- const assemblyExpressId = writeRampEntity(w, deriveIfcGuidSync(`elem:RAMP:${rampKey}`), rampName, predefinedType, ownerHistoryId, rampPlacementId);
9727
+ const assemblyExpressId = writeRampEntity(w, assemblyGuid, rampName, predefinedType, ownerHistoryId, rampPlacementId);
9718
9728
  const flightExpressIds = [];
9719
9729
  const geometrySimplified = true;
9720
9730
  for (const [i, flight] of spec.flights.entries()) try {
@@ -12273,7 +12283,7 @@ async function toIfc(model, meta) {
12273
12283
  for (const stair of stairs) {
12274
12284
  const containingId = findContainerOf(stair.localId, relationships);
12275
12285
  const storeyPlacementId = containingId !== null ? placementMap.get(containingId) ?? null : null;
12276
- const result = writeStairAssembly(w, stair.spec, `${stair.localId}`, ownerHistoryId, geomSubContextId, storeyPlacementId);
12286
+ const result = writeStairAssembly(w, stair.spec, `${stair.localId}`, stair.guid, ownerHistoryId, geomSubContextId, storeyPlacementId);
12277
12287
  if (!result.ok) return (0, brepjs.err)(result.error);
12278
12288
  idMap.set(stair.localId, result.value.assemblyExpressId);
12279
12289
  writeStairCommonPset(w, ownerHistoryId, result.value.assemblyExpressId, stair.spec);
@@ -12281,7 +12291,7 @@ async function toIfc(model, meta) {
12281
12291
  for (const ramp of ramps) {
12282
12292
  const containingId = findContainerOf(ramp.localId, relationships);
12283
12293
  const storeyPlacementId = containingId !== null ? placementMap.get(containingId) ?? null : null;
12284
- const result = writeRampAssembly(w, ramp.spec, `${ramp.localId}`, ownerHistoryId, geomSubContextId, storeyPlacementId);
12294
+ const result = writeRampAssembly(w, ramp.spec, `${ramp.localId}`, ramp.guid, ownerHistoryId, geomSubContextId, storeyPlacementId);
12285
12295
  if (!result.ok) return (0, brepjs.err)(result.error);
12286
12296
  idMap.set(ramp.localId, result.value.assemblyExpressId);
12287
12297
  writeRampCommonPset(w, ownerHistoryId, result.value.assemblyExpressId, ramp.spec);
@@ -16495,7 +16505,7 @@ function assignNum(target, key, value) {
16495
16505
  * while the IR path serves the viewport and dedup. GlobalIds derive from
16496
16506
  * families key paths (stable under reordering), not insertion order.
16497
16507
  *
16498
- * Scope: Storey containers, Wall/Slab/Column elements, and wall openings — a
16508
+ * Scope: Storey containers, Wall/Slab/Column/Beam/Roof/Stair elements, and wall openings — a
16499
16509
  * fill-role void (Door/Window family) maps onto addDoor/addWindow, which cut
16500
16510
  * the wall and wire IfcRelVoidsElement + IfcRelFillsElement; the opening and
16501
16511
  * filler GlobalIds derive from the synthesized key paths. Anonymous (non-fill)
@@ -16525,6 +16535,65 @@ var GEOMETRY_PROPS = /* @__PURE__ */ new Set([
16525
16535
  "transform",
16526
16536
  "psets"
16527
16537
  ]);
16538
+ var SPEC_ROUTES = {
16539
+ Wall: {
16540
+ parse: parseWallSpec,
16541
+ add: (m, spec, key) => m.addWall(spec, { stableKey: key })
16542
+ },
16543
+ Slab: {
16544
+ parse: parseSlabSpec,
16545
+ add: (m, spec, key) => m.addSlab(spec, { stableKey: key })
16546
+ },
16547
+ Column: {
16548
+ parse: parseColumnSpec,
16549
+ add: (m, spec, key) => m.addColumn(spec, { stableKey: key })
16550
+ },
16551
+ Beam: {
16552
+ parse: parseBeamSpec,
16553
+ add: (m, spec, key) => m.addBeam(spec, { stableKey: key })
16554
+ },
16555
+ Roof: {
16556
+ parse: parseRoofSpec,
16557
+ add: (m, spec, key) => m.addRoof(spec, { stableKey: key })
16558
+ },
16559
+ Stair: {
16560
+ parse: parseStairSpec,
16561
+ add: (m, spec, key) => m.addStair(spec, { stableKey: key }),
16562
+ input: stairSpecInput
16563
+ }
16564
+ };
16565
+ /** StairSpec has no top-level origin — placement lives per flight — so the
16566
+ * element's folded translate lands on every flight's origin instead. */
16567
+ function stairSpecInput(el) {
16568
+ const base = specInput(el);
16569
+ const fold = base["origin"] ?? [
16570
+ 0,
16571
+ 0,
16572
+ 0
16573
+ ];
16574
+ const flights = Array.isArray(base["flights"]) ? base["flights"] : [];
16575
+ return {
16576
+ ...base,
16577
+ flights: flights.map((f) => {
16578
+ const fo = f["origin"] ?? [
16579
+ 0,
16580
+ 0,
16581
+ 0
16582
+ ];
16583
+ return {
16584
+ ...f,
16585
+ origin: [
16586
+ fo[0] + fold[0],
16587
+ fo[1] + fold[1],
16588
+ fo[2] + fold[2]
16589
+ ]
16590
+ };
16591
+ })
16592
+ };
16593
+ }
16594
+ function specRoute(type) {
16595
+ return Object.hasOwn(SPEC_ROUTES, type) ? SPEC_ROUTES[type] : void 0;
16596
+ }
16528
16597
  /** Total of the resolved geometry's OUTER literal translate chain. The
16529
16598
  * transform vocabulary is translate-only, so frame differences are exact
16530
16599
  * subtractions. Parameter-driven translations stop the peel. */
@@ -16670,6 +16739,7 @@ function familiesToBim(root, options) {
16670
16739
  const idByKeyPath = /* @__PURE__ */ new Map();
16671
16740
  const walk = (el, storeyId) => {
16672
16741
  let containerId = storeyId;
16742
+ const route = specRoute(el.type);
16673
16743
  if (el.type === "Storey") {
16674
16744
  const keyed = requireKeyed(el);
16675
16745
  if (!keyed.ok) return keyed;
@@ -16680,12 +16750,12 @@ function familiesToBim(root, options) {
16680
16750
  model.aggregate(buildingId, id);
16681
16751
  idByKeyPath.set(el.keyPath, id);
16682
16752
  containerId = id;
16683
- } else if (el.type === "Wall" || el.type === "Slab" || el.type === "Column") {
16753
+ } else if (route !== void 0) {
16684
16754
  const keyed = requireKeyed(el);
16685
16755
  if (!keyed.ok) return keyed;
16686
- const parsed = el.type === "Wall" ? parseWallSpec(specInput(el)) : el.type === "Slab" ? parseSlabSpec(specInput(el)) : parseColumnSpec(specInput(el));
16756
+ const parsed = route.parse(("input" in route ? route.input : specInput)(el));
16687
16757
  if (!parsed.ok) return parsed;
16688
- const added = el.type === "Wall" ? model.addWall(parsed.value, { stableKey: el.keyPath }) : el.type === "Slab" ? model.addSlab(parsed.value, { stableKey: el.keyPath }) : model.addColumn(parsed.value, { stableKey: el.keyPath });
16758
+ const added = route.add(model, parsed.value, el.keyPath);
16689
16759
  if (!added.ok) return added;
16690
16760
  idByKeyPath.set(el.keyPath, added.value);
16691
16761
  if (containerId === null) return (0, brepjs.err)(specError("FAMILIES_NO_STOREY", `familiesToBim: '${el.keyPath}' has no Storey ancestor — IFC elements need spatial containment`));
@@ -6355,10 +6355,12 @@ var BimModel = class {
6355
6355
  this.#associateClassification(id, spec);
6356
6356
  return ok(id);
6357
6357
  }
6358
- addBeam(spec) {
6358
+ addBeam(spec, options) {
6359
+ const keyCheck = this.#checkStableKey(options);
6360
+ if (!keyCheck.ok) return keyCheck;
6359
6361
  const geomResult = beamToSolid(spec);
6360
6362
  if (!geomResult.ok) return err(geomResult.error);
6361
- const id = this.#makeElement("BEAM", spec, geomResult.value);
6363
+ const id = this.#makeElement("BEAM", spec, geomResult.value, options?.stableKey);
6362
6364
  this.#associateMaterial(id, spec);
6363
6365
  this.#associateClassification(id, spec);
6364
6366
  return ok(id);
@@ -6381,10 +6383,12 @@ var BimModel = class {
6381
6383
  this.#associateClassification(id, spec);
6382
6384
  return ok(id);
6383
6385
  }
6384
- addRoof(spec) {
6386
+ addRoof(spec, options) {
6387
+ const keyCheck = this.#checkStableKey(options);
6388
+ if (!keyCheck.ok) return keyCheck;
6385
6389
  const geomResult = roofToSolid(spec);
6386
6390
  if (!geomResult.ok) return err(geomResult.error);
6387
- const id = this.#makeElement("ROOF", spec, geomResult.value);
6391
+ const id = this.#makeElement("ROOF", spec, geomResult.value, options?.stableKey);
6388
6392
  this.#associateMaterial(id, spec);
6389
6393
  this.#associateClassification(id, spec);
6390
6394
  return ok(id);
@@ -6418,8 +6422,10 @@ var BimModel = class {
6418
6422
  * the IFC layer from `spec.flights`; the STAIR element itself carries no solid
6419
6423
  * (the assembly container's Representation is null, valid per IFC4).
6420
6424
  */
6421
- addStair(spec) {
6422
- const id = this.#makeElement("STAIR", spec, null);
6425
+ addStair(spec, options) {
6426
+ const keyCheck = this.#checkStableKey(options);
6427
+ if (!keyCheck.ok) return keyCheck;
6428
+ const id = this.#makeElement("STAIR", spec, null, options?.stableKey);
6423
6429
  this.#associateMaterial(id, spec);
6424
6430
  this.#associateClassification(id, spec);
6425
6431
  return ok(id);
@@ -6428,8 +6434,10 @@ var BimModel = class {
6428
6434
  * Adds an IfcRamp assembly. Geometry for each flight is built and written by the
6429
6435
  * IFC layer from `spec.flights`; the RAMP element carries no solid of its own.
6430
6436
  */
6431
- addRamp(spec) {
6432
- const id = this.#makeElement("RAMP", spec, null);
6437
+ addRamp(spec, options) {
6438
+ const keyCheck = this.#checkStableKey(options);
6439
+ if (!keyCheck.ok) return keyCheck;
6440
+ const id = this.#makeElement("RAMP", spec, null, options?.stableKey);
6433
6441
  this.#associateMaterial(id, spec);
6434
6442
  this.#associateClassification(id, spec);
6435
6443
  return ok(id);
@@ -9605,13 +9613,15 @@ function writeRampEntity(w, guid, name, predefinedType, ownerHistoryId, localPla
9605
9613
  * per `spec.flights[i]` (each with a tessellated stepped-solid body), and a single
9606
9614
  * IfcRelAggregates linking the flights to the stair. Flight placements are
9607
9615
  * relative to the stair placement, which is relative to `parentPlacementId`
9608
- * (typically the storey). GUIDs are deterministic, keyed on `stairKey`/index so
9616
+ * (typically the storey). The assembly's GlobalId is the element's stored guid
9617
+ * (stableKey-derived when the caller provided one); flight, type, and
9618
+ * relationship GUIDs are deterministic, keyed on `stairKey`/index so
9609
9619
  * re-serializing an identical model is byte-stable.
9610
9620
  *
9611
9621
  * Returns a BimError if any flight solid cannot be built. Stair flights are real
9612
9622
  * stepped solids; tessellation failure is flagged via `geometrySimplified`.
9613
9623
  */
9614
- function writeStairAssembly(w, spec, stairKey, ownerHistoryId, geomSubContextId, parentPlacementId) {
9624
+ function writeStairAssembly(w, spec, stairKey, assemblyGuid, ownerHistoryId, geomSubContextId, parentPlacementId) {
9615
9625
  const predefinedType = spec.predefinedType ?? "NOTDEFINED";
9616
9626
  const stairName = spec.name ?? "Stair";
9617
9627
  if (spec.flights[0] === void 0) return {
@@ -9631,7 +9641,7 @@ function writeStairAssembly(w, spec, stairKey, ownerHistoryId, geomSubContextId,
9631
9641
  0,
9632
9642
  0
9633
9643
  ], parentPlacementId);
9634
- const assemblyExpressId = writeStairEntity(w, deriveIfcGuidSync(`elem:STAIR:${stairKey}`), stairName, predefinedType, ownerHistoryId, stairPlacementId);
9644
+ const assemblyExpressId = writeStairEntity(w, assemblyGuid, stairName, predefinedType, ownerHistoryId, stairPlacementId);
9635
9645
  const flightExpressIds = [];
9636
9646
  let geometrySimplified = false;
9637
9647
  for (const [i, flight] of spec.flights.entries()) try {
@@ -9671,7 +9681,7 @@ function writeStairAssembly(w, spec, stairKey, ownerHistoryId, geomSubContextId,
9671
9681
  * inclined-slab solids, so `geometrySimplified` is always true. GUIDs are
9672
9682
  * deterministic keyed on `rampKey`.
9673
9683
  */
9674
- function writeRampAssembly(w, spec, rampKey, ownerHistoryId, geomSubContextId, parentPlacementId) {
9684
+ function writeRampAssembly(w, spec, rampKey, assemblyGuid, ownerHistoryId, geomSubContextId, parentPlacementId) {
9675
9685
  const predefinedType = spec.predefinedType ?? "NOTDEFINED";
9676
9686
  const rampName = spec.name ?? "Ramp";
9677
9687
  if (spec.flights[0] === void 0) return {
@@ -9691,7 +9701,7 @@ function writeRampAssembly(w, spec, rampKey, ownerHistoryId, geomSubContextId, p
9691
9701
  0,
9692
9702
  0
9693
9703
  ], parentPlacementId);
9694
- const assemblyExpressId = writeRampEntity(w, deriveIfcGuidSync(`elem:RAMP:${rampKey}`), rampName, predefinedType, ownerHistoryId, rampPlacementId);
9704
+ const assemblyExpressId = writeRampEntity(w, assemblyGuid, rampName, predefinedType, ownerHistoryId, rampPlacementId);
9695
9705
  const flightExpressIds = [];
9696
9706
  const geometrySimplified = true;
9697
9707
  for (const [i, flight] of spec.flights.entries()) try {
@@ -12250,7 +12260,7 @@ async function toIfc(model, meta) {
12250
12260
  for (const stair of stairs) {
12251
12261
  const containingId = findContainerOf(stair.localId, relationships);
12252
12262
  const storeyPlacementId = containingId !== null ? placementMap.get(containingId) ?? null : null;
12253
- const result = writeStairAssembly(w, stair.spec, `${stair.localId}`, ownerHistoryId, geomSubContextId, storeyPlacementId);
12263
+ const result = writeStairAssembly(w, stair.spec, `${stair.localId}`, stair.guid, ownerHistoryId, geomSubContextId, storeyPlacementId);
12254
12264
  if (!result.ok) return err(result.error);
12255
12265
  idMap.set(stair.localId, result.value.assemblyExpressId);
12256
12266
  writeStairCommonPset(w, ownerHistoryId, result.value.assemblyExpressId, stair.spec);
@@ -12258,7 +12268,7 @@ async function toIfc(model, meta) {
12258
12268
  for (const ramp of ramps) {
12259
12269
  const containingId = findContainerOf(ramp.localId, relationships);
12260
12270
  const storeyPlacementId = containingId !== null ? placementMap.get(containingId) ?? null : null;
12261
- const result = writeRampAssembly(w, ramp.spec, `${ramp.localId}`, ownerHistoryId, geomSubContextId, storeyPlacementId);
12271
+ const result = writeRampAssembly(w, ramp.spec, `${ramp.localId}`, ramp.guid, ownerHistoryId, geomSubContextId, storeyPlacementId);
12262
12272
  if (!result.ok) return err(result.error);
12263
12273
  idMap.set(ramp.localId, result.value.assemblyExpressId);
12264
12274
  writeRampCommonPset(w, ownerHistoryId, result.value.assemblyExpressId, ramp.spec);
@@ -16472,7 +16482,7 @@ function assignNum(target, key, value) {
16472
16482
  * while the IR path serves the viewport and dedup. GlobalIds derive from
16473
16483
  * families key paths (stable under reordering), not insertion order.
16474
16484
  *
16475
- * Scope: Storey containers, Wall/Slab/Column elements, and wall openings — a
16485
+ * Scope: Storey containers, Wall/Slab/Column/Beam/Roof/Stair elements, and wall openings — a
16476
16486
  * fill-role void (Door/Window family) maps onto addDoor/addWindow, which cut
16477
16487
  * the wall and wire IfcRelVoidsElement + IfcRelFillsElement; the opening and
16478
16488
  * filler GlobalIds derive from the synthesized key paths. Anonymous (non-fill)
@@ -16502,6 +16512,65 @@ var GEOMETRY_PROPS = /* @__PURE__ */ new Set([
16502
16512
  "transform",
16503
16513
  "psets"
16504
16514
  ]);
16515
+ var SPEC_ROUTES = {
16516
+ Wall: {
16517
+ parse: parseWallSpec,
16518
+ add: (m, spec, key) => m.addWall(spec, { stableKey: key })
16519
+ },
16520
+ Slab: {
16521
+ parse: parseSlabSpec,
16522
+ add: (m, spec, key) => m.addSlab(spec, { stableKey: key })
16523
+ },
16524
+ Column: {
16525
+ parse: parseColumnSpec,
16526
+ add: (m, spec, key) => m.addColumn(spec, { stableKey: key })
16527
+ },
16528
+ Beam: {
16529
+ parse: parseBeamSpec,
16530
+ add: (m, spec, key) => m.addBeam(spec, { stableKey: key })
16531
+ },
16532
+ Roof: {
16533
+ parse: parseRoofSpec,
16534
+ add: (m, spec, key) => m.addRoof(spec, { stableKey: key })
16535
+ },
16536
+ Stair: {
16537
+ parse: parseStairSpec,
16538
+ add: (m, spec, key) => m.addStair(spec, { stableKey: key }),
16539
+ input: stairSpecInput
16540
+ }
16541
+ };
16542
+ /** StairSpec has no top-level origin — placement lives per flight — so the
16543
+ * element's folded translate lands on every flight's origin instead. */
16544
+ function stairSpecInput(el) {
16545
+ const base = specInput(el);
16546
+ const fold = base["origin"] ?? [
16547
+ 0,
16548
+ 0,
16549
+ 0
16550
+ ];
16551
+ const flights = Array.isArray(base["flights"]) ? base["flights"] : [];
16552
+ return {
16553
+ ...base,
16554
+ flights: flights.map((f) => {
16555
+ const fo = f["origin"] ?? [
16556
+ 0,
16557
+ 0,
16558
+ 0
16559
+ ];
16560
+ return {
16561
+ ...f,
16562
+ origin: [
16563
+ fo[0] + fold[0],
16564
+ fo[1] + fold[1],
16565
+ fo[2] + fold[2]
16566
+ ]
16567
+ };
16568
+ })
16569
+ };
16570
+ }
16571
+ function specRoute(type) {
16572
+ return Object.hasOwn(SPEC_ROUTES, type) ? SPEC_ROUTES[type] : void 0;
16573
+ }
16505
16574
  /** Total of the resolved geometry's OUTER literal translate chain. The
16506
16575
  * transform vocabulary is translate-only, so frame differences are exact
16507
16576
  * subtractions. Parameter-driven translations stop the peel. */
@@ -16647,6 +16716,7 @@ function familiesToBim(root, options) {
16647
16716
  const idByKeyPath = /* @__PURE__ */ new Map();
16648
16717
  const walk = (el, storeyId) => {
16649
16718
  let containerId = storeyId;
16719
+ const route = specRoute(el.type);
16650
16720
  if (el.type === "Storey") {
16651
16721
  const keyed = requireKeyed(el);
16652
16722
  if (!keyed.ok) return keyed;
@@ -16657,12 +16727,12 @@ function familiesToBim(root, options) {
16657
16727
  model.aggregate(buildingId, id);
16658
16728
  idByKeyPath.set(el.keyPath, id);
16659
16729
  containerId = id;
16660
- } else if (el.type === "Wall" || el.type === "Slab" || el.type === "Column") {
16730
+ } else if (route !== void 0) {
16661
16731
  const keyed = requireKeyed(el);
16662
16732
  if (!keyed.ok) return keyed;
16663
- const parsed = el.type === "Wall" ? parseWallSpec(specInput(el)) : el.type === "Slab" ? parseSlabSpec(specInput(el)) : parseColumnSpec(specInput(el));
16733
+ const parsed = route.parse(("input" in route ? route.input : specInput)(el));
16664
16734
  if (!parsed.ok) return parsed;
16665
- const added = el.type === "Wall" ? model.addWall(parsed.value, { stableKey: el.keyPath }) : el.type === "Slab" ? model.addSlab(parsed.value, { stableKey: el.keyPath }) : model.addColumn(parsed.value, { stableKey: el.keyPath });
16735
+ const added = route.add(model, parsed.value, el.keyPath);
16666
16736
  if (!added.ok) return added;
16667
16737
  idByKeyPath.set(el.keyPath, added.value);
16668
16738
  if (containerId === null) return err(specError("FAMILIES_NO_STOREY", `familiesToBim: '${el.keyPath}' has no Storey ancestor — IFC elements need spatial containment`));
@@ -20,13 +20,15 @@ export interface AssemblyWriteResult {
20
20
  * per `spec.flights[i]` (each with a tessellated stepped-solid body), and a single
21
21
  * IfcRelAggregates linking the flights to the stair. Flight placements are
22
22
  * relative to the stair placement, which is relative to `parentPlacementId`
23
- * (typically the storey). GUIDs are deterministic, keyed on `stairKey`/index so
23
+ * (typically the storey). The assembly's GlobalId is the element's stored guid
24
+ * (stableKey-derived when the caller provided one); flight, type, and
25
+ * relationship GUIDs are deterministic, keyed on `stairKey`/index so
24
26
  * re-serializing an identical model is byte-stable.
25
27
  *
26
28
  * Returns a BimError if any flight solid cannot be built. Stair flights are real
27
29
  * stepped solids; tessellation failure is flagged via `geometrySimplified`.
28
30
  */
29
- export declare function writeStairAssembly(w: IfcWriter, spec: StairSpec, stairKey: string, ownerHistoryId: number, geomSubContextId: number, parentPlacementId: number | null): {
31
+ export declare function writeStairAssembly(w: IfcWriter, spec: StairSpec, stairKey: string, assemblyGuid: string, ownerHistoryId: number, geomSubContextId: number, parentPlacementId: number | null): {
30
32
  ok: true;
31
33
  value: AssemblyWriteResult;
32
34
  } | {
@@ -40,7 +42,7 @@ export declare function writeStairAssembly(w: IfcWriter, spec: StairSpec, stairK
40
42
  * inclined-slab solids, so `geometrySimplified` is always true. GUIDs are
41
43
  * deterministic keyed on `rampKey`.
42
44
  */
43
- export declare function writeRampAssembly(w: IfcWriter, spec: RampSpec, rampKey: string, ownerHistoryId: number, geomSubContextId: number, parentPlacementId: number | null): {
45
+ export declare function writeRampAssembly(w: IfcWriter, spec: RampSpec, rampKey: string, assemblyGuid: string, ownerHistoryId: number, geomSubContextId: number, parentPlacementId: number | null): {
44
46
  ok: true;
45
47
  value: AssemblyWriteResult;
46
48
  } | {
@@ -42,10 +42,10 @@ export declare class BimModel {
42
42
  addStorey(spec: StoreySpec, options?: ElementIdentityOptions): LocalId;
43
43
  addWall(spec: WallSpec, options?: ElementIdentityOptions): Result<LocalId, BimError>;
44
44
  addSlab(spec: SlabSpec, options?: ElementIdentityOptions): Result<LocalId, BimError>;
45
- addBeam(spec: BeamSpec): Result<LocalId, BimError>;
45
+ addBeam(spec: BeamSpec, options?: ElementIdentityOptions): Result<LocalId, BimError>;
46
46
  addColumn(spec: ColumnSpec, options?: ElementIdentityOptions): Result<LocalId, BimError>;
47
47
  addSpace(spec: SpaceSpec): Result<LocalId, BimError>;
48
- addRoof(spec: RoofSpec): Result<LocalId, BimError>;
48
+ addRoof(spec: RoofSpec, options?: ElementIdentityOptions): Result<LocalId, BimError>;
49
49
  addCurtainWall(spec: CurtainWallSpec): Result<LocalId, BimError>;
50
50
  addFooting(spec: FootingSpec): Result<LocalId, BimError>;
51
51
  addPile(spec: PileSpec): Result<LocalId, BimError>;
@@ -54,12 +54,12 @@ export declare class BimModel {
54
54
  * the IFC layer from `spec.flights`; the STAIR element itself carries no solid
55
55
  * (the assembly container's Representation is null, valid per IFC4).
56
56
  */
57
- addStair(spec: StairSpec): Result<LocalId, BimError>;
57
+ addStair(spec: StairSpec, options?: ElementIdentityOptions): Result<LocalId, BimError>;
58
58
  /**
59
59
  * Adds an IfcRamp assembly. Geometry for each flight is built and written by the
60
60
  * IFC layer from `spec.flights`; the RAMP element carries no solid of its own.
61
61
  */
62
- addRamp(spec: RampSpec): Result<LocalId, BimError>;
62
+ addRamp(spec: RampSpec, options?: ElementIdentityOptions): Result<LocalId, BimError>;
63
63
  addRailing(spec: RailingSpec): Result<LocalId, BimError>;
64
64
  /**
65
65
  * Adds an IfcCovering. When `hostLocalId` is supplied, an
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brepjs-bim",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "BIM layer for brepjs — IFC4-aligned parametric building elements",
5
5
  "keywords": [
6
6
  "bim",
@@ -43,6 +43,7 @@
43
43
  "build": "vite build",
44
44
  "typecheck": "tsgo --noEmit",
45
45
  "test": "vitest run",
46
+ "test:coverage": "vitest run --coverage",
46
47
  "lint": "eslint src tests"
47
48
  },
48
49
  "peerDependencies": {