@metamesh/modeling 0.10.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 (36) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +56 -17
  3. package/dist/api/engine.d.ts +105 -8
  4. package/dist/api/index.d.ts +754 -771
  5. package/dist/engine/construction-operations.js +3 -1
  6. package/dist/engine/deferred-context.js +182 -0
  7. package/dist/engine/execution.js +38 -4
  8. package/dist/engine/finalization.js +65 -133
  9. package/dist/engine/geometry-recipes.js +12 -0
  10. package/dist/engine/immutable-generation-data.js +186 -0
  11. package/dist/engine/index.js +57 -35
  12. package/dist/engine/kernel/manifold.wasm +0 -0
  13. package/dist/engine/model-dependencies.js +46 -0
  14. package/dist/engine/model-summary.js +43 -0
  15. package/dist/engine/native/ownership.js +11 -5
  16. package/dist/engine/placement.js +24 -0
  17. package/dist/engine/recipe-realization.js +119 -0
  18. package/dist/engine/session-graph.js +442 -0
  19. package/dist/engine/session-native.js +90 -0
  20. package/dist/engine/session-snapshot.js +153 -0
  21. package/dist/engine/session.js +184 -0
  22. package/dist/engine/shape-anchors.js +5 -0
  23. package/dist/geometry/mesh-centroid.js +7 -6
  24. package/dist/geometry/mesh-planar.js +11 -10
  25. package/dist/geometry/mesh-quality.js +15 -14
  26. package/dist/geometry/mesh-signature.js +8 -7
  27. package/dist/geometry/mesh-solid.js +10 -4
  28. package/dist/geometry/mesh.js +36 -31
  29. package/dist/index.js +2 -0
  30. package/dist/model/execution-binding.js +5 -5
  31. package/dist/model/generation.js +27 -0
  32. package/dist/model/immutable-buffer.js +111 -0
  33. package/dist/model/modeling-context.js +1 -0
  34. package/dist/model/operations.js +521 -160
  35. package/dist/model/shape-handles.js +44 -0
  36. package/package.json +4 -4
