brepjs-bim 0.24.2 → 0.24.3

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
@@ -192,3 +192,9 @@ npm run lint --workspace=brepjs-bim
192
192
  npm run build --workspace=brepjs-bim
193
193
  npm run test --workspace=brepjs-bim
194
194
  ```
195
+
196
+ ### Body material measurement
197
+
198
+ Exact Wall quantities measure occupied material across all retained items. Overlapping solids count once. Bounds include every item. Measurement and temporary cleanup failures make these optional measurements unavailable and preserve the borrowed inputs. Cleanup never retries an uncertain native release.
199
+
200
+ The BIM package requires `brepjs >=19.0.5` for native cleanup-error reporting.
@@ -7621,6 +7621,71 @@ function coveringToSolid(spec) {
7621
7621
  }
7622
7622
  }
7623
7623
  //#endregion
7624
+ //#region src/productBodyCleanup.ts
7625
+ var COMPLETE_CLEANUP = Object.freeze({ kind: "COMPLETE" });
7626
+ function cleanupReport(diagnostics) {
7627
+ const [first, ...rest] = diagnostics;
7628
+ if (first === void 0) return COMPLETE_CLEANUP;
7629
+ const snapshot = Object.freeze([first, ...rest]);
7630
+ return Object.freeze({
7631
+ kind: "FAILED",
7632
+ diagnostics: snapshot
7633
+ });
7634
+ }
7635
+ /** Read known cleanup failures through Result, AggregateError and `using` error chains.
7636
+ * This reports prior attempts only; it never takes ownership or retries a release.
7637
+ */
7638
+ function nestedCleanupDiagnostics(error, context) {
7639
+ const diagnostics = [];
7640
+ const seen = /* @__PURE__ */ new Set();
7641
+ const visit = (value) => {
7642
+ if (typeof value !== "object" || value === null || seen.has(value)) return;
7643
+ seen.add(value);
7644
+ if (value instanceof brepjs.GeometryCleanupError) {
7645
+ diagnostics.push(Object.freeze({
7646
+ ...context,
7647
+ resourceKind: value.resourceKind,
7648
+ cause: value.cause
7649
+ }));
7650
+ return;
7651
+ }
7652
+ if (value instanceof AggregateError) value.errors.forEach(visit);
7653
+ if ("cause" in value) visit(value.cause);
7654
+ if ("error" in value && "suppressed" in value) {
7655
+ visit(value.error);
7656
+ visit(value.suppressed);
7657
+ }
7658
+ };
7659
+ visit(error);
7660
+ return diagnostics;
7661
+ }
7662
+ /** Owner-only, attempt-all cleanup. Never retry a release with an uncertain outcome. */
7663
+ function cleanupOwnedResources(items, context) {
7664
+ const attempted = /* @__PURE__ */ new Set();
7665
+ const diagnostics = [];
7666
+ for (const { resource, itemIndex, resourceKind = "SHAPE" } of items) {
7667
+ if (attempted.has(resource)) continue;
7668
+ attempted.add(resource);
7669
+ try {
7670
+ resource[Symbol.dispose]();
7671
+ } catch (cause) {
7672
+ diagnostics.push(Object.freeze({
7673
+ ...context,
7674
+ itemIndex,
7675
+ resourceKind,
7676
+ cause
7677
+ }));
7678
+ }
7679
+ }
7680
+ return cleanupReport(diagnostics);
7681
+ }
7682
+ //#endregion
7683
+ //#region src/productBodyTestHooks.ts
7684
+ var hooks = null;
7685
+ function productBodyTestHooks() {
7686
+ return hooks;
7687
+ }
7688
+ //#endregion
7624
7689
  //#region src/types/productBody.ts
7625
7690
  /** Returns borrowed Product-local solids. The model retains ownership. */
7626
7691
  function bodySolids(body) {
@@ -7633,6 +7698,113 @@ function bodySolids(body) {
7633
7698
  function disposeProductBody(body) {
7634
7699
  for (const solid of bodySolids(body)) solid[Symbol.dispose]();
7635
7700
  }
7701
+ function bodyError(operation, code, message, cause, itemIndex, cleanup = COMPLETE_CLEANUP) {
7702
+ return {
7703
+ kind: "BIM_GEOMETRY",
7704
+ operation,
7705
+ code,
7706
+ message,
7707
+ cause,
7708
+ cleanup,
7709
+ ...itemIndex === void 0 ? {} : { itemIndex }
7710
+ };
7711
+ }
7712
+ /** @internal Exact resource object identity only, without native topology queries. */
7713
+ function wrappedResourceObject(solid) {
7714
+ if (typeof solid !== "object" || solid === null || !("wrapped" in solid)) return void 0;
7715
+ if ("disposed" in solid && solid.disposed) return void 0;
7716
+ const wrapped = solid.wrapped;
7717
+ return typeof wrapped === "object" && wrapped !== null ? wrapped : void 0;
7718
+ }
7719
+ /** Validate the public handle contract and native solid topology before narrowing opaque input. */
7720
+ function isLiveValidSolid(value) {
7721
+ return typeof value === "object" && value !== null && "disposed" in value && value.disposed === false && "wrapped" in value && value.wrapped !== null && value.wrapped !== void 0 && "delete" in value && typeof value.delete === "function" && "onDispose" in value && typeof value.onDispose === "function" && Symbol.dispose in value && typeof value[Symbol.dispose] === "function" && (0, brepjs.getKernel)().shapeType(value.wrapped) === "solid" && (0, brepjs.getKernel)().isValid(value.wrapped);
7722
+ }
7723
+ function validateItems(input, operation) {
7724
+ if (!Array.isArray(input) || input.length === 0) return (0, brepjs.err)(bodyError(operation, "BODY_EMPTY_ITEMS", "Expected a nonempty solids array"));
7725
+ const identities = /* @__PURE__ */ new Set();
7726
+ const resources = /* @__PURE__ */ new Set();
7727
+ const solids = [];
7728
+ for (let itemIndex = 0; itemIndex < input.length; itemIndex++) try {
7729
+ const injected = productBodyTestHooks()?.before?.({
7730
+ step: "validate",
7731
+ itemIndex
7732
+ });
7733
+ if (injected && !injected.ok) return (0, brepjs.err)(bodyError(operation, "BODY_VALIDATION_FAILED", "Item validation failed", injected.error, itemIndex));
7734
+ const item = input[itemIndex];
7735
+ if (!isLiveValidSolid(item)) return (0, brepjs.err)(bodyError(operation, "BODY_INVALID_ITEM", "Expected a live valid solid handle", void 0, itemIndex));
7736
+ const resource = wrappedResourceObject(item);
7737
+ if (identities.has(item) || resource !== void 0 && resources.has(resource)) return (0, brepjs.err)(bodyError(operation, "BODY_DUPLICATE_ITEM", "Item duplicates an earlier handle or its wrapped resource object", void 0, itemIndex));
7738
+ identities.add(item);
7739
+ if (resource !== void 0) resources.add(resource);
7740
+ solids.push(item);
7741
+ } catch (cause) {
7742
+ return (0, brepjs.err)(bodyError(operation, "BODY_VALIDATION_FAILED", "Item validation threw", cause, itemIndex));
7743
+ }
7744
+ const [first, ...rest] = solids;
7745
+ if (first === void 0) return (0, brepjs.err)(bodyError(operation, "BODY_EMPTY_ITEMS", "Expected a nonempty solids array"));
7746
+ return (0, brepjs.ok)(Object.freeze([first, ...rest]));
7747
+ }
7748
+ function diagnostics(report) {
7749
+ return report.kind === "FAILED" ? report.diagnostics : [];
7750
+ }
7751
+ function ownedOperation(operation, work) {
7752
+ const scope = {
7753
+ itemIndex: 0,
7754
+ temporaries: []
7755
+ };
7756
+ let result;
7757
+ try {
7758
+ result = work(scope);
7759
+ } catch (cause) {
7760
+ result = (0, brepjs.err)(bodyError(operation, "BODY_OPERATION_FAILED", "Native Body operation threw", cause, scope.itemIndex));
7761
+ }
7762
+ const retired = cleanupOwnedResources(scope.temporaries, { operation });
7763
+ if (result.ok && retired.kind === "COMPLETE") return result;
7764
+ const cleanup = cleanupReport([...result.ok ? [] : nestedCleanupDiagnostics(result.error, {
7765
+ operation,
7766
+ itemIndex: scope.itemIndex
7767
+ }), ...diagnostics(retired)]);
7768
+ if (result.ok) return (0, brepjs.err)(bodyError(operation, "BODY_CLEANUP_FAILED", "Could not retire operation temporaries", void 0, void 0, cleanup));
7769
+ return (0, brepjs.err)(bodyError(operation, result.error.code, result.error.message, result.error, scope.itemIndex, cleanup));
7770
+ }
7771
+ function before(step, itemIndex) {
7772
+ return productBodyTestHooks()?.before?.({
7773
+ step,
7774
+ itemIndex
7775
+ }) ?? (0, brepjs.ok)(void 0);
7776
+ }
7777
+ /** Occupied material in mm³. Borrowed imported items need no authored Body descriptor. */
7778
+ function measureProductBodyMaterial(solids) {
7779
+ const checked = validateItems(solids, "measureProductBodyMaterial");
7780
+ if (!checked.ok) return checked;
7781
+ return ownedOperation("measureProductBodyMaterial", (scope) => {
7782
+ let material = checked.value[0];
7783
+ if (checked.value.length > 1) {
7784
+ const ready = before("union", 0);
7785
+ if (!ready.ok) return ready;
7786
+ const fused = (0, brepjs.fuseAll)([...checked.value], { trackEvolution: false });
7787
+ if (!fused.ok) return (0, brepjs.err)(fromBrepError(fused.error, "BODY_UNION_FAILED", "Could not union occupied material"));
7788
+ material = fused.value;
7789
+ scope.temporaries.push({
7790
+ resource: material,
7791
+ itemIndex: 0
7792
+ });
7793
+ const injected = productBodyTestHooks()?.afterAllocate?.({
7794
+ step: "union",
7795
+ itemIndex: 0,
7796
+ solid: material
7797
+ });
7798
+ if (injected && !injected.ok) return injected;
7799
+ }
7800
+ const ready = before("measure", 0);
7801
+ if (!ready.ok) return ready;
7802
+ const measured = (productBodyTestHooks()?.measure ?? brepjs.measureVolume)(material);
7803
+ if (!measured.ok) return (0, brepjs.err)(fromBrepError(measured.error, "BODY_MEASUREMENT_FAILED", "Could not measure occupied material"));
7804
+ if (!Number.isFinite(measured.value) || measured.value <= 0) return (0, brepjs.err)(bodyError("measureProductBodyMaterial", "BODY_INVALID_VOLUME", "Expected a positive finite occupied volume in mm³", measured.value));
7805
+ return measured;
7806
+ });
7807
+ }
7636
7808
  //#endregion
7637
7809
  //#region src/model/bimModel.ts
7638
7810
  function exactWallBodyImmutable() {
@@ -15018,41 +15190,21 @@ function preflightExactBody(input) {
15018
15190
  //#endregion
15019
15191
  //#region src/serialize/exactWallQuantities.ts
15020
15192
  function deriveExactWallQuantities(input) {
15021
- const measure = input.dependencies?.measure ?? brepjs.measureVolume;
15022
- let volumeMm3;
15023
- if (input.solids.length === 1) try {
15024
- const measured = measure(input.solids[0]);
15025
- if (!measured.ok) return exactVolumeError(measured.error);
15026
- volumeMm3 = measured.value;
15193
+ try {
15194
+ const measured = measureProductBodyMaterial(input.solids);
15195
+ if (!measured.ok) return wallVolumeError(measured.error);
15196
+ return (0, brepjs.ok)({
15197
+ lengthM: toIfcLengthM(input.spec.length),
15198
+ widthM: toIfcLengthM(input.spec.thickness),
15199
+ heightM: toIfcLengthM(input.spec.height),
15200
+ netVolumeM3: measured.value / 1e9
15201
+ });
15027
15202
  } catch (cause) {
15028
- return exactVolumeError(cause);
15203
+ return wallVolumeError(cause);
15029
15204
  }
15030
- else {
15031
- const fuse = input.dependencies?.fuse ?? brepjs.fuseAll;
15032
- let union = null;
15033
- try {
15034
- const fused = fuse([...input.solids]);
15035
- if (!fused.ok) return exactVolumeError(fused.error);
15036
- union = fused.value;
15037
- const measured = measure(union);
15038
- if (!measured.ok) return exactVolumeError(measured.error);
15039
- volumeMm3 = measured.value;
15040
- } catch (cause) {
15041
- return exactVolumeError(cause);
15042
- } finally {
15043
- union?.[Symbol.dispose]();
15044
- }
15045
- }
15046
- if (!Number.isFinite(volumeMm3) || volumeMm3 <= 0) return exactVolumeError(/* @__PURE__ */ new Error(`Measured exact wall volume was ${volumeMm3}`));
15047
- return (0, brepjs.ok)({
15048
- lengthM: toIfcLengthM(input.spec.length),
15049
- widthM: toIfcLengthM(input.spec.thickness),
15050
- heightM: toIfcLengthM(input.spec.height),
15051
- netVolumeM3: volumeMm3 / 1e9
15052
- });
15053
15205
  }
15054
- function exactVolumeError(cause) {
15055
- return (0, brepjs.err)(ifcError("IFC_EXACT_WALL_QUANTITY_DERIVATION_FAILED", "Failed to derive a positive finite NetVolume for an exact wall Product Body", cause));
15206
+ function wallVolumeError(cause) {
15207
+ return (0, brepjs.err)(ifcError("IFC_EXACT_WALL_QUANTITY_DERIVATION_FAILED", "Failed to derive a positive finite NetVolume for the retained Wall Body", cause));
15056
15208
  }
15057
15209
  //#endregion
15058
15210
  //#region src/serialize/toIfc.ts
@@ -1,4 +1,4 @@
1
- import { addHoles, applyMatrix, autoHeal, box, castShape, clone, convexHull, cut, err, extrude, fuse, fuseAll, getBounds, getKernel, getSolids, isClosedWire, isOk, isPlanarWire, isSolid, isValid, isValidSolid, locate, measureVolume, mesh, ok, outerWire, polygon, revolve, rotate, translate, validSolid } from "brepjs";
1
+ import { GeometryCleanupError, addHoles, applyMatrix, autoHeal, box, castShape, clone, convexHull, cut, err, extrude, fuse, fuseAll, getBounds, getKernel, getSolids, isClosedWire, isOk, isPlanarWire, isSolid, isValid, isValidSolid, locate, measureVolume, mesh, ok, outerWire, polygon, revolve, rotate, translate, validSolid } from "brepjs";
2
2
  import * as WebIFC from "web-ifc";
3
3
  import { Handle, IFCROOT, IfcAPI } from "web-ifc";
4
4
  //#region src/identity/ifcGuid.ts
@@ -7598,6 +7598,71 @@ function coveringToSolid(spec) {
7598
7598
  }
7599
7599
  }
7600
7600
  //#endregion
7601
+ //#region src/productBodyCleanup.ts
7602
+ var COMPLETE_CLEANUP = Object.freeze({ kind: "COMPLETE" });
7603
+ function cleanupReport(diagnostics) {
7604
+ const [first, ...rest] = diagnostics;
7605
+ if (first === void 0) return COMPLETE_CLEANUP;
7606
+ const snapshot = Object.freeze([first, ...rest]);
7607
+ return Object.freeze({
7608
+ kind: "FAILED",
7609
+ diagnostics: snapshot
7610
+ });
7611
+ }
7612
+ /** Read known cleanup failures through Result, AggregateError and `using` error chains.
7613
+ * This reports prior attempts only; it never takes ownership or retries a release.
7614
+ */
7615
+ function nestedCleanupDiagnostics(error, context) {
7616
+ const diagnostics = [];
7617
+ const seen = /* @__PURE__ */ new Set();
7618
+ const visit = (value) => {
7619
+ if (typeof value !== "object" || value === null || seen.has(value)) return;
7620
+ seen.add(value);
7621
+ if (value instanceof GeometryCleanupError) {
7622
+ diagnostics.push(Object.freeze({
7623
+ ...context,
7624
+ resourceKind: value.resourceKind,
7625
+ cause: value.cause
7626
+ }));
7627
+ return;
7628
+ }
7629
+ if (value instanceof AggregateError) value.errors.forEach(visit);
7630
+ if ("cause" in value) visit(value.cause);
7631
+ if ("error" in value && "suppressed" in value) {
7632
+ visit(value.error);
7633
+ visit(value.suppressed);
7634
+ }
7635
+ };
7636
+ visit(error);
7637
+ return diagnostics;
7638
+ }
7639
+ /** Owner-only, attempt-all cleanup. Never retry a release with an uncertain outcome. */
7640
+ function cleanupOwnedResources(items, context) {
7641
+ const attempted = /* @__PURE__ */ new Set();
7642
+ const diagnostics = [];
7643
+ for (const { resource, itemIndex, resourceKind = "SHAPE" } of items) {
7644
+ if (attempted.has(resource)) continue;
7645
+ attempted.add(resource);
7646
+ try {
7647
+ resource[Symbol.dispose]();
7648
+ } catch (cause) {
7649
+ diagnostics.push(Object.freeze({
7650
+ ...context,
7651
+ itemIndex,
7652
+ resourceKind,
7653
+ cause
7654
+ }));
7655
+ }
7656
+ }
7657
+ return cleanupReport(diagnostics);
7658
+ }
7659
+ //#endregion
7660
+ //#region src/productBodyTestHooks.ts
7661
+ var hooks = null;
7662
+ function productBodyTestHooks() {
7663
+ return hooks;
7664
+ }
7665
+ //#endregion
7601
7666
  //#region src/types/productBody.ts
7602
7667
  /** Returns borrowed Product-local solids. The model retains ownership. */
7603
7668
  function bodySolids(body) {
@@ -7610,6 +7675,113 @@ function bodySolids(body) {
7610
7675
  function disposeProductBody(body) {
7611
7676
  for (const solid of bodySolids(body)) solid[Symbol.dispose]();
7612
7677
  }
7678
+ function bodyError(operation, code, message, cause, itemIndex, cleanup = COMPLETE_CLEANUP) {
7679
+ return {
7680
+ kind: "BIM_GEOMETRY",
7681
+ operation,
7682
+ code,
7683
+ message,
7684
+ cause,
7685
+ cleanup,
7686
+ ...itemIndex === void 0 ? {} : { itemIndex }
7687
+ };
7688
+ }
7689
+ /** @internal Exact resource object identity only, without native topology queries. */
7690
+ function wrappedResourceObject(solid) {
7691
+ if (typeof solid !== "object" || solid === null || !("wrapped" in solid)) return void 0;
7692
+ if ("disposed" in solid && solid.disposed) return void 0;
7693
+ const wrapped = solid.wrapped;
7694
+ return typeof wrapped === "object" && wrapped !== null ? wrapped : void 0;
7695
+ }
7696
+ /** Validate the public handle contract and native solid topology before narrowing opaque input. */
7697
+ function isLiveValidSolid(value) {
7698
+ return typeof value === "object" && value !== null && "disposed" in value && value.disposed === false && "wrapped" in value && value.wrapped !== null && value.wrapped !== void 0 && "delete" in value && typeof value.delete === "function" && "onDispose" in value && typeof value.onDispose === "function" && Symbol.dispose in value && typeof value[Symbol.dispose] === "function" && getKernel().shapeType(value.wrapped) === "solid" && getKernel().isValid(value.wrapped);
7699
+ }
7700
+ function validateItems(input, operation) {
7701
+ if (!Array.isArray(input) || input.length === 0) return err(bodyError(operation, "BODY_EMPTY_ITEMS", "Expected a nonempty solids array"));
7702
+ const identities = /* @__PURE__ */ new Set();
7703
+ const resources = /* @__PURE__ */ new Set();
7704
+ const solids = [];
7705
+ for (let itemIndex = 0; itemIndex < input.length; itemIndex++) try {
7706
+ const injected = productBodyTestHooks()?.before?.({
7707
+ step: "validate",
7708
+ itemIndex
7709
+ });
7710
+ if (injected && !injected.ok) return err(bodyError(operation, "BODY_VALIDATION_FAILED", "Item validation failed", injected.error, itemIndex));
7711
+ const item = input[itemIndex];
7712
+ if (!isLiveValidSolid(item)) return err(bodyError(operation, "BODY_INVALID_ITEM", "Expected a live valid solid handle", void 0, itemIndex));
7713
+ const resource = wrappedResourceObject(item);
7714
+ if (identities.has(item) || resource !== void 0 && resources.has(resource)) return err(bodyError(operation, "BODY_DUPLICATE_ITEM", "Item duplicates an earlier handle or its wrapped resource object", void 0, itemIndex));
7715
+ identities.add(item);
7716
+ if (resource !== void 0) resources.add(resource);
7717
+ solids.push(item);
7718
+ } catch (cause) {
7719
+ return err(bodyError(operation, "BODY_VALIDATION_FAILED", "Item validation threw", cause, itemIndex));
7720
+ }
7721
+ const [first, ...rest] = solids;
7722
+ if (first === void 0) return err(bodyError(operation, "BODY_EMPTY_ITEMS", "Expected a nonempty solids array"));
7723
+ return ok(Object.freeze([first, ...rest]));
7724
+ }
7725
+ function diagnostics(report) {
7726
+ return report.kind === "FAILED" ? report.diagnostics : [];
7727
+ }
7728
+ function ownedOperation(operation, work) {
7729
+ const scope = {
7730
+ itemIndex: 0,
7731
+ temporaries: []
7732
+ };
7733
+ let result;
7734
+ try {
7735
+ result = work(scope);
7736
+ } catch (cause) {
7737
+ result = err(bodyError(operation, "BODY_OPERATION_FAILED", "Native Body operation threw", cause, scope.itemIndex));
7738
+ }
7739
+ const retired = cleanupOwnedResources(scope.temporaries, { operation });
7740
+ if (result.ok && retired.kind === "COMPLETE") return result;
7741
+ const cleanup = cleanupReport([...result.ok ? [] : nestedCleanupDiagnostics(result.error, {
7742
+ operation,
7743
+ itemIndex: scope.itemIndex
7744
+ }), ...diagnostics(retired)]);
7745
+ if (result.ok) return err(bodyError(operation, "BODY_CLEANUP_FAILED", "Could not retire operation temporaries", void 0, void 0, cleanup));
7746
+ return err(bodyError(operation, result.error.code, result.error.message, result.error, scope.itemIndex, cleanup));
7747
+ }
7748
+ function before(step, itemIndex) {
7749
+ return productBodyTestHooks()?.before?.({
7750
+ step,
7751
+ itemIndex
7752
+ }) ?? ok(void 0);
7753
+ }
7754
+ /** Occupied material in mm³. Borrowed imported items need no authored Body descriptor. */
7755
+ function measureProductBodyMaterial(solids) {
7756
+ const checked = validateItems(solids, "measureProductBodyMaterial");
7757
+ if (!checked.ok) return checked;
7758
+ return ownedOperation("measureProductBodyMaterial", (scope) => {
7759
+ let material = checked.value[0];
7760
+ if (checked.value.length > 1) {
7761
+ const ready = before("union", 0);
7762
+ if (!ready.ok) return ready;
7763
+ const fused = fuseAll([...checked.value], { trackEvolution: false });
7764
+ if (!fused.ok) return err(fromBrepError(fused.error, "BODY_UNION_FAILED", "Could not union occupied material"));
7765
+ material = fused.value;
7766
+ scope.temporaries.push({
7767
+ resource: material,
7768
+ itemIndex: 0
7769
+ });
7770
+ const injected = productBodyTestHooks()?.afterAllocate?.({
7771
+ step: "union",
7772
+ itemIndex: 0,
7773
+ solid: material
7774
+ });
7775
+ if (injected && !injected.ok) return injected;
7776
+ }
7777
+ const ready = before("measure", 0);
7778
+ if (!ready.ok) return ready;
7779
+ const measured = (productBodyTestHooks()?.measure ?? measureVolume)(material);
7780
+ if (!measured.ok) return err(fromBrepError(measured.error, "BODY_MEASUREMENT_FAILED", "Could not measure occupied material"));
7781
+ if (!Number.isFinite(measured.value) || measured.value <= 0) return err(bodyError("measureProductBodyMaterial", "BODY_INVALID_VOLUME", "Expected a positive finite occupied volume in mm³", measured.value));
7782
+ return measured;
7783
+ });
7784
+ }
7613
7785
  //#endregion
7614
7786
  //#region src/model/bimModel.ts
7615
7787
  function exactWallBodyImmutable() {
@@ -14995,41 +15167,21 @@ function preflightExactBody(input) {
14995
15167
  //#endregion
14996
15168
  //#region src/serialize/exactWallQuantities.ts
14997
15169
  function deriveExactWallQuantities(input) {
14998
- const measure = input.dependencies?.measure ?? measureVolume;
14999
- let volumeMm3;
15000
- if (input.solids.length === 1) try {
15001
- const measured = measure(input.solids[0]);
15002
- if (!measured.ok) return exactVolumeError(measured.error);
15003
- volumeMm3 = measured.value;
15170
+ try {
15171
+ const measured = measureProductBodyMaterial(input.solids);
15172
+ if (!measured.ok) return wallVolumeError(measured.error);
15173
+ return ok({
15174
+ lengthM: toIfcLengthM(input.spec.length),
15175
+ widthM: toIfcLengthM(input.spec.thickness),
15176
+ heightM: toIfcLengthM(input.spec.height),
15177
+ netVolumeM3: measured.value / 1e9
15178
+ });
15004
15179
  } catch (cause) {
15005
- return exactVolumeError(cause);
15180
+ return wallVolumeError(cause);
15006
15181
  }
15007
- else {
15008
- const fuse = input.dependencies?.fuse ?? fuseAll;
15009
- let union = null;
15010
- try {
15011
- const fused = fuse([...input.solids]);
15012
- if (!fused.ok) return exactVolumeError(fused.error);
15013
- union = fused.value;
15014
- const measured = measure(union);
15015
- if (!measured.ok) return exactVolumeError(measured.error);
15016
- volumeMm3 = measured.value;
15017
- } catch (cause) {
15018
- return exactVolumeError(cause);
15019
- } finally {
15020
- union?.[Symbol.dispose]();
15021
- }
15022
- }
15023
- if (!Number.isFinite(volumeMm3) || volumeMm3 <= 0) return exactVolumeError(/* @__PURE__ */ new Error(`Measured exact wall volume was ${volumeMm3}`));
15024
- return ok({
15025
- lengthM: toIfcLengthM(input.spec.length),
15026
- widthM: toIfcLengthM(input.spec.thickness),
15027
- heightM: toIfcLengthM(input.spec.height),
15028
- netVolumeM3: volumeMm3 / 1e9
15029
- });
15030
15182
  }
15031
- function exactVolumeError(cause) {
15032
- return err(ifcError("IFC_EXACT_WALL_QUANTITY_DERIVATION_FAILED", "Failed to derive a positive finite NetVolume for an exact wall Product Body", cause));
15183
+ function wallVolumeError(cause) {
15184
+ return err(ifcError("IFC_EXACT_WALL_QUANTITY_DERIVATION_FAILED", "Failed to derive a positive finite NetVolume for the retained Wall Body", cause));
15033
15185
  }
15034
15186
  //#endregion
15035
15187
  //#region src/serialize/toIfc.ts
@@ -0,0 +1,33 @@
1
+ import { NonEmpty } from './types/productBody.js';
2
+ export interface GeometryCleanupDiagnostic {
3
+ readonly operation: string;
4
+ readonly itemIndex: number;
5
+ readonly resourceKind: 'SHAPE' | 'TRANSFORM';
6
+ readonly localId?: number;
7
+ readonly cause: unknown;
8
+ }
9
+ export type CleanupReport = {
10
+ readonly kind: 'COMPLETE';
11
+ } | {
12
+ readonly kind: 'FAILED';
13
+ readonly diagnostics: NonEmpty<GeometryCleanupDiagnostic>;
14
+ };
15
+ export interface OwnedBodyResource {
16
+ readonly resource: Disposable;
17
+ readonly itemIndex: number;
18
+ readonly resourceKind?: 'SHAPE' | 'TRANSFORM';
19
+ }
20
+ export declare const COMPLETE_CLEANUP: CleanupReport;
21
+ export declare function cleanupReport(diagnostics: readonly GeometryCleanupDiagnostic[]): CleanupReport;
22
+ /** Read known cleanup failures through Result, AggregateError and `using` error chains.
23
+ * This reports prior attempts only; it never takes ownership or retries a release.
24
+ */
25
+ export declare function nestedCleanupDiagnostics(error: unknown, context: {
26
+ readonly operation: string;
27
+ readonly itemIndex: number;
28
+ }): readonly GeometryCleanupDiagnostic[];
29
+ /** Owner-only, attempt-all cleanup. Never retry a release with an uncertain outcome. */
30
+ export declare function cleanupOwnedResources(items: readonly OwnedBodyResource[], context: {
31
+ readonly operation: string;
32
+ readonly localId?: number;
33
+ }): CleanupReport;
@@ -0,0 +1,19 @@
1
+ import { BimError } from './errors/bimError.js';
2
+ import { Result, ValidSolid, measureVolume, getBounds } from 'brepjs';
3
+ export type BodyNativeStep = 'validate' | 'union' | 'measure' | 'bounds';
4
+ export interface BodyNativeEvent {
5
+ readonly step: BodyNativeStep;
6
+ readonly itemIndex: number;
7
+ }
8
+ export interface ProductBodyTestHooks {
9
+ readonly before?: (event: BodyNativeEvent) => Result<void, BimError> | void;
10
+ /** Called only after the new handle has been registered for failure cleanup. */
11
+ readonly afterAllocate?: (event: BodyNativeEvent & {
12
+ readonly solid: ValidSolid;
13
+ }) => Result<void, BimError> | void;
14
+ readonly measure?: typeof measureVolume;
15
+ readonly bounds?: typeof getBounds;
16
+ }
17
+ /** Package-internal seam. Tests keep native geometry real and reset after each case. */
18
+ export declare function setProductBodyTestHooksForTesting(value: ProductBodyTestHooks | null): void;
19
+ export declare function productBodyTestHooks(): ProductBodyTestHooks | null;
@@ -1,20 +1,16 @@
1
- import { fuseAll, measureVolume, Result, ValidSolid } from 'brepjs';
1
+ import { Result, ValidSolid } from 'brepjs';
2
2
  import { BimError } from '../errors/bimError.js';
3
3
  import { WallSpec } from '../specs/wallSpec.js';
4
4
  import { NonEmpty } from '../types/productBody.js';
5
- export interface ExactWallQuantityValues {
5
+ interface ExactWallQuantityValues {
6
6
  readonly lengthM: number;
7
7
  readonly widthM: number;
8
8
  readonly heightM: number;
9
9
  readonly netVolumeM3: number;
10
10
  }
11
- export interface ExactWallQuantityDependencies {
12
- readonly fuse?: typeof fuseAll | undefined;
13
- readonly measure?: typeof measureVolume | undefined;
14
- }
15
- export interface ExactWallQuantityInput {
11
+ interface ExactWallQuantityInput {
16
12
  readonly spec: WallSpec;
17
13
  readonly solids: NonEmpty<ValidSolid>;
18
- readonly dependencies?: ExactWallQuantityDependencies | undefined;
19
14
  }
20
15
  export declare function deriveExactWallQuantities(input: ExactWallQuantityInput): Result<ExactWallQuantityValues, BimError>;
16
+ export {};
@@ -1,4 +1,6 @@
1
- import { ValidSolid } from 'brepjs';
1
+ import { Bounds3D, Result, ValidSolid } from 'brepjs';
2
+ import { BimError } from '../errors/bimError.js';
3
+ import { CleanupReport } from '../productBodyCleanup.js';
2
4
  export type NonEmpty<T> = readonly [T, ...T[]];
3
5
  export type ProductBody = {
4
6
  readonly kind: 'PARAMETRIC';
@@ -11,3 +13,17 @@ export type ProductBody = {
11
13
  export declare function bodySolids(body: ProductBody): NonEmpty<ValidSolid>;
12
14
  /** Model-owner cleanup. Borrowers must use {@link bodySolids} without disposing its items. */
13
15
  export declare function disposeProductBody(body: ProductBody): void;
16
+ export type ProductBodyOperation = 'productBodyBounds' | 'measureProductBodyMaterial';
17
+ export interface ProductBodyError extends BimError {
18
+ readonly operation: ProductBodyOperation;
19
+ readonly itemIndex?: number;
20
+ readonly cleanup: CleanupReport;
21
+ }
22
+ /** @internal Exact resource object identity only, without native topology queries. */
23
+ export declare function wrappedResourceObject(solid: unknown): object | undefined;
24
+ /** Occupied material in mm³. Borrowed imported items need no authored Body descriptor. */
25
+ export declare function measureProductBodyMaterial(solids: NonEmpty<ValidSolid>): Result<number, ProductBodyError>;
26
+ /** Borrow all items and query their tight bounds in their existing coordinates. */
27
+ export declare function productBodyBounds(solids: NonEmpty<ValidSolid>): Result<{
28
+ readonly bounds: Readonly<Bounds3D>;
29
+ }, ProductBodyError>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brepjs-bim",
3
- "version": "0.24.2",
3
+ "version": "0.24.3",
4
4
  "description": "BIM layer for brepjs — IFC4-aligned parametric building elements",
5
5
  "keywords": [
6
6
  "bim",
@@ -47,7 +47,7 @@
47
47
  "lint": "eslint src tests"
48
48
  },
49
49
  "peerDependencies": {
50
- "brepjs": ">=18.0.0",
50
+ "brepjs": ">=19.0.5",
51
51
  "web-ifc": ">=0.0.50"
52
52
  },
53
53
  "devDependencies": {