@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.
Files changed (129) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/README.md +313 -6
  3. package/THIRD_PARTY_NOTICES.md +153 -0
  4. package/dist/api/engine.d.ts +54 -0
  5. package/dist/api/geometry.d.ts +2912 -0
  6. package/dist/api/index.d.ts +1648 -0
  7. package/dist/api/text.d.ts +77 -0
  8. package/dist/engine/construction-operations.js +189 -0
  9. package/dist/engine/execution.js +227 -0
  10. package/dist/engine/export-3mf.js +100 -0
  11. package/dist/engine/extrusion-finishing.js +82 -0
  12. package/dist/engine/finalization.js +214 -0
  13. package/dist/engine/index.js +72 -0
  14. package/dist/engine/inspection-operations.js +52 -0
  15. package/dist/engine/kernel/LICENSE +201 -0
  16. package/dist/engine/kernel/manifold.js +2 -0
  17. package/dist/engine/kernel/manifold.wasm +0 -0
  18. package/dist/engine/loft-operations.js +156 -0
  19. package/dist/engine/mechanical-operations.js +51 -0
  20. package/dist/engine/modeling-context.js +38 -0
  21. package/dist/engine/native/adapter.js +20 -0
  22. package/dist/engine/native/context.js +209 -0
  23. package/dist/engine/native/copied-views.js +46 -0
  24. package/dist/engine/native/index.js +13 -0
  25. package/dist/engine/native/operations.js +115 -0
  26. package/dist/engine/native/ownership.js +98 -0
  27. package/dist/engine/native/regions.js +138 -0
  28. package/dist/engine/native/scoped-primitives.js +127 -0
  29. package/dist/engine/native/scoped-threads.js +34 -0
  30. package/dist/engine/native/threads.js +52 -0
  31. package/dist/engine/native/transforms.js +111 -0
  32. package/dist/engine/operation-environment.js +20 -0
  33. package/dist/engine/pattern-operations.js +36 -0
  34. package/dist/engine/pattern-placements.js +176 -0
  35. package/dist/engine/primitive-anchors.js +70 -0
  36. package/dist/engine/primitive-correspondence.js +74 -0
  37. package/dist/engine/primitive-operations.js +151 -0
  38. package/dist/engine/primitive-profile-types.js +1 -0
  39. package/dist/engine/primitive-profiles.js +250 -0
  40. package/dist/engine/profile-mesh.js +279 -0
  41. package/dist/engine/section-operations.js +93 -0
  42. package/dist/engine/shape-anchors.js +88 -0
  43. package/dist/engine/transform-operations.js +225 -0
  44. package/dist/engine/types.js +1 -0
  45. package/dist/geometry/anchors.js +226 -0
  46. package/dist/geometry/curves/evaluation.js +162 -0
  47. package/dist/geometry/curves/sampling.js +265 -0
  48. package/dist/geometry/curves/types.js +1 -0
  49. package/dist/geometry/cylinder.js +117 -0
  50. package/dist/geometry/edge-finishing.js +160 -0
  51. package/dist/geometry/errors.js +76 -0
  52. package/dist/geometry/extrusion.js +150 -0
  53. package/dist/geometry/heightfield.js +107 -0
  54. package/dist/geometry/index.js +42 -0
  55. package/dist/geometry/loft-correspondence.js +204 -0
  56. package/dist/geometry/loft-geometry.js +184 -0
  57. package/dist/geometry/loft-options.js +56 -0
  58. package/dist/geometry/loft-simple-mesh.js +92 -0
  59. package/dist/geometry/loft-triangulation.js +64 -0
  60. package/dist/geometry/loft.js +80 -0
  61. package/dist/geometry/math/vector3.js +41 -0
  62. package/dist/geometry/mechanical/gear-motion.js +94 -0
  63. package/dist/geometry/mechanical/gears.js +484 -0
  64. package/dist/geometry/mechanical/index.js +13 -0
  65. package/dist/geometry/mechanical/standards.js +67 -0
  66. package/dist/geometry/mechanical/thread-helices.js +44 -0
  67. package/dist/geometry/mechanical/thread-mesh-plan.js +358 -0
  68. package/dist/geometry/mechanical/thread-mesh.js +142 -0
  69. package/dist/geometry/mechanical/thread-profiles.js +368 -0
  70. package/dist/geometry/mechanical/thread-solids.js +230 -0
  71. package/dist/geometry/mechanical/thread-surface.js +433 -0
  72. package/dist/geometry/mechanical/threads.js +138 -0
  73. package/dist/geometry/mesh-planar.js +408 -0
  74. package/dist/geometry/mesh-quality.js +396 -0
  75. package/dist/geometry/mesh-signature.js +137 -0
  76. package/dist/geometry/mesh.js +300 -0
  77. package/dist/geometry/model-limits.js +50 -0
  78. package/dist/geometry/numeric.js +11 -0
  79. package/dist/geometry/paths/frames.js +118 -0
  80. package/dist/geometry/paths/helix.js +49 -0
  81. package/dist/geometry/paths/path.js +30 -0
  82. package/dist/geometry/paths/planar.js +135 -0
  83. package/dist/geometry/paths/resampling.js +124 -0
  84. package/dist/geometry/polygon-clipping.js +52 -0
  85. package/dist/geometry/primitive-options.js +1 -0
  86. package/dist/geometry/primitive-resolution.js +17 -0
  87. package/dist/geometry/primitive-resolvers.js +235 -0
  88. package/dist/geometry/profiles/matched.js +184 -0
  89. package/dist/geometry/profiles/open-profile.js +137 -0
  90. package/dist/geometry/profiles/profiles.js +70 -0
  91. package/dist/geometry/profiles/rectangle.js +235 -0
  92. package/dist/geometry/profiles/shapes.js +34 -0
  93. package/dist/geometry/profiles/span-sampling.js +28 -0
  94. package/dist/geometry/profiles/types.js +1 -0
  95. package/dist/geometry/profiles/validation.js +20 -0
  96. package/dist/geometry/regions.js +371 -0
  97. package/dist/geometry/resolution.js +236 -0
  98. package/dist/geometry/revolve/revolve-mesh.js +123 -0
  99. package/dist/geometry/sampling-limits.js +29 -0
  100. package/dist/geometry/stroke-path.js +181 -0
  101. package/dist/geometry/svg/errors.js +23 -0
  102. package/dist/geometry/svg/fill.js +172 -0
  103. package/dist/geometry/svg/index.js +1 -0
  104. package/dist/geometry/svg/matrix.js +130 -0
  105. package/dist/geometry/svg/parser.js +536 -0
  106. package/dist/geometry/svg/path.js +349 -0
  107. package/dist/geometry/svg/types.js +1 -0
  108. package/dist/geometry/svg-import.js +662 -0
  109. package/dist/geometry/sweeps/options.js +28 -0
  110. package/dist/geometry/sweeps/orientation.js +106 -0
  111. package/dist/geometry/sweeps/stations.js +161 -0
  112. package/dist/geometry/sweeps/sweep-closed.js +96 -0
  113. package/dist/geometry/sweeps/sweep-mesh.js +20 -0
  114. package/dist/geometry/sweeps/sweep-open.js +76 -0
  115. package/dist/geometry/sweeps/validation.js +116 -0
  116. package/dist/geometry/text-contours.js +32 -0
  117. package/dist/geometry/text-layout.js +1 -0
  118. package/dist/geometry/tolerance.js +53 -0
  119. package/dist/geometry/transform-geometry.js +41 -0
  120. package/dist/geometry/transform3.js +340 -0
  121. package/dist/geometry/types.js +1 -0
  122. package/dist/index.js +1 -0
  123. package/dist/model/execution-binding.js +30 -0
  124. package/dist/model/operations.js +152 -0
  125. package/dist/model/options.js +1 -0
  126. package/dist/model/types.js +1 -0
  127. package/dist/text/index.js +48 -0
  128. package/dist/text/vector-text-layout.js +279 -0
  129. package/package.json +89 -8
