@metamesh/modeling 0.0.0-bootstrap.0 → 0.6.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 +105 -0
- package/README.md +313 -6
- package/THIRD_PARTY_NOTICES.md +153 -0
- package/dist/api/engine.d.ts +54 -0
- package/dist/api/geometry.d.ts +2912 -0
- package/dist/api/index.d.ts +1648 -0
- package/dist/api/text.d.ts +77 -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 +72 -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 +265 -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 +107 -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 +80 -0
- package/dist/geometry/math/vector3.js +41 -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 +368 -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-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 +300 -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 +30 -0
- package/dist/geometry/paths/planar.js +135 -0
- package/dist/geometry/paths/resampling.js +124 -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 +184 -0
- package/dist/geometry/profiles/open-profile.js +137 -0
- package/dist/geometry/profiles/profiles.js +70 -0
- package/dist/geometry/profiles/rectangle.js +235 -0
- package/dist/geometry/profiles/shapes.js +34 -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 +123 -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 +20 -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 +152 -0
- package/dist/model/options.js +1 -0
- package/dist/model/types.js +1 -0
- package/dist/text/index.js +48 -0
- package/dist/text/vector-text-layout.js +279 -0
- package/package.json +89 -8
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/** Limits apply before parsing or shaping untrusted font data. */
|
|
2
|
+
export declare const MAX_VECTOR_FONT_BYTES: number;
|
|
3
|
+
|
|
4
|
+
/** Parses bounded font bytes using optional fontkit and WOFF2 peers; no font fetching occurs here. */
|
|
5
|
+
export declare function prepareVectorFont(bytes: Uint8Array, options?: VectorFontOptions): Promise<VectorFont>;
|
|
6
|
+
|
|
7
|
+
/** A prepared font owns its parsed bytes; keep handles local to their Worker or invocation. */
|
|
8
|
+
export declare interface VectorFont {
|
|
9
|
+
/** Shape and position up to 512 UTF-16 units and 16 lines; supports empty or whitespace text. */
|
|
10
|
+
readonly layout: (text: string, options: VectorTextLayoutOptions) => VectorTextLayout;
|
|
11
|
+
/** Measure the same cached vector layout; all returned lengths are in millimetres. */
|
|
12
|
+
readonly measure: (text: string, options: VectorTextLayoutOptions) => VectorTextMetrics;
|
|
13
|
+
/** Serialize visible multiline outlines as bounded path-only SVG in physical millimetres. */
|
|
14
|
+
readonly toSvg: (text: string, options: VectorTextLayoutOptions) => string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Identifies the actual weight of a static face, without synthesizing a different weight. */
|
|
18
|
+
export declare interface VectorFontOptions {
|
|
19
|
+
/** Actual weight of a static face, 1–1000; defaults to 400. */
|
|
20
|
+
readonly staticWeight?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Exact curve bounds in millimetres; positive Y points upward. */
|
|
24
|
+
export declare interface VectorTextBounds {
|
|
25
|
+
/** Minimum X and Y of visible outlines, including accents, descenders, and overhangs. */
|
|
26
|
+
readonly min: readonly [number, number];
|
|
27
|
+
/** Maximum X and Y of visible outlines. */
|
|
28
|
+
readonly max: readonly [number, number];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Immutable vector layout with no browser, font parser, or kernel ownership. */
|
|
32
|
+
export declare interface VectorTextLayout {
|
|
33
|
+
/** Nonzero-filled path data in millimetres, positive Y upward; empty for text with no ink. */
|
|
34
|
+
readonly paths: readonly string[];
|
|
35
|
+
/** Exact measurements of these paths and their logical text layout. */
|
|
36
|
+
readonly metrics: VectorTextMetrics;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Horizontal multiline text, bounded to 512 UTF-16 units and 16 lines without wrapping. */
|
|
40
|
+
export declare interface VectorTextLayoutOptions extends VectorTextOptions {
|
|
41
|
+
/** Place each line's advance at the origin; defaults to left. Ink overhang is preserved. */
|
|
42
|
+
readonly align?: "left" | "center" | "right";
|
|
43
|
+
/** Baseline spacing in ems, 0.5–5; omitted uses the selected face's natural vertical metrics. */
|
|
44
|
+
readonly lineHeight?: number;
|
|
45
|
+
/** Place the first baseline or the complete ink bounds at Y=0; defaults to baseline. */
|
|
46
|
+
readonly verticalAlign?: "baseline" | "top" | "center" | "bottom";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Text measurements from the same immutable layout that supplies geometry and SVG outlines. */
|
|
50
|
+
export declare interface VectorTextMetrics {
|
|
51
|
+
/** Visible curve bounds after alignment, or null when the text has no ink. */
|
|
52
|
+
readonly bounds: VectorTextBounds | null;
|
|
53
|
+
/** Unaligned pen displacement: final line advance and negative baseline displacement, in mm. */
|
|
54
|
+
readonly advance: readonly [number, number];
|
|
55
|
+
/** Maximum line advance in millimetres, including spaces; distinct from visible ink width. */
|
|
56
|
+
readonly logicalWidth: number;
|
|
57
|
+
/** Selected font's ascender distance above a baseline in millimetres, independent of the text. */
|
|
58
|
+
readonly ascent: number;
|
|
59
|
+
/** Selected font's descender distance below a baseline in millimetres, a non-negative value. */
|
|
60
|
+
readonly descent: number;
|
|
61
|
+
/** Actual distance between baselines in millimetres. */
|
|
62
|
+
readonly lineHeight: number;
|
|
63
|
+
/** Number of lines, including empty and trailing lines; empty text occupies one line. */
|
|
64
|
+
readonly lineCount: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Shaping controls for a prepared exact font face; all lengths are in millimetres. */
|
|
68
|
+
export declare interface VectorTextOptions {
|
|
69
|
+
/** Em size in millimetres, greater than zero and at most 10000; not the visible glyph height. */
|
|
70
|
+
readonly fontSize: number;
|
|
71
|
+
/** Actual static or variable weight, 1–1000; defaults to the prepared face's static weight. */
|
|
72
|
+
readonly fontWeight?: number;
|
|
73
|
+
/** Tracking between glyph clusters in millimetres, -100–100; defaults to zero. */
|
|
74
|
+
readonly letterSpacing?: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export { }
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { GeometryError } from "../geometry/errors.js";
|
|
2
|
+
import { resolveExtrusionContourDivisions, resolveExtrusionControls, } from "../geometry/extrusion.js";
|
|
3
|
+
import { heightfieldMesh, preflightHeightfield } from "../geometry/heightfield.js";
|
|
4
|
+
import { assertMeshAllocationWithinLimits } from "../geometry/model-limits.js";
|
|
5
|
+
import { assertPrimitiveOptions } from "../geometry/primitive-resolvers.js";
|
|
6
|
+
import { matchProfiles } from "../geometry/profiles/matched.js";
|
|
7
|
+
import { resolveGeometryResolution } from "../geometry/resolution.js";
|
|
8
|
+
import { assertSamplingCount } from "../geometry/sampling-limits.js";
|
|
9
|
+
import { buildSweepMesh } from "../geometry/sweeps/sweep-mesh.js";
|
|
10
|
+
import { buildFinishedExtrusion } from "./extrusion-finishing.js";
|
|
11
|
+
import { createLoftOperation } from "./loft-operations.js";
|
|
12
|
+
import { triangleMeshToManifold } from "./native/adapter.js";
|
|
13
|
+
import { ownScopedManifoldValue, scopedManifoldScope, trackScopedManifoldValue, } from "./native/context.js";
|
|
14
|
+
import * as kernel from "./native/index.js";
|
|
15
|
+
import { buildProfileSweepMesh } from "./profile-mesh.js";
|
|
16
|
+
/** Internal host-bound construction operations. */
|
|
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;
|
|
19
|
+
const fromContours = sections.sectionFromContours;
|
|
20
|
+
const corresponding = (value) => typeof value === "object" && value !== null && "components" in value;
|
|
21
|
+
const shapeSection = (shape) => corresponding(shape)
|
|
22
|
+
? nativeSection(fromContours(shape.components.flatMap((component) => [component.outer, ...component.holes].map((contour) => contour.points))))
|
|
23
|
+
: nativeSection(shape);
|
|
24
|
+
const sectionProfile = environment.sectionProfileFor;
|
|
25
|
+
const extrude = (handle, value) => {
|
|
26
|
+
assertPrimitiveOptions(value, "extrude");
|
|
27
|
+
const native = shapeSection(handle), resolved = withResolutionDefaults(value);
|
|
28
|
+
if (value.ends !== undefined) {
|
|
29
|
+
const profile = corresponding(handle) ? handle : environment.sectionProfileFor(handle);
|
|
30
|
+
return solidFromMesh(buildFinishedExtrusion(module, profile, resolved));
|
|
31
|
+
}
|
|
32
|
+
assertSamplingCount(native.numVert(), "extrude", resolved.limits.maxProfilePoints);
|
|
33
|
+
const controls = resolveExtrusionControls(value.height, resolved, "extrude");
|
|
34
|
+
let divisions = value.divisions;
|
|
35
|
+
if (divisions === undefined) {
|
|
36
|
+
const policy = resolveGeometryResolution(resolved.resolution, resolved.limits);
|
|
37
|
+
// Untwisted uniform taper has planar sides. Its default two-cap topology
|
|
38
|
+
// needs no native contour extraction or longitudinal interpolation.
|
|
39
|
+
if (controls.twistDegrees === 0 &&
|
|
40
|
+
controls.scales[0] === controls.scales[1] &&
|
|
41
|
+
policy.maxSegmentLength === undefined)
|
|
42
|
+
divisions = 0;
|
|
43
|
+
else {
|
|
44
|
+
const profile = corresponding(handle) ? handle : sectionProfile(handle);
|
|
45
|
+
divisions = resolveExtrusionContourDivisions(profile.components.flatMap((component) => [component.outer, ...component.holes].map((contour) => contour.points)), value.height, resolved);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const vertices = native.numVert() * (divisions + 2),
|
|
49
|
+
// A connected extrusion has T = 2V + 4H - 4. Treating every extra contour as
|
|
50
|
+
// a hole also bounds disconnected Sections, without extracting native point data.
|
|
51
|
+
triangles = Math.max(0, vertices * 2 + 4 * native.numContour() - 8);
|
|
52
|
+
assertMeshAllocationWithinLimits(vertices, triangles, vertices * 12 + triangles * 12, "extrude", resolved.limits);
|
|
53
|
+
assertMeshAllocation(vertices, triangles, vertices * 12 + triangles * 12, "extrude");
|
|
54
|
+
return ownSolid(native.extrude(value.height, divisions, controls.twistDegrees, controls.scales, controls.center));
|
|
55
|
+
};
|
|
56
|
+
const boolean = (values, operation) => {
|
|
57
|
+
assertOperandCount(values, operation);
|
|
58
|
+
const natives = values.map(resolveHandle), kind = natives[0].kind;
|
|
59
|
+
if (natives.some((native) => native.kind !== kind))
|
|
60
|
+
throwGeometryError("INVALID_ARGUMENT", operation, "Boolean operands must have the same dimensionality.");
|
|
61
|
+
if (kind === "solid") {
|
|
62
|
+
const solids = values.map((value) => nativeSolid(value));
|
|
63
|
+
return wrapSolid(operation === "union" ? kernel.union(scope, solids) : kernel.intersect(scope, solids));
|
|
64
|
+
}
|
|
65
|
+
const sections = values.map((value) => nativeSection(value));
|
|
66
|
+
return ownSection(operation === "union"
|
|
67
|
+
? module.CrossSection.union(sections)
|
|
68
|
+
: module.CrossSection.intersection(sections));
|
|
69
|
+
};
|
|
70
|
+
const subtract = ((value, cutters) => {
|
|
71
|
+
assertOperandCount(cutters, "subtract", 0);
|
|
72
|
+
const native = resolveHandle(value), natives = cutters.map(resolveHandle);
|
|
73
|
+
if (natives.some((cutter) => cutter.kind !== native.kind))
|
|
74
|
+
throwGeometryError("INVALID_ARGUMENT", "subtract", "Boolean operands must have the same dimensionality.");
|
|
75
|
+
if (cutters.length === 0)
|
|
76
|
+
return value;
|
|
77
|
+
return native.kind === "solid"
|
|
78
|
+
? wrapSolid(kernel.subtract(scope, native.value, cutters.map((cutter) => nativeSolid(cutter))))
|
|
79
|
+
: ownSection(module.CrossSection.difference([
|
|
80
|
+
native.value,
|
|
81
|
+
...cutters.map((cutter) => nativeSection(cutter)),
|
|
82
|
+
]));
|
|
83
|
+
});
|
|
84
|
+
const solidFromMesh = (mesh) => {
|
|
85
|
+
assertActive();
|
|
86
|
+
assertMeshAllocation(mesh.positions.length / 3, mesh.indices.length / 3, mesh.positions.byteLength + mesh.indices.byteLength, "solidFromMesh");
|
|
87
|
+
return wrapSolid(trackScopedManifoldValue(scope, triangleMeshToManifold(module, mesh, scopedManifoldScope(scope))));
|
|
88
|
+
};
|
|
89
|
+
const operations = {
|
|
90
|
+
union: ((values) => boolean(values, "union")),
|
|
91
|
+
subtract,
|
|
92
|
+
intersect: ((values) => boolean(values, "intersect")),
|
|
93
|
+
compose: (values) => wrapSolid(kernel.compose(scope, values.map(nativeSolid))),
|
|
94
|
+
hull: (value) => {
|
|
95
|
+
assertActive();
|
|
96
|
+
return wrapSolid(kernel.hull(scope, value.solids === undefined
|
|
97
|
+
? { points: value.points }
|
|
98
|
+
: { solids: value.solids.map(nativeSolid) }));
|
|
99
|
+
},
|
|
100
|
+
minkowskiSum: (a, b) => wrapSolid(kernel.minkowskiSum(scope, nativeSolid(a), nativeSolid(b))),
|
|
101
|
+
solidFromMesh,
|
|
102
|
+
extrude,
|
|
103
|
+
revolve: (handle, value = {}) => {
|
|
104
|
+
assertPrimitiveOptions(value, "revolve");
|
|
105
|
+
const native = shapeSection(handle), bounds = native.bounds();
|
|
106
|
+
const degrees = value.revolveDegrees ?? 360;
|
|
107
|
+
if (requirePositiveNumber(degrees, "revolve") > 360)
|
|
108
|
+
throwGeometryError("INVALID_ARGUMENT", "revolve", "Sweep must not exceed 360 degrees.");
|
|
109
|
+
const selected = withResolutionDefaults(value);
|
|
110
|
+
assertSamplingCount(native.numVert(), "revolve", selected.limits.maxProfilePoints);
|
|
111
|
+
const count = resolveCircularSegments(Math.max(Math.abs(bounds.min[0]), Math.abs(bounds.max[0])), value);
|
|
112
|
+
// The public exact/resolved count describes a complete circle, while the native
|
|
113
|
+
// kernel's count describes this angular sweep, including a partial revolution.
|
|
114
|
+
const sampledIntervals = degrees === 360 ? count : Math.max(1, Math.ceil((count * degrees) / 360));
|
|
115
|
+
// Counts below three select Manifold's ambient automatic tessellation instead of an
|
|
116
|
+
// exact interval count (and can yield an empty tiny-angle result). Adaptive policies
|
|
117
|
+
// may refine to three; an explicit full-circle count must retain its exact meaning.
|
|
118
|
+
if (sampledIntervals < 3 &&
|
|
119
|
+
(value.segments !== undefined || selected.resolution.circularSegments !== undefined))
|
|
120
|
+
throw new GeometryError("INVALID_ARGUMENT", `Exact partial revolution resolves ${sampledIntervals} angular intervals; native filled revolutions require at least three. Use at least ${Math.floor(720 / degrees) + 1} full-circle segments.`, {
|
|
121
|
+
operation: "revolve",
|
|
122
|
+
details: {
|
|
123
|
+
requiredCount: 3,
|
|
124
|
+
allowedCount: sampledIntervals,
|
|
125
|
+
requiredCircularSegments: Math.floor(720 / degrees) + 1,
|
|
126
|
+
circularSegments: count,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
const sweepSegments = Math.max(3, sampledIntervals);
|
|
130
|
+
assertSamplingCount(sweepSegments, "revolve", selected.limits.maxCircularSegments);
|
|
131
|
+
const rings = degrees === 360 ? sweepSegments : sweepSegments + 1;
|
|
132
|
+
const vertices = native.numVert() * rings, triangles = Math.max(0, vertices * 2 + (degrees === 360 ? 0 : 4 * native.numContour() - 8));
|
|
133
|
+
assertMeshAllocationWithinLimits(vertices, triangles, vertices * 12 + triangles * 12, "revolve", selected.limits);
|
|
134
|
+
return ownSolid(native.revolve(sweepSegments, degrees));
|
|
135
|
+
},
|
|
136
|
+
loft: createLoftOperation(environment, solidFromMesh),
|
|
137
|
+
matchProfiles: (profiles, value = {}) => {
|
|
138
|
+
assertActive();
|
|
139
|
+
return matchProfiles(profiles, withResolutionDefaults(value));
|
|
140
|
+
},
|
|
141
|
+
sweep: (shape, path, value = {}) => {
|
|
142
|
+
assertActive();
|
|
143
|
+
const selected = withResolutionDefaults(value);
|
|
144
|
+
if (corresponding(shape))
|
|
145
|
+
return solidFromMesh(buildProfileSweepMesh(module, shape, path, selected, selected.limits));
|
|
146
|
+
const profile = sectionProfile(shape);
|
|
147
|
+
const singleComponent = profile.components.length === 1 ? profile.components[0] : undefined;
|
|
148
|
+
const contours = singleComponent?.holes.length === 0 ? [singleComponent.outer.points] : [];
|
|
149
|
+
if (contours.length === 1)
|
|
150
|
+
return solidFromMesh(buildSweepMesh(contours[0], path, selected, assertMeshAllocation));
|
|
151
|
+
return solidFromMesh(buildProfileSweepMesh(module, profile, path, selected, selected.limits));
|
|
152
|
+
},
|
|
153
|
+
heightfield: (value) => {
|
|
154
|
+
assertActive();
|
|
155
|
+
const facts = preflightHeightfield(value, limits);
|
|
156
|
+
assertMeshAllocation(facts.vertexCount, facts.triangleCount, facts.byteCount, "heightfield");
|
|
157
|
+
return solidFromMesh(heightfieldMesh(value, limits));
|
|
158
|
+
},
|
|
159
|
+
implicit: (value) => {
|
|
160
|
+
assertActive();
|
|
161
|
+
validateCoordinates(value.bounds.min, 3, "implicit");
|
|
162
|
+
validateCoordinates(value.bounds.max, 3, "implicit");
|
|
163
|
+
requirePositiveNumber(value.edgeLength, "implicit");
|
|
164
|
+
const dimensions = value.bounds.max.map((max, axis) => requirePositiveNumber(max - value.bounds.min[axis], "implicit"));
|
|
165
|
+
// Native levelSet allocates two BCC lattices using power-of-two Y/Z strides.
|
|
166
|
+
// Bound that definite sampling work separately from the unknown surface crossings.
|
|
167
|
+
const [gx, gy, gz] = dimensions.map((extent) => Math.floor(extent / value.edgeLength + 1));
|
|
168
|
+
const sy = 2 ** Math.ceil(Math.log2(gy + 3));
|
|
169
|
+
const sz = 2 ** Math.ceil(Math.log2(gz + 3));
|
|
170
|
+
const slots = 1 + 2 * (gz + 2) + 2 * (gy + 2) * sz + 2 * (gx + 2) * sy * sz;
|
|
171
|
+
if (!Number.isSafeInteger(slots) || slots > 4_000_000)
|
|
172
|
+
throwGeometryError("COMPLEXITY_LIMIT", "implicit", "Implicit sampling exceeds 4,000,000 grid slots.");
|
|
173
|
+
let calls = 0;
|
|
174
|
+
const field = (point) => {
|
|
175
|
+
if (++calls > 8_000_000)
|
|
176
|
+
throwGeometryError("COMPLEXITY_LIMIT", "implicit", "Implicit field evaluation budget exceeded.");
|
|
177
|
+
return validateFiniteNumber(value.field(point), "implicit");
|
|
178
|
+
};
|
|
179
|
+
if (value.level !== undefined)
|
|
180
|
+
validateFiniteNumber(value.level, "implicit");
|
|
181
|
+
if (value.tolerance !== undefined)
|
|
182
|
+
requirePositiveNumber(value.tolerance, "implicit");
|
|
183
|
+
const native = ownScopedManifoldValue(scope, module.Manifold.levelSet(field, { min: [...value.bounds.min], max: [...value.bounds.max] }, value.edgeLength, value.level, value.tolerance));
|
|
184
|
+
assertMeshAllocation(native.numVert(), native.numTri(), native.numVert() * 12 + native.numTri() * 12, "implicit");
|
|
185
|
+
return wrapSolid(native);
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
return operations;
|
|
189
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { GeometryError } from "../geometry/errors.js";
|
|
2
|
+
import { analyzeTriangleMesh, } from "../geometry/mesh.js";
|
|
3
|
+
import { assertMeshAllocationWithinLimits } from "../geometry/model-limits.js";
|
|
4
|
+
import {} from "../geometry/resolution.js";
|
|
5
|
+
import { assertSamplingCount } from "../geometry/sampling-limits.js";
|
|
6
|
+
import { createModelingContext } from "./modeling-context.js";
|
|
7
|
+
import { ownScopedManifoldValue } from "./native/context.js";
|
|
8
|
+
import { copiedFloat32Array, copiedUint32Array } from "./native/copied-views.js";
|
|
9
|
+
import * as kernel from "./native/index.js";
|
|
10
|
+
import { createShapeAnchorOwner } from "./shape-anchors.js";
|
|
11
|
+
/** Internal execution owner; no native value or constructor crosses the public facade. */
|
|
12
|
+
export function createExecutionContext(module, scope, limits, initialResolution = {}) {
|
|
13
|
+
const handles = new WeakMap();
|
|
14
|
+
// Native solids are immutable. Keep canonical extraction private until finalization; public
|
|
15
|
+
// mesh inspection receives independent copies, and this run-owned cache is cleared on exit.
|
|
16
|
+
const canonicalSolidMeshes = new Map();
|
|
17
|
+
const canonicalSectionProfiles = new Map();
|
|
18
|
+
const primitiveSectionProfiles = new Map();
|
|
19
|
+
let active = true;
|
|
20
|
+
const throwGeometryError = (code, operation, message) => {
|
|
21
|
+
throw new GeometryError(code, message, { operation });
|
|
22
|
+
};
|
|
23
|
+
const assertActive = () => {
|
|
24
|
+
if (!active)
|
|
25
|
+
throwGeometryError("INVALID_ARGUMENT", "model", "The modeling context has expired.");
|
|
26
|
+
};
|
|
27
|
+
const resolveHandle = (handle) => {
|
|
28
|
+
assertActive();
|
|
29
|
+
const native = typeof handle === "object" && handle !== null ? handles.get(handle) : undefined;
|
|
30
|
+
return (native ??
|
|
31
|
+
throwGeometryError("INVALID_ARGUMENT", "model", "Geometry must belong to this exact active build."));
|
|
32
|
+
};
|
|
33
|
+
const nativeSolid = (handle) => {
|
|
34
|
+
const native = resolveHandle(handle);
|
|
35
|
+
return native.kind === "solid"
|
|
36
|
+
? native.value
|
|
37
|
+
: throwGeometryError("INVALID_ARGUMENT", "model", "Expected a solid.");
|
|
38
|
+
};
|
|
39
|
+
const nativeSection = (handle) => {
|
|
40
|
+
const native = resolveHandle(handle);
|
|
41
|
+
return native.kind === "section"
|
|
42
|
+
? native.value
|
|
43
|
+
: throwGeometryError("INVALID_ARGUMENT", "model", "Expected a filled section.");
|
|
44
|
+
};
|
|
45
|
+
const wrapSolid = (value) => {
|
|
46
|
+
assertActive();
|
|
47
|
+
const handle = Object.freeze({});
|
|
48
|
+
handles.set(handle, { kind: "solid", value });
|
|
49
|
+
return handle;
|
|
50
|
+
};
|
|
51
|
+
const wrapSection = (value) => {
|
|
52
|
+
assertActive();
|
|
53
|
+
assertSamplingCount(value.numVert(), "section", limits.maxProfilePoints);
|
|
54
|
+
const handle = Object.freeze({});
|
|
55
|
+
handles.set(handle, { kind: "section", value });
|
|
56
|
+
return handle;
|
|
57
|
+
};
|
|
58
|
+
const ownSolid = (value) => wrapSolid(ownScopedManifoldValue(scope, value));
|
|
59
|
+
const ownSection = (value) => {
|
|
60
|
+
const owned = ownScopedManifoldValue(scope, value);
|
|
61
|
+
return wrapSection(owned);
|
|
62
|
+
};
|
|
63
|
+
const shapeAnchors = createShapeAnchorOwner((shape) => {
|
|
64
|
+
resolveHandle(shape);
|
|
65
|
+
}, (shape) => {
|
|
66
|
+
const native = resolveHandle(shape);
|
|
67
|
+
const alias = native.kind === "solid" ? wrapSolid(native.value) : wrapSection(native.value);
|
|
68
|
+
if (native.kind === "section") {
|
|
69
|
+
const profile = primitiveSectionProfiles.get(shape);
|
|
70
|
+
if (profile !== undefined)
|
|
71
|
+
primitiveSectionProfiles.set(alias, profile);
|
|
72
|
+
}
|
|
73
|
+
return alias;
|
|
74
|
+
});
|
|
75
|
+
const validateFiniteNumber = (value, operation) => Number.isFinite(value)
|
|
76
|
+
? value
|
|
77
|
+
: throwGeometryError("NON_FINITE_VALUE", operation, "Values must be finite.");
|
|
78
|
+
const requirePositiveNumber = (value, operation) => validateFiniteNumber(value, operation) > 0
|
|
79
|
+
? value
|
|
80
|
+
: throwGeometryError("INVALID_ARGUMENT", operation, "Dimensions must be positive.");
|
|
81
|
+
const validateCoordinates = (value, length, operation) => {
|
|
82
|
+
if (!Array.isArray(value) || value.length !== length)
|
|
83
|
+
throwGeometryError("INVALID_ARGUMENT", operation, `Expected ${length} finite coordinates.`);
|
|
84
|
+
if (!Array.from(value).every(Number.isFinite))
|
|
85
|
+
throwGeometryError("NON_FINITE_VALUE", operation, "Coordinates must be finite.");
|
|
86
|
+
};
|
|
87
|
+
const assertOperandCount = (values, operation, minimum = 1) => {
|
|
88
|
+
if (!Array.isArray(values) || values.length < minimum)
|
|
89
|
+
throwGeometryError("INVALID_ARGUMENT", operation, `Expected at least ${minimum} operands.`);
|
|
90
|
+
if (values.length > 1_000)
|
|
91
|
+
throwGeometryError("COMPLEXITY_LIMIT", operation, "Operations accept at most 1000 operands.");
|
|
92
|
+
};
|
|
93
|
+
const assertMeshAllocation = (vertices, triangles, bytes, operation) => {
|
|
94
|
+
assertMeshAllocationWithinLimits(vertices, triangles, bytes, operation, limits);
|
|
95
|
+
};
|
|
96
|
+
const copyNativeMesh = (native) => {
|
|
97
|
+
const raw = native.getMesh();
|
|
98
|
+
assertMeshAllocation(raw.numVert, raw.numTri, (raw.numVert + raw.numTri) * 12, "copyMesh");
|
|
99
|
+
const positions = copiedFloat32Array(raw.numVert * 3);
|
|
100
|
+
for (let vertex = 0; vertex < raw.numVert; vertex += 1) {
|
|
101
|
+
positions[vertex * 3] = raw.vertProperties[vertex * raw.numProp];
|
|
102
|
+
positions[vertex * 3 + 1] = raw.vertProperties[vertex * raw.numProp + 1];
|
|
103
|
+
positions[vertex * 3 + 2] = raw.vertProperties[vertex * raw.numProp + 2];
|
|
104
|
+
}
|
|
105
|
+
return { raw, mesh: { positions, indices: copiedUint32Array(raw.triVerts) } };
|
|
106
|
+
};
|
|
107
|
+
const analyzedMeshFor = (handle) => {
|
|
108
|
+
const native = nativeSolid(handle);
|
|
109
|
+
const cached = canonicalSolidMeshes.get(native);
|
|
110
|
+
if (cached !== undefined)
|
|
111
|
+
return cached;
|
|
112
|
+
const remember = (mesh) => {
|
|
113
|
+
const result = Object.freeze({
|
|
114
|
+
mesh: Object.freeze(mesh),
|
|
115
|
+
measurements: analyzeTriangleMesh(mesh, {
|
|
116
|
+
requireClosed: true,
|
|
117
|
+
includeTopology: true,
|
|
118
|
+
}),
|
|
119
|
+
});
|
|
120
|
+
canonicalSolidMeshes.set(native, result);
|
|
121
|
+
return result;
|
|
122
|
+
};
|
|
123
|
+
const first = copyNativeMesh(native);
|
|
124
|
+
try {
|
|
125
|
+
return remember(first.mesh);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
// Preserve the kernel's merge metadata for its one bounded collapsed-stitch retry.
|
|
129
|
+
if (!(error instanceof GeometryError))
|
|
130
|
+
throw error;
|
|
131
|
+
const reconstructionMesh = new module.Mesh({
|
|
132
|
+
numProp: 3,
|
|
133
|
+
vertProperties: first.mesh.positions,
|
|
134
|
+
triVerts: first.mesh.indices,
|
|
135
|
+
mergeFromVert: new Uint32Array(first.raw.mergeFromVert),
|
|
136
|
+
mergeToVert: new Uint32Array(first.raw.mergeToVert),
|
|
137
|
+
tolerance: first.raw.tolerance,
|
|
138
|
+
});
|
|
139
|
+
const reconstructed = ownScopedManifoldValue(scope, new module.Manifold(reconstructionMesh));
|
|
140
|
+
if (reconstructed.status() !== "NoError")
|
|
141
|
+
throw error;
|
|
142
|
+
const second = copyNativeMesh(reconstructed);
|
|
143
|
+
return remember(second.mesh);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const sectionProfileFor = (handle) => {
|
|
147
|
+
const native = nativeSection(handle);
|
|
148
|
+
const cached = canonicalSectionProfiles.get(native);
|
|
149
|
+
if (cached !== undefined)
|
|
150
|
+
return cached;
|
|
151
|
+
const components = kernel
|
|
152
|
+
.crossSectionToRegion(scope, native, limits)
|
|
153
|
+
.map((component, index) => {
|
|
154
|
+
const contour = (id, points) => Object.freeze({
|
|
155
|
+
id,
|
|
156
|
+
points,
|
|
157
|
+
spans: Object.freeze(points.map((_, point) => `point-${point}`)),
|
|
158
|
+
});
|
|
159
|
+
return Object.freeze({
|
|
160
|
+
id: `component-${index}`,
|
|
161
|
+
outer: contour("outer", component.outer),
|
|
162
|
+
holes: Object.freeze(component.holes.map((points, hole) => contour(`hole-${hole}`, points))),
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
const profile = Object.freeze({ components: Object.freeze(components) });
|
|
166
|
+
canonicalSectionProfiles.set(native, profile);
|
|
167
|
+
return profile;
|
|
168
|
+
};
|
|
169
|
+
const primitiveProfileFor = (handle) => {
|
|
170
|
+
nativeSection(handle);
|
|
171
|
+
return primitiveSectionProfiles.get(handle);
|
|
172
|
+
};
|
|
173
|
+
const rememberPrimitiveProfile = (handle, profile) => {
|
|
174
|
+
nativeSection(handle);
|
|
175
|
+
primitiveSectionProfiles.set(handle, profile);
|
|
176
|
+
return handle;
|
|
177
|
+
};
|
|
178
|
+
const owner = {
|
|
179
|
+
module,
|
|
180
|
+
scope,
|
|
181
|
+
limits,
|
|
182
|
+
assertActive,
|
|
183
|
+
throwGeometryError,
|
|
184
|
+
resolveHandle,
|
|
185
|
+
nativeSolid,
|
|
186
|
+
nativeSection,
|
|
187
|
+
wrapSolid,
|
|
188
|
+
wrapSection,
|
|
189
|
+
ownSolid,
|
|
190
|
+
ownSection,
|
|
191
|
+
validateFiniteNumber,
|
|
192
|
+
requirePositiveNumber,
|
|
193
|
+
validateCoordinates,
|
|
194
|
+
assertOperandCount,
|
|
195
|
+
assertMeshAllocation,
|
|
196
|
+
analyzedMeshFor,
|
|
197
|
+
sectionProfileFor,
|
|
198
|
+
primitiveProfileFor,
|
|
199
|
+
rememberPrimitiveProfile,
|
|
200
|
+
anchor: shapeAnchors.anchor,
|
|
201
|
+
withAnchors: shapeAnchors.withAnchors,
|
|
202
|
+
rememberAnchors: shapeAnchors.remember,
|
|
203
|
+
preserveAnchors: shapeAnchors.preserve,
|
|
204
|
+
};
|
|
205
|
+
return {
|
|
206
|
+
context: createModelingContext(owner, initialResolution),
|
|
207
|
+
analyzedMeshFor,
|
|
208
|
+
isSolid: (value) => typeof value === "object" && value !== null && handles.get(value)?.kind === "solid",
|
|
209
|
+
validateMesh: (mesh) => {
|
|
210
|
+
const input = new module.Mesh({
|
|
211
|
+
numProp: 3,
|
|
212
|
+
vertProperties: mesh.positions,
|
|
213
|
+
triVerts: mesh.indices,
|
|
214
|
+
});
|
|
215
|
+
const native = ownScopedManifoldValue(scope, new module.Manifold(input));
|
|
216
|
+
if (native.status() !== "NoError")
|
|
217
|
+
throwGeometryError("INVALID_TOPOLOGY", "finalize", "The geometry kernel rejected the finalized manufacturing mesh.");
|
|
218
|
+
},
|
|
219
|
+
dispose: () => {
|
|
220
|
+
active = false;
|
|
221
|
+
canonicalSolidMeshes.clear();
|
|
222
|
+
canonicalSectionProfiles.clear();
|
|
223
|
+
primitiveSectionProfiles.clear();
|
|
224
|
+
shapeAnchors.dispose();
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Zip, ZipDeflate } from "fflate";
|
|
2
|
+
import { GeometryError } from "../geometry/errors.js";
|
|
3
|
+
import { analyzeTriangleMesh } from "../geometry/mesh.js";
|
|
4
|
+
const namespace = "http://schemas.microsoft.com/3dmanufacturing/core/2015/02";
|
|
5
|
+
const declaration = '<?xml version="1.0" encoding="UTF-8"?>';
|
|
6
|
+
const encoder = new TextEncoder();
|
|
7
|
+
const escapeXml = (value) => value.replace(/[&<>"']/g, (character) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[character]);
|
|
8
|
+
/** Private core 3MF serializer. It preserves the selected buffers and streams bounded XML chunks. */
|
|
9
|
+
export function meshesTo3mf(parts, options) {
|
|
10
|
+
const name = options.name ?? "MetaMesh model";
|
|
11
|
+
if (typeof name !== "string" ||
|
|
12
|
+
name.length === 0 ||
|
|
13
|
+
name.length > 1024 ||
|
|
14
|
+
// eslint-disable-next-line no-control-regex -- XML 1.0 disallows these control characters.
|
|
15
|
+
/[\u0000-\u0008\u000b\u000c\u000e-\u001f\ud800-\udfff\ufffe\uffff]/u.test(name))
|
|
16
|
+
throw new GeometryError("INVALID_ARGUMENT", "3MF names require one through 1024 valid XML characters.", { operation: "export3mf" });
|
|
17
|
+
for (const part of parts) {
|
|
18
|
+
const facts = analyzeTriangleMesh(part, { requireClosed: true, includeTopology: true });
|
|
19
|
+
if (!(facts.signedVolume > 0) || facts.vertexCount >= 2 ** 31 || facts.triangleCount >= 2 ** 31)
|
|
20
|
+
throw new GeometryError("INVALID_TOPOLOGY", "3MF parts require positive oriented volume and valid core resource counts.", { operation: "export3mf" });
|
|
21
|
+
}
|
|
22
|
+
const chunks = [];
|
|
23
|
+
let bytes = 0, complete = false;
|
|
24
|
+
const zip = new Zip((error, data, final) => {
|
|
25
|
+
if (error !== null)
|
|
26
|
+
throw error;
|
|
27
|
+
bytes += data.byteLength;
|
|
28
|
+
if (bytes >= 0xffffffff)
|
|
29
|
+
throw new GeometryError("COMPLEXITY_LIMIT", "3MF ZIP output exceeds its 32-bit container limit.", { operation: "export3mf" });
|
|
30
|
+
chunks.push(data);
|
|
31
|
+
complete = final;
|
|
32
|
+
});
|
|
33
|
+
const add = (path) => {
|
|
34
|
+
const file = new ZipDeflate(path, { level: 6 });
|
|
35
|
+
file.mtime = new Date(1980, 0, 1);
|
|
36
|
+
zip.add(file);
|
|
37
|
+
return file;
|
|
38
|
+
};
|
|
39
|
+
const addText = (path, contents) => add(path).push(encoder.encode(contents), true);
|
|
40
|
+
try {
|
|
41
|
+
addText("[Content_Types].xml", `${declaration}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="model" ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml"/></Types>`);
|
|
42
|
+
addText("_rels/.rels", `${declaration}<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Target="/3D/3dmodel.model" Id="model" Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel"/></Relationships>`);
|
|
43
|
+
const model = add("3D/3dmodel.model");
|
|
44
|
+
let xml = "", xmlBytes = 0;
|
|
45
|
+
const flush = (final = false) => {
|
|
46
|
+
const data = encoder.encode(xml);
|
|
47
|
+
xmlBytes += data.byteLength;
|
|
48
|
+
if (xmlBytes >= 0xffffffff)
|
|
49
|
+
throw new GeometryError("COMPLEXITY_LIMIT", "3MF model XML exceeds its 32-bit ZIP entry limit.", { operation: "export3mf" });
|
|
50
|
+
model.push(data, final);
|
|
51
|
+
xml = "";
|
|
52
|
+
};
|
|
53
|
+
const append = (value) => {
|
|
54
|
+
xml += value;
|
|
55
|
+
if (xml.length >= 32_768)
|
|
56
|
+
flush();
|
|
57
|
+
};
|
|
58
|
+
append(`${declaration}<model unit="millimeter" xml:lang="en-US" xmlns="${namespace}"><metadata name="Application">MetaMesh Modeling</metadata><metadata name="Title">${escapeXml(name)}</metadata><resources>`);
|
|
59
|
+
const colors = new Map();
|
|
60
|
+
for (const part of parts)
|
|
61
|
+
if (part.color !== undefined && !colors.has(part.color.toLowerCase()))
|
|
62
|
+
colors.set(part.color.toLowerCase(), colors.size);
|
|
63
|
+
const materialId = parts.length + 1;
|
|
64
|
+
if (colors.size) {
|
|
65
|
+
append(`<basematerials id="${materialId}">`);
|
|
66
|
+
for (const color of colors.keys())
|
|
67
|
+
append(`<base name="Color ${color.slice(1)}" displaycolor="${color.toUpperCase()}FF"/>`);
|
|
68
|
+
append("</basematerials>");
|
|
69
|
+
}
|
|
70
|
+
parts.forEach((part, index) => {
|
|
71
|
+
const material = part.color === undefined
|
|
72
|
+
? ""
|
|
73
|
+
: ` pid="${materialId}" pindex="${colors.get(part.color.toLowerCase())}"`;
|
|
74
|
+
append(`<object id="${index + 1}" name="${escapeXml(part.id)}" partnumber="${escapeXml(part.id)}" type="model"${material}><mesh><vertices>`);
|
|
75
|
+
for (let offset = 0; offset < part.positions.length; offset += 3)
|
|
76
|
+
append(`<vertex x="${part.positions[offset]}" y="${part.positions[offset + 1]}" z="${part.positions[offset + 2]}"/>`);
|
|
77
|
+
append("</vertices><triangles>");
|
|
78
|
+
for (let offset = 0; offset < part.indices.length; offset += 3)
|
|
79
|
+
append(`<triangle v1="${part.indices[offset]}" v2="${part.indices[offset + 1]}" v3="${part.indices[offset + 2]}"/>`);
|
|
80
|
+
append("</triangles></mesh></object>");
|
|
81
|
+
});
|
|
82
|
+
append("</resources><build>");
|
|
83
|
+
parts.forEach((_, index) => append(`<item objectid="${index + 1}"/>`));
|
|
84
|
+
append("</build></model>");
|
|
85
|
+
flush(true);
|
|
86
|
+
zip.end();
|
|
87
|
+
if (!complete)
|
|
88
|
+
throw new Error("The synchronous 3MF compressor did not complete.");
|
|
89
|
+
const result = new Uint8Array(bytes);
|
|
90
|
+
let offset = 0;
|
|
91
|
+
for (const chunk of chunks) {
|
|
92
|
+
result.set(chunk, offset);
|
|
93
|
+
offset += chunk.byteLength;
|
|
94
|
+
}
|
|
95
|
+
return result.buffer;
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
zip.terminate();
|
|
99
|
+
}
|
|
100
|
+
}
|