brepjs-bim 0.17.1 → 0.18.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/README.md +7 -0
- package/dist/brepjs-bim.cjs +55 -11
- package/dist/brepjs-bim.js +55 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,6 +14,13 @@ and serializes the result to a valid **IFC-SPF** file — with a matching import
|
|
|
14
14
|
Pipeline: **author spec → `BimModel` (typed element + brepjs geometry) → spatial structure +
|
|
15
15
|
property sets + classification → export IFC / COBie, validate, round-trip.**
|
|
16
16
|
|
|
17
|
+
The declarative entry point is
|
|
18
|
+
[`brepjs-families`](https://www.npmjs.com/package/brepjs-families): author the building as a tree of
|
|
19
|
+
prop-driven components, then `familiesToBim(tree, { project })` projects it onto a `BimModel` with
|
|
20
|
+
stable GlobalIds derived from component key paths. Hand-authoring specs against `BimModel` remains
|
|
21
|
+
the low-level path (and covers elements the declarative route doesn't yet). See the
|
|
22
|
+
[Declarative Models guide](https://brepjs.dev/families/overview).
|
|
23
|
+
|
|
17
24
|
## Scope
|
|
18
25
|
|
|
19
26
|
Parametric authoring of the common IFC4 building elements plus the data layers that make a model
|
package/dist/brepjs-bim.cjs
CHANGED
|
@@ -27624,7 +27624,8 @@ function assignNum(target, key, value) {
|
|
|
27624
27624
|
* fill-role void (Door/Window family) maps onto addDoor/addWindow, which cut
|
|
27625
27625
|
* the wall and wire IfcRelVoidsElement + IfcRelFillsElement; the opening and
|
|
27626
27626
|
* filler GlobalIds derive from the synthesized key paths. Anonymous (non-fill)
|
|
27627
|
-
* voids
|
|
27627
|
+
* voids are rejected: they cut only the IR/viewport geometry, and exporting
|
|
27628
|
+
* the uncut spec body would silently diverge from what the user sees.
|
|
27628
27629
|
*/
|
|
27629
27630
|
var SPEC_DEFAULTS = {
|
|
27630
27631
|
origin: [
|
|
@@ -27734,6 +27735,16 @@ function peelTranslates(node) {
|
|
|
27734
27735
|
moved
|
|
27735
27736
|
};
|
|
27736
27737
|
}
|
|
27738
|
+
/** True when the element's transform PROP carries a rotation. The spec path
|
|
27739
|
+
* folds only translations into IfcLocalPlacement (walls orient via `axisX`),
|
|
27740
|
+
* so a tRotate placement would export un-rotated while the viewport shows it
|
|
27741
|
+
* rotated — reject instead of diverging. Rotations a family render bakes
|
|
27742
|
+
* into its own body geometry (e.g. a circular beam oriented along axisX) are
|
|
27743
|
+
* fine: the spec rebuilds that body parametrically from props. */
|
|
27744
|
+
function hasRotateOp(el) {
|
|
27745
|
+
const ops = el.props["transform"];
|
|
27746
|
+
return Array.isArray(ops) && ops.some((op) => typeof op === "object" && op !== null && op.op === "rotate");
|
|
27747
|
+
}
|
|
27737
27748
|
/** Fold the resolved geometry's outer translate chain into the spec placement
|
|
27738
27749
|
* origin, so IfcLocalPlacement matches the IR world frame no matter where the
|
|
27739
27750
|
* transform came from (family-internal or prop-level). */
|
|
@@ -27765,18 +27776,43 @@ function specInput(el) {
|
|
|
27765
27776
|
...collectSpecProps(el)
|
|
27766
27777
|
};
|
|
27767
27778
|
}
|
|
27779
|
+
/** `Pset_<Type>Common` fields the specs model first-class: mapped onto their
|
|
27780
|
+
* spec names so the writer emits them in the element's own common pset. */
|
|
27781
|
+
var COMMON_PSET_FIELDS = {
|
|
27782
|
+
IsExternal: "isExternal",
|
|
27783
|
+
FireRating: "fireRating",
|
|
27784
|
+
AcousticRating: "acousticRating",
|
|
27785
|
+
ThermalTransmittance: "thermalTransmittance",
|
|
27786
|
+
LoadBearing: "loadBearing",
|
|
27787
|
+
Status: "status"
|
|
27788
|
+
};
|
|
27768
27789
|
function collectSpecProps(el) {
|
|
27769
|
-
const psets = el.attributes["psets"];
|
|
27770
27790
|
const out = {};
|
|
27791
|
+
const material = el.attributes["material"];
|
|
27792
|
+
if (typeof material === "string" && el.props["materialName"] === void 0) out["materialName"] = material;
|
|
27793
|
+
const psets = el.attributes["psets"];
|
|
27771
27794
|
if (psets && typeof psets === "object") {
|
|
27772
|
-
const
|
|
27773
|
-
|
|
27774
|
-
|
|
27775
|
-
if (typeof
|
|
27776
|
-
|
|
27795
|
+
const custom = {};
|
|
27796
|
+
const ownCommonPset = `Pset_${el.type}Common`;
|
|
27797
|
+
for (const [psetName, fields] of Object.entries(psets)) {
|
|
27798
|
+
if (!fields || typeof fields !== "object") continue;
|
|
27799
|
+
const record = fields;
|
|
27800
|
+
if (psetName === ownCommonPset) {
|
|
27801
|
+
for (const [field, specKey] of Object.entries(COMMON_PSET_FIELDS)) if (record[field] !== void 0) out[specKey] = record[field];
|
|
27802
|
+
} else {
|
|
27803
|
+
const values = {};
|
|
27804
|
+
for (const [field, value] of Object.entries(record)) if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") values[field] = value;
|
|
27805
|
+
if (Object.keys(values).length > 0) custom[psetName] = values;
|
|
27806
|
+
}
|
|
27807
|
+
}
|
|
27808
|
+
if (Object.keys(custom).length > 0) {
|
|
27809
|
+
const declared = el.props["customProperties"];
|
|
27810
|
+
out["customProperties"] = declared && typeof declared === "object" ? {
|
|
27811
|
+
...custom,
|
|
27812
|
+
...declared
|
|
27813
|
+
} : custom;
|
|
27777
27814
|
}
|
|
27778
27815
|
}
|
|
27779
|
-
delete out["psets"];
|
|
27780
27816
|
return out;
|
|
27781
27817
|
}
|
|
27782
27818
|
function addFill(model, fill, input, identity) {
|
|
@@ -27817,6 +27853,7 @@ function addOpenings(model, host, wallId, containerId, idByKeyPath) {
|
|
|
27817
27853
|
const added = addFill(model, fill, {
|
|
27818
27854
|
materialName: SPEC_DEFAULTS.materialName,
|
|
27819
27855
|
...stripGeometryProps(fill.props),
|
|
27856
|
+
...collectSpecProps(fill),
|
|
27820
27857
|
wallLocalId: wallId,
|
|
27821
27858
|
offsetAlongWall: delta[0] * axisX[0] + delta[1] * axisX[1] + delta[2] * axisX[2],
|
|
27822
27859
|
offsetFromFloor: delta[2]
|
|
@@ -27863,7 +27900,8 @@ function familiesToBim(root, options) {
|
|
|
27863
27900
|
if (project !== null) model.aggregate(project.localId, siteResult.value);
|
|
27864
27901
|
model.aggregate(siteResult.value, buildingId);
|
|
27865
27902
|
const idByKeyPath = /* @__PURE__ */ new Map();
|
|
27866
|
-
const walk = (el, storeyId) => {
|
|
27903
|
+
const walk = (el, storeyId, rotated) => {
|
|
27904
|
+
const rotatedHere = rotated || hasRotateOp(el);
|
|
27867
27905
|
let containerId = storeyId;
|
|
27868
27906
|
const route = specRoute(el.type);
|
|
27869
27907
|
if (el.type === "Storey") {
|
|
@@ -27880,6 +27918,12 @@ function familiesToBim(root, options) {
|
|
|
27880
27918
|
} else if (route !== void 0) {
|
|
27881
27919
|
const keyed = requireKeyed(el);
|
|
27882
27920
|
if (!keyed.ok) return keyed;
|
|
27921
|
+
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`));
|
|
27922
|
+
const voids = el.props["voids"];
|
|
27923
|
+
if (Array.isArray(voids)) {
|
|
27924
|
+
const openings = el.children.filter((c) => c.type === "Opening").length;
|
|
27925
|
+
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`));
|
|
27926
|
+
}
|
|
27883
27927
|
const parsed = route.parse(("input" in route ? route.input : specInput)(el));
|
|
27884
27928
|
if (!parsed.ok) return parsed;
|
|
27885
27929
|
const added = route.add(model, parsed.value, el.keyPath);
|
|
@@ -27895,12 +27939,12 @@ function familiesToBim(root, options) {
|
|
|
27895
27939
|
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}'`));
|
|
27896
27940
|
for (const child of el.children) {
|
|
27897
27941
|
if (el.type === "Wall" && child.type === "Opening") continue;
|
|
27898
|
-
const r = walk(child, containerId);
|
|
27942
|
+
const r = walk(child, containerId, rotatedHere);
|
|
27899
27943
|
if (!r.ok) return r;
|
|
27900
27944
|
}
|
|
27901
27945
|
return (0, brepjs.ok)(void 0);
|
|
27902
27946
|
};
|
|
27903
|
-
const walked = walk(root, null);
|
|
27947
|
+
const walked = walk(root, null, false);
|
|
27904
27948
|
if (!walked.ok) {
|
|
27905
27949
|
model[Symbol.dispose]();
|
|
27906
27950
|
return walked;
|
package/dist/brepjs-bim.js
CHANGED
|
@@ -27601,7 +27601,8 @@ function assignNum(target, key, value) {
|
|
|
27601
27601
|
* fill-role void (Door/Window family) maps onto addDoor/addWindow, which cut
|
|
27602
27602
|
* the wall and wire IfcRelVoidsElement + IfcRelFillsElement; the opening and
|
|
27603
27603
|
* filler GlobalIds derive from the synthesized key paths. Anonymous (non-fill)
|
|
27604
|
-
* voids
|
|
27604
|
+
* voids are rejected: they cut only the IR/viewport geometry, and exporting
|
|
27605
|
+
* the uncut spec body would silently diverge from what the user sees.
|
|
27605
27606
|
*/
|
|
27606
27607
|
var SPEC_DEFAULTS = {
|
|
27607
27608
|
origin: [
|
|
@@ -27711,6 +27712,16 @@ function peelTranslates(node) {
|
|
|
27711
27712
|
moved
|
|
27712
27713
|
};
|
|
27713
27714
|
}
|
|
27715
|
+
/** True when the element's transform PROP carries a rotation. The spec path
|
|
27716
|
+
* folds only translations into IfcLocalPlacement (walls orient via `axisX`),
|
|
27717
|
+
* so a tRotate placement would export un-rotated while the viewport shows it
|
|
27718
|
+
* rotated — reject instead of diverging. Rotations a family render bakes
|
|
27719
|
+
* into its own body geometry (e.g. a circular beam oriented along axisX) are
|
|
27720
|
+
* fine: the spec rebuilds that body parametrically from props. */
|
|
27721
|
+
function hasRotateOp(el) {
|
|
27722
|
+
const ops = el.props["transform"];
|
|
27723
|
+
return Array.isArray(ops) && ops.some((op) => typeof op === "object" && op !== null && op.op === "rotate");
|
|
27724
|
+
}
|
|
27714
27725
|
/** Fold the resolved geometry's outer translate chain into the spec placement
|
|
27715
27726
|
* origin, so IfcLocalPlacement matches the IR world frame no matter where the
|
|
27716
27727
|
* transform came from (family-internal or prop-level). */
|
|
@@ -27742,18 +27753,43 @@ function specInput(el) {
|
|
|
27742
27753
|
...collectSpecProps(el)
|
|
27743
27754
|
};
|
|
27744
27755
|
}
|
|
27756
|
+
/** `Pset_<Type>Common` fields the specs model first-class: mapped onto their
|
|
27757
|
+
* spec names so the writer emits them in the element's own common pset. */
|
|
27758
|
+
var COMMON_PSET_FIELDS = {
|
|
27759
|
+
IsExternal: "isExternal",
|
|
27760
|
+
FireRating: "fireRating",
|
|
27761
|
+
AcousticRating: "acousticRating",
|
|
27762
|
+
ThermalTransmittance: "thermalTransmittance",
|
|
27763
|
+
LoadBearing: "loadBearing",
|
|
27764
|
+
Status: "status"
|
|
27765
|
+
};
|
|
27745
27766
|
function collectSpecProps(el) {
|
|
27746
|
-
const psets = el.attributes["psets"];
|
|
27747
27767
|
const out = {};
|
|
27768
|
+
const material = el.attributes["material"];
|
|
27769
|
+
if (typeof material === "string" && el.props["materialName"] === void 0) out["materialName"] = material;
|
|
27770
|
+
const psets = el.attributes["psets"];
|
|
27748
27771
|
if (psets && typeof psets === "object") {
|
|
27749
|
-
const
|
|
27750
|
-
|
|
27751
|
-
|
|
27752
|
-
if (typeof
|
|
27753
|
-
|
|
27772
|
+
const custom = {};
|
|
27773
|
+
const ownCommonPset = `Pset_${el.type}Common`;
|
|
27774
|
+
for (const [psetName, fields] of Object.entries(psets)) {
|
|
27775
|
+
if (!fields || typeof fields !== "object") continue;
|
|
27776
|
+
const record = fields;
|
|
27777
|
+
if (psetName === ownCommonPset) {
|
|
27778
|
+
for (const [field, specKey] of Object.entries(COMMON_PSET_FIELDS)) if (record[field] !== void 0) out[specKey] = record[field];
|
|
27779
|
+
} else {
|
|
27780
|
+
const values = {};
|
|
27781
|
+
for (const [field, value] of Object.entries(record)) if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") values[field] = value;
|
|
27782
|
+
if (Object.keys(values).length > 0) custom[psetName] = values;
|
|
27783
|
+
}
|
|
27784
|
+
}
|
|
27785
|
+
if (Object.keys(custom).length > 0) {
|
|
27786
|
+
const declared = el.props["customProperties"];
|
|
27787
|
+
out["customProperties"] = declared && typeof declared === "object" ? {
|
|
27788
|
+
...custom,
|
|
27789
|
+
...declared
|
|
27790
|
+
} : custom;
|
|
27754
27791
|
}
|
|
27755
27792
|
}
|
|
27756
|
-
delete out["psets"];
|
|
27757
27793
|
return out;
|
|
27758
27794
|
}
|
|
27759
27795
|
function addFill(model, fill, input, identity) {
|
|
@@ -27794,6 +27830,7 @@ function addOpenings(model, host, wallId, containerId, idByKeyPath) {
|
|
|
27794
27830
|
const added = addFill(model, fill, {
|
|
27795
27831
|
materialName: SPEC_DEFAULTS.materialName,
|
|
27796
27832
|
...stripGeometryProps(fill.props),
|
|
27833
|
+
...collectSpecProps(fill),
|
|
27797
27834
|
wallLocalId: wallId,
|
|
27798
27835
|
offsetAlongWall: delta[0] * axisX[0] + delta[1] * axisX[1] + delta[2] * axisX[2],
|
|
27799
27836
|
offsetFromFloor: delta[2]
|
|
@@ -27840,7 +27877,8 @@ function familiesToBim(root, options) {
|
|
|
27840
27877
|
if (project !== null) model.aggregate(project.localId, siteResult.value);
|
|
27841
27878
|
model.aggregate(siteResult.value, buildingId);
|
|
27842
27879
|
const idByKeyPath = /* @__PURE__ */ new Map();
|
|
27843
|
-
const walk = (el, storeyId) => {
|
|
27880
|
+
const walk = (el, storeyId, rotated) => {
|
|
27881
|
+
const rotatedHere = rotated || hasRotateOp(el);
|
|
27844
27882
|
let containerId = storeyId;
|
|
27845
27883
|
const route = specRoute(el.type);
|
|
27846
27884
|
if (el.type === "Storey") {
|
|
@@ -27857,6 +27895,12 @@ function familiesToBim(root, options) {
|
|
|
27857
27895
|
} else if (route !== void 0) {
|
|
27858
27896
|
const keyed = requireKeyed(el);
|
|
27859
27897
|
if (!keyed.ok) return keyed;
|
|
27898
|
+
if (rotatedHere) return err(specError("FAMILIES_UNSUPPORTED_TRANSFORM", `familiesToBim: '${el.keyPath}' carries a rotated placement — the spec path folds only translations into IfcLocalPlacement; orient walls via axisX instead of tRotate`));
|
|
27899
|
+
const voids = el.props["voids"];
|
|
27900
|
+
if (Array.isArray(voids)) {
|
|
27901
|
+
const openings = el.children.filter((c) => c.type === "Opening").length;
|
|
27902
|
+
if (voids.length > openings) return err(specError("FAMILIES_ANONYMOUS_VOID", `familiesToBim: '${el.keyPath}' has ${voids.length - openings} anonymous void(s) the IFC body cannot carry — use a fill-role family (Door/Window) for each void`));
|
|
27903
|
+
}
|
|
27860
27904
|
const parsed = route.parse(("input" in route ? route.input : specInput)(el));
|
|
27861
27905
|
if (!parsed.ok) return parsed;
|
|
27862
27906
|
const added = route.add(model, parsed.value, el.keyPath);
|
|
@@ -27872,12 +27916,12 @@ function familiesToBim(root, options) {
|
|
|
27872
27916
|
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}'`));
|
|
27873
27917
|
for (const child of el.children) {
|
|
27874
27918
|
if (el.type === "Wall" && child.type === "Opening") continue;
|
|
27875
|
-
const r = walk(child, containerId);
|
|
27919
|
+
const r = walk(child, containerId, rotatedHere);
|
|
27876
27920
|
if (!r.ok) return r;
|
|
27877
27921
|
}
|
|
27878
27922
|
return ok(void 0);
|
|
27879
27923
|
};
|
|
27880
|
-
const walked = walk(root, null);
|
|
27924
|
+
const walked = walk(root, null, false);
|
|
27881
27925
|
if (!walked.ok) {
|
|
27882
27926
|
model[Symbol.dispose]();
|
|
27883
27927
|
return walked;
|