@metamesh/modeling 0.9.0 → 0.11.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +66 -422
  3. package/THIRD_PARTY_NOTICES.md +4 -148
  4. package/dist/api/engine.d.ts +105 -8
  5. package/dist/api/geometry.d.ts +95 -83
  6. package/dist/api/index.d.ts +767 -772
  7. package/dist/engine/construction-operations.js +3 -1
  8. package/dist/engine/deferred-context.js +182 -0
  9. package/dist/engine/execution.js +38 -4
  10. package/dist/engine/finalization.js +65 -133
  11. package/dist/engine/geometry-recipes.js +12 -0
  12. package/dist/engine/immutable-generation-data.js +186 -0
  13. package/dist/engine/index.js +57 -35
  14. package/dist/engine/kernel/manifold.wasm +0 -0
  15. package/dist/engine/model-dependencies.js +46 -0
  16. package/dist/engine/model-summary.js +43 -0
  17. package/dist/engine/native/ownership.js +11 -5
  18. package/dist/engine/placement.js +24 -0
  19. package/dist/engine/recipe-realization.js +119 -0
  20. package/dist/engine/session-graph.js +442 -0
  21. package/dist/engine/session-native.js +90 -0
  22. package/dist/engine/session-snapshot.js +153 -0
  23. package/dist/engine/session.js +184 -0
  24. package/dist/engine/shape-anchors.js +5 -0
  25. package/dist/geometry/index.js +2 -2
  26. package/dist/geometry/mesh-centroid.js +7 -6
  27. package/dist/geometry/mesh-planar.js +11 -10
  28. package/dist/geometry/mesh-quality.js +15 -14
  29. package/dist/geometry/mesh-signature.js +8 -7
  30. package/dist/geometry/mesh-solid.js +10 -4
  31. package/dist/geometry/mesh.js +36 -31
  32. package/dist/geometry/profiles/{cross-section.js → contour.js} +3 -3
  33. package/dist/geometry/profiles/{open-profile.js → side-profile.js} +3 -3
  34. package/dist/geometry/svg-import.js +1 -16
  35. package/dist/index.js +2 -0
  36. package/dist/model/execution-binding.js +5 -5
  37. package/dist/model/generation.js +27 -0
  38. package/dist/model/immutable-buffer.js +111 -0
  39. package/dist/model/modeling-context.js +1 -0
  40. package/dist/model/operations.js +521 -160
  41. package/dist/model/shape-handles.js +44 -0
  42. package/package.json +4 -4
@@ -7,6 +7,7 @@ import { matchProfiles } from "../geometry/profiles/matched.js";
7
7
  import { resolveGeometryResolution } from "../geometry/resolution.js";
8
8
  import { assertSamplingCount } from "../geometry/sampling-limits.js";
9
9
  import { sweepMesh } from "../geometry/sweeps/sweep-mesh.js";
10
+ import { immutableMeshView } from "../model/immutable-buffer.js";
10
11
  import { buildFinishedExtrusion } from "./extrusion-finishing.js";
11
12
  import { createLoftOperation } from "./loft-operations.js";
12
13
  import { triangleMeshToManifold } from "./native/adapter.js";
@@ -91,8 +92,9 @@ export function createConstructionOperations(environment, sections) {
91
92
  ...cutters.map((cutter) => nativeSection(cutter)),
92
93
  ]));
93
94
  });
