brepjs-bim 0.17.2 → 0.19.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 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
@@ -535,7 +535,7 @@ var propertyKeyTypes = /* @__PURE__*/ new Set([
535
535
  function escapeRegex(str) {
536
536
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
537
537
  }
538
- function clone(inst, def, params) {
538
+ function clone$1(inst, def, params) {
539
539
  const cl = new inst._zod.constr(def ?? inst._zod.def);
540
540
  if (!def || params?.parent) cl._zod.parent = inst;
541
541
  return cl;
@@ -571,7 +571,7 @@ function pick(schema, mask) {
571
571
  const currDef = schema._zod.def;
572
572
  const checks = currDef.checks;
573
573
  if (checks && checks.length > 0) throw new Error(".pick() cannot be used on object schemas containing refinements");
574
- return clone(schema, mergeDefs(schema._zod.def, {
574
+ return clone$1(schema, mergeDefs(schema._zod.def, {
575
575
  get shape() {
576
576
  const newShape = {};
577
577
  for (const key in mask) {
@@ -589,7 +589,7 @@ function omit(schema, mask) {
589
589
  const currDef = schema._zod.def;
590
590
  const checks = currDef.checks;
591
591
  if (checks && checks.length > 0) throw new Error(".omit() cannot be used on object schemas containing refinements");
592
- return clone(schema, mergeDefs(schema._zod.def, {
592
+ return clone$1(schema, mergeDefs(schema._zod.def, {
593
593
  get shape() {
594
594
  const newShape = { ...schema._zod.def.shape };
595
595
  for (const key in mask) {
@@ -610,7 +610,7 @@ function extend(schema, shape) {
610
610
  const existingShape = schema._zod.def.shape;
611
611
  for (const key in shape) if (Object.getOwnPropertyDescriptor(existingShape, key) !== void 0) throw new Error("Cannot overwrite keys on object schemas containing refinements. Use `.safeExtend()` instead.");
612
612
  }
613
- return clone(schema, mergeDefs(schema._zod.def, { get shape() {
613
+ return clone$1(schema, mergeDefs(schema._zod.def, { get shape() {
614
614
  const _shape = {
615
615
  ...schema._zod.def.shape,
616
616
  ...shape
@@ -621,7 +621,7 @@ function extend(schema, shape) {
621
621
  }
622
622
  function safeExtend(schema, shape) {
623
623
  if (!isPlainObject(shape)) throw new Error("Invalid input to safeExtend: expected a plain object");
624
- return clone(schema, mergeDefs(schema._zod.def, { get shape() {
624
+ return clone$1(schema, mergeDefs(schema._zod.def, { get shape() {
625
625
  const _shape = {
626
626
  ...schema._zod.def.shape,
627
627
  ...shape
@@ -632,7 +632,7 @@ function safeExtend(schema, shape) {
632
632
  }
633
633
  function merge(a, b) {
634
634
  if (a._zod.def.checks?.length) throw new Error(".merge() cannot be used on object schemas containing refinements. Use .safeExtend() instead.");
635
- return clone(a, mergeDefs(a._zod.def, {
635
+ return clone$1(a, mergeDefs(a._zod.def, {
636
636
  get shape() {
637
637
  const _shape = {
638
638
  ...a._zod.def.shape,
@@ -650,7 +650,7 @@ function merge(a, b) {
650
650
  function partial(Class, schema, mask) {
651
651
  const checks = schema._zod.def.checks;
652
652
  if (checks && checks.length > 0) throw new Error(".partial() cannot be used on object schemas containing refinements");
653
- return clone(schema, mergeDefs(schema._zod.def, {
653
+ return clone$1(schema, mergeDefs(schema._zod.def, {
654
654
  get shape() {
655
655
  const oldShape = schema._zod.def.shape;
656
656
  const shape = { ...oldShape };
@@ -673,7 +673,7 @@ function partial(Class, schema, mask) {
673
673
  }));
674
674
  }
675
675
  function required(Class, schema, mask) {
676
- return clone(schema, mergeDefs(schema._zod.def, { get shape() {
676
+ return clone$1(schema, mergeDefs(schema._zod.def, { get shape() {
677
677
  const oldShape = schema._zod.def.shape;
678
678
  const shape = { ...oldShape };
679
679
  if (mask) for (const key in mask) {
@@ -4042,7 +4042,7 @@ var ZodType = /*@__PURE__*/ $constructor("ZodType", (inst, def) => {
4042
4042
  return this.check(...chks);
4043
4043
  },
4044
4044
  clone(def, params) {
4045
- return clone(this, def, params);
4045
+ return clone$1(this, def, params);
4046
4046
  },
4047
4047
  brand() {
4048
4048
  return this;
@@ -27735,6 +27735,16 @@ function peelTranslates(node) {
27735
27735
  moved
27736
27736
  };
27737
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
+ }
27738
27748
  /** Fold the resolved geometry's outer translate chain into the spec placement
27739
27749
  * origin, so IfcLocalPlacement matches the IR world frame no matter where the
27740
27750
  * transform came from (family-internal or prop-level). */
@@ -27818,6 +27828,36 @@ function addFill(model, fill, input, identity) {
27818
27828
  }
27819
27829
  return (0, brepjs.err)(specError("FAMILIES_UNSUPPORTED_FILL", `familiesToBim: no fill mapping for element type '${fill.type}' at '${fill.keyPath}'`));
27820
27830
  }
27831
+ /** Materialize an unrouted element's IR and add it as a proxy. The body is
27832
+ * authoritative for a proxy (no parametric spec to diverge from), so baked
27833
+ * transforms — rotations included — are fine here. */
27834
+ function addProxyElement(model, el, evaluator) {
27835
+ const evaluated = evaluator.evaluate(el.geometry);
27836
+ if (!evaluated.ok) return (0, brepjs.err)(specError("FAMILIES_PROXY_EVAL_FAILED", `familiesToBim: '${el.keyPath}' failed to materialize for the proxy route: ${evaluated.error.message}`, evaluated.error));
27837
+ let source = evaluated.value;
27838
+ if (!(0, brepjs.isSolid)(source)) {
27839
+ const solids = (0, brepjs.getSolids)(source);
27840
+ const only = solids.length === 1 ? solids[0] : void 0;
27841
+ if (only === void 0) return (0, brepjs.err)(specError("FAMILIES_PROXY_NOT_SOLID", `familiesToBim: '${el.keyPath}' materialized to ${solids.length} solids — a proxy body must be exactly one`));
27842
+ source = only;
27843
+ }
27844
+ const copy = (0, brepjs.clone)(source);
27845
+ if (!copy.ok) return (0, brepjs.err)(specError("FAMILIES_PROXY_EVAL_FAILED", `familiesToBim: '${el.keyPath}' could not copy the materialized body`, copy.error));
27846
+ const valid = (0, brepjs.validSolid)(copy.value);
27847
+ if (!valid.ok) {
27848
+ copy.value[Symbol.dispose]();
27849
+ return (0, brepjs.err)(specError("FAMILIES_PROXY_INVALID", `familiesToBim: '${el.keyPath}' materialized to an invalid solid: ${valid.error}`));
27850
+ }
27851
+ const nameAttr = el.attributes["name"];
27852
+ const materialProp = el.props["materialName"];
27853
+ const specProps = collectSpecProps(el);
27854
+ return model.addProxy({
27855
+ name: typeof nameAttr === "string" ? nameAttr : el.type,
27856
+ solid: valid.value,
27857
+ materialName: typeof materialProp === "string" ? materialProp : specProps["materialName"],
27858
+ customProperties: specProps["customProperties"]
27859
+ }, { stableKey: el.keyPath });
27860
+ }
27821
27861
  /** Map a wall's synthesized Opening children onto addDoor/addWindow. The
27822
27862
  * wall-relative offsets come from the void geometry's frame minus the host's:
27823
27863
  * exact because both carry the same outer host transform. */
@@ -27890,7 +27930,9 @@ function familiesToBim(root, options) {
27890
27930
  if (project !== null) model.aggregate(project.localId, siteResult.value);
27891
27931
  model.aggregate(siteResult.value, buildingId);
27892
27932
  const idByKeyPath = /* @__PURE__ */ new Map();
27893
- const walk = (el, storeyId) => {
27933
+ const walk = (el, storeyId, rotated) => {
27934
+ const rotatedHere = rotated || hasRotateOp(el);
27935
+ let proxied = false;
27894
27936
  let containerId = storeyId;
27895
27937
  const route = specRoute(el.type);
27896
27938
  if (el.type === "Storey") {
@@ -27907,6 +27949,7 @@ function familiesToBim(root, options) {
27907
27949
  } else if (route !== void 0) {
27908
27950
  const keyed = requireKeyed(el);
27909
27951
  if (!keyed.ok) return keyed;
27952
+ 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`));
27910
27953
  const voids = el.props["voids"];
27911
27954
  if (Array.isArray(voids)) {
27912
27955
  const openings = el.children.filter((c) => c.type === "Opening").length;
@@ -27924,15 +27967,25 @@ function familiesToBim(root, options) {
27924
27967
  if (!opened.ok) return opened;
27925
27968
  }
27926
27969
  } 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`));
27927
- 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}'`));
27970
+ else if (el.type !== "Group" && el.geometry.kind !== "Empty") {
27971
+ if (options.proxyEvaluator === void 0) return (0, brepjs.err)(specError("FAMILIES_UNSUPPORTED_TYPE", `familiesToBim: no spec mapping for element type '${el.type}' at '${el.keyPath}' — add a spec route, or pass proxyEvaluator to export it as an IfcBuildingElementProxy`));
27972
+ const keyed = requireKeyed(el);
27973
+ if (!keyed.ok) return keyed;
27974
+ if (containerId === null) return (0, brepjs.err)(specError("FAMILIES_NO_STOREY", `familiesToBim: '${el.keyPath}' has no Storey ancestor — IFC elements need spatial containment`));
27975
+ const added = addProxyElement(model, el, options.proxyEvaluator);
27976
+ if (!added.ok) return added;
27977
+ model.placeIn(added.value, containerId);
27978
+ idByKeyPath.set(el.keyPath, added.value);
27979
+ proxied = true;
27980
+ }
27928
27981
  for (const child of el.children) {
27929
- if (el.type === "Wall" && child.type === "Opening") continue;
27930
- const r = walk(child, containerId);
27982
+ if ((el.type === "Wall" || proxied) && child.type === "Opening") continue;
27983
+ const r = walk(child, containerId, rotatedHere);
27931
27984
  if (!r.ok) return r;
27932
27985
  }
27933
27986
  return (0, brepjs.ok)(void 0);
27934
27987
  };
27935
- const walked = walk(root, null);
27988
+ const walked = walk(root, null, false);
27936
27989
  if (!walked.ok) {
27937
27990
  model[Symbol.dispose]();
27938
27991
  return walked;
@@ -1,4 +1,4 @@
1
- import { addHoles, applyMatrix, autoHeal, box, castShape, convexHull, cut, err, extrude, fuse, getKernel, isClosedWire, isOk, isPlanarWire, isSolid, isValid, isValidSolid, measureVolume, mesh, ok, outerWire, polygon, revolve, rotate, validSolid } from "brepjs";
1
+ import { addHoles, applyMatrix, autoHeal, box, castShape, clone, convexHull, cut, err, extrude, fuse, getKernel, getSolids, isClosedWire, isOk, isPlanarWire, isSolid, isValid, isValidSolid, measureVolume, mesh, ok, outerWire, polygon, revolve, rotate, validSolid } from "brepjs";
2
2
  import * as WebIFC from "web-ifc";
3
3
  import { Handle, IfcAPI } from "web-ifc";
4
4
  //#region src/identity/ifcGuid.ts
@@ -512,7 +512,7 @@ var propertyKeyTypes = /* @__PURE__*/ new Set([
512
512
  function escapeRegex(str) {
513
513
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
514
514
  }
515
- function clone(inst, def, params) {
515
+ function clone$1(inst, def, params) {
516
516
  const cl = new inst._zod.constr(def ?? inst._zod.def);
517
517
  if (!def || params?.parent) cl._zod.parent = inst;
518
518
  return cl;
@@ -548,7 +548,7 @@ function pick(schema, mask) {
548
548
  const currDef = schema._zod.def;
549
549
  const checks = currDef.checks;
550
550
  if (checks && checks.length > 0) throw new Error(".pick() cannot be used on object schemas containing refinements");
551
- return clone(schema, mergeDefs(schema._zod.def, {
551
+ return clone$1(schema, mergeDefs(schema._zod.def, {
552
552
  get shape() {
553
553
  const newShape = {};
554
554
  for (const key in mask) {
@@ -566,7 +566,7 @@ function omit(schema, mask) {
566
566
  const currDef = schema._zod.def;
567
567
  const checks = currDef.checks;
568
568
  if (checks && checks.length > 0) throw new Error(".omit() cannot be used on object schemas containing refinements");
569
- return clone(schema, mergeDefs(schema._zod.def, {
569
+ return clone$1(schema, mergeDefs(schema._zod.def, {
570
570
  get shape() {
571
571
  const newShape = { ...schema._zod.def.shape };
572
572
  for (const key in mask) {
@@ -587,7 +587,7 @@ function extend(schema, shape) {
587
587
  const existingShape = schema._zod.def.shape;
588
588
  for (const key in shape) if (Object.getOwnPropertyDescriptor(existingShape, key) !== void 0) throw new Error("Cannot overwrite keys on object schemas containing refinements. Use `.safeExtend()` instead.");
589
589
  }
590
- return clone(schema, mergeDefs(schema._zod.def, { get shape() {
590
+ return clone$1(schema, mergeDefs(schema._zod.def, { get shape() {
591
591
  const _shape = {
592
592
  ...schema._zod.def.shape,
593
593
  ...shape
@@ -598,7 +598,7 @@ function extend(schema, shape) {
598
598
  }
599
599
  function safeExtend(schema, shape) {
600
600
  if (!isPlainObject(shape)) throw new Error("Invalid input to safeExtend: expected a plain object");
601
- return clone(schema, mergeDefs(schema._zod.def, { get shape() {
601
+ return clone$1(schema, mergeDefs(schema._zod.def, { get shape() {
602
602
  const _shape = {
603
603
  ...schema._zod.def.shape,
604
604
  ...shape
@@ -609,7 +609,7 @@ function safeExtend(schema, shape) {
609
609
  }
610
610
  function merge(a, b) {
611
611
  if (a._zod.def.checks?.length) throw new Error(".merge() cannot be used on object schemas containing refinements. Use .safeExtend() instead.");
612
- return clone(a, mergeDefs(a._zod.def, {
612
+ return clone$1(a, mergeDefs(a._zod.def, {
613
613
  get shape() {
614
614
  const _shape = {
615
615
  ...a._zod.def.shape,
@@ -627,7 +627,7 @@ function merge(a, b) {
627
627
  function partial(Class, schema, mask) {
628
628
  const checks = schema._zod.def.checks;
629
629
  if (checks && checks.length > 0) throw new Error(".partial() cannot be used on object schemas containing refinements");
630
- return clone(schema, mergeDefs(schema._zod.def, {
630
+ return clone$1(schema, mergeDefs(schema._zod.def, {
631
631
  get shape() {
632
632
  const oldShape = schema._zod.def.shape;
633
633
  const shape = { ...oldShape };
@@ -650,7 +650,7 @@ function partial(Class, schema, mask) {
650
650
  }));
651
651
  }
652
652
  function required(Class, schema, mask) {
653
- return clone(schema, mergeDefs(schema._zod.def, { get shape() {
653
+ return clone$1(schema, mergeDefs(schema._zod.def, { get shape() {
654
654
  const oldShape = schema._zod.def.shape;
655
655
  const shape = { ...oldShape };
656
656
  if (mask) for (const key in mask) {
@@ -4019,7 +4019,7 @@ var ZodType = /*@__PURE__*/ $constructor("ZodType", (inst, def) => {
4019
4019
  return this.check(...chks);
4020
4020
  },
4021
4021
  clone(def, params) {
4022
- return clone(this, def, params);
4022
+ return clone$1(this, def, params);
4023
4023
  },
4024
4024
  brand() {
4025
4025
  return this;
@@ -27712,6 +27712,16 @@ function peelTranslates(node) {
27712
27712
  moved
27713
27713
  };
27714
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
+ }
27715
27725
  /** Fold the resolved geometry's outer translate chain into the spec placement
27716
27726
  * origin, so IfcLocalPlacement matches the IR world frame no matter where the
27717
27727
  * transform came from (family-internal or prop-level). */
@@ -27795,6 +27805,36 @@ function addFill(model, fill, input, identity) {
27795
27805
  }
27796
27806
  return err(specError("FAMILIES_UNSUPPORTED_FILL", `familiesToBim: no fill mapping for element type '${fill.type}' at '${fill.keyPath}'`));
27797
27807
  }
27808
+ /** Materialize an unrouted element's IR and add it as a proxy. The body is
27809
+ * authoritative for a proxy (no parametric spec to diverge from), so baked
27810
+ * transforms — rotations included — are fine here. */
27811
+ function addProxyElement(model, el, evaluator) {
27812
+ const evaluated = evaluator.evaluate(el.geometry);
27813
+ if (!evaluated.ok) return err(specError("FAMILIES_PROXY_EVAL_FAILED", `familiesToBim: '${el.keyPath}' failed to materialize for the proxy route: ${evaluated.error.message}`, evaluated.error));
27814
+ let source = evaluated.value;
27815
+ if (!isSolid(source)) {
27816
+ const solids = getSolids(source);
27817
+ const only = solids.length === 1 ? solids[0] : void 0;
27818
+ if (only === void 0) return err(specError("FAMILIES_PROXY_NOT_SOLID", `familiesToBim: '${el.keyPath}' materialized to ${solids.length} solids — a proxy body must be exactly one`));
27819
+ source = only;
27820
+ }
27821
+ const copy = clone(source);
27822
+ if (!copy.ok) return err(specError("FAMILIES_PROXY_EVAL_FAILED", `familiesToBim: '${el.keyPath}' could not copy the materialized body`, copy.error));
27823
+ const valid = validSolid(copy.value);
27824
+ if (!valid.ok) {
27825
+ copy.value[Symbol.dispose]();
27826
+ return err(specError("FAMILIES_PROXY_INVALID", `familiesToBim: '${el.keyPath}' materialized to an invalid solid: ${valid.error}`));
27827
+ }
27828
+ const nameAttr = el.attributes["name"];
27829
+ const materialProp = el.props["materialName"];
27830
+ const specProps = collectSpecProps(el);
27831
+ return model.addProxy({
27832
+ name: typeof nameAttr === "string" ? nameAttr : el.type,
27833
+ solid: valid.value,
27834
+ materialName: typeof materialProp === "string" ? materialProp : specProps["materialName"],
27835
+ customProperties: specProps["customProperties"]
27836
+ }, { stableKey: el.keyPath });
27837
+ }
27798
27838
  /** Map a wall's synthesized Opening children onto addDoor/addWindow. The
27799
27839
  * wall-relative offsets come from the void geometry's frame minus the host's:
27800
27840
  * exact because both carry the same outer host transform. */
@@ -27867,7 +27907,9 @@ function familiesToBim(root, options) {
27867
27907
  if (project !== null) model.aggregate(project.localId, siteResult.value);
27868
27908
  model.aggregate(siteResult.value, buildingId);
27869
27909
  const idByKeyPath = /* @__PURE__ */ new Map();
27870
- const walk = (el, storeyId) => {
27910
+ const walk = (el, storeyId, rotated) => {
27911
+ const rotatedHere = rotated || hasRotateOp(el);
27912
+ let proxied = false;
27871
27913
  let containerId = storeyId;
27872
27914
  const route = specRoute(el.type);
27873
27915
  if (el.type === "Storey") {
@@ -27884,6 +27926,7 @@ function familiesToBim(root, options) {
27884
27926
  } else if (route !== void 0) {
27885
27927
  const keyed = requireKeyed(el);
27886
27928
  if (!keyed.ok) return keyed;
27929
+ 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`));
27887
27930
  const voids = el.props["voids"];
27888
27931
  if (Array.isArray(voids)) {
27889
27932
  const openings = el.children.filter((c) => c.type === "Opening").length;
@@ -27901,15 +27944,25 @@ function familiesToBim(root, options) {
27901
27944
  if (!opened.ok) return opened;
27902
27945
  }
27903
27946
  } 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`));
27904
- 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}'`));
27947
+ else if (el.type !== "Group" && el.geometry.kind !== "Empty") {
27948
+ if (options.proxyEvaluator === void 0) return err(specError("FAMILIES_UNSUPPORTED_TYPE", `familiesToBim: no spec mapping for element type '${el.type}' at '${el.keyPath}' — add a spec route, or pass proxyEvaluator to export it as an IfcBuildingElementProxy`));
27949
+ const keyed = requireKeyed(el);
27950
+ if (!keyed.ok) return keyed;
27951
+ if (containerId === null) return err(specError("FAMILIES_NO_STOREY", `familiesToBim: '${el.keyPath}' has no Storey ancestor — IFC elements need spatial containment`));
27952
+ const added = addProxyElement(model, el, options.proxyEvaluator);
27953
+ if (!added.ok) return added;
27954
+ model.placeIn(added.value, containerId);
27955
+ idByKeyPath.set(el.keyPath, added.value);
27956
+ proxied = true;
27957
+ }
27905
27958
  for (const child of el.children) {
27906
- if (el.type === "Wall" && child.type === "Opening") continue;
27907
- const r = walk(child, containerId);
27959
+ if ((el.type === "Wall" || proxied) && child.type === "Opening") continue;
27960
+ const r = walk(child, containerId, rotatedHere);
27908
27961
  if (!r.ok) return r;
27909
27962
  }
27910
27963
  return ok(void 0);
27911
27964
  };
27912
- const walked = walk(root, null);
27965
+ const walked = walk(root, null, false);
27913
27966
  if (!walked.ok) {
27914
27967
  model[Symbol.dispose]();
27915
27968
  return walked;
@@ -1,4 +1,4 @@
1
- import { Result } from 'brepjs';
1
+ import { Result, csg } from 'brepjs';
2
2
  import { ResolvedElement } from 'brepjs-families';
3
3
  import { BimModel } from './model/bimModel.js';
4
4
  import { LocalId } from './identity/localId.js';
@@ -8,6 +8,15 @@ export interface FamiliesToBimOptions {
8
8
  readonly project: ProjectSpec;
9
9
  readonly siteName?: string | undefined;
10
10
  readonly buildingName?: string | undefined;
11
+ /**
12
+ * Enables the proxy route: an unrouted geometry-bearing element is
13
+ * materialized through this evaluator and exported as an
14
+ * IfcBuildingElementProxy (tessellated, world-frame body). Without it,
15
+ * unrouted types stay a hard FAMILIES_UNSUPPORTED_TYPE error. The
16
+ * evaluator's handles stay borrowed; the adapter clones what it hands the
17
+ * model.
18
+ */
19
+ readonly proxyEvaluator?: csg.Evaluator | undefined;
11
20
  }
12
21
  export interface FamiliesBimResult {
13
22
  readonly model: BimModel;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brepjs-bim",
3
- "version": "0.17.2",
3
+ "version": "0.19.0",
4
4
  "description": "BIM layer for brepjs — IFC4-aligned parametric building elements",
5
5
  "keywords": [
6
6
  "bim",