@@ -0,0 +1,186 @@
1
+ import { GeometryError } from "../geometry/errors.js";
2
+ import { adoptImmutableBuffer, immutableBuffer, immutableBufferView, isImmutableBuffer, } from "../model/immutable-buffer.js";
3
+ import { geometryRecipe, isGeometryRecipe } from "./geometry-recipes.js";
4
+ const mutableBufferMessage = "Generation data cannot contain mutable typed arrays or raw ArrayBuffers. Use immutableBuffer(array) or immutableMesh(mesh) before caching or binding them.";
5
+ /** Iterates plain fields; recipe capture may read each enumerable accessor once. */
6
+ export function* generationDataEntries(value, captureAccessors = false) {
7
+ const prototype = Object.getPrototypeOf(value);
8
+ if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null)
9
+ throw new TypeError("Generation values must be immutable plain data or Modeling recipes.");
10
+ for (const key of Reflect.ownKeys(value)) {
11
+ if (Array.isArray(value) && key === "length")
12
+ continue;
13
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
14
+ if (!captureAccessors && !("value" in descriptor))
15
+ throw new GeometryError("INVALID_ARGUMENT", "Generation data requires own data fields, not accessors.", { operation: "computed" });
16
+ if (typeof key !== "string" || !descriptor.enumerable)
17
+ throw new TypeError("Generation data requires enumerable string-keyed fields.");
18
+ yield [
19
+ key,
20
+ "value" in descriptor ? descriptor.value : Reflect.get(value, key),
21
+ ];
22
+ }
23
+ }
24
+ /** Conservative retained-data estimate used only to evict optional session caches. */
25
+ export function generationValueBytes(values, ceiling) {
26
+ const seen = new Set();
27
+ const buffers = new Set();
28
+ let bytes = 0;
29
+ const visit = (value, depth) => {
30
+ if (bytes > ceiling)
31
+ return;
32
+ if (depth > 128) {
33
+ bytes = Infinity;
34
+ return;
35
+ }
36
+ if (typeof value === "string") {
37
+ bytes += value.length * 2;
38
+ return;
39
+ }
40
+ if (typeof value !== "object" || value === null) {
41
+ bytes += 8;
42
+ return;
43
+ }
44
+ if (seen.has(value))
45
+ return;
46
+ seen.add(value);
47
+ const array = isImmutableBuffer(value)
48
+ ? immutableBufferView(value)
49
+ : ArrayBuffer.isView(value)
50
+ ? value
51
+ : undefined;
52
+ if (array !== undefined) {
53
+ if (!buffers.has(array.buffer)) {
54
+ buffers.add(array.buffer);
55
+ bytes += array.buffer.byteLength;
56
+ }
57
+ return;
58
+ }
59
+ const recipe = geometryRecipe(value);
60
+ // Empty records and long field names still occupy retained storage.
61
+ bytes += 32;
62
+ if (recipe !== undefined) {
63
+ visit(recipe.call, depth + 1);
64
+ return;
65
+ }
66
+ for (const [key, item] of Object.entries(value)) {
67
+ bytes += 16 + key.length * 2;
68
+ visit(item, depth + 1);
69
+ }
70
+ };
71
+ values.forEach((value) => visit(value, 0));
72
+ return bytes;
73
+ }
74
+ /** Bounded development probe, deliberately separate from normal cache equality or production work. */
75
+ export function compareGenerationValues(left, right) {
76
+ let remaining = 10_000;
77
+ const visited = new Map();
78
+ const compare = (a, b, depth = 0) => {
79
+ if (--remaining < 0 || depth > 128)
80
+ return "unsupported";
81
+ if (Object.is(a, b))
82
+ return "equal";
83
+ if (typeof a !== typeof b || a === null || b === null)
84
+ return "different";
85
+ if (typeof a === "function")
86
+ return "unsupported";
87
+ if (typeof a !== "object" || typeof b !== "object")
88
+ return "different";
89
+ if (visited.get(a)?.has(b))
90
+ return "equal";
91
+ const matches = visited.get(a) ?? new Set();
92
+ matches.add(b);
93
+ visited.set(a, matches);
94
+ const recipeA = geometryRecipe(a), recipeB = geometryRecipe(b);
95
+ if (recipeA !== undefined || recipeB !== undefined) {
96
+ if (recipeA === undefined ||
97
+ recipeB === undefined ||
98
+ recipeA.owner !== recipeB.owner ||
99
+ recipeA.kind !== recipeB.kind ||
100
+ recipeA.call.method !== recipeB.call.method ||
101
+ recipeA.index !== recipeB.index)
102
+ return "different";
103
+ const resolution = compare(recipeA.call.resolution, recipeB.call.resolution, depth + 1);
104
+ return resolution === "equal"
105
+ ? compare(recipeA.call.args, recipeB.call.args, depth + 1)
106
+ : resolution;
107
+ }
108
+ const arrayA = isImmutableBuffer(a)
109
+ ? immutableBufferView(a)
110
+ : ArrayBuffer.isView(a)
111
+ ? a
112
+ : undefined;
113
+ const arrayB = isImmutableBuffer(b)
114
+ ? immutableBufferView(b)
115
+ : ArrayBuffer.isView(b)
116
+ ? b
117
+ : undefined;
118
+ if (arrayA !== undefined || arrayB !== undefined) {
119
+ if (arrayA === undefined ||
120
+ arrayB === undefined ||
121
+ Object.getPrototypeOf(arrayA) !== Object.getPrototypeOf(arrayB) ||
122
+ arrayA.byteLength !== arrayB.byteLength)
123
+ return "different";
124
+ const bytesA = new Uint8Array(arrayA.buffer, arrayA.byteOffset, arrayA.byteLength), bytesB = new Uint8Array(arrayB.buffer, arrayB.byteOffset, arrayB.byteLength);
125
+ if (bytesA.length > remaining)
126
+ return "unsupported";
127
+ remaining -= bytesA.length;
128
+ return bytesA.every((value, index) => value === bytesB[index]) ? "equal" : "different";
129
+ }
130
+ if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b) ||
131
+ (Array.isArray(a) && Array.isArray(b) && a.length !== b.length))
132
+ return "different";
133
+ const keysA = Object.keys(a), keysB = Object.keys(b);
134
+ if (keysA.length > remaining)
135
+ return "unsupported";
136
+ if (keysA.length !== keysB.length || keysA.some((key) => !Object.hasOwn(b, key)))
137
+ return "different";
138
+ for (const key of keysA) {
139
+ const result = compare(Reflect.get(a, key), Reflect.get(b, key), depth + 1);
140
+ if (result !== "equal")
141
+ return result;
142
+ }
143
+ return "equal";
144
+ };
145
+ return compare(left, right);
146
+ }
147
+ function captureValue(value, recipeInputs, readerAllowed = () => false) {
148
+ const seen = new Map();
149
+ const copy = (input) => {
150
+ if (typeof input === "function" && !recipeInputs && !readerAllowed(input))
151
+ throw new GeometryError("INVALID_ARGUMENT", "Computed values and bindings cannot contain arbitrary function closures.", { operation: "computed" });
152
+ if (typeof input !== "object" || input === null || isGeometryRecipe(input))
153
+ return input;
154
+ if (isImmutableBuffer(input))
155
+ return adoptImmutableBuffer(input);
156
+ const old = seen.get(input);
157
+ if (old !== undefined)
158
+ return old;
159
+ if (ArrayBuffer.isView(input)) {
160
+ if (!recipeInputs)
161
+ throw new TypeError(mutableBufferMessage);
162
+ const captured = immutableBuffer(input);
163
+ seen.set(input, captured);
164
+ return captured;
165
+ }
166
+ if (input instanceof ArrayBuffer)
167
+ throw new TypeError(mutableBufferMessage);
168
+ const entries = generationDataEntries(input, recipeInputs);
169
+ const result = Array.isArray(input)
170
+ ? new Array(input.length)
171
+ : Object.create(Object.getPrototypeOf(input) === null ? null : Object.prototype);
172
+ seen.set(input, result);
173
+ for (const [key, field] of entries)
174
+ Object.defineProperty(result, key, { value: copy(field), enumerable: true });
175
+ return Object.freeze(result);
176
+ };
177
+ return copy(value);
178
+ }
179
+ /** Validates while capturing immutable data; only explicitly allowed session readers may be functions. */
180
+ export function captureGenerationValue(value, readerAllowed) {
181
+ return captureValue(value, false, readerAllowed);
182
+ }
183
+ /** Captures ordinary inputs once, including accessor values and mutable numeric arrays. */
184
+ export function captureRecipeInputs(values) {
185
+ return captureValue(values, true);
186
+ }
@@ -1,18 +1,29 @@
1
1
  import { GeometryError } from "../geometry/errors.js";
