@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.
- package/CHANGELOG.md +46 -0
- package/README.md +66 -422
- package/THIRD_PARTY_NOTICES.md +4 -148
- package/dist/api/engine.d.ts +105 -8
- package/dist/api/geometry.d.ts +95 -83
- package/dist/api/index.d.ts +767 -772
- package/dist/engine/construction-operations.js +3 -1
- package/dist/engine/deferred-context.js +182 -0
- package/dist/engine/execution.js +38 -4
- package/dist/engine/finalization.js +65 -133
- package/dist/engine/geometry-recipes.js +12 -0
- package/dist/engine/immutable-generation-data.js +186 -0
- package/dist/engine/index.js +57 -35
- package/dist/engine/kernel/manifold.wasm +0 -0
- package/dist/engine/model-dependencies.js +46 -0
- package/dist/engine/model-summary.js +43 -0
- package/dist/engine/native/ownership.js +11 -5
- package/dist/engine/placement.js +24 -0
- package/dist/engine/recipe-realization.js +119 -0
- package/dist/engine/session-graph.js +442 -0
- package/dist/engine/session-native.js +90 -0
- package/dist/engine/session-snapshot.js +153 -0
- package/dist/engine/session.js +184 -0
- package/dist/engine/shape-anchors.js +5 -0
- package/dist/geometry/index.js +2 -2
- package/dist/geometry/mesh-centroid.js +7 -6
- package/dist/geometry/mesh-planar.js +11 -10
- package/dist/geometry/mesh-quality.js +15 -14
- package/dist/geometry/mesh-signature.js +8 -7
- package/dist/geometry/mesh-solid.js +10 -4
- package/dist/geometry/mesh.js +36 -31
- package/dist/geometry/profiles/{cross-section.js → contour.js} +3 -3
- package/dist/geometry/profiles/{open-profile.js → side-profile.js} +3 -3
- package/dist/geometry/svg-import.js +1 -16
- package/dist/index.js +2 -0
- package/dist/model/execution-binding.js +5 -5
- package/dist/model/generation.js +27 -0
- package/dist/model/immutable-buffer.js +111 -0
- package/dist/model/modeling-context.js +1 -0
- package/dist/model/operations.js +521 -160
- package/dist/model/shape-handles.js +44 -0
- 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 = (
|
|
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
|
+
}
|
package/dist/engine/execution.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
/**
|
|
30
|
-
export function
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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 =
|
|
88
|
-
let measurements = nativeAnalysis?.measurements
|
|
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
|
-
|
|
97
|
-
|
|
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(
|
|
63
|
+
const optimized = optimizePlanarPatches(mesh);
|
|
121
64
|
if (optimized.changed) {
|
|
122
|
-
const
|
|
123
|
-
positions:
|
|
65
|
+
const candidate = {
|
|
66
|
+
positions: mesh.positions,
|
|
124
67
|
indices: copiedUint32Array(optimized.mesh.indices),
|
|
125
68
|
};
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
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
|
|
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
|
+
}
|