@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,279 @@
|
|
|
1
|
+
import { GeometryError } from "../geometry/errors.js";
|
|
2
|
+
import { loftFrame } from "../geometry/loft-options.js";
|
|
3
|
+
import { assertMeshAllocationWithinLimits } from "../geometry/model-limits.js";
|
|
4
|
+
import { writeMeshVertex } from "../geometry/numeric.js";
|
|
5
|
+
import { normalizeRegion2, signedAreaLoop2 } from "../geometry/regions.js";
|
|
6
|
+
import { assertSamplingCount } from "../geometry/sampling-limits.js";
|
|
7
|
+
import { resolveSweepScales } from "../geometry/sweeps/options.js";
|
|
8
|
+
import { computeSweepFrames, validateSweepOptions } from "../geometry/sweeps/orientation.js";
|
|
9
|
+
import { planSweepStations, refineSweepStations } from "../geometry/sweeps/stations.js";
|
|
10
|
+
import { normalizeSweepPath } from "../geometry/sweeps/validation.js";
|
|
11
|
+
import { resolveGeometryTolerance } from "../geometry/tolerance.js";
|
|
12
|
+
import { frameTransform3 } from "../geometry/transform3.js";
|
|
13
|
+
const fail = (message) => {
|
|
14
|
+
throw new GeometryError("INVALID_ARGUMENT", message, { operation: "loft" });
|
|
15
|
+
};
|
|
16
|
+
const isArray = (value) => Array.isArray(value);
|
|
17
|
+
const contourList = (profile) => profile.components.flatMap((component) => [component.outer, ...component.holes]);
|
|
18
|
+
function profileMeshAllocation(first, stationCount, limits, closed) {
|
|
19
|
+
if (!isArray(first.components) || first.components.length === 0 || first.components.length > 1000)
|
|
20
|
+
fail("Matched profiles require one through 1000 identified components.");
|
|
21
|
+
const validIdentity = (id) => typeof id === "string" && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(id) && id.length <= 64;
|
|
22
|
+
const componentIds = new Set();
|
|
23
|
+
for (const component of first.components) {
|
|
24
|
+
if (!validIdentity(component.id) || componentIds.has(component.id) || !isArray(component.holes))
|
|
25
|
+
fail("Matched component identities must be canonical and unique.");
|
|
26
|
+
componentIds.add(component.id);
|
|
27
|
+
const contourIds = new Set();
|
|
28
|
+
for (const contour of [component.outer, ...component.holes]) {
|
|
29
|
+
if (!validIdentity(contour.id) || contourIds.has(contour.id))
|
|
30
|
+
fail("Matched contour identities must be canonical and unique within a component.");
|
|
31
|
+
contourIds.add(contour.id);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const firstContours = contourList(first);
|
|
35
|
+
let logicalCount = 0;
|
|
36
|
+
for (const contour of firstContours) {
|
|
37
|
+
if (!isArray(contour.points) ||
|
|
38
|
+
!isArray(contour.spans) ||
|
|
39
|
+
contour.spans.some((span) => !validIdentity(span)) ||
|
|
40
|
+
contour.points.length !== contour.spans.length ||
|
|
41
|
+
contour.points.length < 3)
|
|
42
|
+
fail("Matched contours require at least three logical points and paired span identities.");
|
|
43
|
+
logicalCount += contour.points.length;
|
|
44
|
+
}
|
|
45
|
+
const maximumVertices = logicalCount * stationCount;
|
|
46
|
+
const capTriangles = 2 * (logicalCount + firstContours.length * 2);
|
|
47
|
+
const maximumTriangles = (closed ? stationCount : stationCount - 1) * logicalCount * 2 + (closed ? 0 : capTriangles);
|
|
48
|
+
assertSamplingCount(logicalCount, "loft", limits.maxProfilePoints);
|
|
49
|
+
assertMeshAllocationWithinLimits(maximumVertices, maximumTriangles, 12 * (maximumVertices + maximumTriangles), "loft", limits);
|
|
50
|
+
return { firstContours, maximumVertices, maximumTriangles };
|
|
51
|
+
}
|
|
52
|
+
/** Internal native hole-cap triangulation over copied, explicit matched logical slots. */
|
|
53
|
+
export function buildProfileLoftMesh(module, stations, limits, closed = false, seamProfileOffset = 0, positionFor) {
|
|
54
|
+
if (!isArray(stations) || stations.length < (closed ? 3 : 2))
|
|
55
|
+
fail("Profile lofts require a bounded station array with two stations, or three for a closed path.");
|
|
56
|
+
assertSamplingCount(stations.length, "loft", limits.maxPathPoints);
|
|
57
|
+
const first = stations[0].profile;
|
|
58
|
+
const { firstContours, maximumVertices, maximumTriangles } = profileMeshAllocation(first, stations.length, limits, closed);
|
|
59
|
+
const positions = new Float32Array(maximumVertices * 3);
|
|
60
|
+
const indices = new Uint32Array(maximumTriangles * 3);
|
|
61
|
+
let vertexCount = 0, cursor = 0;
|
|
62
|
+
const physicalStations = stations.map((station, stationIndex) => {
|
|
63
|
+
const profile = station.profile;
|
|
64
|
+
if (!isArray(profile.components) || profile.components.length !== first.components.length)
|
|
65
|
+
fail("Matched component topology cannot change between stations.");
|
|
66
|
+
profile.components.forEach((component, index) => {
|
|
67
|
+
const expected = first.components[index];
|
|
68
|
+
if (component.id !== expected.id ||
|
|
69
|
+
!isArray(component.holes) ||
|
|
70
|
+
component.holes.length !== expected.holes.length)
|
|
71
|
+
fail("Matched component identities and hole counts must preserve their order.");
|
|
72
|
+
});
|
|
73
|
+
const transform = frameTransform3(loftFrame(station));
|
|
74
|
+
const physical = contourList(profile).map((contour, contourIndex) => {
|
|
75
|
+
const expected = firstContours[contourIndex];
|
|
76
|
+
if (contour.id !== expected.id ||
|
|
77
|
+
!isArray(contour.points) ||
|
|
78
|
+
!isArray(contour.spans) ||
|
|
79
|
+
contour.points.length !== expected.points.length ||
|
|
80
|
+
contour.spans.length !== expected.spans.length ||
|
|
81
|
+
contour.spans.some((span, index) => span !== expected.spans[index]))
|
|
82
|
+
fail("Matched contours must preserve identities, logical slots, and span order.");
|
|
83
|
+
const points = [], vertices = [], slots = [];
|
|
84
|
+
for (let slot = 0; slot < contour.points.length; slot++) {
|
|
85
|
+
const point = contour.points[slot];
|
|
86
|
+
if (!isArray(point) || point.length !== 2 || !point.every(Number.isFinite))
|
|
87
|
+
fail("Matched profile points must be finite XY tuples.");
|
|
88
|
+
const [x, y] = point;
|
|
89
|
+
const placed = positionFor?.(stationIndex, contourIndex, slot);
|
|
90
|
+
const world = placed === undefined
|
|
91
|
+
? [
|
|
92
|
+
Math.fround(transform[0] * x + transform[1] * y + transform[3]),
|
|
93
|
+
Math.fround(transform[4] * x + transform[5] * y + transform[7]),
|
|
94
|
+
Math.fround(transform[8] * x + transform[9] * y + transform[11]),
|
|
95
|
+
]
|
|
96
|
+
: [Math.fround(placed[0]), Math.fround(placed[1]), Math.fround(placed[2])];
|
|
97
|
+
const previous = vertices.at(-1);
|
|
98
|
+
if (previous !== undefined &&
|
|
99
|
+
world.every((value, axis) => value === positions[previous * 3 + axis])) {
|
|
100
|
+
slots.push(previous);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
writeMeshVertex(positions, vertexCount * 3, world, "loft");
|
|
104
|
+
vertices.push(vertexCount);
|
|
105
|
+
slots.push(vertexCount++);
|
|
106
|
+
points.push([x, y]);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const start = vertices[0], end = vertices.at(-1);
|
|
110
|
+
if (start !== undefined &&
|
|
111
|
+
end !== undefined &&
|
|
112
|
+
start !== end &&
|
|
113
|
+
[0, 1, 2].every((axis) => positions[start * 3 + axis] === positions[end * 3 + axis])) {
|
|
114
|
+
slots.forEach((value, index) => {
|
|
115
|
+
if (value === end)
|
|
116
|
+
slots[index] = start;
|
|
117
|
+
});
|
|
118
|
+
vertices.pop();
|
|
119
|
+
points.pop();
|
|
120
|
+
vertexCount -= 1;
|
|
121
|
+
}
|
|
122
|
+
if (vertices.length < 3)
|
|
123
|
+
fail("Contours cannot collapse their filled topology at a station.");
|
|
124
|
+
return { points, vertices, slots };
|
|
125
|
+
});
|
|
126
|
+
const intermediateWorldGrid = positionFor !== undefined && stationIndex > 0 && stationIndex < stations.length - 1;
|
|
127
|
+
let contourIndex = 0;
|
|
128
|
+
const region = profile.components.map((component) => {
|
|
129
|
+
const outer = physical[contourIndex++];
|
|
130
|
+
if (!intermediateWorldGrid && !(signedAreaLoop2(outer.points) > 0))
|
|
131
|
+
fail("Matched outer contours must retain CCW winding.");
|
|
132
|
+
const holes = component.holes.map(() => {
|
|
133
|
+
const hole = physical[contourIndex++];
|
|
134
|
+
if (!intermediateWorldGrid && !(signedAreaLoop2(hole.points) < 0))
|
|
135
|
+
fail("Matched hole contours must retain CW winding.");
|
|
136
|
+
return hole.points;
|
|
137
|
+
});
|
|
138
|
+
return { outer: outer.points, holes };
|
|
139
|
+
});
|
|
140
|
+
const normalized = intermediateWorldGrid ? [] : normalizeRegion2(region);
|
|
141
|
+
normalized.forEach((component, index) => {
|
|
142
|
+
if (component.outer.length !== region[index].outer.length ||
|
|
143
|
+
component.holes.some((hole, holeIndex) => hole.length !== region[index].holes[holeIndex].length))
|
|
144
|
+
fail("Profile features collapsed below the geometry tolerance; matching does not remove logical topology.");
|
|
145
|
+
});
|
|
146
|
+
return physical;
|
|
147
|
+
});
|
|
148
|
+
const triangle = (a, b, c) => {
|
|
149
|
+
if (a === b || b === c || c === a)
|
|
150
|
+
return;
|
|
151
|
+
const ax = positions[a * 3], ay = positions[a * 3 + 1], az = positions[a * 3 + 2];
|
|
152
|
+
const abx = positions[b * 3] - ax, aby = positions[b * 3 + 1] - ay, abz = positions[b * 3 + 2] - az;
|
|
153
|
+
const acx = positions[c * 3] - ax, acy = positions[c * 3 + 1] - ay, acz = positions[c * 3 + 2] - az;
|
|
154
|
+
if (aby * acz - abz * acy === 0 && abz * acx - abx * acz === 0 && abx * acy - aby * acx === 0)
|
|
155
|
+
return;
|
|
156
|
+
indices[cursor++] = a;
|
|
157
|
+
indices[cursor++] = b;
|
|
158
|
+
indices[cursor++] = c;
|
|
159
|
+
};
|
|
160
|
+
for (let station = 0; station < (closed ? stations.length : stations.length - 1); station += 1) {
|
|
161
|
+
const lower = physicalStations[station], upper = physicalStations[(station + 1) % stations.length];
|
|
162
|
+
lower.forEach((contour, contourIndex) => {
|
|
163
|
+
const paired = upper[contourIndex];
|
|
164
|
+
for (let index = 0; index < contour.slots.length; index += 1) {
|
|
165
|
+
const next = (index + 1) % contour.slots.length;
|
|
166
|
+
const advance = signedAreaLoop2(paired.points) < 0 ? -seamProfileOffset : seamProfileOffset;
|
|
167
|
+
const offset = closed && station === stations.length - 1
|
|
168
|
+
? ((advance % paired.slots.length) + paired.slots.length) % paired.slots.length
|
|
169
|
+
: 0;
|
|
170
|
+
const pairedIndex = (index + offset) % paired.slots.length;
|
|
171
|
+
const pairedNext = (next + offset) % paired.slots.length;
|
|
172
|
+
triangle(contour.slots[index], contour.slots[next], paired.slots[pairedNext]);
|
|
173
|
+
triangle(contour.slots[index], paired.slots[pairedNext], paired.slots[pairedIndex]);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
if (!closed) {
|
|
178
|
+
for (const [stationIndex, reverse] of [
|
|
179
|
+
[0, true],
|
|
180
|
+
[stations.length - 1, false],
|
|
181
|
+
]) {
|
|
182
|
+
const physical = physicalStations[stationIndex];
|
|
183
|
+
let contourIndex = 0;
|
|
184
|
+
for (const component of first.components) {
|
|
185
|
+
const contours = physical.slice(contourIndex, contourIndex + component.holes.length + 1);
|
|
186
|
+
contourIndex += component.holes.length + 1;
|
|
187
|
+
const flat = contours.flatMap((contour) => contour.vertices);
|
|
188
|
+
const caps = module.triangulate(contours.map((contour) => contour.points.map(([x, y]) => [x, y])), 0, false);
|
|
189
|
+
for (const [a, b, c] of caps) {
|
|
190
|
+
if (reverse)
|
|
191
|
+
triangle(flat[a], flat[c], flat[b]);
|
|
192
|
+
else
|
|
193
|
+
triangle(flat[a], flat[b], flat[c]);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return { positions: positions.slice(0, vertexCount * 3), indices: indices.slice(0, cursor) };
|
|
199
|
+
}
|
|
200
|
+
/** Internal hollow sweep using explicit matched contours and transported right-handed frames. */
|
|
201
|
+
export function buildProfileSweepMesh(module, profile, inputPath, options, limits) {
|
|
202
|
+
const closed = options.closed ?? false;
|
|
203
|
+
validateSweepOptions(options);
|
|
204
|
+
let path = normalizeSweepPath(inputPath, closed, "sweep", limits);
|
|
205
|
+
assertSamplingCount(contourList(profile).reduce((count, contour) => count + contour.points.length, 0), "sweep", limits.maxSweepProfilePoints);
|
|
206
|
+
profileMeshAllocation(profile, path.length, limits, closed);
|
|
207
|
+
let scales = resolveSweepScales(options.scale, path.length);
|
|
208
|
+
const twist = options.twistDegrees ?? 0;
|
|
209
|
+
if (!Number.isFinite(twist))
|
|
210
|
+
fail("Sweep twistDegrees must be finite.");
|
|
211
|
+
const seamProfileOffset = options.seamProfileOffset ?? 0;
|
|
212
|
+
if (!Number.isSafeInteger(seamProfileOffset) || (!closed && seamProfileOffset !== 0))
|
|
213
|
+
fail("A finite integer seamProfileOffset requires a closed path.");
|
|
214
|
+
if (closed) {
|
|
215
|
+
const [scaleX, scaleY] = scales[0];
|
|
216
|
+
const angle = (twist * Math.PI) / 180, cosine = Math.cos(angle), sine = Math.sin(angle);
|
|
217
|
+
for (const contour of contourList(profile)) {
|
|
218
|
+
const extent = contour.points.reduce((maximum, [x, y]) => Math.max(maximum, Math.abs(x * scaleX), Math.abs(y * scaleY)), 1);
|
|
219
|
+
const tolerance = resolveGeometryTolerance(extent).linear;
|
|
220
|
+
const advance = signedAreaLoop2(contour.points) < 0 ? -seamProfileOffset : seamProfileOffset;
|
|
221
|
+
const offset = ((advance % contour.points.length) + contour.points.length) % contour.points.length;
|
|
222
|
+
for (let index = 0; index < contour.points.length; index++) {
|
|
223
|
+
const [x, y] = contour.points[index], target = contour.points[(index + offset) % contour.points.length];
|
|
224
|
+
if (Math.hypot(x * scaleX * cosine - y * scaleY * sine - target[0] * scaleX, x * scaleX * sine + y * scaleY * cosine - target[1] * scaleY) > tolerance)
|
|
225
|
+
fail("Sweep twist must match the explicit seam offset on every component and contour.");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (twist !== 0) {
|
|
230
|
+
const plan = planSweepStations(contourList(profile).map((contour) => contour.points), path, scales, options);
|
|
231
|
+
profileMeshAllocation(profile, plan.stationCount, limits, closed);
|
|
232
|
+
}
|
|
233
|
+
let frames = computeSweepFrames(path, options);
|
|
234
|
+
let twists;
|
|
235
|
+
if (twist !== 0) {
|
|
236
|
+
const plan = planSweepStations(contourList(profile).map((contour) => contour.points), path, scales, options, frames);
|
|
237
|
+
profileMeshAllocation(profile, plan.stationCount, limits, closed);
|
|
238
|
+
const refined = refineSweepStations(path, scales, frames, options, plan);
|
|
239
|
+
path = refined.path;
|
|
240
|
+
scales = refined.scales;
|
|
241
|
+
frames = refined.frames;
|
|
242
|
+
twists = refined.twists;
|
|
243
|
+
}
|
|
244
|
+
const stations = path.map((origin, index) => {
|
|
245
|
+
const frame = frames[index];
|
|
246
|
+
const angle = twists?.[index] ??
|
|
247
|
+
(((twist * Math.PI) / 180) * index) / (closed ? path.length : path.length - 1), cosine = Math.cos(angle), sine = Math.sin(angle);
|
|
248
|
+
const combine = (a, b, ac, bc) => [
|
|
249
|
+
a[0] * ac + b[0] * bc,
|
|
250
|
+
a[1] * ac + b[1] * bc,
|
|
251
|
+
a[2] * ac + b[2] * bc,
|
|
252
|
+
];
|
|
253
|
+
return {
|
|
254
|
+
profile: {
|
|
255
|
+
components: profile.components.map((component) => {
|
|
256
|
+
const scaled = (contour) => ({
|
|
257
|
+
...contour,
|
|
258
|
+
points: contour.points.map(([x, y]) => [
|
|
259
|
+
x * scales[index][0],
|
|
260
|
+
y * scales[index][1],
|
|
261
|
+
]),
|
|
262
|
+
});
|
|
263
|
+
return {
|
|
264
|
+
...component,
|
|
265
|
+
outer: scaled(component.outer),
|
|
266
|
+
holes: component.holes.map(scaled),
|
|
267
|
+
};
|
|
268
|
+
}),
|
|
269
|
+
},
|
|
270
|
+
frame: {
|
|
271
|
+
origin,
|
|
272
|
+
xAxis: combine(frame.normal, frame.binormal, cosine, sine),
|
|
273
|
+
yAxis: combine(frame.normal, frame.binormal, -sine, cosine),
|
|
274
|
+
zAxis: frame.tangent,
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
});
|
|
278
|
+
return buildProfileLoftMesh(module, stations, limits, closed, seamProfileOffset);
|
|
279
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { assertSamplingCount } from "../geometry/sampling-limits.js";
|
|
2
|
+
import { preflightSvgRegion } from "../geometry/svg-import.js";
|
|
3
|
+
import { parseSvgPath } from "../geometry/svg/index.js";
|
|
4
|
+
import { sampleTextContours } from "../geometry/text-contours.js";
|
|
5
|
+
import * as kernel from "./native/index.js";
|
|
6
|
+
/** Internal host-bound section operations. */
|
|
7
|
+
export function createSectionOperations(environment, transforms) {
|
|
8
|
+
const { module, limits, scope, assertActive, throwGeometryError, nativeSolid, nativeSection, wrapSection, ownSection, validateFiniteNumber, requirePositiveNumber, validateCoordinates, assertOperandCount, withResolutionDefaults, resolveCircularSegments, } = environment;
|
|
9
|
+
const { translate } = transforms.operations;
|
|
10
|
+
const fromContours = (contours, value = {}) => {
|
|
11
|
+
assertActive();
|
|
12
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
13
|
+
throwGeometryError("INVALID_ARGUMENT", "sectionFromContours", "Options must be an object.");
|
|
14
|
+
assertOperandCount(contours, "sectionFromContours", 0);
|
|
15
|
+
let count = 0;
|
|
16
|
+
const copied = Array.from(contours, (contour) => {
|
|
17
|
+
if (!Array.isArray(contour) || contour.length < 3)
|
|
18
|
+
throwGeometryError("INVALID_ARGUMENT", "sectionFromContours", "Each contour needs at least three points.");
|
|
19
|
+
count += contour.length;
|
|
20
|
+
assertSamplingCount(count, "sectionFromContours", limits.maxProfilePoints);
|
|
21
|
+
return Array.from(contour, (point) => {
|
|
22
|
+
validateCoordinates(point, 2, "sectionFromContours");
|
|
23
|
+
return [point[0], point[1]];
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
if (value.fillRule !== undefined &&
|
|
27
|
+
value.fillRule !== "nonzero" &&
|
|
28
|
+
value.fillRule !== "evenodd")
|
|
29
|
+
throwGeometryError("INVALID_ARGUMENT", "sectionFromContours", "Unsupported fill rule.");
|
|
30
|
+
if (copied.length === 0)
|
|
31
|
+
return ownSection(module.CrossSection.union([]));
|
|
32
|
+
return ownSection(new module.CrossSection(copied, value.fillRule === "evenodd" ? "EvenOdd" : "NonZero"));
|
|
33
|
+
};
|
|
34
|
+
const operations = {
|
|
35
|
+
sectionFromContours: fromContours,
|
|
36
|
+
fromRegion: (region) => {
|
|
37
|
+
assertActive();
|
|
38
|
+
return wrapSection(kernel.regionToCrossSection(scope, region, limits));
|
|
39
|
+
},
|
|
40
|
+
toRegion: (handle) => {
|
|
41
|
+
const profile = environment.sectionProfileFor(handle);
|
|
42
|
+
const copiedLoop = (points) => Object.freeze(points.map(([x, y]) => Object.freeze([x, y])));
|
|
43
|
+
return Object.freeze(profile.components.map((component) => Object.freeze({
|
|
44
|
+
outer: copiedLoop(component.outer.points),
|
|
45
|
+
holes: Object.freeze(component.holes.map((contour) => copiedLoop(contour.points))),
|
|
46
|
+
})));
|
|
47
|
+
},
|
|
48
|
+
offset: (handle, distance, value = {}) => {
|
|
49
|
+
const native = nativeSection(handle);
|
|
50
|
+
validateFiniteNumber(distance, "offset");
|
|
51
|
+
const join = value.join ?? "round";
|
|
52
|
+
if (!["round", "miter", "square"].includes(join))
|
|
53
|
+
throwGeometryError("INVALID_ARGUMENT", "offset", "Unsupported offset join.");
|
|
54
|
+
if (value.miterLimit !== undefined)
|
|
55
|
+
requirePositiveNumber(value.miterLimit, "offset");
|
|
56
|
+
return ownSection(native.offset(distance, join === "round" ? "Round" : join === "miter" ? "Miter" : "Square", value.miterLimit, resolveCircularSegments(Math.max(Math.abs(distance), 0.001), {
|
|
57
|
+
...(value.circularSegments === undefined ? {} : { segments: value.circularSegments }),
|
|
58
|
+
...(value.resolution === undefined ? {} : { resolution: value.resolution }),
|
|
59
|
+
})));
|
|
60
|
+
},
|
|
61
|
+
projectSection: (value) => ownSection(nativeSolid(value).project()),
|
|
62
|
+
sliceSection: (value, z) => ownSection(nativeSolid(value).slice(validateFiniteNumber(z, "sliceSection"))),
|
|
63
|
+
textSection: (layout, value = {}) => {
|
|
64
|
+
const selected = withResolutionDefaults(value);
|
|
65
|
+
return fromContours(sampleTextContours(layout, selected.resolution, selected.limits));
|
|
66
|
+
},
|
|
67
|
+
svgSection: (svg, value = {}) => {
|
|
68
|
+
let result = wrapSection(kernel.regionToCrossSection(scope, preflightSvgRegion(svg, { ...withResolutionDefaults(value), origin: "document" }).region, limits));
|
|
69
|
+
if ((value.origin ?? "center") === "center") {
|
|
70
|
+
const bounds = nativeSection(result).bounds();
|
|
71
|
+
result = translate(result, [
|
|
72
|
+
-(bounds.min[0] + bounds.max[0]) / 2,
|
|
73
|
+
-(bounds.min[1] + bounds.max[1]) / 2,
|
|
74
|
+
]);
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
},
|
|
78
|
+
svgPathSection: (path, value = {}) => {
|
|
79
|
+
const parsed = parseSvgPath(path, withResolutionDefaults(value));
|
|
80
|
+
const filled = parsed.groups.map((group) => fromContours(group.contours, { fillRule: group.fillRule }));
|
|
81
|
+
let result = ownSection(module.CrossSection.union(filled.map(nativeSection)));
|
|
82
|
+
if ((value.origin ?? "center") === "center") {
|
|
83
|
+
const bounds = nativeSection(result).bounds();
|
|
84
|
+
result = translate(result, [
|
|
85
|
+
-(bounds.min[0] + bounds.max[0]) / 2,
|
|
86
|
+
-(bounds.min[1] + bounds.max[1]) / 2,
|
|
87
|
+
]);
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
return operations;
|
|
93
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createAnchor3 } from "../geometry/anchors.js";
|
|
2
|
+
import { GeometryError } from "../geometry/errors.js";
|
|
3
|
+
import { applyTransformDirection3, applyTransformPoint3, inverseTransform3, } from "../geometry/transform3.js";
|
|
4
|
+
const maximumAnchors = 128;
|
|
5
|
+
function fail(operation, message) {
|
|
6
|
+
throw new GeometryError("INVALID_ARGUMENT", message, { operation });
|
|
7
|
+
}
|
|
8
|
+
function anchorName(name, operation) {
|
|
9
|
+
if (typeof name !== "string" || name.length > 64 || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name))
|
|
10
|
+
fail(operation, "Anchor names must use lowercase kebab-case and at most 64 characters.");
|
|
11
|
+
}
|
|
12
|
+
function copyAnchor(anchor, operation) {
|
|
13
|
+
if (typeof anchor !== "object" || anchor === null)
|
|
14
|
+
fail(operation, "Anchor definitions require position, direction and up vectors.");
|
|
15
|
+
for (const vector of [anchor.position, anchor.direction, anchor.up])
|
|
16
|
+
if (!Array.isArray(vector) || vector.length !== 3 || !vector.every(Number.isFinite))
|
|
17
|
+
fail(operation, "Anchor vectors must contain exactly three finite coordinates.");
|
|
18
|
+
return createAnchor3(anchor.position, anchor.direction, anchor.up);
|
|
19
|
+
}
|
|
20
|
+
function unit(vector) {
|
|
21
|
+
const maximum = Math.max(...vector.map(Math.abs));
|
|
22
|
+
if (!Number.isFinite(maximum) || maximum === 0)
|
|
23
|
+
fail("transform", "Transformed anchor directions must remain finite and nonzero.");
|
|
24
|
+
const scaled = [vector[0] / maximum, vector[1] / maximum, vector[2] / maximum];
|
|
25
|
+
const length = Math.hypot(...scaled);
|
|
26
|
+
return [scaled[0] / length, scaled[1] / length, scaled[2] / length];
|
|
27
|
+
}
|
|
28
|
+
/** Maps positions affinely, outward normals by inverse transpose, and roll tangents linearly. */
|
|
29
|
+
function transformedAnchor(anchor, matrix, inverse) {
|
|
30
|
+
const normal = anchor.direction;
|
|
31
|
+
const direction = [
|
|
32
|
+
inverse[0] * normal[0] + inverse[4] * normal[1] + inverse[8] * normal[2],
|
|
33
|
+
inverse[1] * normal[0] + inverse[5] * normal[1] + inverse[9] * normal[2],
|
|
34
|
+
inverse[2] * normal[0] + inverse[6] * normal[1] + inverse[10] * normal[2],
|
|
35
|
+
];
|
|
36
|
+
return createAnchor3(applyTransformPoint3(matrix, anchor.position), unit(direction), unit(applyTransformDirection3(matrix, anchor.up)));
|
|
37
|
+
}
|
|
38
|
+
/** Run-owned immutable anchor metadata. Geometry operations acquire metadata only deliberately. */
|
|
39
|
+
export function createShapeAnchorOwner(assertShape, aliasShape) {
|
|
40
|
+
const anchors = new Map();
|
|
41
|
+
const remember = (shape, definitions) => {
|
|
42
|
+
assertShape(shape);
|
|
43
|
+
if (typeof definitions !== "object" || definitions === null || Array.isArray(definitions))
|
|
44
|
+
fail("withAnchors", "Named anchors require an object of anchor definitions.");
|
|
45
|
+
const entries = Object.entries(definitions);
|
|
46
|
+
if (entries.length > maximumAnchors)
|
|
47
|
+
fail("withAnchors", `A shape may define at most ${maximumAnchors} anchors.`);
|
|
48
|
+
const copied = entries.map(([name, value]) => {
|
|
49
|
+
anchorName(name, "withAnchors");
|
|
50
|
+
return [name, copyAnchor(value, "withAnchors")];
|
|
51
|
+
});
|
|
52
|
+
anchors.set(shape, Object.freeze(Object.fromEntries(copied)));
|
|
53
|
+
return shape;
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
remember,
|
|
57
|
+
anchor(shape, name) {
|
|
58
|
+
assertShape(shape);
|
|
59
|
+
anchorName(name, "anchor");
|
|
60
|
+
const definitions = anchors.get(shape);
|
|
61
|
+
if (definitions !== undefined && Object.hasOwn(definitions, name))
|
|
62
|
+
return definitions[name];
|
|
63
|
+
const available = Object.keys(definitions ?? {});
|
|
64
|
+
return fail("anchor", `Shape has no anchor "${name}".${available.length === 0 ? " Define named anchors explicitly for constructed or Boolean geometry." : ` Available anchors: ${available.join(", ")}.`}`);
|
|
65
|
+
},
|
|
66
|
+
withAnchors(shape, definitions) {
|
|
67
|
+
assertShape(shape);
|
|
68
|
+
if (typeof definitions !== "object" || definitions === null || Array.isArray(definitions))
|
|
69
|
+
fail("withAnchors", "Named anchors require an object of anchor definitions.");
|
|
70
|
+
return remember(aliasShape(shape), { ...anchors.get(shape), ...definitions });
|
|
71
|
+
},
|
|
72
|
+
preserve(source, result, matrix) {
|
|
73
|
+
assertShape(source);
|
|
74
|
+
assertShape(result);
|
|
75
|
+
const definitions = anchors.get(source);
|
|
76
|
+
if (definitions === undefined)
|
|
77
|
+
return result;
|
|
78
|
+
const inverse = inverseTransform3(matrix);
|
|
79
|
+
return remember(result, Object.fromEntries(Object.entries(definitions).map(([name, value]) => [
|
|
80
|
+
name,
|
|
81
|
+
transformedAnchor(value, matrix, inverse),
|
|
82
|
+
])));
|
|
83
|
+
},
|
|
84
|
+
dispose() {
|
|
85
|
+
anchors.clear();
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|