94
- const solidFromMesh = (mesh) => {
95
+ const solidFromMesh = (input) => {
95
96
  assertActive();
97
+ const mesh = immutableMeshView(input);
96
98
  assertMeshAllocation(mesh.positions.length / 3, mesh.indices.length / 3, mesh.positions.byteLength + mesh.indices.byteLength, "solidFromMesh");
97
99
  return wrapSolid(trackScopedManifoldValue(scope, triangleMeshToManifold(module, mesh, scopedManifoldScope(scope))));
98
100
  };
@@ -0,0 +1,182 @@
1
+ import { GeometryError } from "../geometry/errors.js";
2
+ import { geometryRecipe, isGeometryRecipe, recordGeometryRecipe, } from "./geometry-recipes.js";
3
+ import { captureRecipeInputs } from "./immutable-generation-data.js";
4
+ import { gridPatternPlan, linearPatternPlan, pathPatternPlan, radialPatternPlan, } from "./pattern-placements.js";
5
+ import { RecipeRealization } from "./recipe-realization.js";
6
+ // Every public operation must explicitly choose its realization and result behavior.
7
+ const operationBehaviors = {
8
+ withResolution: "resolution",
9
+ box: "solid",
10
+ cylinder: "solid",
11
+ tube: "solid",
12
+ sphere: "solid",
13
+ torus: "solid",
14
+ heightfield: "solid",
15
+ implicit: "solid",
16
+ extrude: "solid",
17
+ revolve: "solid",
18
+ loft: "solid",
19
+ sweep: "solid",
20
+ compose: "solid",
21
+ minkowskiSum: "solid",
22
+ solidFromMesh: "solid",
23
+ setTolerance: "solid",
24
+ threadRidge: "solid",
25
+ threadedRod: "solid",
26
+ threadedNut: "solid",
27
+ internalThreadCutter: "solid",
28
+ sectionFromContours: "section",
29
+ fromRegion: "section",
30
+ rectangle: "section",
31
+ circle: "section",
32
+ ellipse: "section",
33
+ regularPolygon: "section",
34
+ star: "section",
35
+ projectSection: "section",
36
+ sliceSection: "section",
37
+ textSection: "section",
38
+ svgSection: "section",
39
+ svgPathSection: "section",
40
+ translate: "preserve",
41
+ rotate: "preserve",
42
+ scale: "preserve",
43
+ mirror: "preserve",
44
+ transform: "preserve",
45
+ offset: "preserve",
46
+ simplify: "preserve",
47
+ withAnchors: "preserve",
48
+ attach: "preserve",
49
+ union: "preserve",
50
+ intersect: "preserve",
51
+ subtract: "preserve",
52
+ hull: "preserve",
53
+ toTriangleMesh: "observation",
54
+ toRegion: "observation",
55
+ inspectSection: "observation",
56
+ inspectSolid: "observation",
57
+ isEmpty: "observation",
58
+ distance: "observation",
59
+ rayCast: "observation",
60
+ anchor: "observation",
61
+ matchProfiles: "observation",
62
+ preflightThreadedRod: "observation",
63
+ preflightInternalThreadCutter: "observation",
64
+ preflightThreadedNut: "observation",
65
+ split: "split",
66
+ splitByPlane: "split",
67
+ components: "components",
68
+ linearPattern: "pattern",
69
+ radialPattern: "pattern",
70
+ gridPattern: "pattern",
71
+ pathPattern: "pattern",
72
+ };
73
+ /**
74
+ * Binds declaration to one active revision. Recipe handles retain only immutable session identity
75
+ * and captured inputs; native results remain private to the revision's realization owner.
76
+ */
77
+ export function createDeferredContext(owner, native, limits, initialResolution, duringNative, assertActive, restoreNative = () => undefined) {
78
+ const realization = new RecipeRealization(owner, native, duringNative, restoreNative);
79
+ const declaration = { owner, limits, realization, assertActive };
80
+ return {
81
+ context: createModelingContext(initialResolution, declaration),
82
+ resolve: (shape) => realization.resolve(shape),
83
+ isRecipe: isGeometryRecipe,
84
+ realizedShape: (shape) => realization.realizedShape(shape),
85
+ retentionCandidates: (values) => realization.retentionCandidates(values),
86
+ isSolid: (shape) => isGeometryRecipe(shape) && geometryRecipe(shape)?.kind === "solid",
87
+ };
88
+ }
89
+ function createModelingContext(resolution, declaration) {
90
+ return Object.freeze(Object.fromEntries(Object.keys(operationBehaviors).map((key) => [
91
+ key,
92
+ (...supplied) => declareOperation(key, resolution, supplied, declaration),
93
+ ])));
94
+ }
95
+ /** Captures and validates inputs before deciding whether an operation needs native geometry now. */
96
+ function declareOperation(operation, resolution, supplied, declaration) {
97
+ declaration.assertActive();
98
+ const behavior = operationBehaviors[operation];
99
+ if (behavior === "resolution") {
100
+ const nextResolution = Object.freeze({ ...resolution, ...supplied[0] });
101
+ return createModelingContext(nextResolution, declaration);
102
+ }
103
+ if (behavior === "pattern")
104
+ return declarePattern(operation, resolution, supplied, declaration);
105
+ const args = captureRecipeInputs(supplied);
106
+ declaration.realization.assertOwned(args);
107
+ const call = { method: operation, args, resolution };
108
+ if (behavior === "observation")
109
+ return declaration.realization.invoke(call);
110
+ const recipe = {
111
+ owner: declaration.owner,
112
+ kind: resultKind(behavior, args[0]),
113
+ call,
114
+ };
115
+ if (behavior === "split")
116
+ return Object.freeze([
117
+ recordGeometryRecipe({ ...recipe, index: 0 }),
118
+ recordGeometryRecipe({ ...recipe, index: 1 }),
119
+ ]);
120
+ if (behavior === "components") {
121
+ // Cardinality requires realization now; projected handles reuse that same call result later.
122
+ const values = declaration.realization.evaluate(call);
123
+ return Object.freeze(values.map((_value, index) => recordGeometryRecipe({ ...recipe, index })));
124
+ }
125
+ return recordGeometryRecipe(recipe);
126
+ }
127
+ function resultKind(behavior, input) {
128
+ if (behavior === "section")
129
+ return "section";
130
+ if (behavior === "preserve" || behavior === "components")
131
+ return inputKind(input);
132
+ return "solid";
133
+ }
134
+ function inputKind(value) {
135
+ if (Array.isArray(value))
136
+ return inputKind(value[0]);
137
+ if (typeof value !== "object" || value === null)
138
+ return "solid";
139
+ const recipe = geometryRecipe(value);
140
+ if (recipe !== undefined)
141
+ return recipe.kind;
142
+ if ("sections" in value)
143
+ return "section";
144
+ if ("points" in value &&
145
+ Array.isArray(value.points) &&
146
+ Array.isArray(value.points[0]) &&
147
+ value.points[0].length === 2)
148
+ return "section";
149
+ return "solid";
150
+ }
151
+ /** Runs every factory during declaration; each placed copy stays deferred until it is used. */
152
+ function declarePattern(operation, resolution, supplied, declaration) {
153
+ const plan = patternPlan(operation, supplied, declaration.limits);
154
+ if (plan.count > declaration.limits.maxVertices)
155
+ throw new GeometryError("COMPLEXITY_LIMIT", "Pattern declaration exceeds execution limits.", {
156
+ operation,
157
+ });
158
+ const source = supplied[0];
159
+ const copies = [];
160
+ for (let index = 0; index < plan.count; index++) {
161
+ const shape = typeof source === "function"
162
+ ? source(index)
163
+ : source;
164
+ copies.push(declareOperation("transform", resolution, [shape, plan.transformAt(index)], declaration));
165
+ }
166
+ return Object.freeze(copies);
167
+ }
168
+ function patternPlan(operation, supplied, limits) {
169
+ const options = supplied[1];
170
+ switch (operation) {
171
+ case "linearPattern":
172
+ return linearPatternPlan(options);
173
+ case "radialPattern":
174
+ return radialPatternPlan(options);
175
+ case "gridPattern":
176
+ return gridPatternPlan(options);
177
+ case "pathPattern":
178
+ return pathPatternPlan(options, supplied[2], limits);
179
+ default:
180
+ throw new Error(`Operation ${operation} does not declare a pattern.`);
181
+ }
182
+ }
@@ -1,10 +1,10 @@
1
1
  import { GeometryError } from "../geometry/errors.js";
2
2
  import { analyzeTriangleMesh, } from "../geometry/mesh.js";
3
3
  import { assertMeshAllocationWithinLimits } from "../geometry/model-limits.js";
4
- import {} from "../geometry/resolution.js";
5
4
  import { assertSamplingCount } from "../geometry/sampling-limits.js";
5
+ import { createSectionHandle, createSolidHandle } from "../model/shape-handles.js";
6
6
  import { createModelingContext } from "./modeling-context.js";
7
- import { ownScopedManifoldValue } from "./native/context.js";
7
+ import { ownScopedManifoldValue, scopedManifoldScope, trackScopedManifoldValue, } from "./native/context.js";
8
8
  import { copiedFloat32Array, copiedUint32Array } from "./native/copied-views.js";
9
9
  import * as kernel from "./native/index.js";
10
10
  import { assertNativeSolidStatus } from "./native/status.js";
@@ -47,14 +47,14 @@ export function createExecutionContext(module, scope, limits, initialResolution
47
47
  };
48
48
  const wrapSolid = (value) => {
49
49
  assertActive();
50
- const handle = Object.freeze({});
50
+ const handle = createSolidHandle();
51
51
  handles.set(handle, { kind: "solid", value });
52
52
  return handle;
53
53
  };
54
54
  const wrapSection = (value) => {
55
55
  assertActive();
56
56
  assertSamplingCount(value.numVert(), "section", limits.maxProfilePoints);
57
- const handle = Object.freeze({});
57
+ const handle = createSectionHandle();
58
58
  handles.set(handle, { kind: "section", value });
59
59
  return handle;
60
60
  };
@@ -254,6 +254,40 @@ export function createExecutionContext(module, scope, limits, initialResolution
254
254
  };
255
255
  return {
256
256
  context: createModelingContext(owner, initialResolution),
257
+ captureNative(shape) {
258
+ const native = resolveHandle(shape);
259
+ // The kernel does not expose allocation sizes. Charge topology conservatively, including
260
+ // fixed object overhead, and force any lazy geometry while the run still owns it.
261
+ const bytes = native.kind === "solid"
262
+ ? 4096 +
263
+ native.value.numVert() * 64 +
264
+ native.value.numTri() * 96 +
265
+ native.value.numPropVert() * native.value.numProp() * 8
266
+ : 1024 + native.value.numVert() * 64 + native.value.numContour() * 128;
267
+ return {
268
+ native,
269
+ bytes,
270
+ anchors: shapeAnchors.read(shape),
271
+ profile: primitiveSectionProfiles.get(shape),
272
+ };
273
+ },
274
+ /** Transfers cleanup to the session cache without deleting the realized geometry. */
275
+ detachNative(geometry) {
276
+ assertActive();
277
+ scopedManifoldScope(scope).release(geometry.native.value);
278
+ },
279
+ restoreNative(geometry) {
280
+ assertActive();
281
+ trackScopedManifoldValue(scope, geometry.native.value);
282
+ const shape = geometry.native.kind === "solid"
283
+ ? wrapSolid(geometry.native.value)
284
+ : wrapSection(geometry.native.value);
285
+ if (geometry.anchors !== undefined)
286
+ shapeAnchors.remember(shape, geometry.anchors);
287
+ if (geometry.profile !== undefined)
288
+ primitiveSectionProfiles.set(shape, geometry.profile);
289
+ return shape;
290
+ },
257
291
  analyzedMeshFor,
258
292
  isSolid: (value) => typeof value === "object" && value !== null && handles.get(value)?.kind === "solid",
259
293
  validateMesh: (mesh) => {
@@ -2,172 +2,90 @@ import { GeometryError } from "../geometry/errors.js";
2
2
  import { optimizePlanarPatches } from "../geometry/mesh-planar.js";
3
3
  import { validateAnalyzedSolidMesh } from "../geometry/mesh-solid.js";
4
4
  import { analyzeTriangleMesh, } from "../geometry/mesh.js";
5
+ import { immutableMeshView } from "../model/immutable-buffer.js";
5
6
  import { MeshViewCache } from "./mesh-view-cache.js";
6
7
  import { copiedMeshViews, copiedUint32Array } from "./native/copied-views.js";
7
8
  const isArray = (value) => Array.isArray(value);
8
- function finalPartTotals(parts) {
9
- const buffers = new Set();
10
- let vertices = 0, triangles = 0, bytes = 0, volumeMm3 = 0;
11
- for (const part of parts) {
12
- vertices += part.measurements.vertexCount;
13
- triangles += part.measurements.triangleCount;
14
- volumeMm3 += part.measurements.signedVolume;
15
- for (const buffer of [part.positions.buffer, part.indices.buffer]) {
16
- if (!buffers.has(buffer)) {
17
- buffers.add(buffer);
18
- bytes += buffer.byteLength;
19
- }
20
- }
21
- }
22
- return { vertices, triangles, bytes, volumeMm3 };
9
+ const snapshotMeshes = new WeakMap();
10
+ /** Registers trusted snapshot storage without exposing its shared backing arrays publicly. */
11
+ export function rememberSnapshotPart(part, mesh) {
12
+ snapshotMeshes.set(part, mesh);
23
13
  }
24
- function fitsLimits(totals, limits) {
25
- return (totals.vertices <= limits.maxVertices &&
26
- totals.triangles <= limits.maxTriangles &&
27
- totals.bytes <= limits.maxBytes);
14
+ function fitsGeometryLimits(mesh, limits) {
15
+ const buffers = new Set([mesh.positions.buffer, mesh.indices.buffer]);
16
+ let bytes = 0;
17
+ for (const buffer of buffers)
18
+ bytes += buffer.byteLength;
19
+ return (mesh.positions.length / 3 <= limits.maxVertices &&
20
+ mesh.indices.length / 3 <= limits.maxTriangles &&
21
+ bytes <= limits.maxBytes);
28
22
  }
29
- /** Finalizes one complete build under the selected limits, preserving usable originals when optional cleanup fails. */
30
- export function finalizeModel(execution, output, limits, options, startedAt, builtAt) {
23
+ /** Copies, validates and optionally simplifies local geometry, sharing equal views within one transaction. */
24
+ export function createGeometryFinalizer(execution, limits, options) {
31
25
  if (options.meshValidation !== undefined &&
32
26
  options.meshValidation !== "topology" &&
33
27
  options.meshValidation !== "solid")
34
28
  throw new GeometryError("INVALID_ARGUMENT", "Unsupported mesh validation mode.", {
35
29
  operation: "finalize",
36
30
  });
37
- const descriptors = execution.isSolid(output) ? [{ id: "model", solid: output }] : output;
38
- if (!isArray(descriptors) || descriptors.length === 0)
39
- throw new GeometryError("INVALID_ARGUMENT", "Build must return a solid or at least one named part.", { operation: "finalize" });
40
- if (descriptors.length > limits.maxParts)
41
- throw new GeometryError("COMPLEXITY_LIMIT", `Output exceeds ${limits.maxParts} parts.`, {
42
- operation: "finalize",
43
- });
44
- const ids = new Set();
45
- const backingBuffers = new Set();
46
31
  const meshCopies = new Map();
47
- const directMeasurements = new MeshViewCache();
48
- const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
49
- let vertices = 0, triangles = 0, bytes = 0;
50
- let parts = descriptors.map((descriptor) => {
51
- if (typeof descriptor !== "object" ||
52
- descriptor === null ||
53
- typeof descriptor.id !== "string" ||
54
- !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(descriptor.id) ||
55
- descriptor.id.length > 64 ||
56
- ids.has(descriptor.id))
57
- throw new GeometryError("INVALID_ARGUMENT", "Part IDs must be unique canonical lowercase identifiers of at most 64 characters.", { operation: "finalize" });
58
- ids.add(descriptor.id);
59
- if (descriptor.color !== undefined &&
60
- (typeof descriptor.color !== "string" || !/^#[\da-f]{6}$/i.test(descriptor.color)))
61
- throw new GeometryError("INVALID_ARGUMENT", "Part colors must be six-digit hex strings.", {
62
- operation: "finalize",
63
- });
64
- if ((descriptor.solid === undefined) === (descriptor.mesh === undefined))
65
- throw new GeometryError("INVALID_ARGUMENT", "Each part must contain exactly one solid or mesh.", { operation: "finalize" });
66
- const nativeAnalysis = descriptor.solid === undefined ? undefined : execution.analyzedMeshFor(descriptor.solid);
67
- const source = descriptor.solid === undefined ? descriptor.mesh : nativeAnalysis.mesh;
32
+ const finalized = new MeshViewCache();
33
+ return function finalizeGeometry(geometry) {
34
+ const nativeAnalysis = execution.isSolid(geometry)
35
+ ? execution.analyzedMeshFor(geometry)
36
+ : undefined;
37
+ const input = nativeAnalysis?.mesh ?? geometry;
38
+ // Immutable generation buffers expose owned copies publicly. Unwrap only at this trusted
39
+ // copy boundary, so the captured mesh retains shared backing storage without repeated copies.
40
+ const source = immutableMeshView(input);
68
41
  if (!(source.positions instanceof Float32Array) ||
69
42
  !(source.indices instanceof Uint32Array) ||
70
43
  !(source.positions.buffer instanceof ArrayBuffer) ||
71
44
  !(source.indices.buffer instanceof ArrayBuffer))
72
45
  throw new GeometryError("INVALID_MESH", "Parts require transferable Float32 positions and Uint32 indices.", { operation: "finalize" });
73
- const sourceVertices = source.positions.length / 3, sourceTriangles = source.indices.length / 3;
74
- let sourceBytes = 0;
75
- for (const buffer of [source.positions.buffer, source.indices.buffer]) {
76
- if (!backingBuffers.has(buffer)) {
77
- backingBuffers.add(buffer);
78
- sourceBytes += buffer.byteLength;
79
- }
80
- }
81
- vertices += sourceVertices;
82
- triangles += sourceTriangles;
83
- bytes += sourceBytes;
84
- if (vertices > limits.maxVertices || triangles > limits.maxTriangles || bytes > limits.maxBytes)
46
+ if (!fitsGeometryLimits(source, limits))
85
47
  throw new GeometryError("COMPLEXITY_LIMIT", "Aggregate output exceeds the execution limits.", { operation: "finalize" });
48
+ const cached = finalized.get(source);
49
+ if (cached !== undefined)
50
+ return cached;
86
51
  // Direct meshes are copied before analysis so later caller mutation cannot invalidate facts.
87
- const mesh = descriptor.mesh === undefined ? source : copiedMeshViews(source, meshCopies);
88
- let measurements = nativeAnalysis?.measurements ?? directMeasurements.get(mesh);
52
+ const mesh = nativeAnalysis === undefined ? copiedMeshViews(source, meshCopies) : source;
53
+ let measurements = nativeAnalysis?.measurements;
89
54
  if (measurements === undefined) {
90
55
  measurements = analyzeTriangleMesh(mesh, { requireClosed: true, includeTopology: true });
91
56
  execution.validateMesh(mesh);
92
- directMeasurements.set(mesh, measurements);
93
57
  }
94
58
  if (!(measurements.signedVolume > 0))
95
59
  throw new GeometryError("INVALID_TOPOLOGY", "Manufacturing parts must enclose positive oriented volume.", { operation: "finalize" });
96
- for (let axis = 0; axis < 3; axis += 1) {
97
- min[axis] = Math.min(min[axis], measurements.bounds.min[axis]);
98
- max[axis] = Math.max(max[axis], measurements.bounds.max[axis]);
99
- }
100
- return Object.freeze({
101
- id: descriptor.id,
102
- ...mesh,
103
- ...(descriptor.color === undefined ? {} : { color: descriptor.color.toLowerCase() }),
104
- measurements,
105
- });
106
- });
107
- const originalParts = parts;
108
- if (options.meshTopologyMode === "optimized" && triangles <= 200_000) {
109
- const candidates = new MeshViewCache();
110
- parts = parts.map((part) => {
111
- const cached = candidates.get(part);
112
- if (cached !== undefined)
113
- return Object.freeze({ ...part, ...cached });
114
- let selected = {
115
- positions: part.positions,
116
- indices: part.indices,
117
- measurements: part.measurements,
118
- };
60
+ let selected = { ...mesh, measurements };
61
+ if (options.meshTopologyMode === "optimized" && measurements.triangleCount <= 200_000) {
119
62
  try {
120
- const optimized = optimizePlanarPatches(part);
63
+ const optimized = optimizePlanarPatches(mesh);
121
64
  if (optimized.changed) {
122
- const mesh = {
123
- positions: part.positions,
65
+ const candidate = {
66
+ positions: mesh.positions,
124
67
  indices: copiedUint32Array(optimized.mesh.indices),
125
68
  };
126
- const measurements = analyzeTriangleMesh(mesh, {
127
- requireClosed: true,
128
- includeTopology: true,
129
- });
130
- execution.validateMesh(mesh);
131
- selected = { ...mesh, measurements };
69
+ if (fitsGeometryLimits(candidate, limits)) {
70
+ const facts = analyzeTriangleMesh(candidate, {
71
+ requireClosed: true,
72
+ includeTopology: true,
73
+ });
74
+ execution.validateMesh(candidate);
75
+ selected = { ...candidate, measurements: facts };
76
+ }
132
77
  }
133
78
  }
134
79
  catch {
135
80
  // Optional optimization preserves the usable original on unsupported geometry.
136
81
  }
137
- candidates.set(part, selected);
138
- return Object.freeze({ ...part, ...selected });
139
- });
140
- }
141
- // Statistics describe the exact selected buffers after optional finalization.
142
- let totals = finalPartTotals(parts);
143
- if (!fitsLimits(totals, limits) && parts !== originalParts) {
144
- parts = originalParts;
145
- totals = finalPartTotals(parts);
146
- }
147
- if (!fitsLimits(totals, limits))
148
- throw new GeometryError("COMPLEXITY_LIMIT", "Selected output exceeds the execution limits.", {
149
- operation: "finalize",
150
- });
151
- if (options.meshValidation === "solid") {
152
- const checked = new MeshViewCache();
153
- for (const part of parts) {
154
- if (checked.get(part) !== undefined)
155
- continue;
156
- validateAnalyzedSolidMesh(part, part.measurements);
157
- checked.set(part, true);
158
82
  }
159
- }
160
- const finalizedAt = performance.now();
161
- return Object.freeze({
162
- parts: Object.freeze(parts),
163
- bounds: Object.freeze({ min: Object.freeze(min), max: Object.freeze(max) }),
164
- stats: Object.freeze({
165
- ...totals,
166
- buildMs: builtAt - startedAt,
167
- finalizationMs: finalizedAt - builtAt,
168
- generationMs: finalizedAt - startedAt,
169
- }),
170
- });
83
+ if (options.meshValidation === "solid")
84
+ validateAnalyzedSolidMesh(selected, selected.measurements);
85
+ const result = Object.freeze(selected);
86
+ finalized.set(source, result);
87
+ return result;
88
+ };
171
89
  }
172
90
  /** Captures mutable external buffers once before an asynchronous snapshot export begins. */
173
91
  export function captureExportParts(parts, limits) {
@@ -175,10 +93,24 @@ export function captureExportParts(parts, limits) {
175
93
  throw new GeometryError("COMPLEXITY_LIMIT", `Export requires one through ${limits.maxParts} parts.`, {
176
94
  operation: "export3mf",
177
95
  });
96
+ const captured = parts.map((part) => {
97
+ if (typeof part !== "object" || part === null)
98
+ throw new GeometryError("INVALID_ARGUMENT", "Export part IDs must be unique canonical identifiers.", { operation: "export3mf" });
99
+ // Access public owned-copy getters exactly once, or retain trusted private sharing for a
100
+ // snapshot created by this engine. Capture still copies before the asynchronous boundary.
101
+ const mesh = snapshotMeshes.get(part) ?? { positions: part.positions, indices: part.indices };
102
+ const id = part.id, color = part.color;
103
+ return {
104
+ id,
105
+ positions: mesh.positions,
106
+ indices: mesh.indices,
107
+ ...(color === undefined ? {} : { color }),
108
+ };
109
+ });
178
110
  const identifiers = new Set();
179
111
  const buffers = new Set();
180
112
  let vertexCount = 0, triangleCount = 0, byteCount = 0;
181
- for (const part of parts) {
113
+ for (const part of captured) {
182
114
  if (typeof part !== "object" ||
183
115
  part === null ||
184
116
  typeof part.id !== "string" ||
@@ -215,7 +147,7 @@ export function captureExportParts(parts, limits) {
215
147
  });
216
148
  }
217
149
  const copiedBuffers = new Map();
218
- return parts.map((part) => ({
150
+ return captured.map((part) => ({
219
151
  id: part.id,
220
152
  ...copiedMeshViews(part, copiedBuffers),
221
153
  ...(part.color === undefined ? {} : { color: part.color }),
@@ -0,0 +1,12 @@
1
+ import { createSectionHandle, createSolidHandle } from "../model/shape-handles.js";
2
+ const recipes = new WeakMap();
3
+ /** Reads private recipe metadata without requiring an execution context. */
4
+ export const geometryRecipe = (value) => recipes.get(value);
5
+ /** Recognizes only opaque recipe handles issued by this engine. */
6
+ export const isGeometryRecipe = (value) => typeof value === "object" && value !== null && recipes.has(value);
7
+ /** Creates an immutable opaque handle for a recorded operation. */
8
+ export function recordGeometryRecipe(recipe) {
9
+ const handle = recipe.kind === "solid" ? createSolidHandle() : createSectionHandle();
10
+ recipes.set(handle, recipe);
11
+ return handle;
12
+ }