@metamesh/modeling 0.0.0-stage → 0.7.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 +111 -0
- package/LICENSE +200 -0
- package/README.md +376 -2
- package/THIRD_PARTY_NOTICES.md +153 -0
- package/dist/api/engine.d.ts +71 -0
- package/dist/api/geometry.d.ts +3136 -0
- package/dist/api/index.d.ts +2087 -0
- package/dist/api/text.d.ts +93 -0
- package/dist/engine/construction-operations.js +189 -0
- package/dist/engine/execution.js +227 -0
- package/dist/engine/export-3mf.js +100 -0
- package/dist/engine/extrusion-finishing.js +82 -0
- package/dist/engine/finalization.js +214 -0
- package/dist/engine/index.js +83 -0
- package/dist/engine/inspection-operations.js +52 -0
- package/dist/engine/kernel/LICENSE +201 -0
- package/dist/engine/kernel/manifold.js +2 -0
- package/dist/engine/kernel/manifold.wasm +0 -0
- package/dist/engine/loft-operations.js +156 -0
- package/dist/engine/mechanical-operations.js +51 -0
- package/dist/engine/modeling-context.js +38 -0
- package/dist/engine/native/adapter.js +20 -0
- package/dist/engine/native/context.js +209 -0
- package/dist/engine/native/copied-views.js +46 -0
- package/dist/engine/native/index.js +13 -0
- package/dist/engine/native/operations.js +115 -0
- package/dist/engine/native/ownership.js +98 -0
- package/dist/engine/native/regions.js +138 -0
- package/dist/engine/native/scoped-primitives.js +127 -0
- package/dist/engine/native/scoped-threads.js +34 -0
- package/dist/engine/native/threads.js +52 -0
- package/dist/engine/native/transforms.js +111 -0
- package/dist/engine/operation-environment.js +20 -0
- package/dist/engine/pattern-operations.js +36 -0
- package/dist/engine/pattern-placements.js +176 -0
- package/dist/engine/primitive-anchors.js +70 -0
- package/dist/engine/primitive-correspondence.js +74 -0
- package/dist/engine/primitive-operations.js +151 -0
- package/dist/engine/primitive-profile-types.js +1 -0
- package/dist/engine/primitive-profiles.js +250 -0
- package/dist/engine/profile-mesh.js +279 -0
- package/dist/engine/section-operations.js +93 -0
- package/dist/engine/shape-anchors.js +88 -0
- package/dist/engine/transform-operations.js +225 -0
- package/dist/engine/types.js +1 -0
- package/dist/geometry/anchors.js +226 -0
- package/dist/geometry/curves/evaluation.js +162 -0
- package/dist/geometry/curves/sampling.js +266 -0
- package/dist/geometry/curves/types.js +1 -0
- package/dist/geometry/cylinder.js +117 -0
- package/dist/geometry/edge-finishing.js +160 -0
- package/dist/geometry/errors.js +76 -0
- package/dist/geometry/extrusion.js +150 -0
- package/dist/geometry/heightfield.js +115 -0
- package/dist/geometry/index.js +42 -0
- package/dist/geometry/loft-correspondence.js +204 -0
- package/dist/geometry/loft-geometry.js +184 -0
- package/dist/geometry/loft-options.js +56 -0
- package/dist/geometry/loft-simple-mesh.js +92 -0
- package/dist/geometry/loft-triangulation.js +64 -0
- package/dist/geometry/loft.js +95 -0
- package/dist/geometry/math/vector3.js +71 -0
- package/dist/geometry/mechanical/gear-motion.js +94 -0
- package/dist/geometry/mechanical/gears.js +484 -0
- package/dist/geometry/mechanical/index.js +13 -0
- package/dist/geometry/mechanical/standards.js +67 -0
- package/dist/geometry/mechanical/thread-helices.js +44 -0
- package/dist/geometry/mechanical/thread-mesh-plan.js +358 -0
- package/dist/geometry/mechanical/thread-mesh.js +142 -0
- package/dist/geometry/mechanical/thread-profiles.js +373 -0
- package/dist/geometry/mechanical/thread-solids.js +230 -0
- package/dist/geometry/mechanical/thread-surface.js +433 -0
- package/dist/geometry/mechanical/threads.js +138 -0
- package/dist/geometry/mesh-edge-table.js +126 -0
- package/dist/geometry/mesh-planar.js +408 -0
- package/dist/geometry/mesh-quality.js +396 -0
- package/dist/geometry/mesh-signature.js +137 -0
- package/dist/geometry/mesh.js +283 -0
- package/dist/geometry/model-limits.js +50 -0
- package/dist/geometry/numeric.js +11 -0
- package/dist/geometry/paths/frames.js +118 -0
- package/dist/geometry/paths/helix.js +49 -0
- package/dist/geometry/paths/path.js +32 -0
- package/dist/geometry/paths/planar.js +135 -0
- package/dist/geometry/paths/resampling.js +125 -0
- package/dist/geometry/polygon-clipping.js +52 -0
- package/dist/geometry/primitive-options.js +1 -0
- package/dist/geometry/primitive-resolution.js +17 -0
- package/dist/geometry/primitive-resolvers.js +235 -0
- package/dist/geometry/profiles/matched.js +192 -0
- package/dist/geometry/profiles/open-profile.js +137 -0
- package/dist/geometry/profiles/profiles.js +91 -0
- package/dist/geometry/profiles/rectangle.js +249 -0
- package/dist/geometry/profiles/shapes.js +41 -0
- package/dist/geometry/profiles/span-sampling.js +28 -0
- package/dist/geometry/profiles/types.js +1 -0
- package/dist/geometry/profiles/validation.js +20 -0
- package/dist/geometry/regions.js +371 -0
- package/dist/geometry/resolution.js +236 -0
- package/dist/geometry/revolve/revolve-mesh.js +129 -0
- package/dist/geometry/sampling-limits.js +29 -0
- package/dist/geometry/stroke-path.js +181 -0
- package/dist/geometry/svg/errors.js +23 -0
- package/dist/geometry/svg/fill.js +172 -0
- package/dist/geometry/svg/index.js +1 -0
- package/dist/geometry/svg/matrix.js +130 -0
- package/dist/geometry/svg/parser.js +536 -0
- package/dist/geometry/svg/path.js +349 -0
- package/dist/geometry/svg/types.js +1 -0
- package/dist/geometry/svg-import.js +662 -0
- package/dist/geometry/sweeps/options.js +28 -0
- package/dist/geometry/sweeps/orientation.js +106 -0
- package/dist/geometry/sweeps/stations.js +161 -0
- package/dist/geometry/sweeps/sweep-closed.js +96 -0
- package/dist/geometry/sweeps/sweep-mesh.js +30 -0
- package/dist/geometry/sweeps/sweep-open.js +76 -0
- package/dist/geometry/sweeps/validation.js +116 -0
- package/dist/geometry/text-contours.js +32 -0
- package/dist/geometry/text-layout.js +1 -0
- package/dist/geometry/tolerance.js +53 -0
- package/dist/geometry/transform-geometry.js +41 -0
- package/dist/geometry/transform3.js +340 -0
- package/dist/geometry/types.js +1 -0
- package/dist/index.js +1 -0
- package/dist/model/execution-binding.js +30 -0
- package/dist/model/operations.js +154 -0
- package/dist/model/options.js +1 -0
- package/dist/model/types.js +1 -0
- package/dist/text/index.js +58 -0
- package/dist/text/vector-text-layout.js +279 -0
- package/package.json +100 -3
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { isProtectedCopiedView } from "./copied-views.js";
|
|
2
|
+
import { assertSynchronousScopeResult, assertWasmObjectScopeActive, WasmObjectScope, } from "./ownership.js";
|
|
3
|
+
const contexts = new WeakMap();
|
|
4
|
+
const stateFor = (context) => {
|
|
5
|
+
const state = contexts.get(context);
|
|
6
|
+
if (state === undefined || !state.active) {
|
|
7
|
+
throw new Error("Cannot use an expired scoped Manifold context.");
|
|
8
|
+
}
|
|
9
|
+
assertWasmObjectScopeActive(state.scope);
|
|
10
|
+
return state;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Creates a concise Manifold context backed by an existing low-level owner.
|
|
14
|
+
*
|
|
15
|
+
* The caller remains responsible for disposing `scope`. Prefer
|
|
16
|
+
* {@link withManifoldScope} for local builds.
|
|
17
|
+
*/
|
|
18
|
+
export function createScopedManifoldContext(module, scope) {
|
|
19
|
+
assertWasmObjectScopeActive(scope);
|
|
20
|
+
const context = Object.freeze({});
|
|
21
|
+
contexts.set(context, { active: true, module, owned: new WeakSet(), scope });
|
|
22
|
+
return context;
|
|
23
|
+
}
|
|
24
|
+
/** @internal Returns the live upstream module owned by a scoped context. */
|
|
25
|
+
export function scopedManifoldModule(context) {
|
|
26
|
+
return stateFor(context).module;
|
|
27
|
+
}
|
|
28
|
+
/** @internal Returns the live low-level owner behind a scoped context. */
|
|
29
|
+
export function scopedManifoldScope(context) {
|
|
30
|
+
return stateFor(context).scope;
|
|
31
|
+
}
|
|
32
|
+
/** @internal Owns and records a newly allocated WASM value. */
|
|
33
|
+
export function ownScopedManifoldValue(context, value) {
|
|
34
|
+
const state = stateFor(context);
|
|
35
|
+
const owned = state.scope.own(value);
|
|
36
|
+
state.owned.add(owned);
|
|
37
|
+
return owned;
|
|
38
|
+
}
|
|
39
|
+
/** @internal Records a value already transferred to the context's low-level owner. */
|
|
40
|
+
export function trackScopedManifoldValue(context, value) {
|
|
41
|
+
stateFor(context).owned.add(value);
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
/** @internal Verifies that a context remains active without exposing its internals. */
|
|
45
|
+
export function assertScopedManifoldContext(context) {
|
|
46
|
+
stateFor(context);
|
|
47
|
+
}
|
|
48
|
+
/** @internal Verifies that a live WASM value belongs to this exact context. */
|
|
49
|
+
export function assertScopedManifoldValue(context, value) {
|
|
50
|
+
if (!stateFor(context).owned.has(value)) {
|
|
51
|
+
throw new Error("WASM-backed values must be used with the scoped context that owns them.");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const propertyDescriptor = (value, property) => {
|
|
55
|
+
let current = value;
|
|
56
|
+
while (current !== null) {
|
|
57
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, property);
|
|
58
|
+
if (descriptor !== undefined)
|
|
59
|
+
return descriptor;
|
|
60
|
+
current = Object.getPrototypeOf(current);
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
};
|
|
64
|
+
const assertInspectableProperties = (value, context, seen) => {
|
|
65
|
+
for (const property of Reflect.ownKeys(value)) {
|
|
66
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, property);
|
|
67
|
+
if (descriptor === undefined)
|
|
68
|
+
continue;
|
|
69
|
+
if (!("value" in descriptor)) {
|
|
70
|
+
throw new Error("Accessor properties must not escape a scoped Manifold callback.");
|
|
71
|
+
}
|
|
72
|
+
assertNoScopedEscape(descriptor.value, context, seen);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const inspectableViewPrototypes = new Set([
|
|
76
|
+
BigInt64Array.prototype,
|
|
77
|
+
BigUint64Array.prototype,
|
|
78
|
+
DataView.prototype,
|
|
79
|
+
Float32Array.prototype,
|
|
80
|
+
Float64Array.prototype,
|
|
81
|
+
Int8Array.prototype,
|
|
82
|
+
Int16Array.prototype,
|
|
83
|
+
Int32Array.prototype,
|
|
84
|
+
Uint8Array.prototype,
|
|
85
|
+
Uint8ClampedArray.prototype,
|
|
86
|
+
Uint16Array.prototype,
|
|
87
|
+
Uint32Array.prototype,
|
|
88
|
+
]);
|
|
89
|
+
const isCanonicalTypedArrayIndex = (property) => {
|
|
90
|
+
if (typeof property !== "string" || property.length === 0)
|
|
91
|
+
return false;
|
|
92
|
+
const index = Number(property);
|
|
93
|
+
return Number.isSafeInteger(index) && index >= 0 && String(index) === property;
|
|
94
|
+
};
|
|
95
|
+
const assertInspectableViewProperties = (value, context, seen) => {
|
|
96
|
+
const properties = Reflect.ownKeys(value);
|
|
97
|
+
// Integer-indexed elements are primitive numeric/bigint storage. ECMAScript orders every such
|
|
98
|
+
// key before ordinary strings and symbols, so only the short trailing expando suffix needs deep
|
|
99
|
+
// inspection even for a large copied mesh buffer.
|
|
100
|
+
for (let index = properties.length - 1; index >= 0; index -= 1) {
|
|
101
|
+
const property = properties[index];
|
|
102
|
+
if (isCanonicalTypedArrayIndex(property))
|
|
103
|
+
break;
|
|
104
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, property);
|
|
105
|
+
if (descriptor === undefined)
|
|
106
|
+
continue;
|
|
107
|
+
if (!("value" in descriptor)) {
|
|
108
|
+
throw new Error("Accessor properties must not escape a scoped Manifold callback.");
|
|
109
|
+
}
|
|
110
|
+
assertNoScopedEscape(descriptor.value, context, seen);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
const assertNoScopedEscape = (value, context, seen = new WeakSet()) => {
|
|
114
|
+
if (value === context) {
|
|
115
|
+
throw new Error("Scoped Manifold contexts must not escape their callback.");
|
|
116
|
+
}
|
|
117
|
+
if ((typeof value !== "object" && typeof value !== "function") || value === null)
|
|
118
|
+
return;
|
|
119
|
+
if (typeof value === "function") {
|
|
120
|
+
throw new Error("Functions and closures must not escape a scoped Manifold callback.");
|
|
121
|
+
}
|
|
122
|
+
const objectValue = value;
|
|
123
|
+
if (seen.has(objectValue))
|
|
124
|
+
return;
|
|
125
|
+
seen.add(objectValue);
|
|
126
|
+
const state = contexts.get(context);
|
|
127
|
+
if (state?.owned.has(objectValue) === true) {
|
|
128
|
+
throw new Error("WASM-backed values must not escape a scoped Manifold callback.");
|
|
129
|
+
}
|
|
130
|
+
if (isProtectedCopiedView(objectValue))
|
|
131
|
+
return;
|
|
132
|
+
if (ArrayBuffer.isView(objectValue)) {
|
|
133
|
+
if (!inspectableViewPrototypes.has(Object.getPrototypeOf(objectValue))) {
|
|
134
|
+
throw new Error("Custom-prototype objects must not escape a scoped Manifold callback.");
|
|
135
|
+
}
|
|
136
|
+
assertInspectableViewProperties(objectValue, context, seen);
|
|
137
|
+
assertNoScopedEscape(objectValue.buffer, context, seen);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (objectValue instanceof ArrayBuffer) {
|
|
141
|
+
if (Object.getPrototypeOf(objectValue) !== ArrayBuffer.prototype) {
|
|
142
|
+
throw new Error("Custom-prototype objects must not escape a scoped Manifold callback.");
|
|
143
|
+
}
|
|
144
|
+
assertInspectableProperties(objectValue, context, seen);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (objectValue instanceof Map) {
|
|
148
|
+
if (Object.getPrototypeOf(objectValue) !== Map.prototype) {
|
|
149
|
+
throw new Error("Custom-prototype objects must not escape a scoped Manifold callback.");
|
|
150
|
+
}
|
|
151
|
+
assertInspectableProperties(objectValue, context, seen);
|
|
152
|
+
Map.prototype.forEach.call(objectValue, (entry, key) => {
|
|
153
|
+
assertNoScopedEscape(key, context, seen);
|
|
154
|
+
assertNoScopedEscape(entry, context, seen);
|
|
155
|
+
});
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (objectValue instanceof Set) {
|
|
159
|
+
if (Object.getPrototypeOf(objectValue) !== Set.prototype) {
|
|
160
|
+
throw new Error("Custom-prototype objects must not escape a scoped Manifold callback.");
|
|
161
|
+
}
|
|
162
|
+
assertInspectableProperties(objectValue, context, seen);
|
|
163
|
+
Set.prototype.forEach.call(objectValue, (entry) => {
|
|
164
|
+
assertNoScopedEscape(entry, context, seen);
|
|
165
|
+
});
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (Array.isArray(objectValue)) {
|
|
169
|
+
if (Object.getPrototypeOf(objectValue) !== Array.prototype) {
|
|
170
|
+
throw new Error("Custom-prototype objects must not escape a scoped Manifold callback.");
|
|
171
|
+
}
|
|
172
|
+
assertInspectableProperties(objectValue, context, seen);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const deleteDescriptor = propertyDescriptor(objectValue, "delete");
|
|
176
|
+
if (deleteDescriptor !== undefined &&
|
|
177
|
+
"value" in deleteDescriptor &&
|
|
178
|
+
typeof deleteDescriptor.value === "function") {
|
|
179
|
+
throw new Error("WASM-backed values must not escape a scoped Manifold callback.");
|
|
180
|
+
}
|
|
181
|
+
const prototype = Object.getPrototypeOf(objectValue);
|
|
182
|
+
assertInspectableProperties(objectValue, context, seen);
|
|
183
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
184
|
+
// Arbitrary instances may hide WASM-backed values in private fields or other internal slots,
|
|
185
|
+
// which cannot be inspected without executing user code.
|
|
186
|
+
throw new Error("Custom-prototype objects must not escape a scoped Manifold callback.");
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Runs synchronous modeling work in one deterministic Manifold lifetime.
|
|
191
|
+
*
|
|
192
|
+
* The callback may return copied JavaScript data, but not the context or any
|
|
193
|
+
* WASM-backed value. Returning a promise is rejected.
|
|
194
|
+
*/
|
|
195
|
+
export function withManifoldScope(module, callback) {
|
|
196
|
+
const scope = new WasmObjectScope();
|
|
197
|
+
const context = createScopedManifoldContext(module, scope);
|
|
198
|
+
const state = contexts.get(context);
|
|
199
|
+
try {
|
|
200
|
+
const result = callback(context);
|
|
201
|
+
assertSynchronousScopeResult(result, "withManifoldScope callbacks must be synchronous.");
|
|
202
|
+
assertNoScopedEscape(result, context);
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
finally {
|
|
206
|
+
state.active = false;
|
|
207
|
+
scope.dispose();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Only fresh, intrinsic views created by this module enter the registry. Preventing extensions
|
|
2
|
+
// also locks their prototypes, so consumers cannot attach an opaque value after registration.
|
|
3
|
+
const copiedViews = new WeakSet();
|
|
4
|
+
const Float32Copy = Float32Array;
|
|
5
|
+
const Uint32Copy = Uint32Array;
|
|
6
|
+
const protect = (view, prototype) => {
|
|
7
|
+
if (Object.getPrototypeOf(view) !== prototype ||
|
|
8
|
+
Object.getPrototypeOf(view.buffer) !== ArrayBuffer.prototype) {
|
|
9
|
+
throw new Error("Copied mesh buffers must use intrinsic typed-array prototypes.");
|
|
10
|
+
}
|
|
11
|
+
Object.preventExtensions(view.buffer);
|
|
12
|
+
Object.preventExtensions(view);
|
|
13
|
+
copiedViews.add(view.buffer);
|
|
14
|
+
copiedViews.add(view);
|
|
15
|
+
return view;
|
|
16
|
+
};
|
|
17
|
+
/** @internal Allocates a fresh numeric mesh view that cannot acquire escape-bearing properties. */
|
|
18
|
+
export function copiedFloat32Array(source) {
|
|
19
|
+
const view = typeof source === "number" ? new Float32Copy(source) : new Float32Copy(source);
|
|
20
|
+
return protect(view, Float32Copy.prototype);
|
|
21
|
+
}
|
|
22
|
+
/** @internal Copies a fresh index view that cannot acquire escape-bearing properties. */
|
|
23
|
+
export function copiedUint32Array(source) {
|
|
24
|
+
return protect(new Uint32Copy(source), Uint32Copy.prototype);
|
|
25
|
+
}
|
|
26
|
+
/** @internal Recognizes only this module's non-extensible, freshly allocated views and buffers. */
|
|
27
|
+
export function isProtectedCopiedView(value) {
|
|
28
|
+
return copiedViews.has(value);
|
|
29
|
+
}
|
|
30
|
+
/** @internal Copies shared backing storage once while retaining mesh view offsets. */
|
|
31
|
+
export function copiedMeshViews(mesh, buffers) {
|
|
32
|
+
const copy = (buffer) => {
|
|
33
|
+
if (!(buffer instanceof ArrayBuffer))
|
|
34
|
+
throw new Error("Mesh buffers must be transferable ArrayBuffers.");
|
|
35
|
+
let cloned = buffers.get(buffer);
|
|
36
|
+
if (cloned === undefined) {
|
|
37
|
+
cloned = buffer.slice(0);
|
|
38
|
+
buffers.set(buffer, cloned);
|
|
39
|
+
}
|
|
40
|
+
return cloned;
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
positions: protect(new Float32Copy(copy(mesh.positions.buffer), mesh.positions.byteOffset, mesh.positions.length), Float32Copy.prototype),
|
|
44
|
+
indices: protect(new Uint32Copy(copy(mesh.indices.buffer), mesh.indices.byteOffset, mesh.indices.length), Uint32Copy.prototype),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Private host-native implementation details. Import public operations from @metamesh/modeling.
|
|
3
|
+
*
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*/
|
|
6
|
+
export { createScopedManifoldContext, withManifoldScope, } from "./context.js";
|
|
7
|
+
export { box, cylinder, sphere, torus, tube, } from "./scoped-primitives.js";
|
|
8
|
+
export { compose, hull, intersect, minkowskiSum, subtract, union, } from "./operations.js";
|
|
9
|
+
export { attach, mirror, rotate, scale, transform, translate, } from "./transforms.js";
|
|
10
|
+
export { internalThreadCutter, preflightInternalThreadCutter, preflightThreadedNut, preflightThreadedRod, threadRidge, threadedNut, threadedRod, } from "./scoped-threads.js";
|
|
11
|
+
export {} from "./threads.js";
|
|
12
|
+
export { WasmObjectScope, withWasmObjectScope } from "./ownership.js";
|
|
13
|
+
export { crossSectionToRegion, regionToCrossSection } from "./regions.js";
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { GeometryError } from "../../geometry/errors.js";
|
|
2
|
+
import { assertScopedManifoldContext, assertScopedManifoldValue, ownScopedManifoldValue, scopedManifoldModule, } from "./context.js";
|
|
3
|
+
const operands = (context, solids, operation) => {
|
|
4
|
+
assertScopedManifoldContext(context);
|
|
5
|
+
const hasArrayShape = Array.isArray(solids);
|
|
6
|
+
if (!hasArrayShape) {
|
|
7
|
+
throw new GeometryError("INVALID_ARGUMENT", "solids must be an array.", { operation });
|
|
8
|
+
}
|
|
9
|
+
if (solids.length === 0) {
|
|
10
|
+
throw new GeometryError("INVALID_ARGUMENT", `${operation} needs at least one solid.`, {
|
|
11
|
+
operation,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
if (solids.length > 1_000) {
|
|
15
|
+
throw new GeometryError("COMPLEXITY_LIMIT", "Solid operations accept at most 1000 operands.", {
|
|
16
|
+
operation,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
for (const solid of solids)
|
|
20
|
+
assertScopedManifoldValue(context, solid);
|
|
21
|
+
};
|
|
22
|
+
/** Returns the Boolean union of one or more context-owned solids. */
|
|
23
|
+
export function union(context, solids) {
|
|
24
|
+
operands(context, solids, "union");
|
|
25
|
+
if (solids.length === 1)
|
|
26
|
+
return solids[0];
|
|
27
|
+
return ownScopedManifoldValue(context, scopedManifoldModule(context).Manifold.union(solids));
|
|
28
|
+
}
|
|
29
|
+
/** Subtracts every context-owned cutter from a context-owned source solid. */
|
|
30
|
+
export function subtract(context, solid, cutters) {
|
|
31
|
+
assertScopedManifoldValue(context, solid);
|
|
32
|
+
operands(context, [solid, ...cutters], "subtract");
|
|
33
|
+
if (cutters.length === 0)
|
|
34
|
+
return solid;
|
|
35
|
+
return ownScopedManifoldValue(context, scopedManifoldModule(context).Manifold.difference([solid, ...cutters]));
|
|
36
|
+
}
|
|
37
|
+
/** Returns the Boolean intersection of one or more context-owned solids. */
|
|
38
|
+
export function intersect(context, solids) {
|
|
39
|
+
operands(context, solids, "intersect");
|
|
40
|
+
if (solids.length === 1)
|
|
41
|
+
return solids[0];
|
|
42
|
+
return ownScopedManifoldValue(context, scopedManifoldModule(context).Manifold.intersection(solids));
|
|
43
|
+
}
|
|
44
|
+
/** Combines disjoint context-owned solids without resolving overlaps. */
|
|
45
|
+
export function compose(context, solids) {
|
|
46
|
+
operands(context, solids, "compose");
|
|
47
|
+
if (solids.length === 1)
|
|
48
|
+
return solids[0];
|
|
49
|
+
return ownScopedManifoldValue(context, scopedManifoldModule(context).Manifold.compose(solids));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Creates the scope-owned convex hull of finite XYZ millimetre points or solids.
|
|
53
|
+
*
|
|
54
|
+
* Point input needs at least four non-coplanar points and accepts at most 100000 points. Solid
|
|
55
|
+
* input accepts one through 1000 operands belonging to this exact live context.
|
|
56
|
+
*/
|
|
57
|
+
export function hull(context, options) {
|
|
58
|
+
assertScopedManifoldContext(context);
|
|
59
|
+
if (typeof options !== "object" || options === null || Array.isArray(options)) {
|
|
60
|
+
throw new GeometryError("INVALID_ARGUMENT", "Hull options must be an object.", {
|
|
61
|
+
operation: "hull",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if ((options.points === undefined) === (options.solids === undefined)) {
|
|
65
|
+
throw new GeometryError("INVALID_ARGUMENT", "Supply either points or solids to hull.", {
|
|
66
|
+
operation: "hull",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (options.solids !== undefined) {
|
|
70
|
+
operands(context, options.solids, "hull");
|
|
71
|
+
return ownScopedManifoldValue(context, scopedManifoldModule(context).Manifold.hull(options.solids));
|
|
72
|
+
}
|
|
73
|
+
const points = options.points;
|
|
74
|
+
if (!Array.isArray(points) || points.length < 4) {
|
|
75
|
+
throw new GeometryError("INVALID_ARGUMENT", "A point hull needs at least four XYZ points.", {
|
|
76
|
+
operation: "hull",
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (points.length > 100_000) {
|
|
80
|
+
throw new GeometryError("COMPLEXITY_LIMIT", "Point hulls accept at most 100000 points.", {
|
|
81
|
+
operation: "hull",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
for (const point of points) {
|
|
85
|
+
if (!Array.isArray(point) || point.length !== 3) {
|
|
86
|
+
throw new GeometryError("INVALID_ARGUMENT", "Hull points must be exact XYZ tuples.", {
|
|
87
|
+
operation: "hull",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (![point[0], point[1], point[2]].every(Number.isFinite)) {
|
|
91
|
+
throw new GeometryError("NON_FINITE_VALUE", "Hull coordinates must be finite.", {
|
|
92
|
+
operation: "hull",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const result = ownScopedManifoldValue(context, scopedManifoldModule(context).Manifold.hull(points));
|
|
97
|
+
const volume = result.volume();
|
|
98
|
+
if (!Number.isFinite(volume)) {
|
|
99
|
+
throw new GeometryError("NON_FINITE_VALUE", "Point hull volume must be finite.", {
|
|
100
|
+
operation: "hull",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (result.isEmpty() || volume <= 0) {
|
|
104
|
+
throw new GeometryError("DEGENERATE_GEOMETRY", "Hull points must span a nonzero volume.", {
|
|
105
|
+
operation: "hull",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
/** Adds two context-owned solids by a scope-owned Minkowski sum in XYZ millimetres. */
|
|
111
|
+
export function minkowskiSum(context, solid, other) {
|
|
112
|
+
assertScopedManifoldValue(context, solid);
|
|
113
|
+
assertScopedManifoldValue(context, other);
|
|
114
|
+
return ownScopedManifoldValue(context, solid.minkowskiSum(other));
|
|
115
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const disposedScopes = new WeakSet();
|
|
2
|
+
/**
|
|
3
|
+
* Small deterministic owner for WASM-backed values. Values are released in
|
|
4
|
+
* reverse construction order and disposal is idempotent.
|
|
5
|
+
*/
|
|
6
|
+
export class WasmObjectScope {
|
|
7
|
+
#owned = [];
|
|
8
|
+
#disposed = false;
|
|
9
|
+
/**
|
|
10
|
+
* Transfers cleanup responsibility for a live WASM value to this scope.
|
|
11
|
+
* If the scope is already disposed, the value is deleted before throwing.
|
|
12
|
+
*/
|
|
13
|
+
own(value) {
|
|
14
|
+
if (this.#disposed) {
|
|
15
|
+
value.delete();
|
|
16
|
+
throw new Error("Cannot add an object to a disposed WASM scope.");
|
|
17
|
+
}
|
|
18
|
+
this.#owned.push(value);
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Deletes owned values in reverse construction order.
|
|
23
|
+
*
|
|
24
|
+
* Every deletion is attempted even when an earlier deletion fails. Cleanup
|
|
25
|
+
* failures are reported together in one `AggregateError`. Repeated calls
|
|
26
|
+
* are safe and do not retry values whose deletion failed.
|
|
27
|
+
*/
|
|
28
|
+
dispose() {
|
|
29
|
+
if (this.#disposed) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
this.#disposed = true;
|
|
33
|
+
disposedScopes.add(this);
|
|
34
|
+
const errors = [];
|
|
35
|
+
for (let index = this.#owned.length - 1; index >= 0; index -= 1) {
|
|
36
|
+
try {
|
|
37
|
+
this.#owned[index].delete();
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
errors.push(error);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
this.#owned.length = 0;
|
|
44
|
+
if (errors.length > 0) {
|
|
45
|
+
throw new AggregateError(errors, `Failed to delete ${errors.length} WASM object(s).`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** @internal Throws when a scoped Manifold operation uses an expired low-level owner. */
|
|
50
|
+
export function assertWasmObjectScopeActive(scope) {
|
|
51
|
+
if (disposedScopes.has(scope)) {
|
|
52
|
+
throw new Error("Cannot use a disposed WASM scope.");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** @internal Rejects async scope results without invoking custom thenable methods. */
|
|
56
|
+
export function assertSynchronousScopeResult(value, error) {
|
|
57
|
+
if ((typeof value !== "object" && typeof value !== "function") || value === null)
|
|
58
|
+
return;
|
|
59
|
+
let current = value;
|
|
60
|
+
let depth = 0;
|
|
61
|
+
let inspectThen = true;
|
|
62
|
+
while (current !== null) {
|
|
63
|
+
if (++depth > 64)
|
|
64
|
+
throw typeof error === "string" ? new Error(error) : error();
|
|
65
|
+
const descriptor = inspectThen ? Object.getOwnPropertyDescriptor(current, "then") : undefined;
|
|
66
|
+
if (current === Promise.prototype ||
|
|
67
|
+
(descriptor !== undefined && "value" in descriptor && typeof descriptor.value === "function")) {
|
|
68
|
+
try {
|
|
69
|
+
// Consume native-Promise rejections even when its own then method was replaced.
|
|
70
|
+
void Promise.prototype.then.call(value, undefined, () => undefined);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Intrinsic brand or species failures must not replace the misuse error.
|
|
74
|
+
}
|
|
75
|
+
throw typeof error === "string" ? new Error(error) : error();
|
|
76
|
+
}
|
|
77
|
+
if (descriptor !== undefined)
|
|
78
|
+
inspectThen = false;
|
|
79
|
+
current = Object.getPrototypeOf(current);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Runs synchronous construction work with a deterministic WASM owner.
|
|
84
|
+
*
|
|
85
|
+
* Do not return an owned WASM value from `callback`; the scope is disposed
|
|
86
|
+
* before this function returns. Copy mesh or scalar data out instead.
|
|
87
|
+
*/
|
|
88
|
+
export const withWasmObjectScope = (callback) => {
|
|
89
|
+
const scope = new WasmObjectScope();
|
|
90
|
+
try {
|
|
91
|
+
const result = callback(scope);
|
|
92
|
+
assertSynchronousScopeResult(result, "withWasmObjectScope callbacks must be synchronous.");
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
scope.dispose();
|
|
97
|
+
}
|
|
98
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { GeometryError } from "../../geometry/errors.js";
|
|
2
|
+
import { loopContainsPoint, normalizeLoop2, normalizeRegion2, signedAreaLoop2, } from "../../geometry/regions.js";
|
|
3
|
+
import { assertSamplingCount, resolveGeometrySamplingLimits, } from "../../geometry/sampling-limits.js";
|
|
4
|
+
import { assertScopedManifoldValue, ownScopedManifoldValue, scopedManifoldModule, } from "./context.js";
|
|
5
|
+
const MAX_AGGREGATE_CONTOURS = 25_000;
|
|
6
|
+
const assertRegionBounds = (regions, operation, limits = {}) => {
|
|
7
|
+
const hasArrayShape = Array.isArray(regions);
|
|
8
|
+
if (!hasArrayShape) {
|
|
9
|
+
throw new GeometryError("INVALID_ARGUMENT", "Regions must be an array.", { operation });
|
|
10
|
+
}
|
|
11
|
+
let contours = 0;
|
|
12
|
+
let vertices = 0;
|
|
13
|
+
if (regions.length > MAX_AGGREGATE_CONTOURS) {
|
|
14
|
+
throw new GeometryError("COMPLEXITY_LIMIT", "Region collection exceeds 25000 entries.", {
|
|
15
|
+
operation,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
for (const region of regions) {
|
|
19
|
+
const hasRegionShape = Array.isArray(region);
|
|
20
|
+
if (!hasRegionShape) {
|
|
21
|
+
throw new GeometryError("INVALID_ARGUMENT", "Each region must be an array of polygons.", {
|
|
22
|
+
operation,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
for (const polygon of region) {
|
|
26
|
+
const hasOuterShape = Array.isArray(polygon?.outer);
|
|
27
|
+
const hasHoleShape = Array.isArray(polygon?.holes);
|
|
28
|
+
if (typeof polygon !== "object" || polygon === null || !hasOuterShape || !hasHoleShape) {
|
|
29
|
+
throw new GeometryError("INVALID_ARGUMENT", "Polygons need outer and hole arrays.", {
|
|
30
|
+
operation,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
contours += 1 + polygon.holes.length;
|
|
34
|
+
vertices += polygon.outer.length;
|
|
35
|
+
for (const hole of polygon.holes) {
|
|
36
|
+
if (!Array.isArray(hole)) {
|
|
37
|
+
throw new GeometryError("INVALID_ARGUMENT", "Polygon holes must be arrays.", {
|
|
38
|
+
operation,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
vertices += hole.length;
|
|
42
|
+
}
|
|
43
|
+
if (contours > MAX_AGGREGATE_CONTOURS) {
|
|
44
|
+
throw new GeometryError("COMPLEXITY_LIMIT", "Regions exceed 25000 aggregate contours.", {
|
|
45
|
+
operation,
|
|
46
|
+
details: { contours, vertices },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
assertSamplingCount(vertices, operation, resolveGeometrySamplingLimits(limits).maxProfilePoints);
|
|
50
|
+
for (const loop of [polygon.outer, ...polygon.holes]) {
|
|
51
|
+
for (const point of loop) {
|
|
52
|
+
if (!Array.isArray(point) || point.length !== 2) {
|
|
53
|
+
throw new GeometryError("INVALID_ARGUMENT", "Region points must be exact XY tuples.", {
|
|
54
|
+
operation,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (!Number.isFinite(point[0]) || !Number.isFinite(point[1])) {
|
|
58
|
+
throw new GeometryError("NON_FINITE_VALUE", "Region coordinates must be finite.", {
|
|
59
|
+
operation,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const contoursFromRegion = (region) => region
|
|
68
|
+
.flatMap((polygon) => [polygon.outer, ...polygon.holes])
|
|
69
|
+
.map((loop) => loop.map(([x, y]) => [x, y]));
|
|
70
|
+
/**
|
|
71
|
+
* Converts explicit normalized region topology to a scope-owned CrossSection.
|
|
72
|
+
*
|
|
73
|
+
* Manifold CrossSection uses Clipper2's positive fill rule, so CCW exteriors
|
|
74
|
+
* and clockwise holes map directly without XOR-list ambiguity.
|
|
75
|
+
*/
|
|
76
|
+
export function regionToCrossSection(context, region, limits = {}) {
|
|
77
|
+
assertRegionBounds([region], "regionToCrossSection", limits);
|
|
78
|
+
const normalized = normalizeRegion2(region);
|
|
79
|
+
const module = scopedManifoldModule(context);
|
|
80
|
+
if (normalized.length === 0)
|
|
81
|
+
return ownScopedManifoldValue(context, module.CrossSection.union([]));
|
|
82
|
+
return ownScopedManifoldValue(context, new module.CrossSection(contoursFromRegion(normalized)));
|
|
83
|
+
}
|
|
84
|
+
const regionFromLoops = (loops) => {
|
|
85
|
+
const outers = loops
|
|
86
|
+
.filter((loop) => signedAreaLoop2(loop) > 0)
|
|
87
|
+
.map((outer) => ({ outer: normalizeLoop2(outer, true), holes: [] }));
|
|
88
|
+
const holes = loops
|
|
89
|
+
.filter((loop) => signedAreaLoop2(loop) < 0)
|
|
90
|
+
.map((hole) => normalizeLoop2(hole, false));
|
|
91
|
+
if (outers.length === 0 && loops.length > 0) {
|
|
92
|
+
throw new GeometryError("INVALID_TOPOLOGY", "CrossSection did not contain an exterior contour.");
|
|
93
|
+
}
|
|
94
|
+
const outerBounds = new Map(outers.map((polygon) => {
|
|
95
|
+
let minimumX = Number.POSITIVE_INFINITY;
|
|
96
|
+
let minimumY = Number.POSITIVE_INFINITY;
|
|
97
|
+
let maximumX = Number.NEGATIVE_INFINITY;
|
|
98
|
+
let maximumY = Number.NEGATIVE_INFINITY;
|
|
99
|
+
for (const [x, y] of polygon.outer) {
|
|
100
|
+
minimumX = Math.min(minimumX, x);
|
|
101
|
+
minimumY = Math.min(minimumY, y);
|
|
102
|
+
maximumX = Math.max(maximumX, x);
|
|
103
|
+
maximumY = Math.max(maximumY, y);
|
|
104
|
+
}
|
|
105
|
+
return [polygon, { minimumX, minimumY, maximumX, maximumY }];
|
|
106
|
+
}));
|
|
107
|
+
for (const hole of holes) {
|
|
108
|
+
const [x, y] = hole[0];
|
|
109
|
+
const owner = outers
|
|
110
|
+
.filter((polygon) => {
|
|
111
|
+
const bounds = outerBounds.get(polygon);
|
|
112
|
+
return (x >= bounds.minimumX &&
|
|
113
|
+
x <= bounds.maximumX &&
|
|
114
|
+
y >= bounds.minimumY &&
|
|
115
|
+
y <= bounds.maximumY &&
|
|
116
|
+
loopContainsPoint(polygon.outer, hole[0]));
|
|
117
|
+
})
|
|
118
|
+
.sort((a, b) => Math.abs(signedAreaLoop2(a.outer)) - Math.abs(signedAreaLoop2(b.outer)))[0];
|
|
119
|
+
if (owner === undefined) {
|
|
120
|
+
throw new GeometryError("INVALID_TOPOLOGY", "CrossSection hole had no containing exterior.");
|
|
121
|
+
}
|
|
122
|
+
owner.holes.push(hole);
|
|
123
|
+
}
|
|
124
|
+
return normalizeRegion2(outers.map(({ outer, holes }) => ({ outer, holes: Object.freeze(holes) })));
|
|
125
|
+
};
|
|
126
|
+
/** Copies a robust CrossSection into MetaMesh's explicit outer/hole topology. */
|
|
127
|
+
export function crossSectionToRegion(context, section, limits = {}) {
|
|
128
|
+
assertScopedManifoldValue(context, section);
|
|
129
|
+
assertSamplingCount(section.numVert(), "crossSectionToRegion", resolveGeometrySamplingLimits(limits).maxProfilePoints);
|
|
130
|
+
// Normalization owns the one immutable coordinate copy and reuses validated child loops.
|
|
131
|
+
const loops = section.toPolygons();
|
|
132
|
+
if (loops.length > MAX_AGGREGATE_CONTOURS) {
|
|
133
|
+
throw new GeometryError("COMPLEXITY_LIMIT", "Resolved regions exceed 25000 contours.", {
|
|
134
|
+
operation: "crossSectionToRegion",
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return regionFromLoops(loops);
|
|
138
|
+
}
|