2
2
  import { defaultModelingLimits, resolveModelingLimits } from "../geometry/model-limits.js";
3
- import { bindModelContext } from "../model/execution-binding.js";
4
3
  import { createExecutionContext } from "./execution.js";
5
- import { captureExportParts, finalizeModel } from "./finalization.js";
4
+ import { captureExportParts } from "./finalization.js";
6
5
  import createManifoldModule from "./kernel/manifold.js";
7
6
  import { withManifoldScope } from "./native/context.js";
8
- import { assertSynchronousScopeResult } from "./native/ownership.js";
7
+ import { createSession } from "./session.js";
9
8
  export { defaultModelingLimits };
10
9
  /** URL of the package's pinned WASM asset; browser bundlers may replace it with their emitted URL. */
11
10
  export const modelingWasmUrl = new URL("./kernel/manifold.wasm", import.meta.url).href;
12
- /** Creates an engine over one already initialized host kernel.
13
- * Kept internal to host integration tests; ordinary consumers use createModelEngine.
14
- * @internal
11
+ /**
12
+ * Runs declaration and immediate assertions without initializing a kernel or finalizing parts.
13
+ * Geometry observations require an initialized session's `validate` method instead.
15
14
  */
15
+ export function validateModel(program, params, ...settings) {
16
+ const session = createSession(program, settings[0] ?? {}, defaultModelingLimits, (_options, callback) => callback(() => {
17
+ throw new GeometryError("INVALID_ARGUMENT", "Geometry observations require an initialized model session.", { operation: "validateModel", details: { requiresKernel: true } });
18
+ }));
19
+ try {
20
+ return session.validate(params);
21
+ }
22
+ finally {
23
+ session.dispose();
24
+ }
25
+ }
26
+ /** Serializes operations over one initialized kernel; sessions own their caches. */
16
27
  function engineFromModule(module, engineLimits) {
17
28
  let building = false;
18
29
  return Object.freeze({
@@ -31,33 +42,33 @@ function engineFromModule(module, engineLimits) {
31
42
  building = false;
32
43
  }
33
44
  },
34
- build(build, options = {}) {
35
- if (building)
36
- throw new GeometryError("INVALID_ARGUMENT", "An engine cannot run nested builds.", {
37
- operation: "build",
38
- });
39
- const limits = resolveModelingLimits(options.limits, engineLimits);
40
- building = true;
41
- const startedAt = performance.now();
42
- try {
43
- return withManifoldScope(module, (scope) => {
44
- const execution = createExecutionContext(module, scope, limits, options.resolution);
45
- try {
46
- const output = bindModelContext(execution.context, () => build(execution.context));
47
- assertSynchronousScopeResult(output, () => new GeometryError("INVALID_ARGUMENT", "Build callbacks must be synchronous.", {
48
- operation: "build",
49
- }));
50
- const builtAt = performance.now();
51
- return finalizeModel(execution, output, limits, options, startedAt, builtAt);
52
- }
53
- finally {
54
- execution.dispose();
55
- }
56
- });
57
- }
58
- finally {
59
- building = false;
60
- }
45
+ createSession(program, ...settings) {
46
+ return createSession(program, settings[0] ?? {}, engineLimits, (options, callback) => {
47
+ if (building)
48
+ throw new GeometryError("INVALID_ARGUMENT", "An engine cannot run nested operations.", {
49
+ operation: "generate",
50
+ });
51
+ const limits = resolveModelingLimits(options.limits, engineLimits);
52
+ building = true;
53
+ try {
54
+ // The trusted finalizer captures only copied manufacturing buffers. Its snapshot has
55
+ // owned-copy accessors, so publish it after the native scope's structural escape check.
56
+ let captured;
57
+ withManifoldScope(module, (scope) => {
58
+ let execution;
59
+ try {
60
+ captured = callback(() => (execution ??= createExecutionContext(module, scope, limits, options.resolution)));
61
+ }
62
+ finally {
63
+ execution?.dispose();
64
+ }
65
+ });
66
+ return captured;
67
+ }
68
+ finally {
69
+ building = false;
70
+ }
71
+ });
61
72
  },
62
73
  });
63
74
  }
