@code3d/core 0.0.1-alpha.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 (172) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +824 -0
  3. package/THIRD_PARTY.md +28 -0
  4. package/bld/library/alignment-geometry.d.ts +50 -0
  5. package/bld/library/alignment-geometry.d.ts.map +1 -0
  6. package/bld/library/alignment-geometry.js +322 -0
  7. package/bld/library/alignment-geometry.js.map +1 -0
  8. package/bld/library/bound-solver.d.ts +33 -0
  9. package/bld/library/bound-solver.d.ts.map +1 -0
  10. package/bld/library/bound-solver.js +252 -0
  11. package/bld/library/bound-solver.js.map +1 -0
  12. package/bld/library/extrude.d.ts +8 -0
  13. package/bld/library/extrude.d.ts.map +1 -0
  14. package/bld/library/extrude.js +28 -0
  15. package/bld/library/extrude.js.map +1 -0
  16. package/bld/library/index.d.ts +7 -0
  17. package/bld/library/index.d.ts.map +1 -0
  18. package/bld/library/index.js +3 -0
  19. package/bld/library/index.js.map +1 -0
  20. package/bld/library/kernel-artifact-codec.d.ts +3 -0
  21. package/bld/library/kernel-artifact-codec.d.ts.map +1 -0
  22. package/bld/library/kernel-artifact-codec.js +148 -0
  23. package/bld/library/kernel-artifact-codec.js.map +1 -0
  24. package/bld/library/kernel-cache.d.ts +63 -0
  25. package/bld/library/kernel-cache.d.ts.map +1 -0
  26. package/bld/library/kernel-cache.js +245 -0
  27. package/bld/library/kernel-cache.js.map +1 -0
  28. package/bld/library/kernel-shapes.d.ts +20 -0
  29. package/bld/library/kernel-shapes.d.ts.map +1 -0
  30. package/bld/library/kernel-shapes.js +108 -0
  31. package/bld/library/kernel-shapes.js.map +1 -0
  32. package/bld/library/loft.d.ts +7 -0
  33. package/bld/library/loft.d.ts.map +1 -0
  34. package/bld/library/loft.js +161 -0
  35. package/bld/library/loft.js.map +1 -0
  36. package/bld/library/material.d.ts +12 -0
  37. package/bld/library/material.d.ts.map +1 -0
  38. package/bld/library/material.js +176 -0
  39. package/bld/library/material.js.map +1 -0
  40. package/bld/library/model-color.d.ts +8 -0
  41. package/bld/library/model-color.d.ts.map +1 -0
  42. package/bld/library/model-color.js +36 -0
  43. package/bld/library/model-color.js.map +1 -0
  44. package/bld/library/open-cascade-error.d.ts +2 -0
  45. package/bld/library/open-cascade-error.d.ts.map +1 -0
  46. package/bld/library/open-cascade-error.js +22 -0
  47. package/bld/library/open-cascade-error.js.map +1 -0
  48. package/bld/library/relation-solver.d.ts +29 -0
  49. package/bld/library/relation-solver.d.ts.map +1 -0
  50. package/bld/library/relation-solver.js +258 -0
  51. package/bld/library/relation-solver.js.map +1 -0
  52. package/bld/library/replicad.d.ts +17 -0
  53. package/bld/library/replicad.d.ts.map +1 -0
  54. package/bld/library/replicad.js +24 -0
  55. package/bld/library/replicad.js.map +1 -0
  56. package/bld/library/retained-memory.d.ts +7 -0
  57. package/bld/library/retained-memory.d.ts.map +1 -0
  58. package/bld/library/retained-memory.js +27 -0
  59. package/bld/library/retained-memory.js.map +1 -0
  60. package/bld/library/runtime.d.ts +775 -0
  61. package/bld/library/runtime.d.ts.map +1 -0
  62. package/bld/library/runtime.js +2954 -0
  63. package/bld/library/runtime.js.map +1 -0
  64. package/bld/library/shell.d.ts +7 -0
  65. package/bld/library/shell.d.ts.map +1 -0
  66. package/bld/library/shell.js +221 -0
  67. package/bld/library/shell.js.map +1 -0
  68. package/bld/library/sketch-curve-intersections.d.ts +13 -0
  69. package/bld/library/sketch-curve-intersections.d.ts.map +1 -0
  70. package/bld/library/sketch-curve-intersections.js +117 -0
  71. package/bld/library/sketch-curve-intersections.js.map +1 -0
  72. package/bld/library/sketch-curves.d.ts +31 -0
  73. package/bld/library/sketch-curves.d.ts.map +1 -0
  74. package/bld/library/sketch-curves.js +89 -0
  75. package/bld/library/sketch-curves.js.map +1 -0
  76. package/bld/library/sketch-drag-rules.d.ts +35 -0
  77. package/bld/library/sketch-drag-rules.d.ts.map +1 -0
  78. package/bld/library/sketch-drag-rules.js +522 -0
  79. package/bld/library/sketch-drag-rules.js.map +1 -0
  80. package/bld/library/sketch-face.d.ts +4 -0
  81. package/bld/library/sketch-face.d.ts.map +1 -0
  82. package/bld/library/sketch-face.js +72 -0
  83. package/bld/library/sketch-face.js.map +1 -0
  84. package/bld/library/sketch-incidence.d.ts +26 -0
  85. package/bld/library/sketch-incidence.d.ts.map +1 -0
  86. package/bld/library/sketch-incidence.js +114 -0
  87. package/bld/library/sketch-incidence.js.map +1 -0
  88. package/bld/library/sketch-precision.d.ts +19 -0
  89. package/bld/library/sketch-precision.d.ts.map +1 -0
  90. package/bld/library/sketch-precision.js +55 -0
  91. package/bld/library/sketch-precision.js.map +1 -0
  92. package/bld/library/sketch-regions.d.ts +12 -0
  93. package/bld/library/sketch-regions.d.ts.map +1 -0
  94. package/bld/library/sketch-regions.js +193 -0
  95. package/bld/library/sketch-regions.js.map +1 -0
  96. package/bld/library/sketch-solver.d.ts +97 -0
  97. package/bld/library/sketch-solver.d.ts.map +1 -0
  98. package/bld/library/sketch-solver.js +578 -0
  99. package/bld/library/sketch-solver.js.map +1 -0
  100. package/bld/library/sketch.d.ts +142 -0
  101. package/bld/library/sketch.d.ts.map +1 -0
  102. package/bld/library/sketch.js +650 -0
  103. package/bld/library/sketch.js.map +1 -0
  104. package/bld/library/spatial.d.ts +28 -0
  105. package/bld/library/spatial.d.ts.map +1 -0
  106. package/bld/library/spatial.js +195 -0
  107. package/bld/library/spatial.js.map +1 -0
  108. package/bld/library/three.d.ts +3 -0
  109. package/bld/library/three.d.ts.map +1 -0
  110. package/bld/library/three.js +3 -0
  111. package/bld/library/three.js.map +1 -0
  112. package/bld/library/topology-id.d.ts +24 -0
  113. package/bld/library/topology-id.d.ts.map +1 -0
  114. package/bld/library/topology-id.js +67 -0
  115. package/bld/library/topology-id.js.map +1 -0
  116. package/bld/library/topology-inspection.d.ts +64 -0
  117. package/bld/library/topology-inspection.d.ts.map +1 -0
  118. package/bld/library/topology-inspection.js +267 -0
  119. package/bld/library/topology-inspection.js.map +1 -0
  120. package/bld/library/topology.d.ts +94 -0
  121. package/bld/library/topology.d.ts.map +1 -0
  122. package/bld/library/topology.js +455 -0
  123. package/bld/library/topology.js.map +1 -0
  124. package/bld/node/index.d.ts +2 -0
  125. package/bld/node/index.d.ts.map +1 -0
  126. package/bld/node/index.js +14 -0
  127. package/bld/node/index.js.map +1 -0
  128. package/bld/node/replicad.d.ts +3 -0
  129. package/bld/node/replicad.d.ts.map +1 -0
  130. package/bld/node/replicad.js +3 -0
  131. package/bld/node/replicad.js.map +1 -0
  132. package/bld/tooling/index.d.ts +26 -0
  133. package/bld/tooling/index.d.ts.map +1 -0
  134. package/bld/tooling/index.js +19 -0
  135. package/bld/tooling/index.js.map +1 -0
  136. package/package.json +53 -0
  137. package/src/library/alignment-geometry.ts +431 -0
  138. package/src/library/bound-solver.ts +410 -0
  139. package/src/library/extrude.ts +38 -0
  140. package/src/library/index.ts +88 -0
  141. package/src/library/kernel-artifact-codec.ts +166 -0
  142. package/src/library/kernel-cache.ts +337 -0
  143. package/src/library/kernel-shapes.ts +135 -0
  144. package/src/library/loft.ts +196 -0
  145. package/src/library/material.ts +228 -0
  146. package/src/library/model-color.ts +47 -0
  147. package/src/library/open-cascade-error.ts +26 -0
  148. package/src/library/relation-solver.ts +479 -0
  149. package/src/library/replicad.ts +40 -0
  150. package/src/library/retained-memory.ts +23 -0
  151. package/src/library/runtime.ts +5146 -0
  152. package/src/library/shell.ts +281 -0
  153. package/src/library/sketch-curve-intersections.ts +154 -0
  154. package/src/library/sketch-curves.ts +161 -0
  155. package/src/library/sketch-drag-rules.ts +767 -0
  156. package/src/library/sketch-face.ts +89 -0
  157. package/src/library/sketch-incidence.ts +204 -0
  158. package/src/library/sketch-precision.ts +81 -0
  159. package/src/library/sketch-regions.ts +271 -0
  160. package/src/library/sketch-solver.ts +973 -0
  161. package/src/library/sketch.ts +977 -0
  162. package/src/library/spatial.ts +254 -0
  163. package/src/library/three.ts +2 -0
  164. package/src/library/topology-id.ts +81 -0
  165. package/src/library/topology-inspection.ts +368 -0
  166. package/src/library/topology.ts +776 -0
  167. package/src/library/tsconfig.json +11 -0
  168. package/src/node/index.ts +20 -0
  169. package/src/node/replicad.ts +3 -0
  170. package/src/node/tsconfig.json +16 -0
  171. package/src/tooling/index.ts +160 -0
  172. package/src/tooling/tsconfig.json +15 -0
