@metamesh/modeling 0.0.0-stage → 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 (130) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/LICENSE +200 -0
  3. package/README.md +314 -2
  4. package/THIRD_PARTY_NOTICES.md +153 -0
  5. package/dist/api/engine.d.ts +54 -0
  6. package/dist/api/geometry.d.ts +2912 -0
  7. package/dist/api/index.d.ts +1648 -0
  8. package/dist/api/text.d.ts +77 -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 +72 -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 +265 -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 +107 -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 +80 -0
  62. package/dist/geometry/math/vector3.js +41 -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 +368 -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-planar.js +408 -0
  75. package/dist/geometry/mesh-quality.js +396 -0
  76. package/dist/geometry/mesh-signature.js +137 -0
  77. package/dist/geometry/mesh.js +300 -0
  78. package/dist/geometry/model-limits.js +50 -0
  79. package/dist/geometry/numeric.js +11 -0
  80. package/dist/geometry/paths/frames.js +118 -0
  81. package/dist/geometry/paths/helix.js +49 -0
  82. package/dist/geometry/paths/path.js +30 -0
  83. package/dist/geometry/paths/planar.js +135 -0
  84. package/dist/geometry/paths/resampling.js +124 -0
  85. package/dist/geometry/polygon-clipping.js +52 -0
  86. package/dist/geometry/primitive-options.js +1 -0
  87. package/dist/geometry/primitive-resolution.js +17 -0
  88. package/dist/geometry/primitive-resolvers.js +235 -0
  89. package/dist/geometry/profiles/matched.js +184 -0
  90. package/dist/geometry/profiles/open-profile.js +137 -0
  91. package/dist/geometry/profiles/profiles.js +70 -0
  92. package/dist/geometry/profiles/rectangle.js +235 -0
  93. package/dist/geometry/profiles/shapes.js +34 -0
  94. package/dist/geometry/profiles/span-sampling.js +28 -0
  95. package/dist/geometry/profiles/types.js +1 -0
  96. package/dist/geometry/profiles/validation.js +20 -0
  97. package/dist/geometry/regions.js +371 -0
  98. package/dist/geometry/resolution.js +236 -0
  99. package/dist/geometry/revolve/revolve-mesh.js +123 -0
  100. package/dist/geometry/sampling-limits.js +29 -0
  101. package/dist/geometry/stroke-path.js +181 -0
  102. package/dist/geometry/svg/errors.js +23 -0
  103. package/dist/geometry/svg/fill.js +172 -0
  104. package/dist/geometry/svg/index.js +1 -0
  105. package/dist/geometry/svg/matrix.js +130 -0
  106. package/dist/geometry/svg/parser.js +536 -0
  107. package/dist/geometry/svg/path.js +349 -0
  108. package/dist/geometry/svg/types.js +1 -0
  109. package/dist/geometry/svg-import.js +662 -0
  110. package/dist/geometry/sweeps/options.js +28 -0
  111. package/dist/geometry/sweeps/orientation.js +106 -0
  112. package/dist/geometry/sweeps/stations.js +161 -0
  113. package/dist/geometry/sweeps/sweep-closed.js +96 -0
  114. package/dist/geometry/sweeps/sweep-mesh.js +20 -0
  115. package/dist/geometry/sweeps/sweep-open.js +76 -0
  116. package/dist/geometry/sweeps/validation.js +116 -0
  117. package/dist/geometry/text-contours.js +32 -0
  118. package/dist/geometry/text-layout.js +1 -0
  119. package/dist/geometry/tolerance.js +53 -0
  120. package/dist/geometry/transform-geometry.js +41 -0
  121. package/dist/geometry/transform3.js +340 -0
  122. package/dist/geometry/types.js +1 -0
  123. package/dist/index.js +1 -0
  124. package/dist/model/execution-binding.js +30 -0
  125. package/dist/model/operations.js +152 -0
  126. package/dist/model/options.js +1 -0
  127. package/dist/model/types.js +1 -0
  128. package/dist/text/index.js +48 -0
  129. package/dist/text/vector-text-layout.js +279 -0
  130. package/package.json +100 -3