@@ -66,11 +77,22 @@ function engineFromModule(module, engineLimits) {
66
77
  *
67
78
  * @example Build a box in a host application
68
79
  * ```ts
69
- * import { box } from "@metamesh/modeling";
80
+ * import { box, generate } from "@metamesh/modeling";
70
81
  * import { createModelEngine } from "@metamesh/modeling/engine";
71
82
  *
72
83
  * const engine = await createModelEngine();
73
- * const snapshot = engine.build(() => box({ size: [30, 20, 8] }));
84
+ * const model = generate<Record<never, never>>(({ part }) => [part("body", box({ size: [30, 20, 8] }))]);
85
+ * const session = engine.createSession(model);
86
+ * try {
87
+ * const snapshot = session.run({});
88
+ * try {
89
+ * await engine.export3mf(snapshot.parts);
90
+ * } finally {
91
+ * snapshot.dispose();
92
+ * }
93
+ * } finally {
94
+ * session.dispose();
95
+ * }
74
96
  * ```
75
97
  */
76
98
  export async function createModelEngine(options = {}) {
File without changes
@@ -0,0 +1,46 @@
1
+ /** Projects recorded reads into immutable host metadata without evaluating any computation. */
2
+ export function createModelDependencies(output, modelParameters, entries, readerIds) {
3
+ const parameterNames = (entry) => [...(entry?.parameters.keys() ?? [])].filter((key) => typeof key === "string");
4
+ const rootParameters = new Map();
5
+ const parametersFor = (root) => {
6
+ const cached = rootParameters.get(root);
7
+ if (cached !== undefined)
8
+ return cached;
9
+ const parameters = new Set();
10
+ const visited = new Set();
11
+ const pending = [root];
12
+ while (pending.length > 0) {
13
+ const id = pending.pop();
14
+ if (visited.has(id))
15
+ continue;
16
+ visited.add(id);
17
+ const entry = entries.get(id);
18
+ for (const key of parameterNames(entry))
19
+ parameters.add(key);
20
+ for (const dependency of entry?.dependencies.keys() ?? [])
21
+ pending.push(dependency);
22
+ }
23
+ const result = [...parameters];
24
+ rootParameters.set(root, result);
25
+ return result;
26
+ };
27
+ return Object.freeze({
28
+ modelParameters: Object.freeze([...modelParameters].sort()),
29
+ computations: Object.freeze([...entries].map(([id, entry]) => Object.freeze({
30
+ id,
31
+ parameters: Object.freeze(parameterNames(entry).sort()),
32
+ computations: Object.freeze([...(entry?.dependencies.keys() ?? [])].sort()),
33
+ }))),
34
+ parts: Object.freeze(output.map((part) => {
35
+ const roots = [part.geometry, part.position, part.rotation].flatMap((value) => {
36
+ const id = typeof value === "function" ? readerIds.get(value) : undefined;
37
+ return id === undefined ? [] : [id];
38
+ });
39
+ return Object.freeze({
40
+ id: part.id,
41
+ computations: Object.freeze(roots),
42
+ parameters: Object.freeze([...new Set(roots.flatMap(parametersFor))].sort()),
43
+ });
44
+ })),
45
+ });
46
+ }
@@ -0,0 +1,43 @@
1
+ import { GeometryError } from "../geometry/errors.js";
2
+ /** Counts manufacturing instances while charging shared backing storage only once. */
3
+ export function createModelSummary(limits) {
4
+ let vertices = 0, triangles = 0, bytes = 0, volumeMm3 = 0;
5
+ const storage = new Set();
6
+ const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
7
+ return {
8
+ add(geometry) {
9
+ const measurements = geometry.measurements;
10
+ vertices += measurements.vertexCount;
11
+ triangles += measurements.triangleCount;
12
+ volumeMm3 += measurements.signedVolume;
13
+ for (const buffer of [geometry.positions.buffer, geometry.indices.buffer])
14
+ if (!storage.has(buffer)) {
15
+ storage.add(buffer);
16
+ bytes += buffer.byteLength;
17
+ }
18
+ if (vertices > limits.maxVertices ||
19
+ triangles > limits.maxTriangles ||
20
+ bytes > limits.maxBytes)
21
+ throw new GeometryError("COMPLEXITY_LIMIT", "Aggregate output exceeds the execution limits.", { operation: "generate" });
22
+ for (let axis = 0; axis < 3; axis++) {
23
+ min[axis] = Math.min(min[axis], measurements.bounds.min[axis]);
24
+ max[axis] = Math.max(max[axis], measurements.bounds.max[axis]);
25
+ }
26
+ },
27
+ finish(startedAt, builtAt) {
28
+ const endedAt = performance.now();
29
+ return {
30
+ bounds: Object.freeze({ min: Object.freeze(min), max: Object.freeze(max) }),
31
+ stats: Object.freeze({
32
+ vertices,
33
+ triangles,
34
+ bytes,
35
+ volumeMm3,
36
+ buildMs: builtAt - startedAt,
37
+ finalizationMs: endedAt - builtAt,
38
+ generationMs: endedAt - startedAt,
39
+ }),
40
+ };
41
+ },
42
+ };
43
+ }
@@ -4,7 +4,7 @@ const disposedScopes = new WeakSet();
4
4
  * reverse construction order and disposal is idempotent.
5
5
  */
6
6
  export class WasmObjectScope {
7
- #owned = [];
7
+ #owned = new Set();
8
8
  #disposed = false;
9
9
  /**
10
10
  * Transfers cleanup responsibility for a live WASM value to this scope.
@@ -15,9 +15,15 @@ export class WasmObjectScope {
15
15
  value.delete();
16
16
  throw new Error("Cannot add an object to a disposed WASM scope.");
17
17
  }
18
- this.#owned.push(value);
18
+ this.#owned.add(value);
19
19
  return value;
20
20
  }
21
+ /** @internal Transfers one live value to a private owner without deleting it. */
22
+ release(value) {
23
+ if (this.#disposed || !this.#owned.delete(value)) {
24
+ throw new Error("Cannot release a value outside its active WASM scope.");
25
+ }
26
+ }
21
27
  /**
22
28
  * Deletes owned values in reverse construction order.
23
29
  *
@@ -32,15 +38,15 @@ export class WasmObjectScope {
32
38
  this.#disposed = true;
33
39
  disposedScopes.add(this);
34
40
  const errors = [];
35
- for (let index = this.#owned.length - 1; index >= 0; index -= 1) {
41
+ for (const value of [...this.#owned].reverse()) {
36
42
  try {
37
- this.#owned[index].delete();
43
+ value.delete();
38
44
  }
39
45
  catch (error) {
40
46
  errors.push(error);
41
47
  }
42
48
  }
43
- this.#owned.length = 0;
49
+ this.#owned.clear();
44
50
  if (errors.length > 0) {
45
51
  throw new AggregateError(errors, `Failed to delete ${errors.length} WASM object(s).`);
46
52
  }
@@ -0,0 +1,24 @@
1
+ import { validateAnalyzedSolidMesh } from "../geometry/mesh-solid.js";
2
+ import { measureTriangleMesh } from "../geometry/mesh.js";
3
+ import { composeTransform3, eulerTransform3, translationTransform3, } from "../geometry/transform3.js";
4
+ /** Places already validated immutable geometry once, retaining its indexed topology. */
5
+ export function placeFinalizedGeometry(source, position, rotation, meshValidation) {
6
+ if ([...position, ...rotation].every((value) => value === 0))
7
+ return source;
8
+ const matrix = composeTransform3(translationTransform3(position), eulerTransform3(rotation));
9
+ const positions = new Float32Array(source.positions.length);
10
+ for (let offset = 0; offset < positions.length; offset += 3) {
11
+ const x = source.positions[offset], y = source.positions[offset + 1], z = source.positions[offset + 2];
12
+ positions[offset] = matrix[0] * x + matrix[1] * y + matrix[2] * z + matrix[3];
13
+ positions[offset + 1] = matrix[4] * x + matrix[5] * y + matrix[6] * z + matrix[7];
14
+ positions[offset + 2] = matrix[8] * x + matrix[9] * y + matrix[10] * z + matrix[11];
15
+ }
16
+ const mesh = { positions, indices: source.indices };
17
+ // Rigid placement preserves indexed connectivity. Measure actual Float32 world output to keep
18
+ // exact bounds/volume/area without another topology or kernel pass under the default policy.
19
+ const measurements = Object.freeze({ ...source.measurements, ...measureTriangleMesh(mesh) });
20
+ // Float32 placement can close gaps between shells even when the local mesh passed validation.
21
+ if (meshValidation === "solid")
22
+ validateAnalyzedSolidMesh(mesh, measurements);
23
+ return Object.freeze({ ...source, ...mesh, measurements });
24
+ }
@@ -0,0 +1,119 @@
1
+ import { GeometryError } from "../geometry/errors.js";
2
+ import { immutableBufferView, isImmutableBuffer } from "../model/immutable-buffer.js";
3
+ import { geometryRecipe, isGeometryRecipe, } from "./geometry-recipes.js";
4
+ /**
5
+ * Resolves session recipes within one revision. These maps memoize borrowed native handles;
6
+ * the execution scope still owns allocation and cleanup, including handles restored from cache.
7
+ */
8
+ export class RecipeRealization {
9
+ owner;
10
+ native;
11
+ duringNative;
12
+ restoreNative;
13
+ realized = new Map();
14
+ operands = new Set();
15
+ resolvedCalls = new Map();
16
+ nativeContexts = new Map();
17
+ constructor(owner, native, duringNative, restoreNative) {
18
+ this.owner = owner;
19
+ this.native = native;
20
+ this.duringNative = duringNative;
21
+ this.restoreNative = restoreNative;
22
+ }
23
+ /** Rejects foreign recipes before declaration, without realizing any geometry. */
24
+ assertOwned(values) {
25
+ for (const shape of recipeShapes(values))
26
+ this.assertRecipeOwner(geometryRecipe(shape));
27
+ }
28
+ /** Runs an observation or operation with captured inputs and its declaring resolution. */
29
+ invoke(call) {
30
+ let context = this.nativeContexts.get(call.resolution);
31
+ if (context === undefined) {
32
+ context = this.native().withResolution(call.resolution);
33
+ this.nativeContexts.set(call.resolution, context);
34
+ }
35
+ const operation = context[call.method];
36
+ const args = this.resolveValue(call.args, true);
37
+ return this.duringNative(() => Reflect.apply(operation, undefined, args));
38
+ }
39
+ /** Split/component handles share one operation result, even when selected separately. */
40
+ evaluate(call) {
41
+ if (!this.resolvedCalls.has(call))
42
+ this.resolvedCalls.set(call, this.invoke(call));
43
+ return this.resolvedCalls.get(call);
44
+ }
45
+ resolve(shape) {
46
+ return this.resolveValue(shape);
47
+ }
48
+ realizedShape(shape) {
49
+ return this.realized.get(shape);
50
+ }
51
+ /** Prefers intermediate operands when not every computed output fits the retention budget. */
52
+ retentionCandidates(values) {
53
+ const shapes = new Set(recipeShapes(values));
54
+ return [...shapes].sort((left, right) => Number(this.operands.has(right)) - Number(this.operands.has(left)));
55
+ }
56
+ assertRecipeOwner(recipe) {
57
+ if (recipe.owner !== this.owner)
58
+ throw new GeometryError("INVALID_ARGUMENT", "Geometry belongs to another model session.", {
59
+ operation: "generate",
60
+ });
61
+ }
62
+ realizeShape(shape) {
63
+ const recipe = geometryRecipe(shape);
64
+ this.assertRecipeOwner(recipe);
65
+ if (this.realized.has(shape))
66
+ return this.realized.get(shape);
67
+ // Restoring a retained value avoids replay but still borrows through this revision's scope.
68
+ const retained = this.restoreNative(shape);
69
+ if (retained !== undefined) {
70
+ this.realized.set(shape, retained);
71
+ return retained;
72
+ }
73
+ const result = this.evaluate(recipe.call);
74
+ const selected = recipe.index === undefined ? result : result[recipe.index];
75
+ this.realized.set(shape, selected);
76
+ return selected;
77
+ }
78
+ /** Copies argument containers so native code cannot mutate captured recipe inputs. */
79
+ resolveValue(value, operand = false, seen = new Map()) {
80
+ if (isImmutableBuffer(value))
81
+ return immutableBufferView(value);
82
+ if (isGeometryRecipe(value)) {
83
+ if (operand)
84
+ this.operands.add(value);
85
+ return this.realizeShape(value);
86
+ }
87
+ if (typeof value !== "object" ||
88
+ value === null ||
89
+ ArrayBuffer.isView(value) ||
90
+ value instanceof ArrayBuffer)
91
+ return value;
92
+ if (seen.has(value))
93
+ return seen.get(value);
94
+ const resolved = Array.isArray(value)
95
+ ? new Array(value.length)
96
+ : Object.create(Object.getPrototypeOf(value));
97
+ seen.set(value, resolved);
98
+ for (const [key, item] of Object.entries(value))
99
+ Object.defineProperty(resolved, key, {
100
+ value: this.resolveValue(item, operand, seen),
101
+ enumerable: true,
102
+ writable: true,
103
+ configurable: true,
104
+ });
105
+ return resolved;
106
+ }
107
+ }
108
+ /** Walks captured data once, including shared and cyclic containers, without inspecting buffers. */
109
+ function* recipeShapes(values, seen = new Set()) {
110
+ for (const value of values) {
111
+ if (typeof value !== "object" || value === null || seen.has(value))
112
+ continue;
113
+ seen.add(value);
114
+ if (isGeometryRecipe(value))
115
+ yield value;
116
+ else if (!isImmutableBuffer(value) && !ArrayBuffer.isView(value))
117
+ yield* recipeShapes(Object.values(value), seen);
118
+ }
119
+ }