@metamesh/modeling 0.0.0-stage → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/LICENSE +200 -0
  3. package/README.md +376 -2
  4. package/THIRD_PARTY_NOTICES.md +153 -0
  5. package/dist/api/engine.d.ts +71 -0
  6. package/dist/api/geometry.d.ts +3136 -0
  7. package/dist/api/index.d.ts +2087 -0
  8. package/dist/api/text.d.ts +93 -0
  9. package/dist/engine/construction-operations.js +189 -0
  10. package/dist/engine/execution.js +227 -0
  11. package/dist/engine/export-3mf.js +100 -0
  12. package/dist/engine/extrusion-finishing.js +82 -0
  13. package/dist/engine/finalization.js +214 -0
  14. package/dist/engine/index.js +83 -0
  15. package/dist/engine/inspection-operations.js +52 -0
  16. package/dist/engine/kernel/LICENSE +201 -0
  17. package/dist/engine/kernel/manifold.js +2 -0
  18. package/dist/engine/kernel/manifold.wasm +0 -0
  19. package/dist/engine/loft-operations.js +156 -0
  20. package/dist/engine/mechanical-operations.js +51 -0
  21. package/dist/engine/modeling-context.js +38 -0
  22. package/dist/engine/native/adapter.js +20 -0
  23. package/dist/engine/native/context.js +209 -0
  24. package/dist/engine/native/copied-views.js +46 -0
  25. package/dist/engine/native/index.js +13 -0
  26. package/dist/engine/native/operations.js +115 -0
  27. package/dist/engine/native/ownership.js +98 -0
  28. package/dist/engine/native/regions.js +138 -0
  29. package/dist/engine/native/scoped-primitives.js +127 -0
  30. package/dist/engine/native/scoped-threads.js +34 -0
  31. package/dist/engine/native/threads.js +52 -0
  32. package/dist/engine/native/transforms.js +111 -0
  33. package/dist/engine/operation-environment.js +20 -0
  34. package/dist/engine/pattern-operations.js +36 -0
  35. package/dist/engine/pattern-placements.js +176 -0
  36. package/dist/engine/primitive-anchors.js +70 -0
  37. package/dist/engine/primitive-correspondence.js +74 -0
  38. package/dist/engine/primitive-operations.js +151 -0
  39. package/dist/engine/primitive-profile-types.js +1 -0
  40. package/dist/engine/primitive-profiles.js +250 -0
  41. package/dist/engine/profile-mesh.js +279 -0
  42. package/dist/engine/section-operations.js +93 -0
  43. package/dist/engine/shape-anchors.js +88 -0
  44. package/dist/engine/transform-operations.js +225 -0
  45. package/dist/engine/types.js +1 -0
  46. package/dist/geometry/anchors.js +226 -0
  47. package/dist/geometry/curves/evaluation.js +162 -0
  48. package/dist/geometry/curves/sampling.js +266 -0
  49. package/dist/geometry/curves/types.js +1 -0
  50. package/dist/geometry/cylinder.js +117 -0
  51. package/dist/geometry/edge-finishing.js +160 -0
  52. package/dist/geometry/errors.js +76 -0
  53. package/dist/geometry/extrusion.js +150 -0
  54. package/dist/geometry/heightfield.js +115 -0
  55. package/dist/geometry/index.js +42 -0
  56. package/dist/geometry/loft-correspondence.js +204 -0
  57. package/dist/geometry/loft-geometry.js +184 -0
  58. package/dist/geometry/loft-options.js +56 -0
  59. package/dist/geometry/loft-simple-mesh.js +92 -0
  60. package/dist/geometry/loft-triangulation.js +64 -0
  61. package/dist/geometry/loft.js +95 -0
  62. package/dist/geometry/math/vector3.js +71 -0
  63. package/dist/geometry/mechanical/gear-motion.js +94 -0
  64. package/dist/geometry/mechanical/gears.js +484 -0
  65. package/dist/geometry/mechanical/index.js +13 -0
  66. package/dist/geometry/mechanical/standards.js +67 -0
  67. package/dist/geometry/mechanical/thread-helices.js +44 -0
  68. package/dist/geometry/mechanical/thread-mesh-plan.js +358 -0
  69. package/dist/geometry/mechanical/thread-mesh.js +142 -0
  70. package/dist/geometry/mechanical/thread-profiles.js +373 -0
  71. package/dist/geometry/mechanical/thread-solids.js +230 -0
  72. package/dist/geometry/mechanical/thread-surface.js +433 -0
  73. package/dist/geometry/mechanical/threads.js +138 -0
  74. package/dist/geometry/mesh-edge-table.js +126 -0
  75. package/dist/geometry/mesh-planar.js +408 -0
  76. package/dist/geometry/mesh-quality.js +396 -0
  77. package/dist/geometry/mesh-signature.js +137 -0
  78. package/dist/geometry/mesh.js +283 -0
  79. package/dist/geometry/model-limits.js +50 -0
  80. package/dist/geometry/numeric.js +11 -0
  81. package/dist/geometry/paths/frames.js +118 -0
  82. package/dist/geometry/paths/helix.js +49 -0
  83. package/dist/geometry/paths/path.js +32 -0
  84. package/dist/geometry/paths/planar.js +135 -0
  85. package/dist/geometry/paths/resampling.js +125 -0
  86. package/dist/geometry/polygon-clipping.js +52 -0
  87. package/dist/geometry/primitive-options.js +1 -0
  88. package/dist/geometry/primitive-resolution.js +17 -0
  89. package/dist/geometry/primitive-resolvers.js +235 -0
  90. package/dist/geometry/profiles/matched.js +192 -0
  91. package/dist/geometry/profiles/open-profile.js +137 -0
  92. package/dist/geometry/profiles/profiles.js +91 -0
  93. package/dist/geometry/profiles/rectangle.js +249 -0
  94. package/dist/geometry/profiles/shapes.js +41 -0
  95. package/dist/geometry/profiles/span-sampling.js +28 -0
  96. package/dist/geometry/profiles/types.js +1 -0
  97. package/dist/geometry/profiles/validation.js +20 -0
  98. package/dist/geometry/regions.js +371 -0
  99. package/dist/geometry/resolution.js +236 -0
  100. package/dist/geometry/revolve/revolve-mesh.js +129 -0
  101. package/dist/geometry/sampling-limits.js +29 -0
  102. package/dist/geometry/stroke-path.js +181 -0
  103. package/dist/geometry/svg/errors.js +23 -0
  104. package/dist/geometry/svg/fill.js +172 -0
  105. package/dist/geometry/svg/index.js +1 -0
  106. package/dist/geometry/svg/matrix.js +130 -0
  107. package/dist/geometry/svg/parser.js +536 -0
  108. package/dist/geometry/svg/path.js +349 -0
  109. package/dist/geometry/svg/types.js +1 -0
  110. package/dist/geometry/svg-import.js +662 -0
  111. package/dist/geometry/sweeps/options.js +28 -0
  112. package/dist/geometry/sweeps/orientation.js +106 -0
  113. package/dist/geometry/sweeps/stations.js +161 -0
  114. package/dist/geometry/sweeps/sweep-closed.js +96 -0
  115. package/dist/geometry/sweeps/sweep-mesh.js +30 -0
  116. package/dist/geometry/sweeps/sweep-open.js +76 -0
  117. package/dist/geometry/sweeps/validation.js +116 -0
  118. package/dist/geometry/text-contours.js +32 -0
  119. package/dist/geometry/text-layout.js +1 -0
  120. package/dist/geometry/tolerance.js +53 -0
  121. package/dist/geometry/transform-geometry.js +41 -0
  122. package/dist/geometry/transform3.js +340 -0
  123. package/dist/geometry/types.js +1 -0
  124. package/dist/index.js +1 -0
  125. package/dist/model/execution-binding.js +30 -0
  126. package/dist/model/operations.js +154 -0
  127. package/dist/model/options.js +1 -0
  128. package/dist/model/types.js +1 -0
  129. package/dist/text/index.js +58 -0
  130. package/dist/text/vector-text-layout.js +279 -0
  131. package/package.json +100 -3