@@ -0,0 +1,225 @@
1
+ import { attachTransform } from "../geometry/anchors.js";
2
+ import { assertPrimitiveOptions } from "../geometry/primitive-resolvers.js";
3
+ import { eulerTransform3, scaleTransform3, translationTransform3 } from "../geometry/transform3.js";
4
+ import * as kernel from "./native/index.js";
5
+ import { transformPrimitiveSectionProfile } from "./primitive-profiles.js";
6
+ /** Internal host-bound transform operations. */
7
+ export function createTransformOperations(environment) {
8
+ const { scope, throwGeometryError, resolveHandle, nativeSolid, wrapSolid, ownSection, validateFiniteNumber, validateCoordinates, } = environment;
9
+ const preserveProfile = (source, result, matrix) => {
10
+ environment.preserveAnchors(source, result, [
11
+ matrix[0],
12
+ matrix[1],
13
+ 0,
14
+ matrix[2],
15
+ matrix[3],
16
+ matrix[4],
17
+ 0,
18
+ matrix[5],
19
+ 0,
20
+ 0,
21
+ 1,
22
+ 0,
23
+ 0,
24
+ 0,
25
+ 0,
26
+ 1,
27
+ ]);
28
+ const profile = environment.primitiveProfileFor(source);
29
+ return profile === undefined
30
+ ? result
31
+ : environment.rememberPrimitiveProfile(result, transformPrimitiveSectionProfile(profile, matrix));
32
+ };
33
+ const translate = ((value, offset) => {
34
+ const native = resolveHandle(value);
35
+ validateCoordinates(offset, native.kind === "section" ? 2 : 3, "translate");
36
+ return native.kind === "section"
37
+ ? preserveProfile(value, ownSection(native.value.translate(offset)), [
38
+ 1,
39
+ 0,
40
+ offset[0],
41
+ 0,
42
+ 1,
43
+ offset[1],
44
+ 0,
45
+ 0,
46
+ 1,
47
+ ])
48
+ : environment.preserveAnchors(value, wrapSolid(kernel.translate(scope, native.value, offset)), translationTransform3(offset));
49
+ });
50
+ const rotate = ((value, degrees) => {
51
+ const native = resolveHandle(value);
52
+ if (native.kind === "section") {
53
+ if (typeof degrees !== "number")
54
+ return throwGeometryError("INVALID_ARGUMENT", "rotate", "Section rotation requires a scalar angle.");
55
+ const angle = (validateFiniteNumber(degrees, "rotate") * Math.PI) / 180;
56
+ const c = Math.cos(angle), s = Math.sin(angle);
57
+ return preserveProfile(value, ownSection(native.value.rotate(degrees)), [
58
+ c,
59
+ -s,
60
+ 0,
61
+ s,
62
+ c,
63
+ 0,
64
+ 0,
65
+ 0,
66
+ 1,
67
+ ]);
68
+ }
69
+ if (typeof degrees === "number")
70
+ return throwGeometryError("INVALID_ARGUMENT", "rotate", "Solid rotation requires XYZ angles.");
71
+ validateCoordinates(degrees, 3, "rotate");
72
+ return environment.preserveAnchors(value, wrapSolid(kernel.rotate(scope, native.value, degrees)), eulerTransform3(degrees));
73
+ });
74
+ const scale = ((value, factors) => {
75
+ const native = resolveHandle(value);
76
+ const values = typeof factors === "number" ? [factors] : factors;
77
+ if (typeof factors !== "number")
78
+ validateCoordinates(factors, native.kind === "section" ? 2 : 3, "scale");
79
+ if (!values.every(Number.isFinite) || values.some((factor) => factor === 0))
80
+ throwGeometryError("SINGULAR_TRANSFORM", "scale", "Scale factors must be finite and nonzero.");
81
+ return native.kind === "section"
82
+ ? preserveProfile(value, ownSection(native.value.scale(factors)), [
83
+ typeof factors === "number" ? factors : factors[0],
84
+ 0,
85
+ 0,
86
+ 0,
87
+ typeof factors === "number" ? factors : factors[1],
88
+ 0,
89
+ 0,
90
+ 0,
91
+ 1,
92
+ ])
93
+ : environment.preserveAnchors(value, wrapSolid(kernel.scale(scope, native.value, factors)), scaleTransform3(typeof factors === "number" ? [factors, factors, factors] : factors));
94
+ });
95
+ const mirror = ((value, normal) => {
96
+ const native = resolveHandle(value);
97
+ validateCoordinates(normal, native.kind === "section" ? 2 : 3, "mirror");
98
+ if (normal.every((coordinate) => coordinate === 0))
99
+ throwGeometryError("INVALID_ARGUMENT", "mirror", "Mirror normal must be nonzero.");
100
+ return native.kind === "section"
101
+ ? (() => {
102
+ const maximum = Math.max(Math.abs(normal[0]), Math.abs(normal[1]));
103
+ const nx = normal[0] / maximum, ny = normal[1] / maximum;
104
+ const length = Math.hypot(nx, ny), x = nx / length, y = ny / length;
105
+ return preserveProfile(value, ownSection(native.value.mirror([x, y])), [
106
+ 1 - 2 * x * x,
107
+ -2 * x * y,
108
+ 0,
109
+ -2 * x * y,
110
+ 1 - 2 * y * y,
111
+ 0,
112
+ 0,
113
+ 0,
114
+ 1,
115
+ ]);
116
+ })()
117
+ : (() => {
118
+ const maximum = Math.max(...normal.map(Math.abs));
119
+ const scaled = normal.map((coordinate) => coordinate / maximum);
120
+ const length = Math.hypot(...scaled);
121
+ const [x, y, z] = scaled.map((coordinate) => coordinate / length);
122
+ return environment.preserveAnchors(value, wrapSolid(kernel.mirror(scope, native.value, normal)), [
123
+ 1 - 2 * x * x,
124
+ -2 * x * y,
125
+ -2 * x * z,
126
+ 0,
127
+ -2 * x * y,
128
+ 1 - 2 * y * y,
129
+ -2 * y * z,
130
+ 0,
131
+ -2 * x * z,
132
+ -2 * y * z,
133
+ 1 - 2 * z * z,
134
+ 0,
135
+ 0,
136
+ 0,
137
+ 0,
138
+ 1,
139
+ ]);
140
+ })();
141
+ });
142
+ const transform = ((value, matrix) => {
143
+ const native = resolveHandle(value);
144
+ if (native.kind === "solid") {
145
+ validateCoordinates(matrix, 16, "transform");
146
+ return environment.preserveAnchors(value, wrapSolid(kernel.transform(scope, native.value, matrix)), matrix);
147
+ }
148
+ validateCoordinates(matrix, 9, "transform");
149
+ const m = matrix;
150
+ if (m[6] !== 0 || m[7] !== 0 || m[8] !== 1)
151
+ throwGeometryError("INVALID_ARGUMENT", "transform", "Section transform must be affine.");
152
+ if (m[0] * m[4] - m[1] * m[3] === 0)
153
+ throwGeometryError("SINGULAR_TRANSFORM", "transform", "Section transform must be nonsingular.");
154
+ const columnMajor = [m[0], m[3], m[6], m[1], m[4], m[7], m[2], m[5], m[8]];
155
+ return preserveProfile(value, ownSection(native.value.transform(columnMajor)), m);
156
+ });
157
+ const placementMatrix = (placement, shape) => {
158
+ if (placement === undefined)
159
+ return undefined;
160
+ if (typeof placement !== "object" || placement === null || Array.isArray(placement))
161
+ throwGeometryError("INVALID_ARGUMENT", "placement", "placement must be an explicit source/target anchor object.");
162
+ const source = typeof placement.source === "string"
163
+ ? shape === undefined
164
+ ? throwGeometryError("INVALID_ARGUMENT", "placement", "Named source anchors require a constructed shape.")
165
+ : environment.anchor(shape, placement.source)
166
+ : placement.source;
167
+ for (const anchor of [source, placement.target]) {
168
+ if (typeof anchor !== "object" || anchor === null)
169
+ throwGeometryError("INVALID_ARGUMENT", "placement", "Placement requires explicit source and target anchors.");
170
+ validateCoordinates(anchor.position, 3, "placement");
171
+ validateCoordinates(anchor.direction, 3, "placement");
172
+ validateCoordinates(anchor.up, 3, "placement");
173
+ }
174
+ const spinDegrees = validateFiniteNumber(placement.spinDegrees ?? 0, "placement");
175
+ return attachTransform(source, placement.target, {
176
+ ...(placement.alignment === undefined ? {} : { alignment: placement.alignment }),
177
+ ...(placement.gap === undefined ? {} : { gap: placement.gap }),
178
+ ...(placement.tolerance === undefined ? {} : { tolerance: placement.tolerance }),
179
+ spinRadians: (spinDegrees * Math.PI) / 180,
180
+ });
181
+ };
182
+ const placedSolid = (value, create, anchors) => {
183
+ assertPrimitiveOptions(value, "primitive");
184
+ const solid = wrapSolid(create());
185
+ if (anchors !== undefined)
186
+ environment.rememberAnchors(solid, anchors());
187
+ const matrix = placementMatrix(value.placement, solid);
188
+ return matrix === undefined ? solid : transform(solid, matrix);
189
+ };
190
+ const placedSection = (value, create) => {
191
+ assertPrimitiveOptions(value, "primitive");
192
+ const section = create();
193
+ const matrix = placementMatrix(value.placement, section);
194
+ if (matrix === undefined)
195
+ return section;
196
+ if (Math.abs(matrix[8]) > 1e-10 || Math.abs(matrix[9]) > 1e-10 || Math.abs(matrix[11]) > 1e-10)
197
+ throwGeometryError("INVALID_ARGUMENT", "placement", "Section placement must preserve the XY plane.");
198
+ const xy = [
199
+ matrix[0],
200
+ matrix[1],
201
+ matrix[3],
202
+ matrix[4],
203
+ matrix[5],
204
+ matrix[7],
205
+ 0,
206
+ 0,
207
+ 1,
208
+ ];
209
+ const result = transform(section, xy);
210
+ return environment.preserveAnchors(section, result, matrix);
211
+ };
212
+ const operations = {
213
+ translate,
214
+ rotate,
215
+ scale,
216
+ mirror,
217
+ transform,
218
+ attach: (value, source, target, valueOptions) => {
219
+ nativeSolid(value);
220
+ const matrix = placementMatrix({ ...valueOptions, source, target }, value);
221
+ return transform(value, matrix);
222
+ },
223
+ };
224
+ return { operations, placedSolid, placedSection };
225
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,226 @@
1
+ import { GeometryError } from "./errors.js";
2
+ import { resolveGeometryTolerance } from "./tolerance.js";
3
+ import { composeTransform3, frameTransform3, inverseTransform3 } from "./transform3.js";
4
+ const boxFaces = {
5
+ back: [
6
+ [0, -1, 0],
7
+ [0, 0, 1],
8
+ ],
9
+ bottom: [
10
+ [0, 0, -1],
11
+ [0, 1, 0],
12
+ ],
13
+ front: [
14
+ [0, 1, 0],
15
+ [0, 0, 1],
16
+ ],
17
+ left: [
18
+ [-1, 0, 0],
19
+ [0, 0, 1],
20
+ ],
21
+ right: [
22
+ [1, 0, 0],
23
+ [0, 0, 1],
24
+ ],
25
+ top: [
26
+ [0, 0, 1],
27
+ [0, 1, 0],
28
+ ],
29
+ };
30
+ const primitiveAnchorOrigin = (options, operation) => {
31
+ if (options.center !== undefined && typeof options.center !== "boolean")
32
+ throw new GeometryError("INVALID_ARGUMENT", "center must be boolean.", { operation });
33
+ const origin = options.origin === undefined ? [0, 0, 0] : options.origin;
34
+ if (!Array.isArray(origin) || origin.length !== 3)
35
+ throw new GeometryError("INVALID_ARGUMENT", "origin must be an exact XYZ tuple.", {
36
+ operation,
37
+ });
38
+ for (let axis = 0; axis < 3; axis++)
39
+ if (!Number.isFinite(origin[axis]))
40
+ throw new GeometryError("NON_FINITE_VALUE", "Anchor origin must be finite.", { operation });
41
+ return origin;
42
+ };
43
+ const assertAnchorOptions = (options, operation) => {
44
+ if (typeof options !== "object" || options === null || Array.isArray(options))
45
+ throw new GeometryError("INVALID_ARGUMENT", "Anchor options must be an object.", { operation });
46
+ };
47
+ /** Creates an outward face-center anchor using the same local origin convention as box().
48
+ * @remarks origin applies an explicit translation. For rotated components, transform the
49
+ * resulting anchor frame explicitly; no anchor is inherited from a Boolean result.
50
+ */
51
+ export function boxAnchor3(options) {
52
+ const operation = "boxAnchor3";
53
+ assertAnchorOptions(options, operation);
54
+ const origin = primitiveAnchorOrigin(options, operation);
55
+ if (typeof options.size !== "number" &&
56
+ (!Array.isArray(options.size) || options.size.length !== 3))
57
+ throw new GeometryError("INVALID_ARGUMENT", "Box anchor size must be a number or XYZ tuple.", {
58
+ operation,
59
+ });
60
+ const size = typeof options.size === "number" ? [options.size, options.size, options.size] : options.size;
61
+ for (let axis = 0; axis < 3; axis++)
62
+ if (!Number.isFinite(size[axis]) || size[axis] <= 0)
63
+ throw new GeometryError("INVALID_ARGUMENT", "Box anchor dimensions must be positive.", {
64
+ operation,
65
+ });
66
+ if (!Object.hasOwn(boxFaces, options.face))
67
+ throw new GeometryError("INVALID_ARGUMENT", "Unsupported box anchor face.", { operation });
68
+ const [direction, up] = boxFaces[options.face];
69
+ const coordinate = (axis) => {
70
+ if (options.center ?? true)
71
+ return origin[axis] + (direction[axis] * size[axis]) / 2;
72
+ return (origin[axis] + (direction[axis] > 0 ? size[axis] : direction[axis] < 0 ? 0 : size[axis] / 2));
73
+ };
74
+ return createAnchor3([coordinate(0), coordinate(1), coordinate(2)], direction, up);
75
+ }
76
+ /** Creates a cap-center or radial-side anchor using cylinder()'s construction origin.
77
+ * @remarks Both centered and noncentered cylinders extend along +Z. Side angle uses degrees;
78
+ * origin applies an explicit translation, and arbitrary component transforms remain explicit.
79
+ */
80
+ export function cylinderAnchor3(options) {
81
+ const operation = "cylinderAnchor3";
82
+ assertAnchorOptions(options, operation);
83
+ const origin = primitiveAnchorOrigin(options, operation);
84
+ const { radius, height, face } = options;
85
+ const angleDegrees = options.angleDegrees === undefined ? 0 : options.angleDegrees;
86
+ if (!Number.isFinite(radius) || radius <= 0 || !Number.isFinite(height) || height <= 0)
87
+ throw new GeometryError("INVALID_ARGUMENT", "Cylinder anchor dimensions must be positive.", {
88
+ operation,
89
+ });
90
+ if (!Number.isFinite(angleDegrees))
91
+ throw new GeometryError("NON_FINITE_VALUE", "Cylinder anchor angle must be finite.", {
92
+ operation,
93
+ });
94
+ if (face !== "bottom" && face !== "top" && face !== "side")
95
+ throw new GeometryError("INVALID_ARGUMENT", "Unsupported cylinder anchor face.", { operation });
96
+ const center = options.center ?? true;
97
+ if (face === "top")
98
+ return createAnchor3([origin[0], origin[1], origin[2] + (center ? height / 2 : height)], [0, 0, 1], [0, 1, 0]);
99
+ if (face === "bottom")
100
+ return createAnchor3([origin[0], origin[1], origin[2] - (center ? height / 2 : 0)], [0, 0, -1], [0, 1, 0]);
101
+ const angle = angleDegrees * (Math.PI / 180);
102
+ const cosine = Math.cos(angle), sine = Math.sin(angle);
103
+ return createAnchor3([origin[0] + radius * cosine, origin[1] + radius * sine, origin[2] + (center ? 0 : height / 2)], [cosine, sine, 0], [0, 0, 1]);
104
+ }
105
+ const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
106
+ const cross = (a, b) => [
107
+ a[1] * b[2] - a[2] * b[1],
108
+ a[2] * b[0] - a[0] * b[2],
109
+ a[0] * b[1] - a[1] * b[0],
110
+ ];
111
+ const normalized = (vector, label, tolerance) => {
112
+ if (!vector.every(Number.isFinite)) {
113
+ throw new GeometryError("NON_FINITE_VALUE", `${label} coordinates must be finite.`);
114
+ }
115
+ const magnitude = Math.hypot(vector[0], vector[1], vector[2]);
116
+ if (!Number.isFinite(magnitude)) {
117
+ throw new GeometryError("NON_FINITE_VALUE", `${label} length must be finite.`);
118
+ }
119
+ if (magnitude <= tolerance) {
120
+ throw new GeometryError("DEGENERATE_GEOMETRY", `${label} must have non-zero length.`);
121
+ }
122
+ return Object.freeze([vector[0] / magnitude, vector[1] / magnitude, vector[2] / magnitude]);
123
+ };
124
+ /**
125
+ * Constructs a normalized immutable attachment anchor.
126
+ *
127
+ * `up` is projected onto the plane perpendicular to `direction`, removing small
128
+ * input drift while preserving its roll intent.
129
+ *
130
+ * @param position - Finite anchor point in model coordinates.
131
+ * @param direction - Non-zero outward attachment normal.
132
+ * @param up - Non-parallel roll reference; defaults to +Y.
133
+ * @param tolerance - Optional comparison overrides.
134
+ * @returns A frozen anchor with unit, orthogonal direction and up vectors.
135
+ * @throws {@link GeometryError} For non-finite or degenerate inputs.
136
+ */
137
+ export function createAnchor3(position, direction, up = [0, 1, 0], tolerance = {}) {
138
+ if (!position.every(Number.isFinite)) {
139
+ throw new GeometryError("NON_FINITE_VALUE", "Anchor position coordinates must be finite.");
140
+ }
141
+ const resolved = resolveGeometryTolerance(Math.max(1, Math.hypot(direction[0], direction[1], direction[2])), tolerance);
142
+ const zAxis = normalized(direction, "Anchor direction", resolved.linear);
143
+ const projectedUp = [
144
+ up[0] - zAxis[0] * dot(up, zAxis),
145
+ up[1] - zAxis[1] * dot(up, zAxis),
146
+ up[2] - zAxis[2] * dot(up, zAxis),
147
+ ];
148
+ const normalizedUp = normalized(projectedUp, "Anchor up direction", resolved.linear);
149
+ return Object.freeze({
150
+ direction: zAxis,
151
+ position: Object.freeze([position[0], position[1], position[2]]),
152
+ up: normalizedUp,
153
+ });
154
+ }
155
+ /**
156
+ * Converts an anchor into a right-handed frame with direction as local +Z.
157
+ *
158
+ * @param anchor - Anchor created by {@link createAnchor3} or equivalent valid data.
159
+ * @param tolerance - Optional comparison overrides.
160
+ * @returns A normalized right-handed frame at the anchor position.
161
+ */
162
+ export function anchorFrame3(anchor, tolerance = {}) {
163
+ const normalizedAnchor = createAnchor3(anchor.position, anchor.direction, anchor.up, tolerance);
164
+ const xAxis = normalized(cross(normalizedAnchor.up, normalizedAnchor.direction), "Anchor X axis", 0);
165
+ return Object.freeze({
166
+ origin: normalizedAnchor.position,
167
+ xAxis,
168
+ yAxis: normalizedAnchor.up,
169
+ zAxis: normalizedAnchor.direction,
170
+ });
171
+ }
172
+ /**
173
+ * Builds the transform that moves a source anchor onto a target anchor.
174
+ *
175
+ * The default `opposed` alignment points the source outward normal against the
176
+ * target outward normal, producing face-to-face mating. A positive gap moves the
177
+ * attachment point along the target's outward direction. Spin is applied after
178
+ * alignment around the final source direction.
179
+ *
180
+ * @param source - Anchor expressed in the moving object's local coordinates.
181
+ * @param target - Desired anchor pose in destination coordinates.
182
+ * @param options - Alignment, gap, spin, and tolerance policy.
183
+ * @returns A frozen affine transform for the moving object.
184
+ * @throws {@link GeometryError} For invalid anchors or non-finite options.
185
+ */
186
+ export function attachTransform(source, target, options = {}) {
187
+ const gap = options.gap ?? 0;
188
+ const spin = options.spinRadians ?? 0;
189
+ if (options.alignment !== undefined &&
190
+ options.alignment !== "opposed" &&
191
+ options.alignment !== "same") {
192
+ throw new GeometryError("INVALID_ARGUMENT", "Attachment alignment must be opposed or same.");
193
+ }
194
+ if (!Number.isFinite(gap) || !Number.isFinite(spin)) {
195
+ throw new GeometryError("NON_FINITE_VALUE", "Attachment gap and spin must be finite.");
196
+ }
197
+ const sourceFrame = anchorFrame3(source, options.tolerance);
198
+ const targetAnchor = createAnchor3(target.position, target.direction, target.up, options.tolerance);
199
+ const desiredDirection = (options.alignment ?? "opposed") === "opposed"
200
+ ? [-targetAnchor.direction[0], -targetAnchor.direction[1], -targetAnchor.direction[2]]
201
+ : targetAnchor.direction;
202
+ const baseFrame = anchorFrame3(createAnchor3(targetAnchor.position, desiredDirection, targetAnchor.up, options.tolerance), options.tolerance);
203
+ const cosine = Math.cos(spin);
204
+ const sine = Math.sin(spin);
205
+ const spunX = [
206
+ baseFrame.xAxis[0] * cosine + baseFrame.yAxis[0] * sine,
207
+ baseFrame.xAxis[1] * cosine + baseFrame.yAxis[1] * sine,
208
+ baseFrame.xAxis[2] * cosine + baseFrame.yAxis[2] * sine,
209
+ ];
210
+ const spunY = [
211
+ -baseFrame.xAxis[0] * sine + baseFrame.yAxis[0] * cosine,
212
+ -baseFrame.xAxis[1] * sine + baseFrame.yAxis[1] * cosine,
213
+ -baseFrame.xAxis[2] * sine + baseFrame.yAxis[2] * cosine,
214
+ ];
215
+ const desiredFrame = {
216
+ origin: [
217
+ targetAnchor.position[0] + targetAnchor.direction[0] * gap,
218
+ targetAnchor.position[1] + targetAnchor.direction[1] * gap,
219
+ targetAnchor.position[2] + targetAnchor.direction[2] * gap,
220
+ ],
221
+ xAxis: spunX,
222
+ yAxis: spunY,
223
+ zAxis: baseFrame.zAxis,
224
+ };
225
+ return composeTransform3(frameTransform3(desiredFrame, options.tolerance), inverseTransform3(frameTransform3(sourceFrame, options.tolerance)));
226
+ }
@@ -0,0 +1,162 @@
1
+ import { GeometryError } from "../errors.js";
2
+ // This parameter-space probe only resolves stationary tangent direction; it does not control
3
+ // geometric sampling tolerance or move output points. Retain its established one-sided behavior.
4
+ const STATIONARY_TANGENT_PROBE = 0.000001;
5
+ /** @internal Shared structured failures for evaluation and sampling. */
6
+ export function curveFailure(code, message) {
7
+ throw new GeometryError(code, message, { operation: "sampleCurve" });
8
+ }
9
+ /** @internal Validates original inputs and intermediate arithmetic without copying them. */
10
+ export function validateCurvePoint3(point) {
11
+ if (point.length !== 3 || !point.every(Number.isFinite))
12
+ curveFailure("NON_FINITE_VALUE", "Curve coordinates must be finite three-dimensional points.");
13
+ return point;
14
+ }
15
+ /** @internal Planar input promotion is scratch data, never a completed sampled result. */
16
+ export function liftPointTo3D(point) {
17
+ if (point.length !== 2)
18
+ curveFailure("INVALID_ARGUMENT", "Planar curve points must have two coordinates.");
19
+ return validateCurvePoint3([point[0], point[1], 0]);
20
+ }
21
+ /** @internal Promotes planar controls only to mutable evaluation scratch, never a sampled snapshot. */
22
+ export function planarCubicControl(curve) {
23
+ return [
24
+ liftPointTo3D(curve.start),
25
+ liftPointTo3D(curve.control1),
26
+ liftPointTo3D(curve.control2),
27
+ liftPointTo3D(curve.end),
28
+ ];
29
+ }
30
+ /** @internal Rejects overflowing extents even when endpoints are separately finite. */
31
+ export function curveDistance(a, b) {
32
+ const result = Math.hypot(b[0] - a[0], b[1] - a[1], b[2] - a[2]);
33
+ if (!Number.isFinite(result))
34
+ curveFailure("NON_FINITE_VALUE", "Curve extent exceeds finite arithmetic.");
35
+ return result;
36
+ }
37
+ /** @internal Weighted interpolation preserves finite large coordinates and the existing operation order. */
38
+ export function interpolateCurvePoint(a, b, t, result) {
39
+ result[0] = a[0] * (1 - t) + b[0] * t;
40
+ result[1] = a[1] * (1 - t) + b[1] * t;
41
+ result[2] = a[2] * (1 - t) + b[2] * t;
42
+ validateCurvePoint3(result);
43
+ }
44
+ /** @internal Normalization uses hypot and canonical positive zero components. */
45
+ export function normalizeCurveDirection(direction, result) {
46
+ const length = Math.hypot(...direction);
47
+ if (!Number.isFinite(length))
48
+ curveFailure("NON_FINITE_VALUE", "Curve tangent exceeds finite arithmetic.");
49
+ if (length === 0)
50
+ curveFailure("DEGENERATE_GEOMETRY", "A curve must have a usable nonzero tangent.");
51
+ result[0] = direction[0] / length || 0;
52
+ result[1] = direction[1] / length || 0;
53
+ result[2] = direction[2] / length || 0;
54
+ }
55
+ /** @internal Safe to alias result because normalization reads each component before replacing it. */
56
+ export function curveDirection(a, b, result) {
57
+ result[0] = b[0] - a[0];
58
+ result[1] = b[1] - a[1];
59
+ result[2] = b[2] - a[2];
60
+ normalizeCurveDirection(result, result);
61
+ }
62
+ /** @internal Bounding distance to the finite chord, using the existing arithmetic order. */
63
+ export function distanceToCurveChord(point, start, end) {
64
+ const extent = curveDistance(start, end);
65
+ if (extent === 0)
66
+ return curveDistance(point, start);
67
+ const x = (end[0] - start[0]) / extent || 0;
68
+ const y = (end[1] - start[1]) / extent || 0;
69
+ const z = (end[2] - start[2]) / extent || 0;
70
+ const relativeX = point[0] - start[0];
71
+ const relativeY = point[1] - start[1];
72
+ const relativeZ = point[2] - start[2];
73
+ const along = Math.max(0, Math.min(extent, relativeX * x + relativeY * y + relativeZ * z));
74
+ const result = Math.hypot(relativeX - along * x, relativeY - along * y, relativeZ - along * z);
75
+ if (!Number.isFinite(result))
76
+ curveFailure("NON_FINITE_VALUE", "Bézier flatness exceeds finite arithmetic.");
77
+ return result;
78
+ }
79
+ /** @internal Subdivision retains its new control polygons; evaluation reuses separate scratch. */
80
+ export function splitCubicControl(control, parameter = 0.5) {
81
+ const a = [0, 0, 0], b = [0, 0, 0], c = [0, 0, 0];
82
+ const d = [0, 0, 0], e = [0, 0, 0], middle = [0, 0, 0];
83
+ interpolateCurvePoint(control[0], control[1], parameter, a);
84
+ interpolateCurvePoint(control[1], control[2], parameter, b);
85
+ interpolateCurvePoint(control[2], control[3], parameter, c);
86
+ interpolateCurvePoint(a, b, parameter, d);
87
+ interpolateCurvePoint(b, c, parameter, e);
88
+ interpolateCurvePoint(d, e, parameter, middle);
89
+ return [
90
+ [control[0], a, d, middle],
91
+ [middle, e, c, control[3]],
92
+ ];
93
+ }
94
+ /** @internal Derivative controls are constant for a cubic, so calculate them once per invocation. */
95
+ export class CubicEvaluator {
96
+ #control;
97
+ #derivative;
98
+ #a = [0, 0, 0];
99
+ #b = [0, 0, 0];
100
+ #c = [0, 0, 0];
101
+ #d = [0, 0, 0];
102
+ #e = [0, 0, 0];
103
+ #probe = [0, 0, 0];
104
+ constructor(control) {
105
+ this.#control = control;
106
+ }
107
+ point(t, result) {
108
+ if (t === 0 || t === 1) {
109
+ const endpoint = this.#control[t === 0 ? 0 : 3];
110
+ result[0] = endpoint[0];
111
+ result[1] = endpoint[1];
112
+ result[2] = endpoint[2];
113
+ return;
114
+ }
115
+ interpolateCurvePoint(this.#control[0], this.#control[1], t, this.#a);
116
+ interpolateCurvePoint(this.#control[1], this.#control[2], t, this.#b);
117
+ interpolateCurvePoint(this.#control[2], this.#control[3], t, this.#c);
118
+ interpolateCurvePoint(this.#a, this.#b, t, this.#d);
119
+ interpolateCurvePoint(this.#b, this.#c, t, this.#e);
120
+ interpolateCurvePoint(this.#d, this.#e, t, result);
121
+ }
122
+ tangent(t, point, result) {
123
+ // Profile correspondence needs points only. Tangent-bearing curves calculate these once,
124
+ // retaining the established derivative arithmetic without allocating unused controls.
125
+ const control = this.#control;
126
+ const derivative = (this.#derivative ??= [
127
+ [control[1][0] - control[0][0], control[1][1] - control[0][1], control[1][2] - control[0][2]],
128
+ [control[2][0] - control[1][0], control[2][1] - control[1][1], control[2][2] - control[1][2]],
129
+ [control[3][0] - control[2][0], control[3][1] - control[2][1], control[3][2] - control[2][2]],
130
+ ]);
131
+ interpolateCurvePoint(derivative[0], derivative[1], t, this.#a);
132
+ interpolateCurvePoint(derivative[1], derivative[2], t, this.#b);
133
+ interpolateCurvePoint(this.#a, this.#b, t, result);
134
+ if (Math.hypot(...result) > 0) {
135
+ normalizeCurveDirection(result, result);
136
+ return;
137
+ }
138
+ // At stationary endpoints or cusps, use an outgoing one-sided secant when available.
139
+ // A fixed small parameter probe retains the previous fallback without an output-spacing dependency.
140
+ if (t < 1) {
141
+ this.point(Math.min(1, t + STATIONARY_TANGENT_PROBE), this.#probe);
142
+ if (curveDistance(point, this.#probe) > 0) {
143
+ curveDirection(point, this.#probe, result);
144
+ return;
145
+ }
146
+ }
147
+ if (t > 0) {
148
+ this.point(Math.max(0, t - STATIONARY_TANGENT_PROBE), this.#probe);
149
+ if (curveDistance(this.#probe, point) > 0) {
150
+ curveDirection(this.#probe, point, result);
151
+ return;
152
+ }
153
+ }
154
+ // If rounding erases both local probes, retain the first usable directed control edge.
155
+ for (let i = 0; i < 3; i++)
156
+ if (curveDistance(this.#control[i], this.#control[i + 1]) > 0) {
157
+ curveDirection(this.#control[i], this.#control[i + 1], result);
158
+ return;
159
+ }
160
+ curveFailure("DEGENERATE_GEOMETRY", "A constant Bézier curve has no tangent.");
161
+ }
162
+ }