@@ -0,0 +1,150 @@
1
+ import { GeometryError } from "./errors.js";
2
+ import { assertPositivePrimitive, assertPrimitiveOptions } from "./primitive-resolvers.js";
3
+ import { resolveGeometryResolution } from "./resolution.js";
4
+ import { assertSamplingCount, resolveGeometrySamplingLimits } from "./sampling-limits.js";
5
+ const isArray = (value) => Array.isArray(value);
6
+ /** @internal Validates shared extrusion controls and expands top scale before kernel allocation. */
7
+ export function resolveExtrusionControls(height, options, operation) {
8
+ assertPrimitiveOptions(options, operation);
9
+ assertPositivePrimitive(height, "height", operation);
10
+ if (options.center !== undefined && typeof options.center !== "boolean") {
11
+ throw new GeometryError("INVALID_ARGUMENT", "center must be boolean when supplied.", {
12
+ operation,
13
+ });
14
+ }
15
+ const twistDegrees = options.twistDegrees ?? 0;
16
+ if (!Number.isFinite(twistDegrees)) {
17
+ throw new GeometryError("NON_FINITE_VALUE", "twistDegrees must be finite.", { operation });
18
+ }
19
+ const scaleTop = options.scaleTop ?? 1;
20
+ if (typeof scaleTop !== "number" && (!Array.isArray(scaleTop) || scaleTop.length !== 2)) {
21
+ throw new GeometryError("INVALID_ARGUMENT", "scaleTop must be a number or an exact XY tuple.", {
22
+ operation,
23
+ });
24
+ }
25
+ const scales = typeof scaleTop === "number" ? [scaleTop, scaleTop] : scaleTop;
26
+ if (!Number.isFinite(scales[0]) || !Number.isFinite(scales[1])) {
27
+ throw new GeometryError("NON_FINITE_VALUE", "scaleTop values must be finite.", { operation });
28
+ }
29
+ if (options.divisions !== undefined &&
30
+ (!Number.isSafeInteger(options.divisions) || options.divisions < 0)) {
31
+ throw new GeometryError("INVALID_ARGUMENT", "divisions must be a nonnegative safe integer.", {
32
+ operation,
33
+ });
34
+ }
35
+ if (options.divisions !== undefined)
36
+ assertSamplingCount(options.divisions + 2, operation, resolveGeometrySamplingLimits(options.limits).maxPathPoints);
37
+ return { scales, twistDegrees, center: options.center ?? false };
38
+ }
39
+ /**
40
+ * Resolves intermediate profile copies for linear extrusion from physical curve resolution.
41
+ *
42
+ * Accepts one XY loop or a filled XY region, including every hole and disconnected component.
43
+ * Exact `divisions`, including zero, override physical sampling. Otherwise chord error bounds
44
+ * the triangulated side surface, including rotation across each profile edge. Optional segment
45
+ * length bounds station travel including Z. Straight extrusions need no intermediate copies by default.
46
+ * Exceeding the selected maxPathPoints ceiling raises `COMPLEXITY_LIMIT` instead of reducing fidelity.
47
+ *
48
+ * @param profile - Ordered XY millimetre contours with implicit closing edges.
49
+ * @param height - Positive extrusion height in millimetres.
50
+ * @param options - Twist, scale, exact divisions, and physical resolution controls.
51
+ * @returns Number of intermediate copies, excluding the two end profiles.
52
+ */
53
+ export function resolveExtrusionDivisions(profile, height, options = {}) {
54
+ if (!isArray(profile) || profile.length === 0)
55
+ throw new GeometryError("INVALID_ARGUMENT", "Extrusion sampling needs an XY loop or region.", {
56
+ operation: "resolveExtrusionDivisions",
57
+ });
58
+ const contours = [];
59
+ if (Array.isArray(profile[0]))
60
+ contours.push(profile);
61
+ else {
62
+ let count = 0;
63
+ const maximum = resolveGeometrySamplingLimits(options.limits).maxProfilePoints;
64
+ for (const component of profile) {
65
+ if (typeof component !== "object" ||
66
+ component === null ||
67
+ !isArray(component.outer) ||
68
+ !isArray(component.holes))
69
+ throw new GeometryError("INVALID_ARGUMENT", "Extrusion regions need outer and hole contours.", {
70
+ operation: "resolveExtrusionDivisions",
71
+ });
72
+ assertSamplingCount(count + 3 * (component.holes.length + 1), "resolveExtrusionDivisions", maximum);
73
+ for (const contour of [component.outer, ...component.holes]) {
74
+ if (!isArray(contour) || contour.length < 3)
75
+ throw new GeometryError("INVALID_ARGUMENT", "Extrusion contours need at least three XY points.", {
76
+ operation: "resolveExtrusionDivisions",
77
+ });
78
+ count += contour.length;
79
+ assertSamplingCount(count, "resolveExtrusionDivisions", maximum);
80
+ contours.push(contour);
81
+ }
82
+ }
83
+ }
84
+ return resolveExtrusionContourDivisions(contours, height, options);
85
+ }
86
+ /** @internal Shared sampling preflight over explicit contours, without rebuilding filled topology. */
87
+ export function resolveExtrusionContourDivisions(contours, height, options) {
88
+ const operation = "resolveExtrusionDivisions";
89
+ const { scales, twistDegrees } = resolveExtrusionControls(height, options, operation);
90
+ const limits = resolveGeometrySamplingLimits(options.limits);
91
+ let radius = 0, maximumEdgeLength = 0, profileCount = 0;
92
+ for (const contour of contours) {
93
+ if (!isArray(contour) || contour.length < 3)
94
+ throw new GeometryError("INVALID_ARGUMENT", "Extrusion contours need at least three XY points.", {
95
+ operation,
96
+ });
97
+ profileCount += contour.length;
98
+ assertSamplingCount(profileCount, operation, limits.maxProfilePoints);
99
+ for (let index = 0; index < contour.length; index++) {
100
+ const point = contour[index];
101
+ if (!isArray(point) ||
102
+ point.length !== 2 ||
103
+ !Number.isFinite(point[0]) ||
104
+ !Number.isFinite(point[1]))
105
+ throw new GeometryError("NON_FINITE_VALUE", "Extrusion profiles require finite XY points.", {
106
+ operation,
107
+ });
108
+ radius = Math.max(radius, Math.hypot(...point));
109
+ }
110
+ for (let index = 0; index < contour.length; index++) {
111
+ const point = contour[index], next = contour[(index + 1) % contour.length];
112
+ maximumEdgeLength = Math.max(maximumEdgeLength, Math.hypot(next[0] - point[0], next[1] - point[1]));
113
+ }
114
+ }
115
+ if (options.divisions !== undefined)
116
+ return options.divisions;
117
+ const policy = resolveGeometryResolution(options.resolution, options.limits);
118
+ const angle = (Math.abs(twistDegrees) * Math.PI) / 180;
119
+ const maximumScale = Math.max(1, Math.abs(scales[0]), Math.abs(scales[1]));
120
+ const scaleChange = Math.max(Math.abs(scales[0] - 1), Math.abs(scales[1] - 1));
121
+ // A linear scale multiplied by a rotation has second derivative bounded by this magnitude.
122
+ // The interpolation error is at most M / (8*n*n), including nonuniform top scaling.
123
+ const curvature = radius * (maximumScale * angle * angle + 2 * scaleChange * angle);
124
+ const a = curvature / 8,
125
+ // Uniform untwisted taper is planar. Anisotropic taper can introduce a
126
+ // mixed edge/station derivative even when the vertex trajectories are straight.
127
+ b = (maximumEdgeLength * (maximumScale * angle + Math.abs(scales[0] - scales[1]))) / 4;
128
+ const chordSegments = policy.circularSegments === undefined
129
+ ? Math.max(1, Math.ceil((b + Math.hypot(b, 2 * Math.sqrt(a * policy.maxChordError))) /
130
+ (2 * policy.maxChordError)), Math.ceil((policy.minimumSegments * Math.abs(twistDegrees)) / 360))
131
+ : Math.max(1, Math.ceil((policy.circularSegments * Math.abs(twistDegrees)) / 360));
132
+ const angularCap = Math.max(1, scales[0] === scales[1] ? 0 : policy.maximumSegments, Math.ceil((policy.maximumSegments * Math.abs(twistDegrees)) / 360));
133
+ if (chordSegments > angularCap) {
134
+ throw new GeometryError("COMPLEXITY_LIMIT", "Extrusion surface refinement exceeds the full-circle interval safety limit.", {
135
+ operation,
136
+ details: { requiredSegments: chordSegments, maximumSegments: angularCap },
137
+ });
138
+ }
139
+ const angularTravel = radius * maximumScale * angle;
140
+ const scaleTravel = radius * scaleChange;
141
+ // Uniform scaling keeps radial and rotational velocities perpendicular. Anisotropic scaling
142
+ // can align them, so its XY travel needs the triangle inequality rather than a hypot bound.
143
+ const travel = scales[0] === scales[1]
144
+ ? Math.hypot(height, angularTravel, scaleTravel)
145
+ : Math.hypot(height, angularTravel + scaleTravel);
146
+ const lengthSegments = policy.maxSegmentLength === undefined ? 1 : Math.ceil(travel / policy.maxSegmentLength);
147
+ const segments = Math.max(1, chordSegments, lengthSegments);
148
+ assertSamplingCount(segments + 1, operation, resolveGeometrySamplingLimits(options.limits).maxPathPoints);
149
+ return segments - 1;
150
+ }
@@ -0,0 +1,107 @@
1
+ import { GeometryError } from "./errors.js";
2
+ import { validateTriangleMesh } from "./mesh.js";
3
+ import { defaultModelingLimits } from "./model-limits.js";
4
+ import { writeMeshVertex } from "./numeric.js";
5
+ const isArray = (value) => Array.isArray(value);
6
+ /** Checks regular-grid dimensions and bounds allocation before evaluating any height samples. */
7
+ export function preflightHeightfield(options, limits = {}) {
8
+ const operation = "preflightHeightfield";
9
+ if (typeof options !== "object" ||
10
+ options === null ||
11
+ !isArray(options.size) ||
12
+ options.size.length !== 2 ||
13
+ !options.size.every((value) => Number.isFinite(value) && value > 0) ||
14
+ !isArray(options.segments) ||
15
+ options.segments.length !== 2 ||
16
+ !options.segments.every((value) => Number.isSafeInteger(value) && value > 0) ||
17
+ typeof options.height !== "function" ||
18
+ !Number.isFinite(options.baseZ ?? 0) ||
19
+ (options.origin !== undefined &&
20
+ (!isArray(options.origin) ||
21
+ options.origin.length !== 2 ||
22
+ !options.origin.every(Number.isFinite)))) {
23
+ throw new GeometryError("INVALID_ARGUMENT", "Heightfields need a finite positive footprint, integer intervals, finite base, and a height callback.", { operation });
24
+ }
25
+ const [x, y] = options.segments;
26
+ const sampleCount = (x + 1) * (y + 1);
27
+ const perimeter = 2 * (x + y);
28
+ const vertexCount = sampleCount + perimeter + 1;
29
+ const triangleCount = 2 * x * y + 3 * perimeter;
30
+ const byteCount = 12 * (vertexCount + triangleCount);
31
+ const policy = { ...defaultModelingLimits, ...limits };
32
+ if (![policy.maxVertices, policy.maxTriangles, policy.maxBytes].every((value) => Number.isSafeInteger(value) && value > 0))
33
+ throw new GeometryError("INVALID_ARGUMENT", "Heightfield ceilings must be positive safe integers.", { operation });
34
+ if (!Number.isSafeInteger(byteCount) ||
35
+ vertexCount > 0xffff_ffff ||
36
+ triangleCount * 3 > 0xffff_ffff ||
37
+ vertexCount > policy.maxVertices ||
38
+ triangleCount > policy.maxTriangles ||
39
+ byteCount > policy.maxBytes) {
40
+ throw new GeometryError("COMPLEXITY_LIMIT", "Heightfield grid exceeds the configured output limits or numeric index ceiling.", { operation });
41
+ }
42
+ return Object.freeze({ sampleCount, vertexCount, triangleCount, byteCount });
43
+ }
44
+ /**
45
+ * Builds a closed indexed solid from a regularly sampled top, matched boundary walls, and flat base.
46
+ * Heights are evaluated once per grid point after allocation preflight. Physical XY sizes and all
47
+ * output coordinates must fit Float32 storage. Shared wall vertices prevent cracks at the base.
48
+ */
49
+ export function heightfieldMesh(options, limits = {}) {
50
+ const facts = preflightHeightfield(options, limits);
51
+ const operation = "heightfieldMesh";
52
+ const [nx, ny] = options.segments;
53
+ const [width, depth] = options.size;
54
+ const [originX, originY] = options.origin ?? [-width / 2, -depth / 2];
55
+ const baseZ = Math.fround(options.baseZ ?? 0);
56
+ if (!Number.isFinite(baseZ))
57
+ throw new GeometryError("NON_FINITE_VALUE", "Heightfield base must fit Float32 storage.", {
58
+ operation,
59
+ });
60
+ const positions = new Float32Array(facts.vertexCount * 3);
61
+ const indices = new Uint32Array(facts.triangleCount * 3);
62
+ const grid = (x, y) => y * (nx + 1) + x;
63
+ for (let y = 0; y <= ny; y += 1) {
64
+ for (let x = 0; x <= nx; x += 1) {
65
+ const point = [originX + (width * x) / nx, originY + (depth * y) / ny];
66
+ const height = Math.fround(options.height(point));
67
+ if (!Number.isFinite(height) || !(height > baseZ)) {
68
+ throw new GeometryError("INVALID_ARGUMENT", "Every finite Float32 height must be strictly above the Float32 base.", { operation, details: { x, y } });
69
+ }
70
+ writeMeshVertex(positions, grid(x, y) * 3, [point[0], point[1], height], operation);
71
+ }
72
+ }
73
+ let cursor = 0;
74
+ const triangle = (a, b, c) => {
75
+ indices[cursor++] = a;
76
+ indices[cursor++] = b;
77
+ indices[cursor++] = c;
78
+ };
79
+ for (let y = 0; y < ny; y += 1) {
80
+ for (let x = 0; x < nx; x += 1) {
81
+ const a = grid(x, y), b = grid(x + 1, y), c = grid(x + 1, y + 1), d = grid(x, y + 1);
82
+ triangle(a, b, c);
83
+ triangle(a, c, d);
84
+ }
85
+ }
86
+ const boundary = [];
87
+ for (let x = 0; x < nx; x += 1)
88
+ boundary.push(grid(x, 0));
89
+ for (let y = 0; y < ny; y += 1)
90
+ boundary.push(grid(nx, y));
91
+ for (let x = nx; x > 0; x -= 1)
92
+ boundary.push(grid(x, ny));
93
+ for (let y = ny; y > 0; y -= 1)
94
+ boundary.push(grid(0, y));
95
+ const baseStart = facts.sampleCount;
96
+ const baseCenter = facts.vertexCount - 1;
97
+ writeMeshVertex(positions, baseCenter * 3, [originX + width / 2, originY + depth / 2, baseZ], operation);
98
+ for (let offset = 0; offset < boundary.length; offset += 1) {
99
+ const top = boundary[offset];
100
+ writeMeshVertex(positions, (baseStart + offset) * 3, [positions[top * 3], positions[top * 3 + 1], baseZ], operation);
101
+ const next = (offset + 1) % boundary.length;
102
+ triangle(top, baseStart + offset, baseStart + next);
103
+ triangle(top, baseStart + next, boundary[next]);
104
+ triangle(baseCenter, baseStart + next, baseStart + offset);
105
+ }
106
+ return validateTriangleMesh({ positions, indices }, { requireClosed: true });
107
+ }
@@ -0,0 +1,42 @@
1
+ /** Pure geometry foundations without host initialization. @packageDocumentation */
2
+ export { matchProfiles, } from "./profiles/matched.js";
3
+ export { heightfieldMesh, preflightHeightfield, } from "./heightfield.js";
4
+ export { preflightThreadedRodSolid as preflightThreadedRod, preflightInternalThreadCutterSolid as preflightInternalThreadCutter, preflightThreadedNutSolid as preflightThreadedNut, } from "./mechanical/thread-solids.js";
5
+ export { anchorFrame3, attachTransform, boxAnchor3, createAnchor3, cylinderAnchor3, } from "./anchors.js";
6
+ export { defaultGeometrySamplingLimits, resolveGeometrySamplingLimits, } from "./sampling-limits.js";
7
+ export { GeometryError, isGeometryError, } from "./errors.js";
8
+ export { resolveExtrusionDivisions, } from "./extrusion.js";
9
+ export { extrudeMesh, loftMesh, triangulateLoop2, } from "./loft.js";
10
+ export { add3, cross3, dot3, length3, lerp3, normalize3, projectOntoPlane3, rotateAroundAxis3, scale3, subtract3, } from "./math/vector3.js";
11
+ export { minimumCylindricalGearToothCountWithoutUndercut, resolveCylindricalGear, resolveCylindricalGearPair, resolveRackPitch, sampleCylindricalGearOutline, sampleInternalCylindricalGearSpaceOutline, sampleRackToothPath, } from "./mechanical/gears.js";
12
+ export { measureInternalCylindricalGearPairMotion, } from "./mechanical/gear-motion.js";
13
+ export { METRIC_FASTENER_STANDARDS, getMetricClearanceHole, getMetricFastenerStandard, } from "./mechanical/standards.js";
14
+ export { buildHelicalThreadRidgeMesh, buildHelicalThreadSolidMesh, } from "./mechanical/thread-mesh.js";
15
+ export { compensateThreadProfileAxially, compensateThreadProfileForFaceting, createThreadProfile, } from "./mechanical/thread-profiles.js";
16
+ export { preflightHelicalThreadSolidMesh, } from "./mechanical/thread-mesh-plan.js";
17
+ export { sampleConventionalThreadHelices as sampleThreadHelices, } from "./mechanical/thread-helices.js";
18
+ export { PRINTABLE_THREAD_FIT_POLICIES, compensateThreadDiameters, resolvePrintableThreadFitPolicy, resolveThreadCircularSegments, } from "./mechanical/threads.js";
19
+ export { analyzeTriangleMesh, joinTriangleMeshes, measureTriangleMesh, meshBounds, reverseTriangleMesh, transformTriangleMesh, validateTriangleMesh, } from "./mesh.js";
20
+ export { createTriangleMeshSignature, } from "./mesh-signature.js";
21
+ export { optimizePlanarPatches, } from "./mesh-planar.js";
22
+ export { analyzeTriangleMeshQuality, } from "./mesh-quality.js";
23
+ export { computeClosedParallelTransportFrames, computeParallelTransportFrames, } from "./paths/frames.js";
24
+ export { sampleHelix } from "./paths/helix.js";
25
+ export { cumulativePathLengths, pathLength } from "./paths/path.js";
26
+ export { distributeAlongPath, roundClosedPolylinePath, } from "./paths/planar.js";
27
+ export { circleLoop, rectangleLoop, regularPolygonLoop, starLoop, } from "./profiles/profiles.js";
28
+ export { sampleOpenProfile, } from "./profiles/open-profile.js";
29
+ export { ellipseLoop } from "./profiles/shapes.js";
30
+ export { rectangleProfile, } from "./profiles/rectangle.js";
31
+ export { loopContainsPoint, normalizeLoop2, normalizePolygon2, normalizeRegion2, regionArea, regionBounds, signedAreaLoop2, } from "./regions.js";
32
+ export { resolveAlignedCircularSegments, resolveArcSegments, resolveCircularSegments, } from "./resolution.js";
33
+ export { revolveMesh } from "./revolve/revolve-mesh.js";
34
+ export { preflightSvgRegion, } from "./svg-import.js";
35
+ export { strokePathRegion, } from "./stroke-path.js";
36
+ export { sweepMesh } from "./sweeps/sweep-mesh.js";
37
+ export { DEFAULT_GEOMETRY_TOLERANCE, resolveGeometryTolerance, } from "./tolerance.js";
38
+ export { transformLoop2, transformPath2, transformPath3, transformRegion2, } from "./transform-geometry.js";
39
+ export { IDENTITY_TRANSFORM3, applyTransformDirection3, applyTransformPoint3, composeTransform3, eulerTransform3, frameTransform3, inverseTransform3, rotationTransform3, scaleTransform3, transformDeterminant3, translationTransform3, validateTransform3, } from "./transform3.js";
40
+ export { sampleTextContours } from "./text-contours.js";
41
+ export { sampleCurve2, sampleCurve3 } from "./curves/sampling.js";
42
+ export { resamplePolyline2, resamplePolyline3, } from "./paths/resampling.js";
@@ -0,0 +1,204 @@
1
+ import { GeometryError } from "./errors.js";
2
+ import { assertMeshAllocationWithinLimits } from "./model-limits.js";
3
+ import { assertSamplingCount } from "./sampling-limits.js";
4
+ const maximumComparisons = 8_000_000;
5
+ const fail = (message) => {
6
+ throw new GeometryError("INVALID_ARGUMENT", message, { operation: "loft" });
7
+ };
8
+ const consume = (budget, count) => {
9
+ budget.comparisons += count;
10
+ assertSamplingCount(budget.comparisons, "loft", maximumComparisons, "Loft correspondence exceeds its eight-million-comparison work limit.");
11
+ };
12
+ /** @internal Checks a propagated column grid before allocating any expanded rows. */
13
+ export function assertLoftGrid(count, rows, otherContours, limits) {
14
+ const perRow = count + otherContours;
15
+ assertSamplingCount(perRow, "loft", limits.maxProfilePoints);
16
+ assertSamplingCount(perRow * rows, "loft", 1_000_000, "Loft profile grids exceed the one-million-logical-slot work limit.");
17
+ const vertices = perRow * rows;
18
+ assertMeshAllocationWithinLimits(vertices, 0, 12 * vertices, "loft", limits);
19
+ }
20
+ /** @internal Retains every input vertex while distributing extra intervals deterministically. */
21
+ export function loftSubdivisions(row, target, distribution, maxLength) {
22
+ if (!Number.isSafeInteger(target) || target < row.points.length)
23
+ fail("Loft refinement cannot remove original vertices.");
24
+ const lengths = row.world.map((point, index) => {
25
+ const next = row.world[(index + 1) % row.world.length];
26
+ return Math.hypot(next[0] - point[0], next[1] - point[1], next[2] - point[2]);
27
+ });
28
+ const perimeter = lengths.reduce((sum, length) => sum + length, 0);
29
+ if (!Number.isFinite(perimeter) || perimeter <= 0)
30
+ fail("Loft contours need a finite positive perimeter.");
31
+ const extra = target - row.points.length;
32
+ const shares = lengths.map((length) => extra * (distribution === "segments" ? 1 / lengths.length : length / perimeter));
33
+ const counts = shares.map((share) => 1 + Math.floor(share));
34
+ const order = shares
35
+ .map((share, index) => ({ index, fraction: share - Math.floor(share) }))
36
+ .sort((a, b) => b.fraction - a.fraction || a.index - b.index);
37
+ const remaining = target - counts.reduce((sum, count) => sum + count, 0);
38
+ for (let index = 0; index < remaining; index++)
39
+ counts[order[index].index]++;
40
+ if (maxLength !== undefined)
41
+ lengths.forEach((length, index) => {
42
+ counts[index] = Math.max(counts[index], Math.ceil(length / maxLength));
43
+ });
44
+ return counts;
45
+ }
46
+ const interpolate2 = (a, b, t) => [
47
+ a[0] * (1 - t) + b[0] * t,
48
+ a[1] * (1 - t) + b[1] * t,
49
+ ];
50
+ const interpolate3 = (a, b, t) => [
51
+ a[0] * (1 - t) + b[0] * t,
52
+ a[1] * (1 - t) + b[1] * t,
53
+ a[2] * (1 - t) + b[2] * t,
54
+ ];
55
+ /** @internal Applies the same column insertions to local and world data. */
56
+ export function subdivideLoftRow(row, counts) {
57
+ const points = [], world = [];
58
+ counts.forEach((count, index) => {
59
+ const next = (index + 1) % counts.length;
60
+ for (let step = 0; step < count; step++) {
61
+ points.push(step === 0
62
+ ? row.points[index]
63
+ : interpolate2(row.points[index], row.points[next], step / count));
64
+ world.push(step === 0
65
+ ? row.world[index]
66
+ : interpolate3(row.world[index], row.world[next], step / count));
67
+ }
68
+ });
69
+ return { points, world };
70
+ }
71
+ const distance = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
72
+ const rotate = (values, offset) => values.slice(offset).concat(values.slice(0, offset));
73
+ const phaseRow = (row, offset) => ({
74
+ points: rotate(row.points, offset),
75
+ world: rotate(row.world, offset),
76
+ });
77
+ const indexedRow = (row, indices) => ({
78
+ points: indices.map((index) => row.points[index]),
79
+ world: indices.map((index) => row.world[index]),
80
+ });
81
+ /** @internal Selects a cyclic shift by total placed edge distance; ties keep the lowest offset. */
82
+ export function reindexLoftRow(a, b, budget) {
83
+ consume(budget, a.points.length * b.points.length);
84
+ let best = Infinity, offset = 0;
85
+ for (let phase = 0; phase < b.points.length; phase++) {
86
+ let cost = 0;
87
+ for (let index = 0; index < a.points.length; index++)
88
+ cost += distance(a.world[index], b.world[(index + phase) % b.points.length]);
89
+ if (cost < best) {
90
+ best = cost;
91
+ offset = phase;
92
+ }
93
+ }
94
+ return phaseRow(b, offset);
95
+ }
96
+ // Ordered cyclic dynamic programming, including the wrap node once. Each move advances
97
+ // one or both contours; a stationary side repeats its vertex to create a triangular fan.
98
+ function distanceCost(a, b, phase, costs, moves) {
99
+ const n = a.points.length, m = b.points.length, stride = m + 1;
100
+ for (let i = 0; i <= n; i++) {
101
+ const current = (i % 2) * stride, previous = ((i + 1) % 2) * stride;
102
+ costs.fill(Infinity, current, current + stride);
103
+ for (let j = 0; j <= m; j++) {
104
+ if (i === 0 && j === 0) {
105
+ costs[current] = distance(a.world[0], b.world[phase]);
106
+ continue;
107
+ }
108
+ const index = i * stride + j;
109
+ const diagonal = i > 0 && j > 0 ? costs[previous + j - 1] : Infinity;
110
+ const up = i > 0 ? costs[previous + j] : Infinity;
111
+ const left = j > 0 ? costs[current + j - 1] : Infinity;
112
+ const best = Math.min(diagonal, up, left);
113
+ moves[index] = diagonal === best ? 0 : up === best ? 1 : 2;
114
+ costs[current + j] =
115
+ best + (i === n && j === m ? 0 : distance(a.world[i % n], b.world[(j + phase) % m]));
116
+ }
117
+ }
118
+ return costs[(n % 2) * stride + m];
119
+ }
120
+ function traceDistanceMap(moves, n, m) {
121
+ const stride = m + 1;
122
+ const previous = [], next = [];
123
+ let i = n, j = m;
124
+ while (i !== 0 || j !== 0) {
125
+ const move = moves[i * stride + j];
126
+ if (move !== 2)
127
+ i--;
128
+ if (move !== 1)
129
+ j--;
130
+ previous.push(i % n);
131
+ next.push(j % m);
132
+ }
133
+ return { previous: previous.reverse(), next: next.reverse() };
134
+ }
135
+ /** @internal Seam-anchored or cyclic-phase distance association, always bounded before DP allocation. */
136
+ export function distanceLoftMap(a, b, method, budget) {
137
+ const phases = method === "distance" ? b.points.length : 1;
138
+ consume(budget, (a.points.length + 1) * (b.points.length + 1) * phases);
139
+ const n = a.points.length, m = b.points.length;
140
+ // Costs depend only on the preceding row. Reuse both cost rows and the
141
+ // predecessor grid across phases; only an improved phase needs traceback.
142
+ const costs = new Float64Array(2 * (m + 1)), moves = new Uint8Array((n + 1) * (m + 1));
143
+ let cost = distanceCost(a, b, 0, costs, moves), best = traceDistanceMap(moves, n, m), selectedPhase = 0;
144
+ for (let phase = 1; phase < phases; phase++) {
145
+ const candidateCost = distanceCost(a, b, phase, costs, moves);
146
+ if (candidateCost < cost) {
147
+ cost = candidateCost;
148
+ best = traceDistanceMap(moves, n, m);
149
+ selectedPhase = phase;
150
+ }
151
+ }
152
+ return { ...best, cost, row: selectedPhase === 0 ? b : phaseRow(b, selectedPhase) };
153
+ }
154
+ /** @internal Aligns one indexed contour across all stations, propagating every insertion to earlier rows. */
155
+ export function alignLoftRows(input, methods, distribution, refine, finalRows, otherContours, limits, budget, maxLengths) {
156
+ const refined = input.map((row, index) => {
157
+ const counts = loftSubdivisions(row, row.points.length * refine[index], distribution, maxLengths?.[index]);
158
+ const count = counts.reduce((sum, count) => sum + count, 0);
159
+ assertLoftGrid(count, finalRows, otherContours, limits);
160
+ return subdivideLoftRow(row, counts);
161
+ });
162
+ let rows = [refined[0]];
163
+ for (let gap = 0; gap < methods.length; gap++) {
164
+ const previous = rows.at(-1), next = refined[gap + 1], method = methods[gap];
165
+ if (method === "distance" || method === "fast-distance") {
166
+ const mapping = distanceLoftMap(previous, next, method, budget);
167
+ assertLoftGrid(mapping.previous.length, finalRows, otherContours, limits);
168
+ rows = [
169
+ ...rows.map((row) => indexedRow(row, mapping.previous)),
170
+ indexedRow(mapping.row, mapping.next),
171
+ ];
172
+ }
173
+ else {
174
+ const target = Math.max(previous.points.length, next.points.length);
175
+ const previousCounts = loftSubdivisions(previous, target, distribution);
176
+ const nextCounts = loftSubdivisions(next, target, distribution);
177
+ assertLoftGrid(target, finalRows, otherContours, limits);
178
+ const expanded = subdivideLoftRow(next, nextCounts);
179
+ rows = [
180
+ ...rows.map((row) => subdivideLoftRow(row, previousCounts)),
181
+ method === "reindex"
182
+ ? reindexLoftRow(subdivideLoftRow(previous, previousCounts), expanded, budget)
183
+ : expanded,
184
+ ];
185
+ }
186
+ }
187
+ return rows;
188
+ }
189
+ /** @internal Inserts profiles on straight world-space trajectories, without interpolating frames. */
190
+ export function sliceLoftRows(rows, slices) {
191
+ const result = [rows[0]];
192
+ for (let gap = 0; gap < slices.length; gap++) {
193
+ const a = rows[gap], b = rows[gap + 1];
194
+ for (let slice = 1; slice <= slices[gap]; slice++) {
195
+ const t = slice / (slices[gap] + 1);
196
+ result.push({
197
+ points: a.points.map((point, index) => interpolate2(point, b.points[index], t)),
198
+ world: a.world.map((point, index) => interpolate3(point, b.world[index], t)),
199
+ });
200
+ }
201
+ result.push(b);
202
+ }
203
+ return result;
204
+ }