brepjs-bim 0.24.2 → 0.24.4

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,15 @@ 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.
201
+
202
+ ### Wall quantity failures
203
+
204
+ Wall `NetVolume` measures retained occupied material for both `PARAMETRIC` and `EXACT` geometry. Recipe Walls retain supported area and weight quantities. Exact takeover has no recipe quantity eligibility.
205
+
206
+ If measurement or temporary cleanup fails, serialization omits optional Wall quantities. `toIfcValidated` reports an element-specific `WALL_QUANTITY_OMITTED` warning from that same serialization. `toIfc` still returns a bytes `Result`.
@@ -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() {
@@ -12920,7 +13092,7 @@ function pushWeightQuantity(w, qtyIds, volumeM3, densityKgM3) {
12920
13092
  if (densityKgM3 === void 0) return;
12921
13093
  qtyIds.push(writeWeightQuantity(w, "GrossWeight", volumeM3, densityKgM3));
12922
13094
  }
12923
- function writeWallBaseQuantities(w, ownerHistoryId, wallExpressId, spec, openings, densityKgM3) {
13095
+ function writeWallBaseQuantities(w, ownerHistoryId, wallExpressId, spec, openings, netVolumeM3, densityKgM3) {
12924
13096
  const lengthM = toIfcLengthM(spec.length);
12925
13097
  const widthM = toIfcLengthM(spec.thickness);
12926
13098
  const heightM = toIfcLengthM(spec.height);
@@ -12936,7 +13108,6 @@ function writeWallBaseQuantities(w, ownerHistoryId, wallExpressId, spec, opening
12936
13108
  if (toIfcLengthM(op.offsetFromFloor) < FLOOR_TOUCH_EPSILON_M) sumFloorTouchingFootprintM2 += opWidthM * widthM;
12937
13109
  }
12938
13110
  const netSideAreaM2 = grossSideAreaM2 - sumOpeningAreaM2;
12939
- const netVolumeM3 = grossVolumeM3 - sumOpeningAreaM2 * widthM;
12940
13111
  const netFootprintM2 = grossFootprintM2 - sumFloorTouchingFootprintM2;
12941
13112
  const qtyIds = [
12942
13113
  writeQtyLength(w, "Length", lengthM),
@@ -12952,7 +13123,7 @@ function writeWallBaseQuantities(w, ownerHistoryId, wallExpressId, spec, opening
12952
13123
  pushWeightQuantity(w, qtyIds, netVolumeM3, densityKgM3);
12953
13124
  writeRelDefinesByProperties(w, ownerHistoryId, wallExpressId, writeElementQuantity(w, ownerHistoryId, "Qto_WallBaseQuantities", qtyIds));
12954
13125
  }
12955
- function writeExactWallBaseQuantities(w, ownerHistoryId, wallExpressId, values) {
13126
+ function writeWallBodyQuantities(w, ownerHistoryId, wallExpressId, values) {
12956
13127
  writeRelDefinesByProperties(w, ownerHistoryId, wallExpressId, writeElementQuantity(w, ownerHistoryId, "Qto_WallBaseQuantities", [
12957
13128
  writeQtyLength(w, "Length", values.lengthM),
12958
13129
  writeQtyLength(w, "Width", values.widthM),
@@ -15016,43 +15187,54 @@ function preflightExactBody(input) {
15016
15187
  return (0, brepjs.ok)([first.value, ...remainingPrepared]);
15017
15188
  }
15018
15189
  //#endregion
15019
- //#region src/serialize/exactWallQuantities.ts
15020
- 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;
15190
+ //#region src/serialize/wallQuantities.ts
15191
+ function deriveWallQuantities(input) {
15192
+ try {
15193
+ const items = [];
15194
+ for (const retained of input.solids) {
15195
+ if ((0, brepjs.getKernel)().shapeType(retained.wrapped) !== "compound") {
15196
+ items.push(retained);
15197
+ continue;
15198
+ }
15199
+ const cardinality = singleWrapperChild(retained);
15200
+ if (!cardinality.ok) return cardinality;
15201
+ const children = (0, brepjs.getSolids)(retained);
15202
+ const [child] = children;
15203
+ if (children.length !== 1 || child === void 0 || !(0, brepjs.isValidSolid)(child)) return wallVolumeError(/* @__PURE__ */ new Error("Expected one valid solid in a retained recipe wrapper"));
15204
+ items.push(child);
15205
+ }
15206
+ const [first, ...rest] = items;
15207
+ if (first === void 0) return wallVolumeError(/* @__PURE__ */ new Error("Expected retained Wall material"));
15208
+ const measured = measureProductBodyMaterial([first, ...rest]);
15209
+ if (!measured.ok) return wallVolumeError(measured.error);
15210
+ return (0, brepjs.ok)({
15211
+ lengthM: toIfcLengthM(input.spec.length),
15212
+ widthM: toIfcLengthM(input.spec.thickness),
15213
+ heightM: toIfcLengthM(input.spec.height),
15214
+ netVolumeM3: measured.value / 1e9
15215
+ });
15027
15216
  } catch (cause) {
15028
- return exactVolumeError(cause);
15217
+ return wallVolumeError(cause);
15029
15218
  }
15219
+ }
15220
+ /** Count before cached extraction: a failed later cast cannot strand earlier children. */
15221
+ function singleWrapperChild(retained) {
15222
+ const kernel = (0, brepjs.getKernel)();
15223
+ let count;
15224
+ if (kernel.subShapeCount !== void 0) count = kernel.subShapeCount(retained.wrapped, "solid");
15030
15225
  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
- }
15226
+ const rawChildren = kernel.iterShapes(retained.wrapped, "solid");
15227
+ const cleanup = cleanupOwnedResources(rawChildren.map((raw, itemIndex) => ({
15228
+ resource: { [Symbol.dispose]: () => kernel.dispose(raw) },
15229
+ itemIndex
15230
+ })), { operation: "wallQuantityWrapperCount" });
15231
+ if (cleanup.kind === "FAILED") return wallVolumeError({ cleanup });
15232
+ count = rawChildren.length;
15045
15233
  }
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
- });
15234
+ return count === 1 ? (0, brepjs.ok)(void 0) : wallVolumeError(/* @__PURE__ */ new Error("Expected one valid solid in a retained recipe wrapper"));
15053
15235
  }
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));
15236
+ function wallVolumeError(cause) {
15237
+ return (0, brepjs.err)(ifcError("IFC_WALL_QUANTITY_DERIVATION_FAILED", "Failed to derive a positive finite NetVolume for the retained Wall Body", cause));
15056
15238
  }
15057
15239
  //#endregion
15058
15240
  //#region src/serialize/toIfc.ts
@@ -15083,8 +15265,13 @@ function preflightProductBodies(elements) {
15083
15265
  return (0, brepjs.ok)(preflighted);
15084
15266
  }
15085
15267
  async function toIfc(model, meta) {
15268
+ const serialized = await serializeIfc(model, meta);
15269
+ return serialized.ok ? (0, brepjs.ok)(serialized.value.bytes) : serialized;
15270
+ }
15271
+ async function serializeIfc(model, meta) {
15086
15272
  try {
15087
15273
  var _usingCtx$2 = _usingCtx();
15274
+ const issues = [];
15088
15275
  const project = model.getProject();
15089
15276
  if (!project) return (0, brepjs.err)(ifcError("NO_PROJECT", "BimModel has no project — call model.init() first"));
15090
15277
  if ((model.getBridges().length > 0 || model.getBridgeParts().length > 0 || model.getEarthworksFills().length > 0) && (meta.ifcSchema ?? "IFC4") !== "IFC4X3") return (0, brepjs.err)(ifcError("IFC4X3_REQUIRED", "Bridge, Bridge Part, and Earthworks Fill entities require IFC4X3 serialization"));
@@ -15202,10 +15389,10 @@ async function toIfc(model, meta) {
15202
15389
  const wall = preflighted.element;
15203
15390
  const containingId = findContainerOf(wall.localId, relationships);
15204
15391
  const storeyPlacementId = containingId !== null ? placementMap.get(containingId) ?? null : null;
15205
- const exactQuantities = preflighted.kind === "EXACT" ? deriveExactWallQuantities({
15392
+ const bodyQuantities = deriveWallQuantities({
15206
15393
  spec: wall.spec,
15207
- solids: preflighted.solids
15208
- }) : null;
15394
+ solids: bodySolids(wall.geometry)
15395
+ });
15209
15396
  const { localPlacementId, productDefinitionShapeId } = preflighted.kind === "EXACT" ? writeExactBodyGeometry(w, wall.spec, preflighted.items, geomSubContextId, storeyPlacementId) : writeWallGeometry(w, wall.spec, geomSubContextId, storeyPlacementId);
15210
15397
  const wallExpressId = writeWallEntity(w, wall.guid, `Wall ${i + 1}`, ownerHistoryId, localPlacementId, productDefinitionShapeId);
15211
15398
  idMap.set(wall.localId, wallExpressId);
@@ -15213,9 +15400,12 @@ async function toIfc(model, meta) {
15213
15400
  writeWallCommonPset(w, ownerHistoryId, wallExpressId, wall.spec);
15214
15401
  writeManufacturerPset(w, ownerHistoryId, wallExpressId, wall.spec);
15215
15402
  if (wall.spec.customProperties !== void 0) writeCustomPsets(w, ownerHistoryId, wallExpressId, wall.spec.customProperties);
15216
- if (preflighted.kind === "EXACT") {
15217
- if (exactQuantities !== null && exactQuantities.ok) writeExactWallBaseQuantities(w, ownerHistoryId, wallExpressId, exactQuantities.value);
15218
- } else writeWallBaseQuantities(w, ownerHistoryId, wallExpressId, wall.spec, openingsByWall.get(wall.localId) ?? [], densityByElement.get(wall.localId));
15403
+ if (!bodyQuantities.ok) issues.push(issue("warning", "WALL_QUANTITY_OMITTED", "Wall quantities omitted because occupied material volume could not be measured", wall.localId, {
15404
+ guid: wall.guid,
15405
+ cause: bodyQuantities.error
15406
+ }));
15407
+ else if (wall.geometry.kind === "PARAMETRIC") writeWallBaseQuantities(w, ownerHistoryId, wallExpressId, wall.spec, openingsByWall.get(wall.localId) ?? [], bodyQuantities.value.netVolumeM3, densityByElement.get(wall.localId));
15408
+ else writeWallBodyQuantities(w, ownerHistoryId, wallExpressId, bodyQuantities.value);
15219
15409
  }
15220
15410
  for (const [i, slab] of slabs.entries()) {
15221
15411
  const containingId = findContainerOf(slab.localId, relationships);
@@ -15559,7 +15749,11 @@ async function toIfc(model, meta) {
15559
15749
  for (const { ref, ids } of classByKey.values()) byClassification.set(ref, ids);
15560
15750
  if (byClassification.size > 0) writeClassificationRefs(w, ownerHistoryId, byClassification);
15561
15751
  writeTypeLayer(w, ownerHistoryId, model, idMap);
15562
- return w.save();
15752
+ const saved = w.save();
15753
+ return saved.ok ? (0, brepjs.ok)({
15754
+ bytes: saved.value,
15755
+ issues
15756
+ }) : saved;
15563
15757
  } catch (_) {
15564
15758
  _usingCtx$2.e = _;
15565
15759
  } finally {
@@ -15679,15 +15873,16 @@ function toOccurrences(elements, predefined) {
15679
15873
  async function toIfcValidated(model, meta) {
15680
15874
  const integrity = checkReferentialIntegrity(model);
15681
15875
  if (hasErrors(integrity)) return (0, brepjs.err)(ifcError("INTEGRITY_FAILURE", "Model failed referential integrity check", integrity.issues.filter((i) => i.severity === "error")));
15682
- const bytesResult = await toIfc(model, meta);
15683
- if (!bytesResult.ok) return bytesResult;
15684
- const bytes = bytesResult.value;
15876
+ const serialized = await serializeIfc(model, meta);
15877
+ if (!serialized.ok) return serialized;
15878
+ const { bytes } = serialized.value;
15685
15879
  const geometry = collectGeometryIssues(model);
15686
15880
  const schema = await checkSchema(bytes);
15687
15881
  const roundTrip = await checkRoundTrip(bytes);
15688
15882
  const gherkin = await checkGherkinRules(bytes);
15689
15883
  const report = { issues: [
15690
15884
  ...integrity.issues,
15885
+ ...serialized.value.issues,
15691
15886
  ...geometry.issues,
15692
15887
  ...schema.issues,
15693
15888
  ...roundTrip.issues,
@@ -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() {
@@ -12897,7 +13069,7 @@ function pushWeightQuantity(w, qtyIds, volumeM3, densityKgM3) {
12897
13069
  if (densityKgM3 === void 0) return;
12898
13070
  qtyIds.push(writeWeightQuantity(w, "GrossWeight", volumeM3, densityKgM3));
12899
13071
  }
12900
- function writeWallBaseQuantities(w, ownerHistoryId, wallExpressId, spec, openings, densityKgM3) {
13072
+ function writeWallBaseQuantities(w, ownerHistoryId, wallExpressId, spec, openings, netVolumeM3, densityKgM3) {
12901
13073
  const lengthM = toIfcLengthM(spec.length);
12902
13074
  const widthM = toIfcLengthM(spec.thickness);
12903
13075
  const heightM = toIfcLengthM(spec.height);
@@ -12913,7 +13085,6 @@ function writeWallBaseQuantities(w, ownerHistoryId, wallExpressId, spec, opening
12913
13085
  if (toIfcLengthM(op.offsetFromFloor) < FLOOR_TOUCH_EPSILON_M) sumFloorTouchingFootprintM2 += opWidthM * widthM;
12914
13086
  }
12915
13087
  const netSideAreaM2 = grossSideAreaM2 - sumOpeningAreaM2;
12916
- const netVolumeM3 = grossVolumeM3 - sumOpeningAreaM2 * widthM;
12917
13088
  const netFootprintM2 = grossFootprintM2 - sumFloorTouchingFootprintM2;
12918
13089
  const qtyIds = [
12919
13090
  writeQtyLength(w, "Length", lengthM),
@@ -12929,7 +13100,7 @@ function writeWallBaseQuantities(w, ownerHistoryId, wallExpressId, spec, opening
12929
13100
  pushWeightQuantity(w, qtyIds, netVolumeM3, densityKgM3);
12930
13101
  writeRelDefinesByProperties(w, ownerHistoryId, wallExpressId, writeElementQuantity(w, ownerHistoryId, "Qto_WallBaseQuantities", qtyIds));
12931
13102
  }
12932
- function writeExactWallBaseQuantities(w, ownerHistoryId, wallExpressId, values) {
13103
+ function writeWallBodyQuantities(w, ownerHistoryId, wallExpressId, values) {
12933
13104
  writeRelDefinesByProperties(w, ownerHistoryId, wallExpressId, writeElementQuantity(w, ownerHistoryId, "Qto_WallBaseQuantities", [
12934
13105
  writeQtyLength(w, "Length", values.lengthM),
12935
13106
  writeQtyLength(w, "Width", values.widthM),
@@ -14993,43 +15164,54 @@ function preflightExactBody(input) {
14993
15164
  return ok([first.value, ...remainingPrepared]);
14994
15165
  }
14995
15166
  //#endregion
14996
- //#region src/serialize/exactWallQuantities.ts
14997
- 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;
15167
+ //#region src/serialize/wallQuantities.ts
15168
+ function deriveWallQuantities(input) {
15169
+ try {
15170
+ const items = [];
15171
+ for (const retained of input.solids) {
15172
+ if (getKernel().shapeType(retained.wrapped) !== "compound") {
15173
+ items.push(retained);
15174
+ continue;
15175
+ }
15176
+ const cardinality = singleWrapperChild(retained);
15177
+ if (!cardinality.ok) return cardinality;
15178
+ const children = getSolids(retained);
15179
+ const [child] = children;
15180
+ if (children.length !== 1 || child === void 0 || !isValidSolid(child)) return wallVolumeError(/* @__PURE__ */ new Error("Expected one valid solid in a retained recipe wrapper"));
15181
+ items.push(child);
15182
+ }
15183
+ const [first, ...rest] = items;
15184
+ if (first === void 0) return wallVolumeError(/* @__PURE__ */ new Error("Expected retained Wall material"));
15185
+ const measured = measureProductBodyMaterial([first, ...rest]);
15186
+ if (!measured.ok) return wallVolumeError(measured.error);
15187
+ return ok({
15188
+ lengthM: toIfcLengthM(input.spec.length),
15189
+ widthM: toIfcLengthM(input.spec.thickness),
15190
+ heightM: toIfcLengthM(input.spec.height),
15191
+ netVolumeM3: measured.value / 1e9
15192
+ });
15004
15193
  } catch (cause) {
15005
- return exactVolumeError(cause);
15194
+ return wallVolumeError(cause);
15006
15195
  }
15196
+ }
15197
+ /** Count before cached extraction: a failed later cast cannot strand earlier children. */
15198
+ function singleWrapperChild(retained) {
15199
+ const kernel = getKernel();
15200
+ let count;
15201
+ if (kernel.subShapeCount !== void 0) count = kernel.subShapeCount(retained.wrapped, "solid");
15007
15202
  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
- }
15203
+ const rawChildren = kernel.iterShapes(retained.wrapped, "solid");
15204
+ const cleanup = cleanupOwnedResources(rawChildren.map((raw, itemIndex) => ({
15205
+ resource: { [Symbol.dispose]: () => kernel.dispose(raw) },
15206
+ itemIndex
15207
+ })), { operation: "wallQuantityWrapperCount" });
15208
+ if (cleanup.kind === "FAILED") return wallVolumeError({ cleanup });
15209
+ count = rawChildren.length;
15022
15210
  }
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
- });
15211
+ return count === 1 ? ok(void 0) : wallVolumeError(/* @__PURE__ */ new Error("Expected one valid solid in a retained recipe wrapper"));
15030
15212
  }
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));
15213
+ function wallVolumeError(cause) {
15214
+ return err(ifcError("IFC_WALL_QUANTITY_DERIVATION_FAILED", "Failed to derive a positive finite NetVolume for the retained Wall Body", cause));
15033
15215
  }
15034
15216
  //#endregion
15035
15217
  //#region src/serialize/toIfc.ts
@@ -15060,8 +15242,13 @@ function preflightProductBodies(elements) {
15060
15242
  return ok(preflighted);
15061
15243
  }
15062
15244
  async function toIfc(model, meta) {
15245
+ const serialized = await serializeIfc(model, meta);
15246
+ return serialized.ok ? ok(serialized.value.bytes) : serialized;
15247
+ }
15248
+ async function serializeIfc(model, meta) {
15063
15249
  try {
15064
15250
  var _usingCtx$2 = _usingCtx();
15251
+ const issues = [];
15065
15252
  const project = model.getProject();
15066
15253
  if (!project) return err(ifcError("NO_PROJECT", "BimModel has no project — call model.init() first"));
15067
15254
  if ((model.getBridges().length > 0 || model.getBridgeParts().length > 0 || model.getEarthworksFills().length > 0) && (meta.ifcSchema ?? "IFC4") !== "IFC4X3") return err(ifcError("IFC4X3_REQUIRED", "Bridge, Bridge Part, and Earthworks Fill entities require IFC4X3 serialization"));
@@ -15179,10 +15366,10 @@ async function toIfc(model, meta) {
15179
15366
  const wall = preflighted.element;
15180
15367
  const containingId = findContainerOf(wall.localId, relationships);
15181
15368
  const storeyPlacementId = containingId !== null ? placementMap.get(containingId) ?? null : null;
15182
- const exactQuantities = preflighted.kind === "EXACT" ? deriveExactWallQuantities({
15369
+ const bodyQuantities = deriveWallQuantities({
15183
15370
  spec: wall.spec,
15184
- solids: preflighted.solids
15185
- }) : null;
15371
+ solids: bodySolids(wall.geometry)
15372
+ });
15186
15373
  const { localPlacementId, productDefinitionShapeId } = preflighted.kind === "EXACT" ? writeExactBodyGeometry(w, wall.spec, preflighted.items, geomSubContextId, storeyPlacementId) : writeWallGeometry(w, wall.spec, geomSubContextId, storeyPlacementId);
15187
15374
  const wallExpressId = writeWallEntity(w, wall.guid, `Wall ${i + 1}`, ownerHistoryId, localPlacementId, productDefinitionShapeId);
15188
15375
  idMap.set(wall.localId, wallExpressId);
@@ -15190,9 +15377,12 @@ async function toIfc(model, meta) {
15190
15377
  writeWallCommonPset(w, ownerHistoryId, wallExpressId, wall.spec);
15191
15378
  writeManufacturerPset(w, ownerHistoryId, wallExpressId, wall.spec);
15192
15379
  if (wall.spec.customProperties !== void 0) writeCustomPsets(w, ownerHistoryId, wallExpressId, wall.spec.customProperties);
15193
- if (preflighted.kind === "EXACT") {
15194
- if (exactQuantities !== null && exactQuantities.ok) writeExactWallBaseQuantities(w, ownerHistoryId, wallExpressId, exactQuantities.value);
15195
- } else writeWallBaseQuantities(w, ownerHistoryId, wallExpressId, wall.spec, openingsByWall.get(wall.localId) ?? [], densityByElement.get(wall.localId));
15380
+ if (!bodyQuantities.ok) issues.push(issue("warning", "WALL_QUANTITY_OMITTED", "Wall quantities omitted because occupied material volume could not be measured", wall.localId, {
15381
+ guid: wall.guid,
15382
+ cause: bodyQuantities.error
15383
+ }));
15384
+ else if (wall.geometry.kind === "PARAMETRIC") writeWallBaseQuantities(w, ownerHistoryId, wallExpressId, wall.spec, openingsByWall.get(wall.localId) ?? [], bodyQuantities.value.netVolumeM3, densityByElement.get(wall.localId));
15385
+ else writeWallBodyQuantities(w, ownerHistoryId, wallExpressId, bodyQuantities.value);
15196
15386
  }
15197
15387
  for (const [i, slab] of slabs.entries()) {
15198
15388
  const containingId = findContainerOf(slab.localId, relationships);
@@ -15536,7 +15726,11 @@ async function toIfc(model, meta) {
15536
15726
  for (const { ref, ids } of classByKey.values()) byClassification.set(ref, ids);
15537
15727
  if (byClassification.size > 0) writeClassificationRefs(w, ownerHistoryId, byClassification);
15538
15728
  writeTypeLayer(w, ownerHistoryId, model, idMap);
15539
- return w.save();
15729
+ const saved = w.save();
15730
+ return saved.ok ? ok({
15731
+ bytes: saved.value,
15732
+ issues
15733
+ }) : saved;
15540
15734
  } catch (_) {
15541
15735
  _usingCtx$2.e = _;
15542
15736
  } finally {
@@ -15656,15 +15850,16 @@ function toOccurrences(elements, predefined) {
15656
15850
  async function toIfcValidated(model, meta) {
15657
15851
  const integrity = checkReferentialIntegrity(model);
15658
15852
  if (hasErrors(integrity)) return err(ifcError("INTEGRITY_FAILURE", "Model failed referential integrity check", integrity.issues.filter((i) => i.severity === "error")));
15659
- const bytesResult = await toIfc(model, meta);
15660
- if (!bytesResult.ok) return bytesResult;
15661
- const bytes = bytesResult.value;
15853
+ const serialized = await serializeIfc(model, meta);
15854
+ if (!serialized.ok) return serialized;
15855
+ const { bytes } = serialized.value;
15662
15856
  const geometry = collectGeometryIssues(model);
15663
15857
  const schema = await checkSchema(bytes);
15664
15858
  const roundTrip = await checkRoundTrip(bytes);
15665
15859
  const gherkin = await checkGherkinRules(bytes);
15666
15860
  const report = { issues: [
15667
15861
  ...integrity.issues,
15862
+ ...serialized.value.issues,
15668
15863
  ...geometry.issues,
15669
15864
  ...schema.issues,
15670
15865
  ...roundTrip.issues,
@@ -44,8 +44,8 @@ export declare function writeCustomPsets(w: IfcWriter, ownerHistoryId: number, e
44
44
  * so the caller can skip emitting a weight quantity.
45
45
  */
46
46
  export declare function resolveDensityKgM3(materialName: string | undefined, explicitDensityKgM3: number | undefined): number | undefined;
47
- export declare function writeWallBaseQuantities(w: IfcWriter, ownerHistoryId: number, wallExpressId: number, spec: WallSpec, openings: readonly WallOpeningSpec[], densityKgM3?: number): void;
48
- export declare function writeExactWallBaseQuantities(w: IfcWriter, ownerHistoryId: number, wallExpressId: number, values: {
47
+ export declare function writeWallBaseQuantities(w: IfcWriter, ownerHistoryId: number, wallExpressId: number, spec: WallSpec, openings: readonly WallOpeningSpec[], netVolumeM3: number, densityKgM3?: number): void;
48
+ export declare function writeWallBodyQuantities(w: IfcWriter, ownerHistoryId: number, wallExpressId: number, values: {
49
49
  readonly lengthM: number;
50
50
  readonly widthM: number;
51
51
  readonly heightM: number;
@@ -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;
@@ -0,0 +1,16 @@
1
+ import { Result, ValidSolid } from 'brepjs';
2
+ import { BimError } from '../errors/bimError.js';
3
+ import { WallSpec } from '../specs/wallSpec.js';
4
+ import { NonEmpty } from '../types/productBody.js';
5
+ interface WallQuantityValues {
6
+ readonly lengthM: number;
7
+ readonly widthM: number;
8
+ readonly heightM: number;
9
+ readonly netVolumeM3: number;
10
+ }
11
+ interface WallQuantityInput {
12
+ readonly spec: WallSpec;
13
+ readonly solids: NonEmpty<ValidSolid>;
14
+ }
15
+ export declare function deriveWallQuantities(input: WallQuantityInput): Result<WallQuantityValues, 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.4",
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": {
@@ -1,20 +0,0 @@
1
- import { fuseAll, measureVolume, Result, ValidSolid } from 'brepjs';
2
- import { BimError } from '../errors/bimError.js';
3
- import { WallSpec } from '../specs/wallSpec.js';
4
- import { NonEmpty } from '../types/productBody.js';
5
- export interface ExactWallQuantityValues {
6
- readonly lengthM: number;
7
- readonly widthM: number;
8
- readonly heightM: number;
9
- readonly netVolumeM3: number;
10
- }
11
- export interface ExactWallQuantityDependencies {
12
- readonly fuse?: typeof fuseAll | undefined;
13
- readonly measure?: typeof measureVolume | undefined;
14
- }
15
- export interface ExactWallQuantityInput {
16
- readonly spec: WallSpec;
17
- readonly solids: NonEmpty<ValidSolid>;
18
- readonly dependencies?: ExactWallQuantityDependencies | undefined;
19
- }
20
- export declare function deriveExactWallQuantities(input: ExactWallQuantityInput): Result<ExactWallQuantityValues, BimError>;