@metamesh/modeling 0.6.0 → 0.8.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 +30 -0
- package/README.md +157 -22
- package/dist/api/engine.d.ts +25 -4
- package/dist/api/geometry.d.ts +391 -82
- package/dist/api/index.d.ts +732 -108
- package/dist/api/text.d.ts +40 -10
- package/dist/engine/construction-operations.js +52 -10
- package/dist/engine/execution.js +55 -2
- package/dist/engine/export-3mf.js +17 -1
- package/dist/engine/finalization.js +27 -18
- package/dist/engine/index.js +12 -1
- package/dist/engine/inspection-operations.js +26 -22
- package/dist/engine/loft-operations.js +1 -1
- package/dist/engine/mechanical-operations.js +5 -8
- package/dist/engine/mesh-view-cache.js +23 -0
- package/dist/engine/native/adapter.js +2 -4
- package/dist/engine/native/distance.js +42 -0
- package/dist/engine/native/status.js +16 -0
- package/dist/engine/operation-environment.js +2 -1
- package/dist/engine/section-operations.js +3 -3
- package/dist/geometry/curves/sampling.js +2 -1
- package/dist/geometry/heightfield.js +109 -42
- package/dist/geometry/index.js +2 -0
- package/dist/geometry/loft-simple-mesh.js +2 -3
- package/dist/geometry/loft-triangulation.js +12 -9
- package/dist/geometry/loft.js +18 -7
- package/dist/geometry/math/vector3.js +35 -5
- package/dist/geometry/mechanical/standards.js +2 -3
- package/dist/geometry/mechanical/thread-profiles.js +5 -0
- package/dist/geometry/mechanical/threads.js +2 -3
- package/dist/geometry/mesh-centroid.js +36 -0
- package/dist/geometry/mesh-edge-table.js +126 -0
- package/dist/geometry/mesh-solid-intersections.js +123 -0
- package/dist/geometry/mesh-solid.js +284 -0
- package/dist/geometry/mesh.js +8 -25
- package/dist/geometry/paths/path.js +3 -1
- package/dist/geometry/paths/resampling.js +2 -1
- package/dist/geometry/profiles/matched.js +8 -0
- package/dist/geometry/profiles/profiles.js +24 -3
- package/dist/geometry/profiles/rectangle.js +16 -2
- package/dist/geometry/profiles/shapes.js +8 -1
- package/dist/geometry/revolve/revolve-mesh.js +6 -0
- package/dist/geometry/sweeps/sweep-closed.js +4 -4
- package/dist/geometry/sweeps/sweep-mesh.js +13 -7
- package/dist/geometry/sweeps/sweep-open.js +4 -4
- package/dist/geometry/sweeps/validation.js +1 -2
- package/dist/model/operations.js +13 -4
- package/dist/text/index.js +11 -1
- package/dist/text/vector-text-layout.js +46 -3
- package/package.json +1 -1
package/dist/api/text.d.ts
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
/** Limits apply before parsing or shaping untrusted font data. */
|
|
2
2
|
export declare const MAX_VECTOR_FONT_BYTES: number;
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Parses bounded font bytes using optional fontkit and WOFF2 peers; no font fetching occurs here.
|
|
6
|
+
*
|
|
7
|
+
* @example Prepare a label before building geometry
|
|
8
|
+
* ```ts
|
|
9
|
+
* async function prepareLabel(fontBytes: Uint8Array) {
|
|
10
|
+
* const font = await prepareVectorFont(fontBytes);
|
|
11
|
+
* return font.layout("MetaMesh", { fontSize: 8 });
|
|
12
|
+
* }
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
5
15
|
export declare function prepareVectorFont(bytes: Uint8Array, options?: VectorFontOptions): Promise<VectorFont>;
|
|
6
16
|
|
|
7
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* A prepared font owns its parsed bytes; keep handles local to their Worker or invocation.
|
|
19
|
+
*/
|
|
8
20
|
export declare interface VectorFont {
|
|
9
|
-
/** Shape
|
|
21
|
+
/** Shape horizontal lines or one circular line; bounded to 512 UTF-16 units and supports blank text. */
|
|
10
22
|
readonly layout: (text: string, options: VectorTextLayoutOptions) => VectorTextLayout;
|
|
11
23
|
/** Measure the same cached vector layout; all returned lengths are in millimetres. */
|
|
12
24
|
readonly measure: (text: string, options: VectorTextLayoutOptions) => VectorTextMetrics;
|
|
13
|
-
/** Serialize visible
|
|
25
|
+
/** Serialize visible horizontal or circular outlines as bounded path-only SVG in physical millimetres. */
|
|
14
26
|
readonly toSvg: (text: string, options: VectorTextLayoutOptions) => string;
|
|
15
27
|
}
|
|
16
28
|
|
|
@@ -20,6 +32,18 @@ export declare interface VectorFontOptions {
|
|
|
20
32
|
readonly staticWeight?: number;
|
|
21
33
|
}
|
|
22
34
|
|
|
35
|
+
/** A circular baseline centered at the XY origin, with rigid shaped glyph groups. */
|
|
36
|
+
export declare interface VectorTextArcBaseline {
|
|
37
|
+
/** Selects a circular baseline; glyph outlines rotate without bending or stretching. */
|
|
38
|
+
readonly kind: "arc";
|
|
39
|
+
/** Baseline radius in millimetres, greater than zero and at most 10000. */
|
|
40
|
+
readonly radius: number;
|
|
41
|
+
/** Alignment datum in degrees counterclockwise from +X; defaults to 90. */
|
|
42
|
+
readonly centerAngleDegrees?: number;
|
|
43
|
+
/** Reading direction around the circle; defaults to clockwise. */
|
|
44
|
+
readonly direction?: "clockwise" | "counterclockwise";
|
|
45
|
+
}
|
|
46
|
+
|
|
23
47
|
/** Exact curve bounds in millimetres; positive Y points upward. */
|
|
24
48
|
export declare interface VectorTextBounds {
|
|
25
49
|
/** Minimum X and Y of visible outlines, including accents, descenders, and overhangs. */
|
|
@@ -28,7 +52,9 @@ export declare interface VectorTextBounds {
|
|
|
28
52
|
readonly max: readonly [number, number];
|
|
29
53
|
}
|
|
30
54
|
|
|
31
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Immutable vector layout with no browser, font parser, or kernel ownership.
|
|
57
|
+
*/
|
|
32
58
|
export declare interface VectorTextLayout {
|
|
33
59
|
/** Nonzero-filled path data in millimetres, positive Y upward; empty for text with no ink. */
|
|
34
60
|
readonly paths: readonly string[];
|
|
@@ -36,13 +62,17 @@ export declare interface VectorTextLayout {
|
|
|
36
62
|
readonly metrics: VectorTextMetrics;
|
|
37
63
|
}
|
|
38
64
|
|
|
39
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Text bounded to 512 UTF-16 units: horizontal lines or one circular line without wrapping.
|
|
67
|
+
*/
|
|
40
68
|
export declare interface VectorTextLayoutOptions extends VectorTextOptions {
|
|
41
|
-
/** Place
|
|
69
|
+
/** Place the advance's start, midpoint or end at the datum; defaults to center on arcs, otherwise left. */
|
|
42
70
|
readonly align?: "left" | "center" | "right";
|
|
71
|
+
/** Omitted uses horizontal lines. Arcs require one line and baseline vertical alignment. */
|
|
72
|
+
readonly baseline?: VectorTextArcBaseline;
|
|
43
73
|
/** Baseline spacing in ems, 0.5–5; omitted uses the selected face's natural vertical metrics. */
|
|
44
74
|
readonly lineHeight?: number;
|
|
45
|
-
/** Place the first baseline or
|
|
75
|
+
/** Place the first baseline or complete ink bounds at Y=0; arcs only accept baseline. */
|
|
46
76
|
readonly verticalAlign?: "baseline" | "top" | "center" | "bottom";
|
|
47
77
|
}
|
|
48
78
|
|
|
@@ -50,9 +80,9 @@ export declare interface VectorTextLayoutOptions extends VectorTextOptions {
|
|
|
50
80
|
export declare interface VectorTextMetrics {
|
|
51
81
|
/** Visible curve bounds after alignment, or null when the text has no ink. */
|
|
52
82
|
readonly bounds: VectorTextBounds | null;
|
|
53
|
-
/** Unaligned
|
|
83
|
+
/** Unaligned logical advance and negative baseline displacement in mm; arcs retain [arc length, 0]. */
|
|
54
84
|
readonly advance: readonly [number, number];
|
|
55
|
-
/** Maximum line advance in millimetres, including spaces;
|
|
85
|
+
/** Maximum logical line advance in millimetres, including spaces; arc length on circular baselines. */
|
|
56
86
|
readonly logicalWidth: number;
|
|
57
87
|
/** Selected font's ascender distance above a baseline in millimetres, independent of the text. */
|
|
58
88
|
readonly ascent: number;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { GeometryError } from "../geometry/errors.js";
|
|
2
2
|
import { resolveExtrusionContourDivisions, resolveExtrusionControls, } from "../geometry/extrusion.js";
|
|
3
|
-
import { heightfieldMesh
|
|
3
|
+
import { heightfieldMesh } from "../geometry/heightfield.js";
|
|
4
4
|
import { assertMeshAllocationWithinLimits } from "../geometry/model-limits.js";
|
|
5
5
|
import { assertPrimitiveOptions } from "../geometry/primitive-resolvers.js";
|
|
6
6
|
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
|
-
import {
|
|
9
|
+
import { sweepMesh } from "../geometry/sweeps/sweep-mesh.js";
|
|
10
10
|
import { buildFinishedExtrusion } from "./extrusion-finishing.js";
|
|
11
11
|
import { createLoftOperation } from "./loft-operations.js";
|
|
12
12
|
import { triangleMeshToManifold } from "./native/adapter.js";
|
|
@@ -15,7 +15,18 @@ import * as kernel from "./native/index.js";
|
|
|
15
15
|
import { buildProfileSweepMesh } from "./profile-mesh.js";
|
|
16
16
|
/** Internal host-bound construction operations. */
|
|
17
17
|
export function createConstructionOperations(environment, sections) {
|
|
18
|
-
const { module, scope, limits, assertActive, throwGeometryError, resolveHandle, nativeSolid, nativeSection, wrapSolid, ownSolid, ownSection, validateFiniteNumber, requirePositiveNumber, validateCoordinates, assertOperandCount, assertMeshAllocation, withResolutionDefaults, resolveCircularSegments, } = environment;
|
|
18
|
+
const { module, scope, limits, assertActive, throwGeometryError, resolveHandle, nativeSolid, nativeSection, wrapSolid, wrapSection, ownSolid, ownSection, validateFiniteNumber, requirePositiveNumber, validateCoordinates, assertOperandCount, assertMeshAllocation, withResolutionDefaults, resolveCircularSegments, } = environment;
|
|
19
|
+
const cuttingPlane = (value, operation) => {
|
|
20
|
+
assertPrimitiveOptions(value, operation);
|
|
21
|
+
validateCoordinates(value.normal, 3, operation);
|
|
22
|
+
const magnitude = Math.max(...value.normal.map(Math.abs));
|
|
23
|
+
if (magnitude === 0)
|
|
24
|
+
throwGeometryError("DEGENERATE_GEOMETRY", operation, "Plane normals must be nonzero.");
|
|
25
|
+
const scaled = value.normal.map((coordinate) => coordinate / magnitude);
|
|
26
|
+
const length = Math.hypot(...scaled);
|
|
27
|
+
const normal = [scaled[0] / length, scaled[1] / length, scaled[2] / length];
|
|
28
|
+
return { normal, offset: validateFiniteNumber(value.offset ?? 0, operation) };
|
|
29
|
+
};
|
|
19
30
|
const fromContours = sections.sectionFromContours;
|
|
20
31
|
const corresponding = (value) => typeof value === "object" && value !== null && "components" in value;
|
|
21
32
|
const shapeSection = (shape) => corresponding(shape)
|
|
@@ -50,7 +61,6 @@ export function createConstructionOperations(environment, sections) {
|
|
|
50
61
|
// a hole also bounds disconnected Sections, without extracting native point data.
|
|
51
62
|
triangles = Math.max(0, vertices * 2 + 4 * native.numContour() - 8);
|
|
52
63
|
assertMeshAllocationWithinLimits(vertices, triangles, vertices * 12 + triangles * 12, "extrude", resolved.limits);
|
|
53
|
-
assertMeshAllocation(vertices, triangles, vertices * 12 + triangles * 12, "extrude");
|
|
54
64
|
return ownSolid(native.extrude(value.height, divisions, controls.twistDegrees, controls.scales, controls.center));
|
|
55
65
|
};
|
|
56
66
|
const boolean = (values, operation) => {
|
|
@@ -90,13 +100,47 @@ export function createConstructionOperations(environment, sections) {
|
|
|
90
100
|
union: ((values) => boolean(values, "union")),
|
|
91
101
|
subtract,
|
|
92
102
|
intersect: ((values) => boolean(values, "intersect")),
|
|
103
|
+
split: (solid, cutter) => {
|
|
104
|
+
const results = nativeSolid(solid).split(nativeSolid(cutter));
|
|
105
|
+
return Object.freeze([ownSolid(results[0]), ownSolid(results[1])]);
|
|
106
|
+
},
|
|
107
|
+
splitByPlane: (solid, value) => {
|
|
108
|
+
const native = nativeSolid(solid);
|
|
109
|
+
const plane = cuttingPlane(value, "splitByPlane");
|
|
110
|
+
const results = native.splitByPlane(plane.normal, plane.offset);
|
|
111
|
+
return Object.freeze([ownSolid(results[0]), ownSolid(results[1])]);
|
|
112
|
+
},
|
|
93
113
|
compose: (values) => wrapSolid(kernel.compose(scope, values.map(nativeSolid))),
|
|
94
|
-
hull: (value) => {
|
|
114
|
+
hull: ((value) => {
|
|
95
115
|
assertActive();
|
|
96
|
-
|
|
116
|
+
assertPrimitiveOptions(value, "hull");
|
|
117
|
+
const hasSolids = "solids" in value && value.solids !== undefined;
|
|
118
|
+
const hasSections = "sections" in value && value.sections !== undefined;
|
|
119
|
+
if (Number(value.points !== undefined) + Number(hasSolids) + Number(hasSections) !== 1)
|
|
120
|
+
throwGeometryError("INVALID_ARGUMENT", "hull", "Supply exactly one hull input family.");
|
|
121
|
+
if (hasSections) {
|
|
122
|
+
assertOperandCount(value.sections, "hull");
|
|
123
|
+
return ownSection(module.CrossSection.hull(value.sections.map(nativeSection)));
|
|
124
|
+
}
|
|
125
|
+
if (value.points !== undefined && value.points[0]?.length === 2) {
|
|
126
|
+
const points = value.points;
|
|
127
|
+
const pointInputs = points;
|
|
128
|
+
if (!Array.isArray(points) || points.length < 3)
|
|
129
|
+
throwGeometryError("INVALID_ARGUMENT", "hull", "A planar hull needs at least three points.");
|
|
130
|
+
assertSamplingCount(points.length, "hull", Math.min(100_000, limits.maxProfilePoints));
|
|
131
|
+
const copied = pointInputs.map((point) => {
|
|
132
|
+
validateCoordinates(point, 2, "hull");
|
|
133
|
+
return [point[0], point[1]];
|
|
134
|
+
});
|
|
135
|
+
const native = ownScopedManifoldValue(scope, module.CrossSection.hull([copied]));
|
|
136
|
+
if (!(native.area() > 0))
|
|
137
|
+
throwGeometryError("DEGENERATE_GEOMETRY", "hull", "Hull points must span positive area.");
|
|
138
|
+
return wrapSection(native);
|
|
139
|
+
}
|
|
140
|
+
return wrapSolid(kernel.hull(scope, !hasSolids
|
|
97
141
|
? { points: value.points }
|
|
98
142
|
: { solids: value.solids.map(nativeSolid) }));
|
|
99
|
-
},
|
|
143
|
+
}),
|
|
100
144
|
minkowskiSum: (a, b) => wrapSolid(kernel.minkowskiSum(scope, nativeSolid(a), nativeSolid(b))),
|
|
101
145
|
solidFromMesh,
|
|
102
146
|
extrude,
|
|
@@ -147,13 +191,11 @@ export function createConstructionOperations(environment, sections) {
|
|
|
147
191
|
const singleComponent = profile.components.length === 1 ? profile.components[0] : undefined;
|
|
148
192
|
const contours = singleComponent?.holes.length === 0 ? [singleComponent.outer.points] : [];
|
|
149
193
|
if (contours.length === 1)
|
|
150
|
-
return solidFromMesh(
|
|
194
|
+
return solidFromMesh(sweepMesh(contours[0], path, selected));
|
|
151
195
|
return solidFromMesh(buildProfileSweepMesh(module, profile, path, selected, selected.limits));
|
|
152
196
|
},
|
|
153
197
|
heightfield: (value) => {
|
|
154
198
|
assertActive();
|
|
155
|
-
const facts = preflightHeightfield(value, limits);
|
|
156
|
-
assertMeshAllocation(facts.vertexCount, facts.triangleCount, facts.byteCount, "heightfield");
|
|
157
199
|
return solidFromMesh(heightfieldMesh(value, limits));
|
|
158
200
|
},
|
|
159
201
|
implicit: (value) => {
|
package/dist/engine/execution.js
CHANGED
|
@@ -7,6 +7,7 @@ import { createModelingContext } from "./modeling-context.js";
|
|
|
7
7
|
import { ownScopedManifoldValue } from "./native/context.js";
|
|
8
8
|
import { copiedFloat32Array, copiedUint32Array } from "./native/copied-views.js";
|
|
9
9
|
import * as kernel from "./native/index.js";
|
|
10
|
+
import { assertNativeSolidStatus } from "./native/status.js";
|
|
10
11
|
import { createShapeAnchorOwner } from "./shape-anchors.js";
|
|
11
12
|
/** Internal execution owner; no native value or constructor crosses the public facade. */
|
|
12
13
|
export function createExecutionContext(module, scope, limits, initialResolution = {}) {
|
|
@@ -15,6 +16,8 @@ export function createExecutionContext(module, scope, limits, initialResolution
|
|
|
15
16
|
// mesh inspection receives independent copies, and this run-owned cache is cleared on exit.
|
|
16
17
|
const canonicalSolidMeshes = new Map();
|
|
17
18
|
const canonicalSectionProfiles = new Map();
|
|
19
|
+
let solidInspections;
|
|
20
|
+
let sectionInspections;
|
|
18
21
|
const primitiveSectionProfiles = new Map();
|
|
19
22
|
let active = true;
|
|
20
23
|
const throwGeometryError = (code, operation, message) => {
|
|
@@ -60,6 +63,50 @@ export function createExecutionContext(module, scope, limits, initialResolution
|
|
|
60
63
|
const owned = ownScopedManifoldValue(scope, value);
|
|
61
64
|
return wrapSection(owned);
|
|
62
65
|
};
|
|
66
|
+
// Geometry is immutable for a native value. Validate build ownership before every cache hit,
|
|
67
|
+
// and retain only frozen plain facts so callers cannot corrupt later inspections.
|
|
68
|
+
const solidInspectionFor = (handle) => {
|
|
69
|
+
const native = nativeSolid(handle);
|
|
70
|
+
const cached = solidInspections?.get(native);
|
|
71
|
+
if (cached !== undefined)
|
|
72
|
+
return cached;
|
|
73
|
+
assertNativeSolidStatus(native, "inspectSolid");
|
|
74
|
+
const bounds = native.boundingBox();
|
|
75
|
+
const parts = native.decompose().map((part) => ownScopedManifoldValue(scope, part));
|
|
76
|
+
const facts = Object.freeze({
|
|
77
|
+
min: Object.freeze([...bounds.min]),
|
|
78
|
+
max: Object.freeze([...bounds.max]),
|
|
79
|
+
isEmpty: native.isEmpty(),
|
|
80
|
+
parts: parts.length,
|
|
81
|
+
volume: native.volume(),
|
|
82
|
+
surfaceArea: native.surfaceArea(),
|
|
83
|
+
tolerance: native.tolerance(),
|
|
84
|
+
});
|
|
85
|
+
(solidInspections ??= new Map()).set(native, facts);
|
|
86
|
+
return facts;
|
|
87
|
+
};
|
|
88
|
+
const sectionInspectionFor = (handle) => {
|
|
89
|
+
const native = nativeSection(handle);
|
|
90
|
+
const cached = sectionInspections?.get(native);
|
|
91
|
+
if (cached !== undefined)
|
|
92
|
+
return cached;
|
|
93
|
+
const bounds = native.bounds();
|
|
94
|
+
const empty = native.isEmpty();
|
|
95
|
+
const parts = empty
|
|
96
|
+
? []
|
|
97
|
+
: native.decompose().map((part) => ownScopedManifoldValue(scope, part));
|
|
98
|
+
const facts = Object.freeze({
|
|
99
|
+
min: Object.freeze([...bounds.min]),
|
|
100
|
+
max: Object.freeze([...bounds.max]),
|
|
101
|
+
isEmpty: empty,
|
|
102
|
+
area: native.area(),
|
|
103
|
+
contours: native.numContour(),
|
|
104
|
+
parts: parts.length,
|
|
105
|
+
vertices: native.numVert(),
|
|
106
|
+
});
|
|
107
|
+
(sectionInspections ??= new Map()).set(native, facts);
|
|
108
|
+
return facts;
|
|
109
|
+
};
|
|
63
110
|
const shapeAnchors = createShapeAnchorOwner((shape) => {
|
|
64
111
|
resolveHandle(shape);
|
|
65
112
|
}, (shape) => {
|
|
@@ -94,6 +141,7 @@ export function createExecutionContext(module, scope, limits, initialResolution
|
|
|
94
141
|
assertMeshAllocationWithinLimits(vertices, triangles, bytes, operation, limits);
|
|
95
142
|
};
|
|
96
143
|
const copyNativeMesh = (native) => {
|
|
144
|
+
assertNativeSolidStatus(native, "toTriangleMesh");
|
|
97
145
|
const raw = native.getMesh();
|
|
98
146
|
assertMeshAllocation(raw.numVert, raw.numTri, (raw.numVert + raw.numTri) * 12, "copyMesh");
|
|
99
147
|
const positions = copiedFloat32Array(raw.numVert * 3);
|
|
@@ -194,6 +242,8 @@ export function createExecutionContext(module, scope, limits, initialResolution
|
|
|
194
242
|
assertOperandCount,
|
|
195
243
|
assertMeshAllocation,
|
|
196
244
|
analyzedMeshFor,
|
|
245
|
+
solidInspectionFor,
|
|
246
|
+
sectionInspectionFor,
|
|
197
247
|
sectionProfileFor,
|
|
198
248
|
primitiveProfileFor,
|
|
199
249
|
rememberPrimitiveProfile,
|
|
@@ -213,13 +263,16 @@ export function createExecutionContext(module, scope, limits, initialResolution
|
|
|
213
263
|
triVerts: mesh.indices,
|
|
214
264
|
});
|
|
215
265
|
const native = ownScopedManifoldValue(scope, new module.Manifold(input));
|
|
216
|
-
|
|
217
|
-
throwGeometryError("INVALID_TOPOLOGY", "finalize", "The geometry kernel rejected the finalized manufacturing mesh.");
|
|
266
|
+
assertNativeSolidStatus(native, "finalize");
|
|
218
267
|
},
|
|
219
268
|
dispose: () => {
|
|
220
269
|
active = false;
|
|
221
270
|
canonicalSolidMeshes.clear();
|
|
222
271
|
canonicalSectionProfiles.clear();
|
|
272
|
+
solidInspections?.clear();
|
|
273
|
+
sectionInspections?.clear();
|
|
274
|
+
solidInspections = undefined;
|
|
275
|
+
sectionInspections = undefined;
|
|
223
276
|
primitiveSectionProfiles.clear();
|
|
224
277
|
shapeAnchors.dispose();
|
|
225
278
|
},
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { Zip, ZipDeflate } from "fflate";
|
|
2
2
|
import { GeometryError } from "../geometry/errors.js";
|
|
3
|
+
import { analyzeSolidMesh } from "../geometry/mesh-solid.js";
|
|
3
4
|
import { analyzeTriangleMesh } from "../geometry/mesh.js";
|
|
5
|
+
import { MeshViewCache } from "./mesh-view-cache.js";
|
|
4
6
|
const namespace = "http://schemas.microsoft.com/3dmanufacturing/core/2015/02";
|
|
5
7
|
const declaration = '<?xml version="1.0" encoding="UTF-8"?>';
|
|
6
8
|
const encoder = new TextEncoder();
|
|
7
9
|
const escapeXml = (value) => value.replace(/[&<>"']/g, (character) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[character]);
|
|
8
10
|
/** Private core 3MF serializer. It preserves the selected buffers and streams bounded XML chunks. */
|
|
9
11
|
export function meshesTo3mf(parts, options) {
|
|
12
|
+
if (options.meshValidation !== undefined &&
|
|
13
|
+
options.meshValidation !== "topology" &&
|
|
14
|
+
options.meshValidation !== "solid")
|
|
15
|
+
throw new GeometryError("INVALID_ARGUMENT", "Unsupported mesh validation mode.", {
|
|
16
|
+
operation: "export3mf",
|
|
17
|
+
});
|
|
10
18
|
const name = options.name ?? "MetaMesh model";
|
|
11
19
|
if (typeof name !== "string" ||
|
|
12
20
|
name.length === 0 ||
|
|
@@ -14,10 +22,18 @@ export function meshesTo3mf(parts, options) {
|
|
|
14
22
|
// eslint-disable-next-line no-control-regex -- XML 1.0 disallows these control characters.
|
|
15
23
|
/[\u0000-\u0008\u000b\u000c\u000e-\u001f\ud800-\udfff\ufffe\uffff]/u.test(name))
|
|
16
24
|
throw new GeometryError("INVALID_ARGUMENT", "3MF names require one through 1024 valid XML characters.", { operation: "export3mf" });
|
|
25
|
+
const analyzed = new MeshViewCache();
|
|
17
26
|
for (const part of parts) {
|
|
18
|
-
|
|
27
|
+
let facts = analyzed.get(part);
|
|
28
|
+
if (facts !== undefined)
|
|
29
|
+
continue;
|
|
30
|
+
facts =
|
|
31
|
+
options.meshValidation === "solid"
|
|
32
|
+
? analyzeSolidMesh(part)
|
|
33
|
+
: analyzeTriangleMesh(part, { requireClosed: true, includeTopology: true });
|
|
19
34
|
if (!(facts.signedVolume > 0) || facts.vertexCount >= 2 ** 31 || facts.triangleCount >= 2 ** 31)
|
|
20
35
|
throw new GeometryError("INVALID_TOPOLOGY", "3MF parts require positive oriented volume and valid core resource counts.", { operation: "export3mf" });
|
|
36
|
+
analyzed.set(part, facts);
|
|
21
37
|
}
|
|
22
38
|
const chunks = [];
|
|
23
39
|
let bytes = 0, complete = false;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { GeometryError } from "../geometry/errors.js";
|
|
2
2
|
import { optimizePlanarPatches } from "../geometry/mesh-planar.js";
|
|
3
|
-
import {
|
|
3
|
+
import { validateAnalyzedSolidMesh } from "../geometry/mesh-solid.js";
|
|
4
|
+
import { analyzeTriangleMesh, } from "../geometry/mesh.js";
|
|
5
|
+
import { MeshViewCache } from "./mesh-view-cache.js";
|
|
4
6
|
import { copiedMeshViews, copiedUint32Array } from "./native/copied-views.js";
|
|
5
7
|
const isArray = (value) => Array.isArray(value);
|
|
6
8
|
function finalPartTotals(parts) {
|
|
@@ -26,6 +28,12 @@ function fitsLimits(totals, limits) {
|
|
|
26
28
|
}
|
|
27
29
|
/** Finalizes one complete build under the selected limits, preserving usable originals when optional cleanup fails. */
|
|
28
30
|
export function finalizeModel(execution, output, limits, options, startedAt, builtAt) {
|
|
31
|
+
if (options.meshValidation !== undefined &&
|
|
32
|
+
options.meshValidation !== "topology" &&
|
|
33
|
+
options.meshValidation !== "solid")
|
|
34
|
+
throw new GeometryError("INVALID_ARGUMENT", "Unsupported mesh validation mode.", {
|
|
35
|
+
operation: "finalize",
|
|
36
|
+
});
|
|
29
37
|
const descriptors = execution.isSolid(output) ? [{ id: "model", solid: output }] : output;
|
|
30
38
|
if (!isArray(descriptors) || descriptors.length === 0)
|
|
31
39
|
throw new GeometryError("INVALID_ARGUMENT", "Build must return a solid or at least one named part.", { operation: "finalize" });
|
|
@@ -36,6 +44,7 @@ export function finalizeModel(execution, output, limits, options, startedAt, bui
|
|
|
36
44
|
const ids = new Set();
|
|
37
45
|
const backingBuffers = new Set();
|
|
38
46
|
const meshCopies = new Map();
|
|
47
|
+
const directMeasurements = new MeshViewCache();
|
|
39
48
|
const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
|
|
40
49
|
let vertices = 0, triangles = 0, bytes = 0;
|
|
41
50
|
let parts = descriptors.map((descriptor) => {
|
|
@@ -76,10 +85,12 @@ export function finalizeModel(execution, output, limits, options, startedAt, bui
|
|
|
76
85
|
throw new GeometryError("COMPLEXITY_LIMIT", "Aggregate output exceeds the execution limits.", { operation: "finalize" });
|
|
77
86
|
// Direct meshes are copied before analysis so later caller mutation cannot invalidate facts.
|
|
78
87
|
const mesh = descriptor.mesh === undefined ? source : copiedMeshViews(source, meshCopies);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
88
|
+
let measurements = nativeAnalysis?.measurements ?? directMeasurements.get(mesh);
|
|
89
|
+
if (measurements === undefined) {
|
|
90
|
+
measurements = analyzeTriangleMesh(mesh, { requireClosed: true, includeTopology: true });
|
|
82
91
|
execution.validateMesh(mesh);
|
|
92
|
+
directMeasurements.set(mesh, measurements);
|
|
93
|
+
}
|
|
83
94
|
if (!(measurements.signedVolume > 0))
|
|
84
95
|
throw new GeometryError("INVALID_TOPOLOGY", "Manufacturing parts must enclose positive oriented volume.", { operation: "finalize" });
|
|
85
96
|
for (let axis = 0; axis < 3; axis += 1) {
|
|
@@ -95,20 +106,9 @@ export function finalizeModel(execution, output, limits, options, startedAt, bui
|
|
|
95
106
|
});
|
|
96
107
|
const originalParts = parts;
|
|
97
108
|
if (options.meshTopologyMode === "optimized" && triangles <= 200_000) {
|
|
98
|
-
const candidates = new
|
|
109
|
+
const candidates = new MeshViewCache();
|
|
99
110
|
parts = parts.map((part) => {
|
|
100
|
-
|
|
101
|
-
if (indexBuffers === undefined) {
|
|
102
|
-
indexBuffers = new Map();
|
|
103
|
-
candidates.set(part.positions.buffer, indexBuffers);
|
|
104
|
-
}
|
|
105
|
-
let views = indexBuffers.get(part.indices.buffer);
|
|
106
|
-
if (views === undefined) {
|
|
107
|
-
views = new Map();
|
|
108
|
-
indexBuffers.set(part.indices.buffer, views);
|
|
109
|
-
}
|
|
110
|
-
const key = `${part.positions.byteOffset}:${part.positions.length}:${part.indices.byteOffset}:${part.indices.length}`;
|
|
111
|
-
const cached = views.get(key);
|
|
111
|
+
const cached = candidates.get(part);
|
|
112
112
|
if (cached !== undefined)
|
|
113
113
|
return Object.freeze({ ...part, ...cached });
|
|
114
114
|
let selected = {
|
|
@@ -134,7 +134,7 @@ export function finalizeModel(execution, output, limits, options, startedAt, bui
|
|
|
134
134
|
catch {
|
|
135
135
|
// Optional optimization preserves the usable original on unsupported geometry.
|
|
136
136
|
}
|
|
137
|
-
|
|
137
|
+
candidates.set(part, selected);
|
|
138
138
|
return Object.freeze({ ...part, ...selected });
|
|
139
139
|
});
|
|
140
140
|
}
|
|
@@ -148,6 +148,15 @@ export function finalizeModel(execution, output, limits, options, startedAt, bui
|
|
|
148
148
|
throw new GeometryError("COMPLEXITY_LIMIT", "Selected output exceeds the execution limits.", {
|
|
149
149
|
operation: "finalize",
|
|
150
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
|
+
}
|
|
159
|
+
}
|
|
151
160
|
const finalizedAt = performance.now();
|
|
152
161
|
return Object.freeze({
|
|
153
162
|
parts: Object.freeze(parts),
|
package/dist/engine/index.js
CHANGED
|
@@ -61,7 +61,18 @@ function engineFromModule(module, engineLimits) {
|
|
|
61
61
|
},
|
|
62
62
|
});
|
|
63
63
|
}
|
|
64
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Initializes one host kernel and returns a lifecycle-safe engine. No generator initializes WASM.
|
|
66
|
+
*
|
|
67
|
+
* @example Build a box in a host application
|
|
68
|
+
* ```ts
|
|
69
|
+
* import { box } from "@metamesh/modeling";
|
|
70
|
+
* import { createModelEngine } from "@metamesh/modeling/engine";
|
|
71
|
+
*
|
|
72
|
+
* const engine = await createModelEngine();
|
|
73
|
+
* const snapshot = engine.build(() => box({ size: [30, 20, 8] }));
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
65
76
|
export async function createModelEngine(options = {}) {
|
|
66
77
|
const limits = resolveModelingLimits(options.limits);
|
|
67
78
|
const module = await createManifoldModule({
|
|
@@ -1,26 +1,21 @@
|
|
|
1
1
|
import { ownScopedManifoldValue } from "./native/context.js";
|
|
2
2
|
import { copiedMeshViews } from "./native/copied-views.js";
|
|
3
|
+
import { measureSolidDistance } from "./native/distance.js";
|
|
4
|
+
import { assertNativeSolidStatus } from "./native/status.js";
|
|
3
5
|
/** Internal host-bound inspection operations. */
|
|
4
6
|
export function createInspectionOperations(environment) {
|
|
5
|
-
const { scope, throwGeometryError, nativeSolid,
|
|
7
|
+
const { scope, throwGeometryError, nativeSolid, resolveHandle, ownSolid, ownSection, validateFiniteNumber, requirePositiveNumber, validateCoordinates, analyzedMeshFor, solidInspectionFor, sectionInspectionFor, } = environment;
|
|
6
8
|
const operations = {
|
|
7
9
|
toTriangleMesh: (value) => copiedMeshViews(analyzedMeshFor(value).mesh, new Map()),
|
|
8
|
-
inspectSection:
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
parts: parts.length,
|
|
16
|
-
vertices: native.numVert(),
|
|
17
|
-
};
|
|
18
|
-
},
|
|
19
|
-
inspectSolid: (handle) => {
|
|
20
|
-
const native = nativeSolid(handle), bounds = native.boundingBox();
|
|
21
|
-
const parts = native.decompose().map((part) => ownScopedManifoldValue(scope, part));
|
|
22
|
-
return { ...bounds, parts: parts.length, volume: native.volume() };
|
|
10
|
+
inspectSection: sectionInspectionFor,
|
|
11
|
+
inspectSolid: solidInspectionFor,
|
|
12
|
+
isEmpty: (handle) => {
|
|
13
|
+
const native = resolveHandle(handle);
|
|
14
|
+
if (native.kind === "solid")
|
|
15
|
+
assertNativeSolidStatus(native.value, "isEmpty");
|
|
16
|
+
return native.value.isEmpty();
|
|
23
17
|
},
|
|
18
|
+
distance: (handle, other) => measureSolidDistance(nativeSolid(handle), nativeSolid(other)),
|
|
24
19
|
rayCast: (handle, origin, endpoint) => {
|
|
25
20
|
validateCoordinates(origin, 3, "rayCast");
|
|
26
21
|
validateCoordinates(endpoint, 3, "rayCast");
|
|
@@ -36,12 +31,21 @@ export function createInspectionOperations(environment) {
|
|
|
36
31
|
normal: Object.freeze([...hit.normal]),
|
|
37
32
|
})));
|
|
38
33
|
},
|
|
39
|
-
components: (handle) =>
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
34
|
+
components: ((handle) => {
|
|
35
|
+
const native = resolveHandle(handle);
|
|
36
|
+
if (native.kind === "section")
|
|
37
|
+
return Object.freeze(native.value.isEmpty() ? [] : native.value.decompose().map(ownSection));
|
|
38
|
+
assertNativeSolidStatus(native.value, "components");
|
|
39
|
+
return Object.freeze(native.value.decompose().map(ownSolid));
|
|
40
|
+
}),
|
|
41
|
+
simplify: ((handle, tolerance) => {
|
|
42
|
+
const native = resolveHandle(handle);
|
|
43
|
+
const distance = requirePositiveNumber(tolerance, "simplify");
|
|
44
|
+
if (native.kind === "section")
|
|
45
|
+
return ownSection(native.value.simplify(distance));
|
|
46
|
+
const original = ownScopedManifoldValue(scope, native.value.asOriginal());
|
|
47
|
+
return ownSolid(original.simplify(distance));
|
|
48
|
+
}),
|
|
45
49
|
setTolerance: (handle, tolerance) => {
|
|
46
50
|
if (validateFiniteNumber(tolerance, "setTolerance") < 0)
|
|
47
51
|
throwGeometryError("INVALID_ARGUMENT", "setTolerance", "Tolerance must be nonnegative millimetres.");
|
|
@@ -143,7 +143,7 @@ export function createLoftOperation(environment, solidFromMesh) {
|
|
|
143
143
|
const geometry = prepareLoftGeometry(input, selected, selected.limits, authoritative, recipeSampled);
|
|
144
144
|
if (geometry.stations.every((station) => station.profile.components.length === 1 &&
|
|
145
145
|
station.profile.components[0].holes.length === 0))
|
|
146
|
-
return solidFromMesh(simpleLoftMesh(geometry, selected.limits
|
|
146
|
+
return solidFromMesh(simpleLoftMesh(geometry, selected.limits));
|
|
147
147
|
const mesh = buildProfileLoftMesh(environment.module, geometry.stations, selected.limits, false, 0, (station, contour, slot) => geometry.world[station][contour][slot]);
|
|
148
148
|
if (measureTriangleMesh(mesh).signedVolume < 0)
|
|
149
149
|
for (let index = 0; index < mesh.indices.length; index += 3) {
|
|
@@ -2,15 +2,12 @@ import * as kernel from "./native/index.js";
|
|
|
2
2
|
/** Internal host-bound mechanical operations. */
|
|
3
3
|
export function createMechanicalOperations(environment) {
|
|
4
4
|
const { resolution } = environment;
|
|
5
|
-
const { scope, assertActive, wrapSolid,
|
|
5
|
+
const { scope, assertActive, wrapSolid, withResolutionDefaults } = environment;
|
|
6
6
|
const nutOptions = (value) => ({
|
|
7
7
|
...withResolutionDefaults(value),
|
|
8
8
|
bodyResolution: { ...resolution, ...value.bodyResolution },
|
|
9
9
|
});
|
|
10
|
-
|
|
11
|
-
const vertices = (preflight.estimatedRows + 2) * preflight.circularSegments;
|
|
12
|
-
assertMeshAllocation(vertices, preflight.estimatedTriangles, (vertices + preflight.estimatedTriangles) * 12, operation);
|
|
13
|
-
};
|
|
10
|
+
// Explicit preflight preserves the stable error operation names exposed by these constructors.
|
|
14
11
|
const operations = {
|
|
15
12
|
threadRidge: (value) => {
|
|
16
13
|
assertActive();
|
|
@@ -19,19 +16,19 @@ export function createMechanicalOperations(environment) {
|
|
|
19
16
|
threadedRod: (value) => {
|
|
20
17
|
assertActive();
|
|
21
18
|
const resolved = withResolutionDefaults(value);
|
|
22
|
-
|
|
19
|
+
kernel.preflightThreadedRod(resolved);
|
|
23
20
|
return wrapSolid(kernel.threadedRod(scope, resolved));
|
|
24
21
|
},
|
|
25
22
|
internalThreadCutter: (value) => {
|
|
26
23
|
assertActive();
|
|
27
24
|
const resolved = withResolutionDefaults(value);
|
|
28
|
-
|
|
25
|
+
kernel.preflightInternalThreadCutter(resolved);
|
|
29
26
|
return wrapSolid(kernel.internalThreadCutter(scope, resolved));
|
|
30
27
|
},
|
|
31
28
|
threadedNut: (value) => {
|
|
32
29
|
assertActive();
|
|
33
30
|
const resolved = nutOptions(value);
|
|
34
|
-
|
|
31
|
+
kernel.preflightThreadedNut(resolved);
|
|
35
32
|
return wrapSolid(kernel.threadedNut(scope, resolved));
|
|
36
33
|
},
|
|
37
34
|
preflightThreadedRod: (value) => {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** @internal Operation-local facts for identical privately owned mesh buffer ranges. */
|
|
2
|
+
export class MeshViewCache {
|
|
3
|
+
positions = new Map();
|
|
4
|
+
key(mesh) {
|
|
5
|
+
return `${mesh.positions.byteOffset}:${mesh.positions.length}:${mesh.indices.byteOffset}:${mesh.indices.length}`;
|
|
6
|
+
}
|
|
7
|
+
get(mesh) {
|
|
8
|
+
return this.positions.get(mesh.positions.buffer)?.get(mesh.indices.buffer)?.get(this.key(mesh));
|
|
9
|
+
}
|
|
10
|
+
set(mesh, value) {
|
|
11
|
+
let indices = this.positions.get(mesh.positions.buffer);
|
|
12
|
+
if (indices === undefined) {
|
|
13
|
+
indices = new Map();
|
|
14
|
+
this.positions.set(mesh.positions.buffer, indices);
|
|
15
|
+
}
|
|
16
|
+
let ranges = indices.get(mesh.indices.buffer);
|
|
17
|
+
if (ranges === undefined) {
|
|
18
|
+
ranges = new Map();
|
|
19
|
+
indices.set(mesh.indices.buffer, ranges);
|
|
20
|
+
}
|
|
21
|
+
ranges.set(this.key(mesh), value);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { validateTriangleMesh } from "../../geometry/mesh.js";
|
|
2
|
+
import { assertNativeSolidStatus } from "./status.js";
|
|
2
3
|
/**
|
|
3
4
|
* Constructs a live Manifold solid from a copied indexed triangle mesh.
|
|
4
5
|
*
|
|
@@ -12,9 +13,6 @@ export const triangleMeshToManifold = (module, mesh, scope) => {
|
|
|
12
13
|
triVerts: mesh.indices,
|
|
13
14
|
});
|
|
14
15
|
const manifold = scope.own(new module.Manifold(manifoldMesh));
|
|
15
|
-
|
|
16
|
-
if (status !== "NoError") {
|
|
17
|
-
throw new Error(`Manifold rejected generated mesh: ${status}.`);
|
|
18
|
-
}
|
|
16
|
+
assertNativeSolidStatus(manifold, "solidFromMesh");
|
|
19
17
|
return manifold;
|
|
20
18
|
};
|