@@ -0,0 +1,283 @@
1
+ import { GeometryError } from "./errors.js";
2
+ import { IndexedMeshEdgeTable } from "./mesh-edge-table.js";
3
+ import { writeMeshVertex } from "./numeric.js";
4
+ import { resolveGeometryTolerance } from "./tolerance.js";
5
+ import { transformDeterminant3, validateTransform3 } from "./transform3.js";
6
+ function inspectTriangleMesh(mesh, options = {}, mode = "validate", topology = false) {
7
+ const measurements = mode === "measure";
8
+ if (!(mesh.positions instanceof Float32Array) || !(mesh.indices instanceof Uint32Array)) {
9
+ throw new GeometryError("INVALID_MESH", "Triangle meshes require Float32Array positions and Uint32Array indices.");
10
+ }
11
+ if (mesh.positions.length % 3 !== 0) {
12
+ throw new GeometryError("INVALID_MESH", "Mesh positions must contain complete XYZ vertices.");
13
+ }
14
+ if (mesh.indices.length % 3 !== 0) {
15
+ throw new GeometryError("INVALID_MESH", "Mesh indices must contain complete triangles.");
16
+ }
17
+ const vertexCount = mesh.positions.length / 3;
18
+ if ((vertexCount === 0 || mesh.indices.length === 0) &&
19
+ !(options.allowEmpty && vertexCount === 0 && mesh.indices.length === 0)) {
20
+ throw new GeometryError("INVALID_MESH", "A triangle mesh must contain vertices and faces.");
21
+ }
22
+ const min = [Infinity, Infinity, Infinity];
23
+ const max = [-Infinity, -Infinity, -Infinity];
24
+ for (let offset = 0; offset < mesh.positions.length; offset += 3) {
25
+ for (let axis = 0; axis < 3; axis += 1) {
26
+ const coordinate = mesh.positions[offset + axis];
27
+ if (!Number.isFinite(coordinate)) {
28
+ throw new GeometryError("NON_FINITE_VALUE", `Mesh position ${offset + axis} must be finite.`);
29
+ }
30
+ min[axis] = Math.min(min[axis], coordinate);
31
+ max[axis] = Math.max(max[axis], coordinate);
32
+ }
33
+ }
34
+ const extent = vertexCount === 0 ? 0 : Math.max(max[0] - min[0], max[1] - min[1], max[2] - min[2]);
35
+ const minimumDoubleArea = resolveGeometryTolerance(extent, options.tolerance).area;
36
+ const edges = options.requireClosed || topology
37
+ ? new IndexedMeshEdgeTable(mesh.indices.length, topology)
38
+ : null;
39
+ const triangleCount = mesh.indices.length / 3;
40
+ const parents = topology ? new Int32Array(triangleCount) : null;
41
+ const ranks = topology ? new Uint8Array(triangleCount) : null;
42
+ const referenced = topology ? new Uint8Array(vertexCount) : null;
43
+ let connectedComponentCount = 0;
44
+ const root = (triangle) => {
45
+ let current = triangle;
46
+ while (parents[current] !== current)
47
+ current = parents[current];
48
+ while (parents[triangle] !== triangle) {
49
+ const next = parents[triangle];
50
+ parents[triangle] = current;
51
+ triangle = next;
52
+ }
53
+ return current;
54
+ };
55
+ const connect = (a, b) => {
56
+ let left = root(a), right = root(b);
57
+ if (left === right)
58
+ return;
59
+ if (ranks[left] < ranks[right])
60
+ [left, right] = [right, left];
61
+ parents[right] = left;
62
+ if (ranks[left] === ranks[right])
63
+ ranks[left] = ranks[left] + 1;
64
+ connectedComponentCount -= 1;
65
+ };
66
+ let edgeCount = 0;
67
+ const addEdge = (a, b, triangle) => {
68
+ const firstTriangle = edges.touch(a, b, triangle);
69
+ if (firstTriangle === -1)
70
+ edgeCount += 1;
71
+ else if (topology)
72
+ connect(firstTriangle, triangle);
73
+ };
74
+ // Keep the two hot-loop accumulators independent and local: no mutable numeric objects escape.
75
+ let surfaceArea = 0, areaCorrection = 0, signedVolume = 0, volumeCorrection = 0;
76
+ const originX = mesh.positions[0] ?? 0, originY = mesh.positions[1] ?? 0, originZ = mesh.positions[2] ?? 0;
77
+ for (let offset = 0; offset < mesh.indices.length; offset += 3) {
78
+ const a = mesh.indices[offset], b = mesh.indices[offset + 1], c = mesh.indices[offset + 2];
79
+ if (a >= vertexCount || b >= vertexCount || c >= vertexCount) {
80
+ throw new GeometryError("INVALID_MESH", `Triangle ${offset / 3} references a missing vertex.`);
81
+ }
82
+ if (a === b || b === c || c === a) {
83
+ throw new GeometryError("DEGENERATE_GEOMETRY", `Triangle ${offset / 3} repeats a vertex.`);
84
+ }
85
+ const ax = mesh.positions[a * 3], ay = mesh.positions[a * 3 + 1], az = mesh.positions[a * 3 + 2];
86
+ const abx = mesh.positions[b * 3] - ax, aby = mesh.positions[b * 3 + 1] - ay, abz = mesh.positions[b * 3 + 2] - az;
87
+ const acx = mesh.positions[c * 3] - ax, acy = mesh.positions[c * 3 + 1] - ay, acz = mesh.positions[c * 3 + 2] - az;
88
+ const crossX = aby * acz - abz * acy, crossY = abz * acx - abx * acz, crossZ = abx * acy - aby * acx;
89
+ const doubleArea = Math.hypot(crossX, crossY, crossZ);
90
+ if (doubleArea <= minimumDoubleArea) {
91
+ throw new GeometryError("DEGENERATE_GEOMETRY", `Triangle ${offset / 3} has zero area.`);
92
+ }
93
+ if (measurements) {
94
+ const area = doubleArea / 2;
95
+ const nextArea = surfaceArea + area;
96
+ areaCorrection +=
97
+ Math.abs(surfaceArea) >= Math.abs(area)
98
+ ? surfaceArea - nextArea + area
99
+ : area - nextArea + surfaceArea;
100
+ surfaceArea = nextArea;
101
+ const volume = ((ax - originX) * crossX + (ay - originY) * crossY + (az - originZ) * crossZ) / 6;
102
+ const nextVolume = signedVolume + volume;
103
+ volumeCorrection +=
104
+ Math.abs(signedVolume) >= Math.abs(volume)
105
+ ? signedVolume - nextVolume + volume
106
+ : volume - nextVolume + signedVolume;
107
+ signedVolume = nextVolume;
108
+ }
109
+ if (parents !== null) {
110
+ const triangle = offset / 3;
111
+ parents[triangle] = triangle;
112
+ referenced[a] = referenced[b] = referenced[c] = 1;
113
+ connectedComponentCount += 1;
114
+ }
115
+ if (edges !== null) {
116
+ addEdge(a, b, offset / 3);
117
+ addEdge(b, c, offset / 3);
118
+ addEdge(c, a, offset / 3);
119
+ }
120
+ }
121
+ if (options.requireClosed && edges !== null && !edges.isClosed())
122
+ throw new GeometryError("INVALID_MESH", "Closed meshes require exactly two oppositely wound faces at every edge.");
123
+ if (referenced !== null) {
124
+ for (let vertex = 0; vertex < vertexCount; vertex += 1) {
125
+ if (referenced[vertex] === 0) {
126
+ throw new GeometryError("INVALID_MESH", "Mesh signatures require every position-buffer vertex to be referenced by a triangle.");
127
+ }
128
+ }
129
+ }
130
+ // Validation-only calls produce no unused zero-valued metric record. Completed measurements
131
+ // are immutable snapshots with independent storage, including their nested bounds tuples.
132
+ if (mode === "validate")
133
+ return;
134
+ const bounds = Object.freeze({ min: Object.freeze(min), max: Object.freeze(max) });
135
+ if (mode === "bounds")
136
+ return bounds;
137
+ return Object.freeze({
138
+ bounds,
139
+ signedVolume: signedVolume + volumeCorrection,
140
+ surfaceArea: surfaceArea + areaCorrection,
141
+ triangleCount,
142
+ vertexCount,
143
+ ...(topology ? { connectedComponentCount, edgeCount } : {}),
144
+ });
145
+ }
146
+ /**
147
+ * Validates and measures a mesh in one position and one triangle traversal.
148
+ *
149
+ * Bounds, area in square model units and signed volume in cubic model units are computed without
150
+ * copying or changing the input buffers. Volume is meaningful for consistently wound closed
151
+ * geometry; `requireClosed` checks oriented edge closure. Optional indexed topology counts require
152
+ * every position vertex to be referenced and do not establish freedom from self-intersections.
153
+ * Results and nested bounds are immutable snapshots. Caller-owned mutable meshes are freshly
154
+ * inspected on every call, never cached.
155
+ */
156
+ export function analyzeTriangleMesh(mesh, options = {}) {
157
+ return inspectTriangleMesh(mesh, options, "measure", options.includeTopology ?? false);
158
+ }
159
+ /**
160
+ * Validates mesh buffers, indices, triangle area, and optionally closed topology.
161
+ *
162
+ * Closed validation requires each edge to occur exactly twice with opposite
163
+ * directions, rejecting boundary and non-manifold edges. Validation does not
164
+ * mutate or copy the mesh. This establishes edge connectivity, not the absence
165
+ * of geometric self-intersections between faces.
166
+ *
167
+ * @param mesh - Mesh to inspect.
168
+ * @param options - Empty-mesh, topology, and tolerance policy.
169
+ * @returns The original mesh after successful validation.
170
+ * @throws {@link GeometryError} When any requested invariant fails.
171
+ */
172
+ export function validateTriangleMesh(mesh, options = {}) {
173
+ inspectTriangleMesh(mesh, options);
174
+ return mesh;
175
+ }
176
+ /**
177
+ * Computes axis-aligned bounds for a non-empty mesh.
178
+ *
179
+ * @param mesh - Mesh whose finite complete vertices will be measured.
180
+ * @returns Fresh immutable minimum and maximum XYZ tuples.
181
+ * @throws {@link GeometryError} When the mesh is structurally invalid.
182
+ */
183
+ export const meshBounds = (mesh) => {
184
+ return inspectTriangleMesh(mesh, {}, "bounds");
185
+ };
186
+ /**
187
+ * Measures bounds, surface area, and signed volume without changing the mesh.
188
+ *
189
+ * Signed volume is meaningful for a consistently wound closed mesh. Set
190
+ * `requireClosed` when measurements will be used as manufacturing assertions.
191
+ *
192
+ * @param mesh - Mesh to validate and measure.
193
+ * @param options - Validation policy applied before measurement.
194
+ * @returns Immutable deterministic scalar and bounds measurements.
195
+ */
196
+ export function measureTriangleMesh(mesh, options = {}) {
197
+ return inspectTriangleMesh(mesh, options, "measure");
198
+ }
199
+ /**
200
+ * Concatenates meshes into one indexed mesh without welding coincident vertices.
201
+ *
202
+ * Inputs are validated and never mutated. Face order and winding remain stable.
203
+ *
204
+ * @param meshes - One or more meshes to concatenate.
205
+ * @returns Fresh typed arrays with indices offset to their source vertex ranges.
206
+ * @throws {@link GeometryError} When no mesh is supplied or an input is invalid.
207
+ */
208
+ export function joinTriangleMeshes(meshes) {
209
+ if (meshes.length === 0) {
210
+ throw new GeometryError("INVALID_ARGUMENT", "Joining meshes requires at least one mesh.");
211
+ }
212
+ meshes.forEach((mesh) => validateTriangleMesh(mesh));
213
+ const positionLength = meshes.reduce((sum, mesh) => sum + mesh.positions.length, 0);
214
+ const indexLength = meshes.reduce((sum, mesh) => sum + mesh.indices.length, 0);
215
+ if (positionLength / 3 > 0xffff_ffff) {
216
+ throw new GeometryError("INVALID_MESH", "Joined mesh exceeds the Uint32 vertex index range.");
217
+ }
218
+ const positions = new Float32Array(positionLength);
219
+ const indices = new Uint32Array(indexLength);
220
+ let positionOffset = 0;
221
+ let indexOffset = 0;
222
+ let vertexOffset = 0;
223
+ for (const mesh of meshes) {
224
+ positions.set(mesh.positions, positionOffset);
225
+ for (let index = 0; index < mesh.indices.length; index += 1) {
226
+ indices[indexOffset + index] = mesh.indices[index] + vertexOffset;
227
+ }
228
+ positionOffset += mesh.positions.length;
229
+ indexOffset += mesh.indices.length;
230
+ vertexOffset += mesh.positions.length / 3;
231
+ }
232
+ return { indices, positions };
233
+ }
234
+ /**
235
+ * Returns a copy of a mesh with every triangle winding reversed.
236
+ *
237
+ * @param mesh - Mesh to copy and reverse.
238
+ * @returns Fresh position and index buffers; the input remains unchanged.
239
+ */
240
+ export function reverseTriangleMesh(mesh) {
241
+ validateTriangleMesh(mesh);
242
+ const indices = new Uint32Array(mesh.indices);
243
+ for (let offset = 0; offset < indices.length; offset += 3) {
244
+ const second = indices[offset + 1];
245
+ indices[offset + 1] = indices[offset + 2];
246
+ indices[offset + 2] = second;
247
+ }
248
+ return { indices, positions: new Float32Array(mesh.positions) };
249
+ }
250
+ /**
251
+ * Applies an affine transform to a mesh and preserves outward face orientation.
252
+ *
253
+ * A transform with a negative linear determinant reflects space, so this helper
254
+ * automatically reverses every triangle. Singular transforms are rejected.
255
+ *
256
+ * @param mesh - Mesh to transform without mutation.
257
+ * @param transform - Row-major affine transform.
258
+ * @returns A mesh with fresh transformed positions and copied or reversed indices.
259
+ */
260
+ export function transformTriangleMesh(mesh, transform) {
261
+ validateTriangleMesh(mesh);
262
+ validateTransform3(transform);
263
+ const positions = new Float32Array(mesh.positions.length);
264
+ for (let offset = 0; offset < mesh.positions.length; offset += 3) {
265
+ const x = mesh.positions[offset];
266
+ const y = mesh.positions[offset + 1];
267
+ const z = mesh.positions[offset + 2];
268
+ writeMeshVertex(positions, offset, [
269
+ transform[0] * x + transform[1] * y + transform[2] * z + transform[3],
270
+ transform[4] * x + transform[5] * y + transform[6] * z + transform[7],
271
+ transform[8] * x + transform[9] * y + transform[10] * z + transform[11],
272
+ ], "transformTriangleMesh");
273
+ }
274
+ const indices = new Uint32Array(mesh.indices);
275
+ if (transformDeterminant3(transform) < 0) {
276
+ for (let offset = 0; offset < indices.length; offset += 3) {
277
+ const second = indices[offset + 1];
278
+ indices[offset + 1] = indices[offset + 2];
279
+ indices[offset + 2] = second;
280
+ }
281
+ }
282
+ return validateTriangleMesh({ indices, positions });
283
+ }
@@ -0,0 +1,50 @@
1
+ import { GeometryError } from "./errors.js";
2
+ import { defaultGeometrySamplingLimits } from "./sampling-limits.js";
3
+ /** Default engine ceilings, shared with predictable pure mesh constructor preflights. */
4
+ export const defaultModelingLimits = Object.freeze({
5
+ ...defaultGeometrySamplingLimits,
6
+ maxParts: 64,
7
+ maxVertices: 1_000_000,
8
+ maxTriangles: 2_000_000,
9
+ maxBytes: 96 * 1024 * 1024,
10
+ });
11
+ /** @internal Resolves trusted pure/engine policy; a host ceiling restricts local overrides. */
12
+ export function resolveModelingLimits(overrides = {}, ceilings) {
13
+ if (typeof overrides !== "object" || overrides === null || Array.isArray(overrides))
14
+ throw new GeometryError("INVALID_ARGUMENT", "Modeling limits must be an object.", {
15
+ operation: "modelingLimits",
16
+ });
17
+ const result = { ...(ceilings ?? defaultModelingLimits), ...overrides };
18
+ for (const key of Object.keys(defaultModelingLimits)) {
19
+ if (!Number.isSafeInteger(result[key]) ||
20
+ result[key] < (key === "maxCircularSegments" ? 3 : 1) ||
21
+ (ceilings !== undefined && result[key] > ceilings[key]))
22
+ throw new GeometryError("INVALID_ARGUMENT", `${key} must be a positive safe integer${key === "maxCircularSegments" ? " of at least three" : ""}${ceilings === undefined ? "" : " no larger than the configured engine ceiling"}.`, { operation: "modelingLimits" });
23
+ }
24
+ return Object.freeze(result);
25
+ }
26
+ /** @internal Checks predictable typed mesh storage before constructing it. */
27
+ export function assertMeshAllocationWithinLimits(vertices, triangles, bytes, operation, limits) {
28
+ if (!Number.isSafeInteger(vertices) ||
29
+ !Number.isSafeInteger(triangles) ||
30
+ !Number.isSafeInteger(bytes) ||
31
+ vertices < 0 ||
32
+ triangles < 0 ||
33
+ bytes < 0 ||
34
+ vertices * 3 > 0xffff_ffff ||
35
+ triangles * 3 > 0xffff_ffff ||
36
+ vertices > limits.maxVertices ||
37
+ triangles > limits.maxTriangles ||
38
+ bytes > limits.maxBytes)
39
+ throw new GeometryError("COMPLEXITY_LIMIT", `Mesh allocation requires ${vertices} vertices, ${triangles} triangles and ${bytes} bytes; the host execution limits permit ${limits.maxVertices} vertices, ${limits.maxTriangles} triangles and ${limits.maxBytes} bytes, within typed-array capacity.`, {
40
+ operation,
41
+ details: {
42
+ requiredVertices: vertices,
43
+ allowedVertices: limits.maxVertices,
44
+ requiredTriangles: triangles,
45
+ allowedTriangles: limits.maxTriangles,
46
+ requiredBytes: bytes,
47
+ allowedBytes: limits.maxBytes,
48
+ },
49
+ });
50
+ }
@@ -0,0 +1,11 @@
1
+ import { GeometryError } from "./errors.js";
2
+ /** @internal Writes one finite, representable manufacturing vertex before checking mesh topology. */
3
+ export function writeMeshVertex(positions, offset, point, operation) {
4
+ for (let axis = 0; axis < 3; axis += 1) {
5
+ const coordinate = Math.fround(point[axis]);
6
+ if (!Number.isFinite(coordinate)) {
7
+ throw new GeometryError("NON_FINITE_VALUE", "Mesh coordinates exceed finite Float32 storage.", { operation, details: { axis, coordinate: point[axis] } });
8
+ }
9
+ positions[offset + axis] = coordinate;
10
+ }
11
+ }
@@ -0,0 +1,118 @@
1
+ import { cross3, dot3, length3, normalize3, projectOntoPlane3, rotateAroundAxis3, scale3, subtract3, } from "../math/vector3.js";
2
+ import { assertSamplingCount, resolveGeometrySamplingLimits, } from "../sampling-limits.js";
3
+ const tangentAt = (path, index) => {
4
+ if (index === 0) {
5
+ return normalize3(subtract3(path[1], path[0]));
6
+ }
7
+ if (index === path.length - 1) {
8
+ return normalize3(subtract3(path[index], path[index - 1]));
9
+ }
10
+ return normalize3(subtract3(path[index + 1], path[index - 1]));
11
+ };
12
+ const chooseInitialNormal = (tangent, preferred) => {
13
+ const candidates = preferred
14
+ ? [preferred, [1, 0, 0], [0, 1, 0], [0, 0, 1]]
15
+ : [
16
+ [1, 0, 0],
17
+ [0, 1, 0],
18
+ [0, 0, 1],
19
+ ];
20
+ for (const candidate of candidates) {
21
+ const projected = projectOntoPlane3(candidate, tangent);
22
+ if (length3(projected) > 1e-8) {
23
+ return normalize3(projected);
24
+ }
25
+ }
26
+ throw new Error("Unable to choose an initial path normal.");
27
+ };
28
+ const transportNormal = (normal, previousTangent, tangent) => {
29
+ const rotationAxis = cross3(previousTangent, tangent);
30
+ const sine = length3(rotationAxis);
31
+ let transported = normal;
32
+ if (sine > 1e-10) {
33
+ const axis = scale3(rotationAxis, 1 / sine);
34
+ const cosine = Math.max(-1, Math.min(1, dot3(previousTangent, tangent)));
35
+ transported = rotateAroundAxis3(normal, axis, Math.atan2(sine, cosine));
36
+ }
37
+ return normalize3(projectOntoPlane3(transported, tangent));
38
+ };
39
+ /**
40
+ * Builds rotation-minimizing frames by parallel-transporting the previous normal.
41
+ * This avoids the sudden profile flips produced by picking a fresh up-vector per ring.
42
+ * Paths exceeding maxPathPoints (default 100000) reject before frame allocation or guide evaluation.
43
+ */
44
+ export const computeParallelTransportFrames = (path, initialNormal, normalGuide, limits = {}) => {
45
+ if (path.length < 2) {
46
+ throw new Error("A framed path needs at least two points.");
47
+ }
48
+ assertSamplingCount(path.length, "computeParallelTransportFrames", resolveGeometrySamplingLimits(limits).maxPathPoints);
49
+ const tangents = path.map((_, index) => tangentAt(path, index));
50
+ const firstTangent = tangents[0];
51
+ let normal = chooseInitialNormal(firstTangent, normalGuide?.(path[0], 0) ?? initialNormal);
52
+ const frames = [
53
+ {
54
+ tangent: firstTangent,
55
+ normal,
56
+ binormal: normalize3(cross3(firstTangent, normal)),
57
+ },
58
+ ];
59
+ for (let index = 1; index < tangents.length; index += 1) {
60
+ const previousTangent = tangents[index - 1];
61
+ const tangent = tangents[index];
62
+ const guide = normalGuide?.(path[index], index);
63
+ if (guide === undefined) {
64
+ normal = transportNormal(normal, previousTangent, tangent);
65
+ }
66
+ else {
67
+ const guided = normalize3(projectOntoPlane3(guide, tangent));
68
+ // A guide describes an unoriented plane direction. Preserve the prior
69
+ // sign so noisy guide inputs cannot create a 180-degree seam flip.
70
+ normal = dot3(guided, normal) < 0 ? scale3(guided, -1) : guided;
71
+ }
72
+ // Reprojection removes accumulated floating-point drift.
73
+ normal = normalize3(projectOntoPlane3(normal, tangent));
74
+ frames.push({
75
+ tangent,
76
+ normal,
77
+ binormal: normalize3(cross3(tangent, normal)),
78
+ });
79
+ }
80
+ return frames;
81
+ };
82
+ const closedTangentAt = (path, index) => {
83
+ const previous = path[(index - 1 + path.length) % path.length];
84
+ const next = path[(index + 1) % path.length];
85
+ return normalize3(subtract3(next, previous));
86
+ };
87
+ const signedAngleAround = (from, to, axis) => Math.atan2(dot3(axis, cross3(from, to)), Math.max(-1, Math.min(1, dot3(from, to))));
88
+ /**
89
+ * Builds rotation-minimizing frames for a non-duplicated closed path.
90
+ *
91
+ * Centered cyclic tangents are parallel-transported around the loop. The seam
92
+ * holonomy is measured by transporting the last normal back to the first
93
+ * tangent, then its residual rotation is distributed uniformly over all path
94
+ * intervals. This avoids a single visible seam or an accumulated frame flip.
95
+ * The maxPathPoints ceiling (default100000) bounds path points and frames.
96
+ */
97
+ export const computeClosedParallelTransportFrames = (path, initialNormal, limits = {}) => {
98
+ if (path.length < 3) {
99
+ throw new Error("A closed framed path needs at least three points.");
100
+ }
101
+ assertSamplingCount(path.length, "computeClosedParallelTransportFrames", resolveGeometrySamplingLimits(limits).maxPathPoints);
102
+ const tangents = path.map((_, index) => closedTangentAt(path, index));
103
+ const transportedNormals = [chooseInitialNormal(tangents[0], initialNormal)];
104
+ for (let index = 1; index < tangents.length; index += 1) {
105
+ transportedNormals.push(transportNormal(transportedNormals[index - 1], tangents[index - 1], tangents[index]));
106
+ }
107
+ const seamNormal = transportNormal(transportedNormals.at(-1), tangents.at(-1), tangents[0]);
108
+ const seamCorrection = signedAngleAround(seamNormal, transportedNormals[0], tangents[0]);
109
+ return tangents.map((tangent, index) => {
110
+ const correction = (seamCorrection * index) / path.length;
111
+ const normal = normalize3(projectOntoPlane3(rotateAroundAxis3(transportedNormals[index], tangent, correction), tangent));
112
+ return {
113
+ tangent,
114
+ normal,
115
+ binormal: normalize3(cross3(tangent, normal)),
116
+ };
117
+ });
118
+ };
@@ -0,0 +1,49 @@
1
+ import { assertSamplingCount, resolveGeometrySamplingLimits, } from "../sampling-limits.js";
2
+ /**
3
+ * Creates `segments + 1` points including exact helix endpoints from finite positive dimensions.
4
+ * Integrated weights must remain finite. The maxPathPoints ceiling defaults to 100000; excessive
5
+ * exact counts reject before evaluating axial weights or allocating sample arrays.
6
+ */
7
+ export const sampleHelix = ({ turns, radius, height, segments, limits, axialWeight = () => 1, }) => {
8
+ if (![turns, radius, height].every(Number.isFinite) || !(turns > 0 && radius > 0 && height > 0)) {
9
+ throw new Error("Helix turns, radius, and height must be finite and positive.");
10
+ }
11
+ if (!Number.isInteger(segments) || segments < 3) {
12
+ throw new Error("A helix needs at least three segments.");
13
+ }
14
+ assertSamplingCount(segments + 1, "sampleHelix", resolveGeometrySamplingLimits(limits).maxPathPoints);
15
+ const angleRange = Math.PI * 2 * turns;
16
+ if (!Number.isFinite(angleRange)) {
17
+ throw new Error("Helix angle range must be finite.");
18
+ }
19
+ const weights = Array.from({ length: segments }, (_, index) => {
20
+ const value = axialWeight((index + 0.5) / segments);
21
+ if (!Number.isFinite(value) || value <= 0) {
22
+ throw new Error("Helix axial weights must be finite and positive.");
23
+ }
24
+ return value;
25
+ });
26
+ const totalWeight = weights.reduce((sum, weight) => sum + weight, 0);
27
+ if (!Number.isFinite(totalWeight)) {
28
+ throw new Error("Helix integrated axial weight must be finite.");
29
+ }
30
+ let accumulatedWeight = 0;
31
+ const result = [];
32
+ for (let index = 0; index <= segments; index += 1) {
33
+ if (index > 0) {
34
+ accumulatedWeight += weights[index - 1];
35
+ }
36
+ const t = index / segments;
37
+ const angle = angleRange * t;
38
+ const point = [
39
+ radius * Math.cos(angle),
40
+ radius * Math.sin(angle),
41
+ height * (accumulatedWeight / totalWeight),
42
+ ];
43
+ if (!point.every(Number.isFinite)) {
44
+ throw new Error("Helix sampled coordinates must be finite.");
45
+ }
46
+ result.push(point);
47
+ }
48
+ return result;
49
+ };
@@ -0,0 +1,32 @@
1
+ import { length3, subtract3 } from "../math/vector3.js";
2
+ import { assertSamplingCount, resolveGeometrySamplingLimits, } from "../sampling-limits.js";
3
+ /** Returns cumulative arc length for finite points under maxPathPoints (default 100000); rejects overflowing length. */
4
+ export const cumulativePathLengths = (path, limits = {}) => {
5
+ assertSamplingCount(path.length, "cumulativePathLengths", resolveGeometrySamplingLimits(limits).maxPathPoints);
6
+ if (path.length === 0) {
7
+ return [];
8
+ }
9
+ for (const point of path) {
10
+ if (!point.every(Number.isFinite)) {
11
+ throw new Error("Path coordinates must be finite.");
12
+ }
13
+ }
14
+ const cumulative = [0];
15
+ for (let index = 1; index < path.length; index += 1) {
16
+ const previous = path[index - 1];
17
+ const point = path[index];
18
+ if (previous === undefined || point === undefined) {
19
+ throw new Error("Path indexing invariant failed.");
20
+ }
21
+ const distance = cumulative[index - 1] + length3(subtract3(point, previous));
22
+ if (!Number.isFinite(distance)) {
23
+ throw new Error("Path cumulative length must be finite.");
24
+ }
25
+ cumulative.push(distance);
26
+ }
27
+ return cumulative;
28
+ };
29
+ /**
30
+ * Returns total sampled 3D path length, or zero for an empty path.
31
+ */
32
+ export const pathLength = (path, limits = {}) => cumulativePathLengths(path, limits).at(-1) ?? 0;