brepjs-bim 0.4.0 → 0.6.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.
- package/dist/brepjs-bim.cjs +128 -29
- package/dist/brepjs-bim.js +128 -29
- package/dist/index.d.ts +1 -1
- package/dist/model/bimModel.d.ts +8 -3
- package/package.json +1 -1
package/dist/brepjs-bim.cjs
CHANGED
|
@@ -6350,6 +6350,14 @@ var BimModel = class {
|
|
|
6350
6350
|
if (key !== void 0 && this.#usedStableKeys.has(key)) return (0, brepjs.err)(specError("DUPLICATE_STABLE_KEY", `BimModel: duplicate stableKey '${key}'`));
|
|
6351
6351
|
return (0, brepjs.ok)(void 0);
|
|
6352
6352
|
}
|
|
6353
|
+
#checkOpeningKeys(options) {
|
|
6354
|
+
const filler = this.#checkStableKey(options);
|
|
6355
|
+
if (!filler.ok) return filler;
|
|
6356
|
+
const opening = this.#checkStableKey({ stableKey: options?.openingStableKey });
|
|
6357
|
+
if (!opening.ok) return opening;
|
|
6358
|
+
if (options?.stableKey !== void 0 && options.stableKey === options.openingStableKey) return (0, brepjs.err)(specError("DUPLICATE_STABLE_KEY", `BimModel: stableKey and openingStableKey are both '${options.stableKey}'`));
|
|
6359
|
+
return (0, brepjs.ok)(void 0);
|
|
6360
|
+
}
|
|
6353
6361
|
addWall(spec, options) {
|
|
6354
6362
|
const keyCheck = this.#checkStableKey(options);
|
|
6355
6363
|
if (!keyCheck.ok) return keyCheck;
|
|
@@ -6641,7 +6649,9 @@ var BimModel = class {
|
|
|
6641
6649
|
});
|
|
6642
6650
|
return (0, brepjs.ok)(id);
|
|
6643
6651
|
}
|
|
6644
|
-
addDoor(spec) {
|
|
6652
|
+
addDoor(spec, options) {
|
|
6653
|
+
const keyCheck = this.#checkOpeningKeys(options);
|
|
6654
|
+
if (!keyCheck.ok) return keyCheck;
|
|
6645
6655
|
const wall = this.#elements.get(spec.wallLocalId);
|
|
6646
6656
|
if (wall === void 0 || wall.category !== "WALL") return (0, brepjs.err)(specError("DOOR_WALL_NOT_FOUND", `No wall found for localId ${spec.wallLocalId}`));
|
|
6647
6657
|
if (spec.offsetAlongWall + spec.width > wall.spec.length) return (0, brepjs.err)(specError("DOOR_EXCEEDS_WALL_BOUNDS", "Door (offsetAlongWall + width) exceeds wall length"));
|
|
@@ -6656,13 +6666,13 @@ var BimModel = class {
|
|
|
6656
6666
|
const cutResult = this.#cutWallGeometry(wall, openingSpec);
|
|
6657
6667
|
if (!cutResult.ok) return (0, brepjs.err)(cutResult.error);
|
|
6658
6668
|
this.#replaceWallGeometry(wall, cutResult.value);
|
|
6659
|
-
const openingId = this.#makeElement("OPENING", openingSpec, null);
|
|
6669
|
+
const openingId = this.#makeElement("OPENING", openingSpec, null, options?.openingStableKey);
|
|
6660
6670
|
this.#makeRel({
|
|
6661
6671
|
kind: "VOIDS_WALL",
|
|
6662
6672
|
wallLocalId: spec.wallLocalId,
|
|
6663
6673
|
openingLocalId: openingId
|
|
6664
6674
|
});
|
|
6665
|
-
const doorId = this.#makeElement("DOOR", spec, null);
|
|
6675
|
+
const doorId = this.#makeElement("DOOR", spec, null, options?.stableKey);
|
|
6666
6676
|
this.#makeRel({
|
|
6667
6677
|
kind: "FILLS_OPENING",
|
|
6668
6678
|
openingLocalId: openingId,
|
|
@@ -6675,7 +6685,9 @@ var BimModel = class {
|
|
|
6675
6685
|
});
|
|
6676
6686
|
return (0, brepjs.ok)(doorId);
|
|
6677
6687
|
}
|
|
6678
|
-
addWindow(spec) {
|
|
6688
|
+
addWindow(spec, options) {
|
|
6689
|
+
const keyCheck = this.#checkOpeningKeys(options);
|
|
6690
|
+
if (!keyCheck.ok) return keyCheck;
|
|
6679
6691
|
const wall = this.#elements.get(spec.wallLocalId);
|
|
6680
6692
|
if (wall === void 0 || wall.category !== "WALL") return (0, brepjs.err)(specError("WINDOW_WALL_NOT_FOUND", `No wall found for localId ${spec.wallLocalId}`));
|
|
6681
6693
|
if (spec.offsetAlongWall + spec.width > wall.spec.length) return (0, brepjs.err)(specError("WINDOW_EXCEEDS_WALL_BOUNDS", "Window (offsetAlongWall + width) exceeds wall length"));
|
|
@@ -6690,13 +6702,13 @@ var BimModel = class {
|
|
|
6690
6702
|
const cutResult = this.#cutWallGeometry(wall, openingSpec);
|
|
6691
6703
|
if (!cutResult.ok) return (0, brepjs.err)(cutResult.error);
|
|
6692
6704
|
this.#replaceWallGeometry(wall, cutResult.value);
|
|
6693
|
-
const openingId = this.#makeElement("OPENING", openingSpec, null);
|
|
6705
|
+
const openingId = this.#makeElement("OPENING", openingSpec, null, options?.openingStableKey);
|
|
6694
6706
|
this.#makeRel({
|
|
6695
6707
|
kind: "VOIDS_WALL",
|
|
6696
6708
|
wallLocalId: spec.wallLocalId,
|
|
6697
6709
|
openingLocalId: openingId
|
|
6698
6710
|
});
|
|
6699
|
-
const windowId = this.#makeElement("WINDOW", spec, null);
|
|
6711
|
+
const windowId = this.#makeElement("WINDOW", spec, null, options?.stableKey);
|
|
6700
6712
|
this.#makeRel({
|
|
6701
6713
|
kind: "FILLS_OPENING",
|
|
6702
6714
|
openingLocalId: openingId,
|
|
@@ -6709,7 +6721,9 @@ var BimModel = class {
|
|
|
6709
6721
|
});
|
|
6710
6722
|
return (0, brepjs.ok)(windowId);
|
|
6711
6723
|
}
|
|
6712
|
-
addSlabOpening(input) {
|
|
6724
|
+
addSlabOpening(input, options) {
|
|
6725
|
+
const keyCheck = this.#checkStableKey(options);
|
|
6726
|
+
if (!keyCheck.ok) return keyCheck;
|
|
6713
6727
|
const slab = this.#elements.get(input.slabLocalId);
|
|
6714
6728
|
if (slab === void 0 || slab.category !== "SLAB") return (0, brepjs.err)(specError("SLAB_OPENING_SLAB_NOT_FOUND", `No slab found for localId ${input.slabLocalId}`));
|
|
6715
6729
|
if (input.offsetX + input.sizeX > slab.spec.length) return (0, brepjs.err)(specError("SLAB_OPENING_EXCEEDS_SLAB_BOUNDS", "Opening (offsetX + sizeX) exceeds slab length"));
|
|
@@ -6739,7 +6753,7 @@ var BimModel = class {
|
|
|
6739
6753
|
const cutResult = this.#cutSlabGeometry(slab, openingSpec);
|
|
6740
6754
|
if (!cutResult.ok) return (0, brepjs.err)(cutResult.error);
|
|
6741
6755
|
this.#replaceSlabGeometry(slab, cutResult.value);
|
|
6742
|
-
const openingId = this.#makeElement("OPENING", openingSpec, null);
|
|
6756
|
+
const openingId = this.#makeElement("OPENING", openingSpec, null, options?.stableKey);
|
|
6743
6757
|
this.#makeRel({
|
|
6744
6758
|
kind: "VOIDS_SLAB",
|
|
6745
6759
|
slabLocalId: input.slabLocalId,
|
|
@@ -16479,8 +16493,11 @@ function assignNum(target, key, value) {
|
|
|
16479
16493
|
* while the IR path serves the viewport and dedup. GlobalIds derive from
|
|
16480
16494
|
* families key paths (stable under reordering), not insertion order.
|
|
16481
16495
|
*
|
|
16482
|
-
*
|
|
16483
|
-
*
|
|
16496
|
+
* Scope: Storey containers, Wall/Slab elements, and wall openings — a
|
|
16497
|
+
* fill-role void (Door/Window family) maps onto addDoor/addWindow, which cut
|
|
16498
|
+
* the wall and wire IfcRelVoidsElement + IfcRelFillsElement; the opening and
|
|
16499
|
+
* filler GlobalIds derive from the synthesized key paths. Anonymous (non-fill)
|
|
16500
|
+
* voids stay IR-only and never reach the spec path.
|
|
16484
16501
|
*/
|
|
16485
16502
|
var SPEC_DEFAULTS = {
|
|
16486
16503
|
origin: [
|
|
@@ -16506,36 +16523,58 @@ var GEOMETRY_PROPS = /* @__PURE__ */ new Set([
|
|
|
16506
16523
|
"transform",
|
|
16507
16524
|
"psets"
|
|
16508
16525
|
]);
|
|
16509
|
-
/**
|
|
16510
|
-
*
|
|
16511
|
-
*
|
|
16512
|
-
|
|
16513
|
-
|
|
16514
|
-
const out = [...el.props["origin"] ?? [
|
|
16526
|
+
/** Total of the resolved geometry's OUTER literal translate chain. The
|
|
16527
|
+
* transform vocabulary is translate-only, so frame differences are exact
|
|
16528
|
+
* subtractions. Parameter-driven translations stop the peel. */
|
|
16529
|
+
function peelTranslates(node) {
|
|
16530
|
+
const total = [
|
|
16515
16531
|
0,
|
|
16516
16532
|
0,
|
|
16517
16533
|
0
|
|
16518
|
-
]
|
|
16534
|
+
];
|
|
16519
16535
|
let moved = false;
|
|
16520
|
-
let
|
|
16521
|
-
while (
|
|
16522
|
-
const v =
|
|
16536
|
+
let cur = node;
|
|
16537
|
+
while (cur.kind === "Translate") {
|
|
16538
|
+
const v = cur.vector;
|
|
16523
16539
|
if (v.kind !== "Vec3Lit") break;
|
|
16524
|
-
|
|
16525
|
-
|
|
16526
|
-
|
|
16540
|
+
total[0] += v.value[0];
|
|
16541
|
+
total[1] += v.value[1];
|
|
16542
|
+
total[2] += v.value[2];
|
|
16527
16543
|
moved = true;
|
|
16528
|
-
|
|
16544
|
+
cur = cur.target;
|
|
16529
16545
|
}
|
|
16530
|
-
return
|
|
16546
|
+
return {
|
|
16547
|
+
total,
|
|
16548
|
+
moved
|
|
16549
|
+
};
|
|
16550
|
+
}
|
|
16551
|
+
/** Fold the resolved geometry's outer translate chain into the spec placement
|
|
16552
|
+
* origin, so IfcLocalPlacement matches the IR world frame no matter where the
|
|
16553
|
+
* transform came from (family-internal or prop-level). */
|
|
16554
|
+
function composedOrigin(el) {
|
|
16555
|
+
const base = el.props["origin"] ?? [
|
|
16556
|
+
0,
|
|
16557
|
+
0,
|
|
16558
|
+
0
|
|
16559
|
+
];
|
|
16560
|
+
const { total, moved } = peelTranslates(el.geometry);
|
|
16561
|
+
if (!moved && el.props["origin"] === void 0) return void 0;
|
|
16562
|
+
return [
|
|
16563
|
+
base[0] + total[0],
|
|
16564
|
+
base[1] + total[1],
|
|
16565
|
+
base[2] + total[2]
|
|
16566
|
+
];
|
|
16567
|
+
}
|
|
16568
|
+
function stripGeometryProps(props) {
|
|
16569
|
+
const out = {};
|
|
16570
|
+
for (const [k, v] of Object.entries(props)) if (!GEOMETRY_PROPS.has(k)) out[k] = v;
|
|
16571
|
+
return out;
|
|
16531
16572
|
}
|
|
16532
16573
|
function specInput(el) {
|
|
16533
|
-
const fromProps = {};
|
|
16534
|
-
for (const [k, v] of Object.entries(el.props)) if (!GEOMETRY_PROPS.has(k)) fromProps[k] = v;
|
|
16535
16574
|
const origin = composedOrigin(el);
|
|
16536
16575
|
return {
|
|
16537
16576
|
...SPEC_DEFAULTS,
|
|
16538
|
-
...
|
|
16577
|
+
...stripGeometryProps(el.props),
|
|
16539
16578
|
...origin ? { origin } : {},
|
|
16540
16579
|
...collectSpecProps(el)
|
|
16541
16580
|
};
|
|
@@ -16554,6 +16593,56 @@ function collectSpecProps(el) {
|
|
|
16554
16593
|
delete out["psets"];
|
|
16555
16594
|
return out;
|
|
16556
16595
|
}
|
|
16596
|
+
function addFill(model, fill, input, identity) {
|
|
16597
|
+
if (fill.type === "Door") {
|
|
16598
|
+
const parsed = parseDoorSpec(input);
|
|
16599
|
+
if (!parsed.ok) return parsed;
|
|
16600
|
+
return model.addDoor(parsed.value, identity);
|
|
16601
|
+
}
|
|
16602
|
+
if (fill.type === "Window") {
|
|
16603
|
+
const parsed = parseWindowSpec(input);
|
|
16604
|
+
if (!parsed.ok) return parsed;
|
|
16605
|
+
return model.addWindow(parsed.value, identity);
|
|
16606
|
+
}
|
|
16607
|
+
return (0, brepjs.err)(specError("FAMILIES_UNSUPPORTED_FILL", `familiesToBim: no fill mapping for element type '${fill.type}' at '${fill.keyPath}'`));
|
|
16608
|
+
}
|
|
16609
|
+
/** Map a wall's synthesized Opening children onto addDoor/addWindow. The
|
|
16610
|
+
* wall-relative offsets come from the void geometry's frame minus the host's:
|
|
16611
|
+
* exact because both carry the same outer host transform. */
|
|
16612
|
+
function addOpenings(model, host, wallId, containerId, idByKeyPath) {
|
|
16613
|
+
const hostT = peelTranslates(host.geometry).total;
|
|
16614
|
+
for (const opening of host.children) {
|
|
16615
|
+
if (opening.type !== "Opening") continue;
|
|
16616
|
+
const fill = opening.children[0];
|
|
16617
|
+
if (fill === void 0) return (0, brepjs.err)(specError("FAMILIES_OPENING_NO_FILL", `familiesToBim: opening '${opening.keyPath}' has no fill element`));
|
|
16618
|
+
const keyed = requireKeyed(opening);
|
|
16619
|
+
if (!keyed.ok) return keyed;
|
|
16620
|
+
const fillT = peelTranslates(opening.geometry).total;
|
|
16621
|
+
const added = addFill(model, fill, {
|
|
16622
|
+
materialName: SPEC_DEFAULTS.materialName,
|
|
16623
|
+
...stripGeometryProps(fill.props),
|
|
16624
|
+
wallLocalId: wallId,
|
|
16625
|
+
offsetAlongWall: fillT[0] - hostT[0],
|
|
16626
|
+
offsetFromFloor: fillT[2] - hostT[2]
|
|
16627
|
+
}, {
|
|
16628
|
+
stableKey: fill.keyPath,
|
|
16629
|
+
openingStableKey: opening.keyPath
|
|
16630
|
+
});
|
|
16631
|
+
if (!added.ok) return added;
|
|
16632
|
+
model.placeIn(added.value, containerId);
|
|
16633
|
+
idByKeyPath.set(fill.keyPath, added.value);
|
|
16634
|
+
const fillsRel = model.getAllRelationships().find((r) => r.kind === "FILLS_OPENING" && r.fillerLocalId === added.value);
|
|
16635
|
+
if (fillsRel !== void 0) idByKeyPath.set(opening.keyPath, fillsRel.openingLocalId);
|
|
16636
|
+
}
|
|
16637
|
+
return (0, brepjs.ok)(void 0);
|
|
16638
|
+
}
|
|
16639
|
+
/** Every element that mints an IFC identity needs an explicit key: an
|
|
16640
|
+
* index-fallback path is order-dependent, which would silently break the
|
|
16641
|
+
* reorder-stable GlobalId contract. */
|
|
16642
|
+
function requireKeyed(el) {
|
|
16643
|
+
if (el.keyed) return (0, brepjs.ok)(void 0);
|
|
16644
|
+
return (0, brepjs.err)(specError("FAMILIES_UNKEYED_ELEMENT", `familiesToBim: '${el.keyPath}' has no explicit key — IFC identity needs order-independent key paths (add a key to the element)`));
|
|
16645
|
+
}
|
|
16557
16646
|
/**
|
|
16558
16647
|
* Project a resolved families tree into an eager BimModel. The caller owns
|
|
16559
16648
|
* the returned model (`using`); families stays domain-neutral — this adapter
|
|
@@ -16570,6 +16659,8 @@ function familiesToBim(root, options) {
|
|
|
16570
16659
|
const walk = (el, storeyId) => {
|
|
16571
16660
|
let containerId = storeyId;
|
|
16572
16661
|
if (el.type === "Storey") {
|
|
16662
|
+
const keyed = requireKeyed(el);
|
|
16663
|
+
if (!keyed.ok) return keyed;
|
|
16573
16664
|
const id = model.addStorey({
|
|
16574
16665
|
name: el.attributes["name"] ?? el.keyPath,
|
|
16575
16666
|
elevation: el.props["elevation"] ?? 0
|
|
@@ -16578,6 +16669,8 @@ function familiesToBim(root, options) {
|
|
|
16578
16669
|
idByKeyPath.set(el.keyPath, id);
|
|
16579
16670
|
containerId = id;
|
|
16580
16671
|
} else if (el.type === "Wall" || el.type === "Slab") {
|
|
16672
|
+
const keyed = requireKeyed(el);
|
|
16673
|
+
if (!keyed.ok) return keyed;
|
|
16581
16674
|
const parsed = el.type === "Wall" ? parseWallSpec(specInput(el)) : parseSlabSpec(specInput(el));
|
|
16582
16675
|
if (!parsed.ok) return parsed;
|
|
16583
16676
|
const added = el.type === "Wall" ? model.addWall(parsed.value, { stableKey: el.keyPath }) : model.addSlab(parsed.value, { stableKey: el.keyPath });
|
|
@@ -16585,8 +16678,14 @@ function familiesToBim(root, options) {
|
|
|
16585
16678
|
idByKeyPath.set(el.keyPath, added.value);
|
|
16586
16679
|
if (containerId === null) return (0, brepjs.err)(specError("FAMILIES_NO_STOREY", `familiesToBim: '${el.keyPath}' has no Storey ancestor — IFC elements need spatial containment`));
|
|
16587
16680
|
model.placeIn(added.value, containerId);
|
|
16588
|
-
|
|
16681
|
+
if (el.type === "Wall") {
|
|
16682
|
+
const opened = addOpenings(model, el, added.value, containerId, idByKeyPath);
|
|
16683
|
+
if (!opened.ok) return opened;
|
|
16684
|
+
}
|
|
16685
|
+
} else if (el.type === "Opening") return (0, brepjs.err)(specError("FAMILIES_OPENING_OUTSIDE_WALL", `familiesToBim: opening '${el.keyPath}' is not hosted by a Wall — only wall openings are mapped`));
|
|
16686
|
+
else if (el.type !== "Group" && el.geometry.kind !== "Empty") return (0, brepjs.err)(specError("FAMILIES_UNSUPPORTED_TYPE", `familiesToBim: no spec mapping for element type '${el.type}' at '${el.keyPath}'`));
|
|
16589
16687
|
for (const child of el.children) {
|
|
16688
|
+
if (el.type === "Wall" && child.type === "Opening") continue;
|
|
16590
16689
|
const r = walk(child, containerId);
|
|
16591
16690
|
if (!r.ok) return r;
|
|
16592
16691
|
}
|
package/dist/brepjs-bim.js
CHANGED
|
@@ -6327,6 +6327,14 @@ var BimModel = class {
|
|
|
6327
6327
|
if (key !== void 0 && this.#usedStableKeys.has(key)) return err(specError("DUPLICATE_STABLE_KEY", `BimModel: duplicate stableKey '${key}'`));
|
|
6328
6328
|
return ok(void 0);
|
|
6329
6329
|
}
|
|
6330
|
+
#checkOpeningKeys(options) {
|
|
6331
|
+
const filler = this.#checkStableKey(options);
|
|
6332
|
+
if (!filler.ok) return filler;
|
|
6333
|
+
const opening = this.#checkStableKey({ stableKey: options?.openingStableKey });
|
|
6334
|
+
if (!opening.ok) return opening;
|
|
6335
|
+
if (options?.stableKey !== void 0 && options.stableKey === options.openingStableKey) return err(specError("DUPLICATE_STABLE_KEY", `BimModel: stableKey and openingStableKey are both '${options.stableKey}'`));
|
|
6336
|
+
return ok(void 0);
|
|
6337
|
+
}
|
|
6330
6338
|
addWall(spec, options) {
|
|
6331
6339
|
const keyCheck = this.#checkStableKey(options);
|
|
6332
6340
|
if (!keyCheck.ok) return keyCheck;
|
|
@@ -6618,7 +6626,9 @@ var BimModel = class {
|
|
|
6618
6626
|
});
|
|
6619
6627
|
return ok(id);
|
|
6620
6628
|
}
|
|
6621
|
-
addDoor(spec) {
|
|
6629
|
+
addDoor(spec, options) {
|
|
6630
|
+
const keyCheck = this.#checkOpeningKeys(options);
|
|
6631
|
+
if (!keyCheck.ok) return keyCheck;
|
|
6622
6632
|
const wall = this.#elements.get(spec.wallLocalId);
|
|
6623
6633
|
if (wall === void 0 || wall.category !== "WALL") return err(specError("DOOR_WALL_NOT_FOUND", `No wall found for localId ${spec.wallLocalId}`));
|
|
6624
6634
|
if (spec.offsetAlongWall + spec.width > wall.spec.length) return err(specError("DOOR_EXCEEDS_WALL_BOUNDS", "Door (offsetAlongWall + width) exceeds wall length"));
|
|
@@ -6633,13 +6643,13 @@ var BimModel = class {
|
|
|
6633
6643
|
const cutResult = this.#cutWallGeometry(wall, openingSpec);
|
|
6634
6644
|
if (!cutResult.ok) return err(cutResult.error);
|
|
6635
6645
|
this.#replaceWallGeometry(wall, cutResult.value);
|
|
6636
|
-
const openingId = this.#makeElement("OPENING", openingSpec, null);
|
|
6646
|
+
const openingId = this.#makeElement("OPENING", openingSpec, null, options?.openingStableKey);
|
|
6637
6647
|
this.#makeRel({
|
|
6638
6648
|
kind: "VOIDS_WALL",
|
|
6639
6649
|
wallLocalId: spec.wallLocalId,
|
|
6640
6650
|
openingLocalId: openingId
|
|
6641
6651
|
});
|
|
6642
|
-
const doorId = this.#makeElement("DOOR", spec, null);
|
|
6652
|
+
const doorId = this.#makeElement("DOOR", spec, null, options?.stableKey);
|
|
6643
6653
|
this.#makeRel({
|
|
6644
6654
|
kind: "FILLS_OPENING",
|
|
6645
6655
|
openingLocalId: openingId,
|
|
@@ -6652,7 +6662,9 @@ var BimModel = class {
|
|
|
6652
6662
|
});
|
|
6653
6663
|
return ok(doorId);
|
|
6654
6664
|
}
|
|
6655
|
-
addWindow(spec) {
|
|
6665
|
+
addWindow(spec, options) {
|
|
6666
|
+
const keyCheck = this.#checkOpeningKeys(options);
|
|
6667
|
+
if (!keyCheck.ok) return keyCheck;
|
|
6656
6668
|
const wall = this.#elements.get(spec.wallLocalId);
|
|
6657
6669
|
if (wall === void 0 || wall.category !== "WALL") return err(specError("WINDOW_WALL_NOT_FOUND", `No wall found for localId ${spec.wallLocalId}`));
|
|
6658
6670
|
if (spec.offsetAlongWall + spec.width > wall.spec.length) return err(specError("WINDOW_EXCEEDS_WALL_BOUNDS", "Window (offsetAlongWall + width) exceeds wall length"));
|
|
@@ -6667,13 +6679,13 @@ var BimModel = class {
|
|
|
6667
6679
|
const cutResult = this.#cutWallGeometry(wall, openingSpec);
|
|
6668
6680
|
if (!cutResult.ok) return err(cutResult.error);
|
|
6669
6681
|
this.#replaceWallGeometry(wall, cutResult.value);
|
|
6670
|
-
const openingId = this.#makeElement("OPENING", openingSpec, null);
|
|
6682
|
+
const openingId = this.#makeElement("OPENING", openingSpec, null, options?.openingStableKey);
|
|
6671
6683
|
this.#makeRel({
|
|
6672
6684
|
kind: "VOIDS_WALL",
|
|
6673
6685
|
wallLocalId: spec.wallLocalId,
|
|
6674
6686
|
openingLocalId: openingId
|
|
6675
6687
|
});
|
|
6676
|
-
const windowId = this.#makeElement("WINDOW", spec, null);
|
|
6688
|
+
const windowId = this.#makeElement("WINDOW", spec, null, options?.stableKey);
|
|
6677
6689
|
this.#makeRel({
|
|
6678
6690
|
kind: "FILLS_OPENING",
|
|
6679
6691
|
openingLocalId: openingId,
|
|
@@ -6686,7 +6698,9 @@ var BimModel = class {
|
|
|
6686
6698
|
});
|
|
6687
6699
|
return ok(windowId);
|
|
6688
6700
|
}
|
|
6689
|
-
addSlabOpening(input) {
|
|
6701
|
+
addSlabOpening(input, options) {
|
|
6702
|
+
const keyCheck = this.#checkStableKey(options);
|
|
6703
|
+
if (!keyCheck.ok) return keyCheck;
|
|
6690
6704
|
const slab = this.#elements.get(input.slabLocalId);
|
|
6691
6705
|
if (slab === void 0 || slab.category !== "SLAB") return err(specError("SLAB_OPENING_SLAB_NOT_FOUND", `No slab found for localId ${input.slabLocalId}`));
|
|
6692
6706
|
if (input.offsetX + input.sizeX > slab.spec.length) return err(specError("SLAB_OPENING_EXCEEDS_SLAB_BOUNDS", "Opening (offsetX + sizeX) exceeds slab length"));
|
|
@@ -6716,7 +6730,7 @@ var BimModel = class {
|
|
|
6716
6730
|
const cutResult = this.#cutSlabGeometry(slab, openingSpec);
|
|
6717
6731
|
if (!cutResult.ok) return err(cutResult.error);
|
|
6718
6732
|
this.#replaceSlabGeometry(slab, cutResult.value);
|
|
6719
|
-
const openingId = this.#makeElement("OPENING", openingSpec, null);
|
|
6733
|
+
const openingId = this.#makeElement("OPENING", openingSpec, null, options?.stableKey);
|
|
6720
6734
|
this.#makeRel({
|
|
6721
6735
|
kind: "VOIDS_SLAB",
|
|
6722
6736
|
slabLocalId: input.slabLocalId,
|
|
@@ -16456,8 +16470,11 @@ function assignNum(target, key, value) {
|
|
|
16456
16470
|
* while the IR path serves the viewport and dedup. GlobalIds derive from
|
|
16457
16471
|
* families key paths (stable under reordering), not insertion order.
|
|
16458
16472
|
*
|
|
16459
|
-
*
|
|
16460
|
-
*
|
|
16473
|
+
* Scope: Storey containers, Wall/Slab elements, and wall openings — a
|
|
16474
|
+
* fill-role void (Door/Window family) maps onto addDoor/addWindow, which cut
|
|
16475
|
+
* the wall and wire IfcRelVoidsElement + IfcRelFillsElement; the opening and
|
|
16476
|
+
* filler GlobalIds derive from the synthesized key paths. Anonymous (non-fill)
|
|
16477
|
+
* voids stay IR-only and never reach the spec path.
|
|
16461
16478
|
*/
|
|
16462
16479
|
var SPEC_DEFAULTS = {
|
|
16463
16480
|
origin: [
|
|
@@ -16483,36 +16500,58 @@ var GEOMETRY_PROPS = /* @__PURE__ */ new Set([
|
|
|
16483
16500
|
"transform",
|
|
16484
16501
|
"psets"
|
|
16485
16502
|
]);
|
|
16486
|
-
/**
|
|
16487
|
-
*
|
|
16488
|
-
*
|
|
16489
|
-
|
|
16490
|
-
|
|
16491
|
-
const out = [...el.props["origin"] ?? [
|
|
16503
|
+
/** Total of the resolved geometry's OUTER literal translate chain. The
|
|
16504
|
+
* transform vocabulary is translate-only, so frame differences are exact
|
|
16505
|
+
* subtractions. Parameter-driven translations stop the peel. */
|
|
16506
|
+
function peelTranslates(node) {
|
|
16507
|
+
const total = [
|
|
16492
16508
|
0,
|
|
16493
16509
|
0,
|
|
16494
16510
|
0
|
|
16495
|
-
]
|
|
16511
|
+
];
|
|
16496
16512
|
let moved = false;
|
|
16497
|
-
let
|
|
16498
|
-
while (
|
|
16499
|
-
const v =
|
|
16513
|
+
let cur = node;
|
|
16514
|
+
while (cur.kind === "Translate") {
|
|
16515
|
+
const v = cur.vector;
|
|
16500
16516
|
if (v.kind !== "Vec3Lit") break;
|
|
16501
|
-
|
|
16502
|
-
|
|
16503
|
-
|
|
16517
|
+
total[0] += v.value[0];
|
|
16518
|
+
total[1] += v.value[1];
|
|
16519
|
+
total[2] += v.value[2];
|
|
16504
16520
|
moved = true;
|
|
16505
|
-
|
|
16521
|
+
cur = cur.target;
|
|
16506
16522
|
}
|
|
16507
|
-
return
|
|
16523
|
+
return {
|
|
16524
|
+
total,
|
|
16525
|
+
moved
|
|
16526
|
+
};
|
|
16527
|
+
}
|
|
16528
|
+
/** Fold the resolved geometry's outer translate chain into the spec placement
|
|
16529
|
+
* origin, so IfcLocalPlacement matches the IR world frame no matter where the
|
|
16530
|
+
* transform came from (family-internal or prop-level). */
|
|
16531
|
+
function composedOrigin(el) {
|
|
16532
|
+
const base = el.props["origin"] ?? [
|
|
16533
|
+
0,
|
|
16534
|
+
0,
|
|
16535
|
+
0
|
|
16536
|
+
];
|
|
16537
|
+
const { total, moved } = peelTranslates(el.geometry);
|
|
16538
|
+
if (!moved && el.props["origin"] === void 0) return void 0;
|
|
16539
|
+
return [
|
|
16540
|
+
base[0] + total[0],
|
|
16541
|
+
base[1] + total[1],
|
|
16542
|
+
base[2] + total[2]
|
|
16543
|
+
];
|
|
16544
|
+
}
|
|
16545
|
+
function stripGeometryProps(props) {
|
|
16546
|
+
const out = {};
|
|
16547
|
+
for (const [k, v] of Object.entries(props)) if (!GEOMETRY_PROPS.has(k)) out[k] = v;
|
|
16548
|
+
return out;
|
|
16508
16549
|
}
|
|
16509
16550
|
function specInput(el) {
|
|
16510
|
-
const fromProps = {};
|
|
16511
|
-
for (const [k, v] of Object.entries(el.props)) if (!GEOMETRY_PROPS.has(k)) fromProps[k] = v;
|
|
16512
16551
|
const origin = composedOrigin(el);
|
|
16513
16552
|
return {
|
|
16514
16553
|
...SPEC_DEFAULTS,
|
|
16515
|
-
...
|
|
16554
|
+
...stripGeometryProps(el.props),
|
|
16516
16555
|
...origin ? { origin } : {},
|
|
16517
16556
|
...collectSpecProps(el)
|
|
16518
16557
|
};
|
|
@@ -16531,6 +16570,56 @@ function collectSpecProps(el) {
|
|
|
16531
16570
|
delete out["psets"];
|
|
16532
16571
|
return out;
|
|
16533
16572
|
}
|
|
16573
|
+
function addFill(model, fill, input, identity) {
|
|
16574
|
+
if (fill.type === "Door") {
|
|
16575
|
+
const parsed = parseDoorSpec(input);
|
|
16576
|
+
if (!parsed.ok) return parsed;
|
|
16577
|
+
return model.addDoor(parsed.value, identity);
|
|
16578
|
+
}
|
|
16579
|
+
if (fill.type === "Window") {
|
|
16580
|
+
const parsed = parseWindowSpec(input);
|
|
16581
|
+
if (!parsed.ok) return parsed;
|
|
16582
|
+
return model.addWindow(parsed.value, identity);
|
|
16583
|
+
}
|
|
16584
|
+
return err(specError("FAMILIES_UNSUPPORTED_FILL", `familiesToBim: no fill mapping for element type '${fill.type}' at '${fill.keyPath}'`));
|
|
16585
|
+
}
|
|
16586
|
+
/** Map a wall's synthesized Opening children onto addDoor/addWindow. The
|
|
16587
|
+
* wall-relative offsets come from the void geometry's frame minus the host's:
|
|
16588
|
+
* exact because both carry the same outer host transform. */
|
|
16589
|
+
function addOpenings(model, host, wallId, containerId, idByKeyPath) {
|
|
16590
|
+
const hostT = peelTranslates(host.geometry).total;
|
|
16591
|
+
for (const opening of host.children) {
|
|
16592
|
+
if (opening.type !== "Opening") continue;
|
|
16593
|
+
const fill = opening.children[0];
|
|
16594
|
+
if (fill === void 0) return err(specError("FAMILIES_OPENING_NO_FILL", `familiesToBim: opening '${opening.keyPath}' has no fill element`));
|
|
16595
|
+
const keyed = requireKeyed(opening);
|
|
16596
|
+
if (!keyed.ok) return keyed;
|
|
16597
|
+
const fillT = peelTranslates(opening.geometry).total;
|
|
16598
|
+
const added = addFill(model, fill, {
|
|
16599
|
+
materialName: SPEC_DEFAULTS.materialName,
|
|
16600
|
+
...stripGeometryProps(fill.props),
|
|
16601
|
+
wallLocalId: wallId,
|
|
16602
|
+
offsetAlongWall: fillT[0] - hostT[0],
|
|
16603
|
+
offsetFromFloor: fillT[2] - hostT[2]
|
|
16604
|
+
}, {
|
|
16605
|
+
stableKey: fill.keyPath,
|
|
16606
|
+
openingStableKey: opening.keyPath
|
|
16607
|
+
});
|
|
16608
|
+
if (!added.ok) return added;
|
|
16609
|
+
model.placeIn(added.value, containerId);
|
|
16610
|
+
idByKeyPath.set(fill.keyPath, added.value);
|
|
16611
|
+
const fillsRel = model.getAllRelationships().find((r) => r.kind === "FILLS_OPENING" && r.fillerLocalId === added.value);
|
|
16612
|
+
if (fillsRel !== void 0) idByKeyPath.set(opening.keyPath, fillsRel.openingLocalId);
|
|
16613
|
+
}
|
|
16614
|
+
return ok(void 0);
|
|
16615
|
+
}
|
|
16616
|
+
/** Every element that mints an IFC identity needs an explicit key: an
|
|
16617
|
+
* index-fallback path is order-dependent, which would silently break the
|
|
16618
|
+
* reorder-stable GlobalId contract. */
|
|
16619
|
+
function requireKeyed(el) {
|
|
16620
|
+
if (el.keyed) return ok(void 0);
|
|
16621
|
+
return err(specError("FAMILIES_UNKEYED_ELEMENT", `familiesToBim: '${el.keyPath}' has no explicit key — IFC identity needs order-independent key paths (add a key to the element)`));
|
|
16622
|
+
}
|
|
16534
16623
|
/**
|
|
16535
16624
|
* Project a resolved families tree into an eager BimModel. The caller owns
|
|
16536
16625
|
* the returned model (`using`); families stays domain-neutral — this adapter
|
|
@@ -16547,6 +16636,8 @@ function familiesToBim(root, options) {
|
|
|
16547
16636
|
const walk = (el, storeyId) => {
|
|
16548
16637
|
let containerId = storeyId;
|
|
16549
16638
|
if (el.type === "Storey") {
|
|
16639
|
+
const keyed = requireKeyed(el);
|
|
16640
|
+
if (!keyed.ok) return keyed;
|
|
16550
16641
|
const id = model.addStorey({
|
|
16551
16642
|
name: el.attributes["name"] ?? el.keyPath,
|
|
16552
16643
|
elevation: el.props["elevation"] ?? 0
|
|
@@ -16555,6 +16646,8 @@ function familiesToBim(root, options) {
|
|
|
16555
16646
|
idByKeyPath.set(el.keyPath, id);
|
|
16556
16647
|
containerId = id;
|
|
16557
16648
|
} else if (el.type === "Wall" || el.type === "Slab") {
|
|
16649
|
+
const keyed = requireKeyed(el);
|
|
16650
|
+
if (!keyed.ok) return keyed;
|
|
16558
16651
|
const parsed = el.type === "Wall" ? parseWallSpec(specInput(el)) : parseSlabSpec(specInput(el));
|
|
16559
16652
|
if (!parsed.ok) return parsed;
|
|
16560
16653
|
const added = el.type === "Wall" ? model.addWall(parsed.value, { stableKey: el.keyPath }) : model.addSlab(parsed.value, { stableKey: el.keyPath });
|
|
@@ -16562,8 +16655,14 @@ function familiesToBim(root, options) {
|
|
|
16562
16655
|
idByKeyPath.set(el.keyPath, added.value);
|
|
16563
16656
|
if (containerId === null) return err(specError("FAMILIES_NO_STOREY", `familiesToBim: '${el.keyPath}' has no Storey ancestor — IFC elements need spatial containment`));
|
|
16564
16657
|
model.placeIn(added.value, containerId);
|
|
16565
|
-
|
|
16658
|
+
if (el.type === "Wall") {
|
|
16659
|
+
const opened = addOpenings(model, el, added.value, containerId, idByKeyPath);
|
|
16660
|
+
if (!opened.ok) return opened;
|
|
16661
|
+
}
|
|
16662
|
+
} else if (el.type === "Opening") return err(specError("FAMILIES_OPENING_OUTSIDE_WALL", `familiesToBim: opening '${el.keyPath}' is not hosted by a Wall — only wall openings are mapped`));
|
|
16663
|
+
else if (el.type !== "Group" && el.geometry.kind !== "Empty") return err(specError("FAMILIES_UNSUPPORTED_TYPE", `familiesToBim: no spec mapping for element type '${el.type}' at '${el.keyPath}'`));
|
|
16566
16664
|
for (const child of el.children) {
|
|
16665
|
+
if (el.type === "Wall" && child.type === "Opening") continue;
|
|
16567
16666
|
const r = walk(child, containerId);
|
|
16568
16667
|
if (!r.ok) return r;
|
|
16569
16668
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { BimModel } from './model/bimModel.js';
|
|
1
|
+
export { BimModel, type ElementIdentityOptions, type OpeningIdentityOptions, } from './model/bimModel.js';
|
|
2
2
|
export type { BimTreeNode, BimTreeSummary } from './model/treeSummary.js';
|
|
3
3
|
export { placedSolids } from './elementFns/placedGeometry.js';
|
|
4
4
|
export { toIfc, toIfcValidated } from './serialize/toIfc.js';
|
package/dist/model/bimModel.d.ts
CHANGED
|
@@ -28,6 +28,11 @@ import { ProjectSpec, SiteSpec, BuildingSpec, StoreySpec } from '../specs/spatia
|
|
|
28
28
|
export interface ElementIdentityOptions {
|
|
29
29
|
readonly stableKey?: string | undefined;
|
|
30
30
|
}
|
|
31
|
+
/** Identity options for adders that create TWO elements: `stableKey` names
|
|
32
|
+
* the filler (door/window), `openingStableKey` the synthesized opening. */
|
|
33
|
+
export interface OpeningIdentityOptions extends ElementIdentityOptions {
|
|
34
|
+
readonly openingStableKey?: string | undefined;
|
|
35
|
+
}
|
|
31
36
|
export declare class BimModel {
|
|
32
37
|
#private;
|
|
33
38
|
init(spec: ProjectSpec): Result<LocalId, BimError>;
|
|
@@ -126,9 +131,9 @@ export declare class BimModel {
|
|
|
126
131
|
* {@link ProxySpec.solid}).
|
|
127
132
|
*/
|
|
128
133
|
addProxy(spec: ProxySpec): Result<LocalId, BimError>;
|
|
129
|
-
addDoor(spec: DoorSpec): Result<LocalId, BimError>;
|
|
130
|
-
addWindow(spec: WindowSpec): Result<LocalId, BimError>;
|
|
131
|
-
addSlabOpening(input: SlabOpeningInput): Result<LocalId, BimError>;
|
|
134
|
+
addDoor(spec: DoorSpec, options?: OpeningIdentityOptions): Result<LocalId, BimError>;
|
|
135
|
+
addWindow(spec: WindowSpec, options?: OpeningIdentityOptions): Result<LocalId, BimError>;
|
|
136
|
+
addSlabOpening(input: SlabOpeningInput, options?: ElementIdentityOptions): Result<LocalId, BimError>;
|
|
132
137
|
getDoors(): BimElement<'DOOR'>[];
|
|
133
138
|
getWindows(): BimElement<'WINDOW'>[];
|
|
134
139
|
aggregate(parentId: LocalId, childId: LocalId): void;
|