@@ -0,0 +1,431 @@
1
+ import {getOC, type Edge, type Face} from 'replicad';
2
+ import {
3
+ addVectors,
4
+ composeTransforms,
5
+ rotateVector,
6
+ translation,
7
+ type RigidTransform,
8
+ type Vec3,
9
+ } from './spatial.js';
10
+
11
+ export type AlignmentGeometry =
12
+ | Readonly<{kind: 'point'; point: Vec3}>
13
+ | Readonly<{kind: 'line'; point: Vec3; direction: Vec3}>
14
+ | Readonly<{
15
+ kind: 'circle' | 'ellipse';
16
+ point: Vec3;
17
+ normal: Vec3;
18
+ major: Vec3;
19
+ radius: number;
20
+ minorRadius: number;
21
+ }>
22
+ | Readonly<{kind: 'plane'; point: Vec3; normal: Vec3}>
23
+ | Readonly<{
24
+ kind: 'cylinder';
25
+ point: Vec3;
26
+ direction: Vec3;
27
+ radius: number;
28
+ facing: number;
29
+ }>
30
+ | Readonly<{kind: 'sphere'; point: Vec3; radius: number; facing: number}>;
31
+
32
+ export const dot = (a: Vec3, b: Vec3): number =>
33
+ a.reduce((sum, v, i) => sum + v * b[i], 0);
34
+ export const scale = (a: Vec3, s: number): Vec3 => [
35
+ a[0] * s,
36
+ a[1] * s,
37
+ a[2] * s,
38
+ ];
39
+ export const subtract = (a: Vec3, b: Vec3): Vec3 => addVectors(a, scale(b, -1));
40
+ export const cross = (a: Vec3, b: Vec3): Vec3 => [
41
+ a[1] * b[2] - a[2] * b[1],
42
+ a[2] * b[0] - a[0] * b[2],
43
+ a[0] * b[1] - a[1] * b[0],
44
+ ];
45
+ export const unit = (v: Vec3): Vec3 => scale(v, 1 / Math.hypot(...v));
46
+ export const reject = (v: Vec3, normal: Vec3): Vec3 =>
47
+ subtract(v, scale(normal, dot(v, normal)));
48
+ export const perpendicular = (v: Vec3): Vec3 =>
49
+ unit(reject(Math.abs(v[0]) < 0.8 ? [1, 0, 0] : [0, 1, 0], v));
50
+
51
+ function coordinates(value: {
52
+ X(): number;
53
+ Y(): number;
54
+ Z(): number;
55
+ delete(): void;
56
+ }): Vec3 {
57
+ try {
58
+ return [value.X(), value.Y(), value.Z()];
59
+ } finally {
60
+ value.delete();
61
+ }
62
+ }
63
+
64
+ /** Read complete analytic loci, independent of edge/face trimming and parametrization. */
65
+ export function edgeGeometry(edge: Edge, direction: number): AlignmentGeometry {
66
+ const oc = getOC();
67
+ const adaptor = new oc.BRepAdaptor_Curve(edge.wrapped);
68
+ try {
69
+ if (edge.geomType === 'LINE') {
70
+ const point = edge.pointAt(0),
71
+ tangent = edge.tangentAt(0);
72
+ try {
73
+ const axis = scale(tangent.toTuple(), direction);
74
+ return {
75
+ kind: 'line',
76
+ point: reject(point.toTuple(), axis),
77
+ direction: axis,
78
+ };
79
+ } finally {
80
+ point.delete();
81
+ tangent.delete();
82
+ }
83
+ }
84
+ if (edge.geomType === 'CIRCLE' || edge.geomType === 'ELLIPSE') {
85
+ const curve =
86
+ edge.geomType === 'CIRCLE' ? adaptor.Circle() : adaptor.Ellipse();
87
+ const frame = curve.Position();
88
+ try {
89
+ const radius = 'Radius' in curve ? curve.Radius() : curve.MajorRadius();
90
+ const minorRadius = 'Radius' in curve ? radius : curve.MinorRadius();
91
+ return {
92
+ kind: Math.abs(radius - minorRadius) < 1e-10 ? 'circle' : 'ellipse',
93
+ point: coordinates(curve.Location()),
94
+ normal: scale(coordinates(frame.Direction()), direction),
95
+ major: coordinates(frame.XDirection()),
96
+ radius,
97
+ minorRadius,
98
+ };
99
+ } finally {
100
+ frame.delete();
101
+ curve.delete();
102
+ }
103
+ }
104
+ throw new Error(
105
+ `align() does not yet support underlying ${edge.geomType} curves. Select a point on the curve for further positioning.`,
106
+ );
107
+ } finally {
108
+ adaptor.delete();
109
+ }
110
+ }
111
+
112
+ export function faceGeometry(face: Face, facing: number): AlignmentGeometry {
113
+ const adaptor = new (getOC().BRepAdaptor_Surface)(face.wrapped, false);
114
+ try {
115
+ if (face.geomType === 'PLANE') {
116
+ const plane = adaptor.Plane(),
117
+ axis = plane.Axis();
118
+ try {
119
+ const normal = scale(
120
+ coordinates(axis.Direction()),
121
+ facing * (face.orientation === 'forward' ? 1 : -1),
122
+ );
123
+ return {
124
+ kind: 'plane',
125
+ point: scale(normal, dot(coordinates(plane.Location()), normal)),
126
+ normal,
127
+ };
128
+ } finally {
129
+ axis.delete();
130
+ plane.delete();
131
+ }
132
+ }
133
+ if (face.geomType === 'CYLINDRE' || face.geomType === 'SPHERE') {
134
+ const surface =
135
+ face.geomType === 'SPHERE' ? adaptor.Sphere() : adaptor.Cylinder();
136
+ try {
137
+ const point = coordinates(surface.Location());
138
+ const sense =
139
+ facing *
140
+ (face.orientation === 'forward' ? 1 : -1) *
141
+ (surface.Direct() ? 1 : -1);
142
+ if (face.geomType === 'SPHERE')
143
+ return {
144
+ kind: 'sphere',
145
+ point,
146
+ radius: surface.Radius(),
147
+ facing: sense,
148
+ };
149
+ const axis = (surface as ReturnType<typeof adaptor.Cylinder>).Axis();
150
+ try {
151
+ const direction = coordinates(axis.Direction());
152
+ return {
153
+ kind: 'cylinder',
154
+ point: reject(point, direction),
155
+ direction,
156
+ radius: surface.Radius(),
157
+ facing: sense,
158
+ };
159
+ } finally {
160
+ axis.delete();
161
+ }
162
+ } finally {
163
+ surface.delete();
164
+ }
165
+ }
166
+ throw new Error(
167
+ `align() does not yet support underlying ${face.geomType} surfaces.`,
168
+ );
169
+ } finally {
170
+ adaptor.delete();
171
+ }
172
+ }
173
+
174
+ export function transformGeometry(
175
+ geometry: AlignmentGeometry,
176
+ pose: RigidTransform,
177
+ ): AlignmentGeometry {
178
+ const point = composeTransforms(pose, translation(geometry.point)).position;
179
+ if ('direction' in geometry) {
180
+ const direction = rotateVector(geometry.direction, pose.quaternion);
181
+ return {...geometry, point: reject(point, direction), direction};
182
+ }
183
+ if ('normal' in geometry) {
184
+ const normal = rotateVector(geometry.normal, pose.quaternion);
185
+ return 'major' in geometry
186
+ ? {
187
+ ...geometry,
188
+ point,
189
+ normal,
190
+ major: rotateVector(geometry.major, pose.quaternion),
191
+ }
192
+ : {...geometry, point: scale(normal, dot(point, normal)), normal};
193
+ }
194
+ return {...geometry, point};
195
+ }
196
+
197
+ const rank = (g: AlignmentGeometry): number =>
198
+ g.kind === 'point'
199
+ ? 0
200
+ : ['line', 'circle', 'ellipse'].includes(g.kind)
201
+ ? 1
202
+ : 2;
203
+ export function orderedGeometry(
204
+ a: AlignmentGeometry,
205
+ b: AlignmentGeometry,
206
+ ): readonly [AlignmentGeometry, AlignmentGeometry] {
207
+ return rank(a) <= rank(b) ? [a, b] : [b, a];
208
+ }
209
+
210
+ /** Reject only proven incompatibilities; unsupported pairs and iteration failures have distinct errors. */
211
+ export function validateAlignment(
212
+ first: AlignmentGeometry,
213
+ second: AlignmentGeometry,
214
+ ): void {
215
+ const [a, b] = orderedGeometry(first, second);
216
+ const incompatible = (reason: string): never => {
217
+ throw new Error(`Geometrically incompatible align(): ${reason}`);
218
+ };
219
+ const sameSize = (a: number, b: number) =>
220
+ Math.abs(a - b) <= 1e-7 * Math.max(1, a, b);
221
+ if (a.kind === 'point') return;
222
+ if (rank(a) === rank(b)) {
223
+ if (a.kind !== b.kind)
224
+ incompatible(
225
+ `${a.kind} and ${b.kind} cannot coincide under a rigid transform.`,
226
+ );
227
+ if ('radius' in a && 'radius' in b && !sameSize(a.radius, b.radius))
228
+ incompatible('radii differ.');
229
+ if (
230
+ 'minorRadius' in a &&
231
+ 'minorRadius' in b &&
232
+ !sameSize(a.minorRadius, b.minorRadius)
233
+ )
234
+ incompatible('minor radii differ.');
235
+ if ('facing' in a && 'facing' in b && a.facing !== b.facing)
236
+ incompatible(
237
+ 'closed surface normal senses differ; use flip() to select matching facing.',
238
+ );
239
+ return;
240
+ }
241
+ if (b.kind === 'plane') return;
242
+ if (a.kind === 'line' && b.kind === 'cylinder') return;
243
+ if (a.kind === 'line' && b.kind === 'sphere')
244
+ incompatible('a whole straight line cannot lie on a sphere.');
245
+ if (a.kind === 'circle' && b.kind === 'cylinder') {
246
+ if (!sameSize(a.radius, b.radius))
247
+ incompatible('a circle on a cylinder must have its radius.');
248
+ return;
249
+ }
250
+ if (a.kind === 'circle' && b.kind === 'sphere') {
251
+ if (a.radius > b.radius + 1e-7)
252
+ incompatible('the circle is larger than the sphere.');
253
+ return;
254
+ }
255
+ if (a.kind === 'ellipse' && b.kind === 'sphere')
256
+ incompatible('a non-circular ellipse cannot lie on a sphere.');
257
+ if (a.kind === 'ellipse' && b.kind === 'cylinder') {
258
+ if (!sameSize(a.minorRadius, b.radius))
259
+ incompatible(
260
+ 'an elliptic cylinder section must have the cylinder radius as its minor radius.',
261
+ );
262
+ return;
263
+ }
264
+ throw new Error(
265
+ `align() does not yet support ${a.kind}–${b.kind} relations.`,
266
+ );
267
+ }
268
+
269
+ /** All residuals describe complete analytic loci; no trim endpoints or sampled tangent frames. */
270
+ export function alignmentResidual(
271
+ first: AlignmentGeometry,
272
+ second: AlignmentGeometry,
273
+ ): number[] {
274
+ const [a, b] = orderedGeometry(first, second),
275
+ delta = subtract(a.point, b.point);
276
+ if (a.kind === 'point') {
277
+ switch (b.kind) {
278
+ case 'point':
279
+ return [...delta];
280
+ case 'line':
281
+ return [...reject(delta, b.direction)];
282
+ case 'plane':
283
+ return [dot(delta, b.normal)];
284
+ case 'sphere':
285
+ return [Math.hypot(...delta) - b.radius];
286
+ case 'cylinder':
287
+ return [Math.hypot(...reject(delta, b.direction)) - b.radius];
288
+ case 'circle':
289
+ return [
290
+ dot(delta, b.normal),
291
+ Math.hypot(...reject(delta, b.normal)) - b.radius,
292
+ ];
293
+ case 'ellipse': {
294
+ const x = dot(delta, b.major) / b.radius,
295
+ y = dot(delta, cross(b.normal, b.major)) / b.minorRadius;
296
+ return [dot(delta, b.normal), (Math.hypot(x, y) - 1) * b.minorRadius];
297
+ }
298
+ }
299
+ }
300
+ if (a.kind === 'line') {
301
+ if (b.kind === 'line')
302
+ return [
303
+ ...reject(delta, b.direction),
304
+ ...subtract(a.direction, b.direction),
305
+ ];
306
+ if (b.kind === 'plane')
307
+ return [dot(delta, b.normal), dot(a.direction, b.normal)];
308
+ if (b.kind === 'cylinder')
309
+ return [
310
+ ...cross(a.direction, b.direction),
311
+ Math.hypot(...reject(delta, b.direction)) - b.radius,
312
+ ];
313
+ }
314
+ if (a.kind === 'circle' || a.kind === 'ellipse') {
315
+ if (b.kind === 'circle' || b.kind === 'ellipse')
316
+ return [
317
+ ...delta,
318
+ ...subtract(a.normal, b.normal),
319
+ ...(a.kind === 'ellipse' ? cross(a.major, b.major) : []),
320
+ ];
321
+ if (b.kind === 'plane')
322
+ return [dot(delta, b.normal), ...cross(a.normal, b.normal)];
323
+ if (b.kind === 'sphere')
324
+ return [
325
+ ...reject(delta, a.normal),
326
+ dot(delta, delta) + a.radius * a.radius - b.radius * b.radius,
327
+ ];
328
+ if (b.kind === 'cylinder') {
329
+ // Fourier coefficients of squared distance to the cylinder axis. Their
330
+ // vanishing is equivalent to membership of the complete ellipse/circle.
331
+ const c = reject(delta, b.direction),
332
+ x = reject(scale(a.major, a.radius), b.direction),
333
+ y = reject(scale(cross(a.normal, a.major), a.minorRadius), b.direction);
334
+ return [
335
+ dot(c, c) + (dot(x, x) + dot(y, y)) / 2 - b.radius * b.radius,
336
+ dot(c, x),
337
+ dot(c, y),
338
+ dot(x, x) - dot(y, y),
339
+ dot(x, y),
340
+ ];
341
+ }
342
+ }
343
+ if (a.kind === 'plane' && b.kind === 'plane')
344
+ return [dot(delta, b.normal), ...subtract(a.normal, b.normal)];
345
+ if (a.kind === 'cylinder' && b.kind === 'cylinder')
346
+ return [...reject(delta, b.direction), ...cross(a.direction, b.direction)];
347
+ if (a.kind === 'sphere' && b.kind === 'sphere') return [...delta];
348
+ throw new Error(`Unsupported alignment residual: ${a.kind}–${b.kind}.`);
349
+ }
350
+
351
+ export function nearestPoint(point: Vec3, geometry: AlignmentGeometry): Vec3 {
352
+ const delta = subtract(point, geometry.point);
353
+ const radial = (v: Vec3, radius: number, fallback: Vec3) =>
354
+ scale(Math.hypot(...v) > 1e-12 ? unit(v) : fallback, radius);
355
+ switch (geometry.kind) {
356
+ case 'point':
357
+ return geometry.point;
358
+ case 'line':
359
+ return addVectors(
360
+ geometry.point,
361
+ scale(geometry.direction, dot(delta, geometry.direction)),
362
+ );
363
+ case 'plane':
364
+ return subtract(
365
+ point,
366
+ scale(geometry.normal, dot(delta, geometry.normal)),
367
+ );
368
+ case 'sphere':
369
+ return addVectors(
370
+ geometry.point,
371
+ radial(delta, geometry.radius, [1, 0, 0]),
372
+ );
373
+ case 'cylinder':
374
+ return addVectors(
375
+ addVectors(
376
+ geometry.point,
377
+ scale(geometry.direction, dot(delta, geometry.direction)),
378
+ ),
379
+ radial(
380
+ reject(delta, geometry.direction),
381
+ geometry.radius,
382
+ perpendicular(geometry.direction),
383
+ ),
384
+ );
385
+ case 'circle':
386
+ return addVectors(
387
+ geometry.point,
388
+ radial(reject(delta, geometry.normal), geometry.radius, geometry.major),
389
+ );
390
+ case 'ellipse': {
391
+ const y = cross(geometry.normal, geometry.major);
392
+ const x0 = dot(delta, geometry.major),
393
+ y0 = dot(delta, y);
394
+ const distance = (angle: number) =>
395
+ (geometry.radius * Math.cos(angle) - x0) ** 2 +
396
+ (geometry.minorRadius * Math.sin(angle) - y0) ** 2;
397
+ // Bracket all minima of distance on the ellipse, then refine them. This
398
+ // chooses a nearby free parameter; membership still uses the exact locus.
399
+ const step = Math.PI / 16,
400
+ candidates: number[] = [];
401
+ for (let i = 0; i < 32; i++) {
402
+ const angle = i * step;
403
+ if (
404
+ distance(angle) > distance(angle - step) ||
405
+ distance(angle) > distance(angle + step)
406
+ )
407
+ continue;
408
+ let lower = angle - step,
409
+ upper = angle + step;
410
+ for (let iteration = 0; iteration < 64; iteration++) {
411
+ const a = lower + (upper - lower) * 0.3819660112501051,
412
+ b = upper - (upper - lower) * 0.3819660112501051;
413
+ if (distance(a) < distance(b)) upper = b;
414
+ else lower = a;
415
+ }
416
+ candidates.push((lower + upper) / 2);
417
+ }
418
+ const angle = candidates.reduce(
419
+ (best, angle) => (distance(angle) < distance(best) ? angle : best),
420
+ candidates[0],
421
+ );
422
+ return addVectors(
423
+ geometry.point,
424
+ addVectors(
425
+ scale(geometry.major, geometry.radius * Math.cos(angle)),
426
+ scale(y, geometry.minorRadius * Math.sin(angle)),
427
+ ),
428
+ );
429
+ }
430
+ }
431
+ }