@woosh/meep-engine 2.146.0 → 2.148.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 (105) hide show
  1. package/package.json +1 -1
  2. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite.d.ts +4 -4
  3. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite.d.ts.map +1 -1
  4. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite.js +48 -52
  5. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_2d.d.ts +23 -21
  6. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_2d.d.ts.map +1 -1
  7. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_2d.js +41 -406
  8. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_nd.d.ts +5 -4
  9. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_nd.d.ts.map +1 -1
  10. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_nd.js +400 -395
  11. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +0 -11
  12. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
  13. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +8 -6
  14. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
  15. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +552 -551
  16. package/src/engine/control/first-person/abilities/LedgeGrab.d.ts +8 -3
  17. package/src/engine/control/first-person/abilities/LedgeGrab.d.ts.map +1 -1
  18. package/src/engine/control/first-person/abilities/LedgeGrab.js +213 -199
  19. package/src/engine/control/first-person/abilities/Mantle.d.ts.map +1 -1
  20. package/src/engine/control/first-person/abilities/Mantle.js +195 -188
  21. package/src/engine/control/first-person/abilities/WallRun.d.ts.map +1 -1
  22. package/src/engine/control/first-person/abilities/WallRun.js +183 -175
  23. package/src/engine/control/first-person/sensors/FirstPersonSensors.d.ts +9 -0
  24. package/src/engine/control/first-person/sensors/FirstPersonSensors.d.ts.map +1 -1
  25. package/src/engine/control/first-person/sensors/FirstPersonSensors.js +87 -77
  26. package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.d.ts +8 -0
  27. package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.d.ts.map +1 -1
  28. package/src/engine/control/first-person/sensors/FirstPersonSensorsSystem.js +229 -196
  29. package/src/engine/ecs/EntityManager.d.ts +34 -11
  30. package/src/engine/ecs/EntityManager.d.ts.map +1 -1
  31. package/src/engine/ecs/EntityManager.js +71 -42
  32. package/src/engine/interpolation/BinaryInterpolationAdapter.d.ts.map +1 -0
  33. package/src/engine/interpolation/Interpoland.d.ts +48 -0
  34. package/src/engine/interpolation/Interpoland.d.ts.map +1 -0
  35. package/src/engine/interpolation/Interpoland.js +49 -0
  36. package/src/engine/interpolation/Interpolated.d.ts +101 -0
  37. package/src/engine/interpolation/Interpolated.d.ts.map +1 -0
  38. package/src/engine/interpolation/Interpolated.js +149 -0
  39. package/src/engine/{network/sim → interpolation}/InterpolationLog.d.ts +1 -1
  40. package/src/engine/interpolation/InterpolationLog.d.ts.map +1 -0
  41. package/src/engine/{network/sim → interpolation}/InterpolationLog.js +2 -2
  42. package/src/engine/interpolation/InterpolationSystem.d.ts +116 -0
  43. package/src/engine/interpolation/InterpolationSystem.d.ts.map +1 -0
  44. package/src/engine/interpolation/InterpolationSystem.js +233 -0
  45. package/src/engine/interpolation/PoseInterpolationAdapter.d.ts +17 -0
  46. package/src/engine/interpolation/PoseInterpolationAdapter.d.ts.map +1 -0
  47. package/src/engine/interpolation/PoseInterpolationAdapter.js +61 -0
  48. package/src/engine/interpolation/TransformPoseSerializationAdapter.d.ts +35 -0
  49. package/src/engine/interpolation/TransformPoseSerializationAdapter.d.ts.map +1 -0
  50. package/src/engine/interpolation/TransformPoseSerializationAdapter.js +57 -0
  51. package/src/engine/interpolation/pose_interpoland.d.ts +18 -0
  52. package/src/engine/interpolation/pose_interpoland.d.ts.map +1 -0
  53. package/src/engine/interpolation/pose_interpoland.js +27 -0
  54. package/src/engine/network/NetworkSession.d.ts +2 -2
  55. package/src/engine/network/NetworkSession.d.ts.map +1 -1
  56. package/src/engine/network/NetworkSession.js +2 -2
  57. package/src/engine/network/adapters/QuaternionInterpolationAdapter.d.ts +1 -1
  58. package/src/engine/network/adapters/QuaternionInterpolationAdapter.d.ts.map +1 -1
  59. package/src/engine/network/adapters/QuaternionInterpolationAdapter.js +1 -1
  60. package/src/engine/network/adapters/TransformInterpolationAdapter.d.ts +1 -1
  61. package/src/engine/network/adapters/TransformInterpolationAdapter.d.ts.map +1 -1
  62. package/src/engine/network/adapters/TransformInterpolationAdapter.js +1 -1
  63. package/src/engine/network/adapters/Vector3InterpolationAdapter.d.ts +1 -1
  64. package/src/engine/network/adapters/Vector3InterpolationAdapter.d.ts.map +1 -1
  65. package/src/engine/network/adapters/Vector3InterpolationAdapter.js +1 -1
  66. package/src/engine/physics/INTEPOLATION_SYSTEM_PLAN.md +287 -0
  67. package/src/engine/physics/PLAN.md +10 -9
  68. package/src/engine/physics/body/SolverBodyState.d.ts +142 -0
  69. package/src/engine/physics/body/SolverBodyState.d.ts.map +1 -0
  70. package/src/engine/physics/body/SolverBodyState.js +251 -0
  71. package/src/engine/physics/broadphase/generate_pairs.d.ts +2 -1
  72. package/src/engine/physics/broadphase/generate_pairs.d.ts.map +1 -1
  73. package/src/engine/physics/broadphase/generate_pairs.js +5 -3
  74. package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -1
  75. package/src/engine/physics/constraint/solve_constraints.js +691 -673
  76. package/src/engine/physics/ecs/PhysicsSystem.d.ts +82 -15
  77. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  78. package/src/engine/physics/ecs/PhysicsSystem.js +387 -87
  79. package/src/engine/physics/inertia/world_inverse_inertia.d.ts +23 -0
  80. package/src/engine/physics/inertia/world_inverse_inertia.d.ts.map +1 -1
  81. package/src/engine/physics/inertia/world_inverse_inertia.js +116 -77
  82. package/src/engine/physics/integration/integrate_position.d.ts +11 -1
  83. package/src/engine/physics/integration/integrate_position.d.ts.map +1 -1
  84. package/src/engine/physics/integration/integrate_position.js +97 -79
  85. package/src/engine/physics/integration/integrate_velocity.d.ts +12 -3
  86. package/src/engine/physics/integration/integrate_velocity.d.ts.map +1 -1
  87. package/src/engine/physics/integration/integrate_velocity.js +201 -160
  88. package/src/engine/physics/narrowphase/box_box_manifold.d.ts.map +1 -1
  89. package/src/engine/physics/narrowphase/box_box_manifold.js +750 -665
  90. package/src/engine/physics/narrowphase/box_triangle_contact.d.ts.map +1 -1
  91. package/src/engine/physics/narrowphase/box_triangle_contact.js +19 -46
  92. package/src/engine/physics/narrowphase/clip_against_axis_uv.d.ts +16 -0
  93. package/src/engine/physics/narrowphase/clip_against_axis_uv.d.ts.map +1 -0
  94. package/src/engine/physics/narrowphase/clip_against_axis_uv.js +49 -0
  95. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  96. package/src/engine/physics/narrowphase/narrowphase_step.js +52 -4
  97. package/src/engine/physics/queries/raycast.d.ts.map +1 -1
  98. package/src/engine/physics/queries/raycast.js +7 -4
  99. package/src/engine/physics/solver/solve_contacts.d.ts +2 -2
  100. package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
  101. package/src/engine/physics/solver/solve_contacts.js +1341 -1173
  102. package/src/engine/network/sim/BinaryInterpolationAdapter.d.ts.map +0 -1
  103. package/src/engine/network/sim/InterpolationLog.d.ts.map +0 -1
  104. /package/src/engine/{network/sim → interpolation}/BinaryInterpolationAdapter.d.ts +0 -0
  105. /package/src/engine/{network/sim → interpolation}/BinaryInterpolationAdapter.js +0 -0
@@ -1,395 +1,400 @@
1
- import { cubic_residual_times_derivative_accumulate } from "../linalg/cubic_residual_times_derivative_accumulate.js";
2
- import { polynomial_cubic_horner_eval } from "../linalg/polynomial_cubic_horner_eval.js";
3
- import { polynomial_real_roots_in_interval } from "../linalg/polynomial_real_roots_in_interval.js";
4
- import { spline3_hermite_to_monomial } from "./spline3_hermite_to_monomial.js";
5
-
6
- /*
7
- ND specialization (dim ≥ 3). Closest approach is the global minimum of
8
-
9
- Φ(s, t) = Σ_d (A_d(s) B_d(t))²
10
-
11
- over the closed unit square [0, 1]². It can occur at:
12
- 1. an interior critical point where ∇Φ = 0
13
- 2. a boundary edge minimum (one parameter pinned at 0 or 1)
14
- 3. a corner
15
-
16
- Interior critical points are roots of the bivariate gradient system
17
-
18
- F(s, t) = ½ ∂Φ/∂s = Σ_d (A_d(s) − B_d(t)) A_d′(s) (bidegree (5, 3))
19
- G(s, t) = ½ ∂Φ/∂t = −Σ_d (A_d(s) − B_d(t)) B_d′(t) (bidegree (3, 5))
20
-
21
- The fully analytical route eliminates t through the 8×8 Sylvester resultant
22
- of F and G, yielding a univariate of degree 34 in s. Implementing that
23
- with exact polynomial-coefficient arithmetic is genuinely large code, well
24
- beyond the scope of this change. As a first cut, this implementation finds
25
- interior critical points by 2D Newton from a starting grid — quadratically
26
- convergent once close, but reliant on the grid being fine enough to cover
27
- the basins of attraction of all real critical points. It is enough for the
28
- routine geometric configurations the rest of this proposal targets and
29
- leaves a clear seam for the resultant route to land later without changing
30
- the public API.
31
-
32
- Boundary edges and corners are handled exactly: each edge reduces to a
33
- quintic-derivative root-finding (degree 5) of the kind already used in the
34
- 2D path, and corners are direct evaluations.
35
- */
36
-
37
- const MAX_DIM = 16;
38
-
39
- // Per-dim monomial coefficient buffers, one each for A and B. Length 4*MAX_DIM.
40
- const _a_mono = new Float64Array(4 * MAX_DIM);
41
- const _b_mono = new Float64Array(4 * MAX_DIM);
42
-
43
- const _candidate_st = new Float64Array(2);
44
- const _edge_fixed_point = new Float64Array(MAX_DIM);
45
-
46
- const NEWTON_MAX_ITER = 32;
47
- const NEWTON_GRADIENT_TOLERANCE = 1e-13;
48
- const NEWTON_STEP_TOLERANCE = 1e-15;
49
-
50
- const GRID_SIDE = 9; // 9x9 = 81 starting points
51
-
52
- const EDGE_QUINTIC_LEN = 6;
53
- const _edge_quintic = new Float64Array(EDGE_QUINTIC_LEN);
54
- const _edge_roots = new Float64Array(5);
55
-
56
- const CRITICAL_POINT_DEDUPE_TOLERANCE = 1e-7;
57
- const _seen_s = new Float64Array(GRID_SIDE * GRID_SIDE + 8);
58
- const _seen_t = new Float64Array(GRID_SIDE * GRID_SIDE + 8);
59
-
60
- // The gradient system (F, G) = (½ ∂Φ/∂s, ½ ∂Φ/∂t) and the 2×2 Jacobian
61
- // of that gradient are inlined directly into `newton_2d`'s hot loop — see
62
- // the module preamble for the algebraic definitions of F, G, and J.
63
-
64
- /**
65
- * 2D Newton on (F, G) from a starting (s, t). Writes the converged (s, t) into
66
- * `out` and returns true iff it converged inside [0, 1]² with small gradient.
67
- */
68
- // Tolerance for declaring the 2x2 Jacobian singular during the inlined
69
- // Newton solve. Matches `256 * FLT_EPSILON_64` used by
70
- // solve_linear_system_GEPP_2x2 to keep behaviour identical on edge cases.
71
- const NEWTON_SINGULAR_TOLERANCE = 256 * 1.1102230246251565e-16;
72
-
73
- function newton_2d(dim, s_init, t_init, out) {
74
- let s = s_init;
75
- let t = t_init;
76
-
77
- // F, G at the current (s, t) — promoted out of the heap-resident
78
- // `_newton_FG` scratch into iteration-local doubles.
79
- let F = 0, G = 0;
80
- let fg_valid_at_current_st = false;
81
-
82
- for (let iter = 0; iter < NEWTON_MAX_ITER; iter++) {
83
- // ── fused F/G + Jacobian over all dimensions ──
84
- // A single per-iteration sweep over dims computes the gradient
85
- // (F, G) and the symmetric Hessian (Jss, J_cross, Jtt) from one
86
- // load of each cubic's 4 coefficients. Eagerly computing the
87
- // Hessian costs us one extra second-derivative + 3 accumulations
88
- // per dim on the iteration that ends up converging cheap insurance
89
- // for skipping a duplicate cubic-eval sweep on every non-converging
90
- // iteration. J_st = J_ts so the symmetric cross term is computed
91
- // once into J_cross.
92
- F = 0;
93
- G = 0;
94
- let Jss = 0, J_cross = 0, Jtt = 0;
95
-
96
- for (let d = 0; d < dim; d++) {
97
- const base = 4 * d;
98
- const a0 = _a_mono[base];
99
- const a1 = _a_mono[base + 1];
100
- const a2 = _a_mono[base + 2];
101
- const a3 = _a_mono[base + 3];
102
- const b0 = _b_mono[base];
103
- const b1 = _b_mono[base + 1];
104
- const b2 = _b_mono[base + 2];
105
- const b3 = _b_mono[base + 3];
106
-
107
- const A_v = a0 + s * (a1 + s * (a2 + s * a3));
108
- const A_p = a1 + s * (2 * a2 + s * 3 * a3);
109
- const A_pp = 2 * a2 + 6 * a3 * s;
110
- const B_v = b0 + t * (b1 + t * (b2 + t * b3));
111
- const B_p = b1 + t * (2 * b2 + t * 3 * b3);
112
- const B_pp = 2 * b2 + 6 * b3 * t;
113
-
114
- const diff = A_v - B_v;
115
- F += diff * A_p;
116
- G -= diff * B_p;
117
- Jss += A_p * A_p + diff * A_pp;
118
- J_cross += -A_p * B_p;
119
- Jtt += B_p * B_p - diff * B_pp;
120
- }
121
- fg_valid_at_current_st = true;
122
-
123
- if (Math.abs(F) < NEWTON_GRADIENT_TOLERANCE && Math.abs(G) < NEWTON_GRADIENT_TOLERANCE) {
124
- break;
125
- }
126
-
127
- // ── inline solve_linear_system_GEPP_2x2 on column-major J ──
128
- // J = [a00, a10; a01, a11] = [Jss, J_cross; J_cross, Jtt]; rhs = [F, G]
129
- // Partial pivoting on column 0, then forward eliminate, then back-sub.
130
- // The "exact zero" special cases handled by the full solver are skipped:
131
- // for a smooth residual Newton, exact zeros essentially never arise
132
- // (random + analytic inputs), and a singular Jacobian gets rejected
133
- // below the same way the full solver would.
134
- let a00 = Jss, a10 = J_cross, a01 = J_cross, a11 = Jtt;
135
- let b0 = F, b1 = G;
136
- if (Math.abs(a10) > Math.abs(a00)) {
137
- // Row swap
138
- let tmp = a00; a00 = a10; a10 = tmp;
139
- tmp = a01; a01 = a11; a11 = tmp;
140
- tmp = b0; b0 = b1; b1 = tmp;
141
- }
142
- if (a00 === 0) {
143
- return false; // singular column 0
144
- }
145
- const f = -a10 / a00;
146
- a11 += a01 * f;
147
- b1 += b0 * f;
148
- if (Math.abs(a11) < NEWTON_SINGULAR_TOLERANCE) {
149
- return false;
150
- }
151
- const dt = b1 / a11;
152
- const ds = (b0 - a01 * dt) / a00;
153
- if (!Number.isFinite(ds) || !Number.isFinite(dt)) {
154
- return false;
155
- }
156
-
157
- s -= ds;
158
- t -= dt;
159
-
160
- // (s, t) has just changed → cached F, G no longer match.
161
- fg_valid_at_current_st = false;
162
-
163
- // Early bail: if (s, t) has wandered well outside [0, 1] the start is
164
- // diverging or homed in on a critical point of the gradient system
165
- // that lies far from the parameter square. Newton won't reel it back.
166
- if (s < -0.5 || s > 1.5 || t < -0.5 || t > 1.5) {
167
- return false;
168
- }
169
-
170
- if (Math.abs(ds) < NEWTON_STEP_TOLERANCE && Math.abs(dt) < NEWTON_STEP_TOLERANCE) {
171
- break;
172
- }
173
- }
174
-
175
- if (s < -1e-9 || s > 1 + 1e-9) return false;
176
- if (t < -1e-9 || t > 1 + 1e-9) return false;
177
-
178
- // Validate gradient at the final (s, t). When `fg_valid_at_current_st`
179
- // is true we exited the loop via the gradient-tolerance break and the
180
- // validation already passed (tolerance 1e-13 < 1e-6); otherwise we
181
- // exited via the step-tolerance break or maxed out — re-evaluate to
182
- // confirm.
183
- if (!fg_valid_at_current_st) {
184
- F = 0;
185
- G = 0;
186
- for (let d = 0; d < dim; d++) {
187
- const base = 4 * d;
188
- const a0 = _a_mono[base];
189
- const a1 = _a_mono[base + 1];
190
- const a2 = _a_mono[base + 2];
191
- const a3 = _a_mono[base + 3];
192
- const b0 = _b_mono[base];
193
- const b1 = _b_mono[base + 1];
194
- const b2 = _b_mono[base + 2];
195
- const b3 = _b_mono[base + 3];
196
-
197
- const A_v = a0 + s * (a1 + s * (a2 + s * a3));
198
- const A_p = a1 + s * (2 * a2 + s * 3 * a3);
199
- const B_v = b0 + t * (b1 + t * (b2 + t * b3));
200
- const B_p = b1 + t * (2 * b2 + t * 3 * b3);
201
-
202
- const diff = A_v - B_v;
203
- F += diff * A_p;
204
- G -= diff * B_p;
205
- }
206
- if (Math.abs(F) > 1e-6 || Math.abs(G) > 1e-6) return false;
207
- }
208
-
209
- out[0] = s < 0 ? 0 : (s > 1 ? 1 : s);
210
- out[1] = t < 0 ? 0 : (t > 1 ? 1 : t);
211
- return true;
212
- }
213
-
214
- // ── boundary edge: nearest point on (varying-axis) curve to a fixed N-D point ──
215
-
216
- /**
217
- * Minimise ‖(curve_mono...)(t) − point[]‖² over t ∈ [0, 1], where the curve
218
- * is `dim`-dimensional with monomial coefficients packed as
219
- * `[α0_0, α1_0, α2_0, α3_0, α0_1, α1_1, ..., α3_{dim-1}]`.
220
- *
221
- * The squared distance is a degree-6 polynomial in t; its derivative is a
222
- * quintic. We hand the quintic to the polynomial root finder, then test the
223
- * roots plus the endpoints t = 0 and t = 1.
224
- *
225
- * Writes `[t_min, dist²]` into `out` (length 2).
226
- */
227
- function nearest_t_to_fixed_point(curve_mono, dim, point, point_offset, out) {
228
- for (let i = 0; i < EDGE_QUINTIC_LEN; i++){
229
- _edge_quintic[i] = 0;
230
- }
231
-
232
- for (let d = 0; d < dim; d++) {
233
- cubic_residual_times_derivative_accumulate(
234
- _edge_quintic, curve_mono, 4 * d, point[point_offset + d]
235
- );
236
- }
237
-
238
- // Aberth iteration cap of 32 is plenty for the degree-5 quintic that
239
- // comes out of `(p(t) - offset)·p'(t)` on a smooth Hermite cubic — the
240
- // routine's own convergence check still fires early in the common case.
241
- // Default cap (80) is way more than needed and is the worst-case work
242
- // budget; lowering it is a flat ~10-20% saving on degenerate inputs.
243
- const root_count = polynomial_real_roots_in_interval(
244
- _edge_quintic, 5, 0, 1, _edge_roots, 0,
245
- 32
246
- );
247
-
248
- let best_t = 0;
249
- let best_d2 = Number.POSITIVE_INFINITY;
250
-
251
- for (let i = -2; i < root_count; i++) {
252
-
253
- const t = i === -2 ? 0 : (i === -1 ? 1 : _edge_roots[i]);
254
- let d2 = 0;
255
-
256
- for (let d = 0; d < dim; d++) {
257
- const base = 4 * d;
258
- const v = polynomial_cubic_horner_eval(curve_mono, base, t);
259
- const dv = v - point[point_offset + d];
260
- d2 += dv * dv;
261
- }
262
-
263
- if (d2 < best_d2) {
264
- best_d2 = d2;
265
- best_t = t;
266
- }
267
-
268
- }
269
-
270
- out[0] = best_t;
271
- out[1] = best_d2;
272
- }
273
-
274
- // ── public ────────────────────────────────────────────────────────────────
275
-
276
- /**
277
- * Upper bound on the number of (s, t) pairs the ND variant can return:
278
- * one per starting point of the GRID_SIDE × GRID_SIDE grid Newton, plus the
279
- * four boundary edges, plus a small slack. The corresponding result-buffer
280
- * size in floats is `2 * ND_MAX_ROOTS`.
281
- *
282
- * @type {number}
283
- */
284
- export const ND_MAX_ROOTS = GRID_SIDE * GRID_SIDE + 8;
285
-
286
- /**
287
- * Critical-point enumerator for the ND Hermite curve-pair intersection
288
- * problem (dim 2, optimal for dim ≥ 3). Writes (s, t) pairs sequentially
289
- * into `result` starting at `result_offset` and returns the count.
290
- *
291
- * What is reported (deduplicated within
292
- * CRITICAL_POINT_DEDUPE_TOLERANCE = 1e-7 in each parameter):
293
- * - Interior critical points of squared distance found by 2D Newton
294
- * started from a GRID_SIDE × GRID_SIDE grid in [0,1]² and accepted
295
- * when they converge inside [0,1]² with small gradient.
296
- * - Four boundary closest-approach pairs, one per edge of [0,1]², each
297
- * coming from a quintic root-find of (curve − fixed point)·curve′.
298
- *
299
- * Caller is responsible for evaluating both curves at each (s, t) and
300
- * picking whichever pair(s) they care about (closest, within threshold,
301
- * etc.).
302
- *
303
- * Required buffer size:
304
- * `result.length >= result_offset + 2 * ND_MAX_ROOTS` floats (currently 178).
305
- *
306
- * @param {Float64Array|number[]} a length 4*dim
307
- * @param {Float64Array|number[]} b length 4*dim
308
- * @param {number} dim ≥ 2 (also correct for dim ≥ 2 but slower than the 2D path)
309
- * @param {Float64Array|number[]} result length >= result_offset + 2 * ND_MAX_ROOTS
310
- * @param {number} result_offset
311
- * @returns {number} number of (s, t) pairs written
312
- */
313
- export function spline3_hermite_intersection_spline3_hermite_nd(
314
- a, b, dim,
315
- result, result_offset
316
- ) {
317
- if (dim > MAX_DIM) {
318
- throw new Error(`dim=${dim} exceeds MAX_DIM=${MAX_DIM}`);
319
- }
320
-
321
- // Pack monomial coefficients per-axis into the module buffers.
322
- for (let d = 0; d < dim; d++) {
323
- const off = 4 * d;
324
- spline3_hermite_to_monomial(_a_mono, off, 1, a[off], a[off + 1], a[off + 2], a[off + 3]);
325
- spline3_hermite_to_monomial(_b_mono, off, 1, b[off], b[off + 1], b[off + 2], b[off + 3]);
326
- }
327
-
328
- // We dedupe near-identical critical points discovered from different grid
329
- // starts. _seen_s / _seen_t double as the "candidates written so far" and
330
- // are mirrored into the caller's `result` buffer as we go.
331
- let count = 0;
332
- let write = result_offset;
333
-
334
- const try_candidate = (s, t) => {
335
- for (let i = 0; i < count; i++) {
336
- if (Math.abs(_seen_s[i] - s) < CRITICAL_POINT_DEDUPE_TOLERANCE
337
- && Math.abs(_seen_t[i] - t) < CRITICAL_POINT_DEDUPE_TOLERANCE) {
338
- return;
339
- }
340
- }
341
- _seen_s[count] = s;
342
- _seen_t[count] = t;
343
- result[write++] = s;
344
- result[write++] = t;
345
- count++;
346
- };
347
-
348
- // Interior critical points via Newton from a grid.
349
- const step = 1 / (GRID_SIDE - 1);
350
- for (let i = 0; i < GRID_SIDE; i++) {
351
- const s_init = i * step;
352
- for (let j = 0; j < GRID_SIDE; j++) {
353
- const t_init = j * step;
354
- if (newton_2d(dim, s_init, t_init, _candidate_st)) {
355
- try_candidate(_candidate_st[0], _candidate_st[1]);
356
- }
357
- }
358
- }
359
-
360
- // Boundary edges. Each edge problem reduces to "nearest point on the
361
- // *other* curve to a fixed point on the boundary curve".
362
- const tmp_pt = _edge_fixed_point;
363
-
364
- // Edge s = 0: fixed point is A(0); minimise over t on B.
365
- for (let d = 0; d < dim; d++){
366
- tmp_pt[d] = _a_mono[4 * d]; // α0
367
- }
368
- nearest_t_to_fixed_point(_b_mono, dim, tmp_pt, 0, _candidate_st);
369
- try_candidate(0, _candidate_st[0]);
370
-
371
- // Edge s = 1: A(1) = α0 + α1 + α2 + α3.
372
- for (let d = 0; d < dim; d++) {
373
- const base = 4 * d;
374
- tmp_pt[d] = _a_mono[base] + _a_mono[base + 1] + _a_mono[base + 2] + _a_mono[base + 3];
375
- }
376
- nearest_t_to_fixed_point(_b_mono, dim, tmp_pt, 0, _candidate_st);
377
- try_candidate(1, _candidate_st[0]);
378
-
379
- // Edge t = 0: fixed point is B(0); minimise over s on A.
380
- for (let d = 0; d < dim; d++){
381
- tmp_pt[d] = _b_mono[4 * d];
382
- }
383
- nearest_t_to_fixed_point(_a_mono, dim, tmp_pt, 0, _candidate_st);
384
- try_candidate(_candidate_st[0], 0);
385
-
386
- // Edge t = 1.
387
- for (let d = 0; d < dim; d++) {
388
- const base = 4 * d;
389
- tmp_pt[d] = _b_mono[base] + _b_mono[base + 1] + _b_mono[base + 2] + _b_mono[base + 3];
390
- }
391
- nearest_t_to_fixed_point(_a_mono, dim, tmp_pt, 0, _candidate_st);
392
- try_candidate(_candidate_st[0], 1);
393
-
394
- return count;
395
- }
1
+ import { cubic_residual_times_derivative_accumulate } from "../linalg/cubic_residual_times_derivative_accumulate.js";
2
+ import { polynomial_cubic_horner_eval } from "../linalg/polynomial_cubic_horner_eval.js";
3
+ import { polynomial_real_roots_in_interval } from "../linalg/polynomial_real_roots_in_interval.js";
4
+ import { spline3_hermite_to_monomial } from "./spline3_hermite_to_monomial.js";
5
+
6
+ /*
7
+ Dimension-agnostic curve-pair closest-approach / intersection enumerator
8
+ (dim ≥ 2). This is the path the dispatcher uses for all dim ≥ 2: the former
9
+ bespoke 2D Bezout specialization was removed because it enumerated interior
10
+ *intersections* plus boundary minima only and so missed interior
11
+ non-intersection closest approaches. Closest approach is the global minimum of
12
+
13
+ Φ(s, t) = Σ_d (A_d(s) B_d(t)
14
+
15
+ over the closed unit square [0, 1]². It can occur at:
16
+ 1. an interior critical point where ∇Φ = 0
17
+ 2. a boundary edge minimum (one parameter pinned at 0 or 1)
18
+ 3. a corner
19
+
20
+ Interior critical points are roots of the bivariate gradient system
21
+
22
+ F(s, t) = ½ ∂Φ/∂s = Σ_d (A_d(s) B_d(t)) A_d′(s) (bidegree (5, 3))
23
+ G(s, t) = ½ ∂Φ/∂t = −Σ_d (A_d(s) − B_d(t)) B_d′(t) (bidegree (3, 5))
24
+
25
+ This module finds those interior critical points by 2D Newton started from a
26
+ GRID_SIDE × GRID_SIDE grid in [0, 1]² quadratically convergent once close,
27
+ but reliant on the grid being dense enough to cover the basins of attraction
28
+ of all real critical points (9×9 = 81 starts is dense enough for the
29
+ geometric configurations seen in practice). A fully analytical alternative
30
+ eliminates t through the 8×8 Sylvester resultant of F and G, yielding a
31
+ univariate of degree ≤ 34 in s; it is exact and basin-independent but
32
+ genuinely large code with delicate degree-34 root-finding numerics, so it is
33
+ left as a possible future drop-in behind this same public API rather than
34
+ implemented here.
35
+
36
+ Boundary edges and corners are handled exactly: each edge reduces to a
37
+ quintic-derivative root-finding (degree 5), and corners fall out as the
38
+ endpoints tested inside each edge solve.
39
+ */
40
+
41
+ const MAX_DIM = 16;
42
+
43
+ // Per-dim monomial coefficient buffers, one each for A and B. Length 4*MAX_DIM.
44
+ const _a_mono = new Float64Array(4 * MAX_DIM);
45
+ const _b_mono = new Float64Array(4 * MAX_DIM);
46
+
47
+ const _candidate_st = new Float64Array(2);
48
+ const _edge_fixed_point = new Float64Array(MAX_DIM);
49
+
50
+ const NEWTON_MAX_ITER = 32;
51
+ const NEWTON_GRADIENT_TOLERANCE = 1e-13;
52
+ const NEWTON_STEP_TOLERANCE = 1e-15;
53
+
54
+ const GRID_SIDE = 9; // 9x9 = 81 starting points
55
+
56
+ const EDGE_QUINTIC_LEN = 6;
57
+ const _edge_quintic = new Float64Array(EDGE_QUINTIC_LEN);
58
+ const _edge_roots = new Float64Array(5);
59
+
60
+ const CRITICAL_POINT_DEDUPE_TOLERANCE = 1e-7;
61
+ const _seen_s = new Float64Array(GRID_SIDE * GRID_SIDE + 8);
62
+ const _seen_t = new Float64Array(GRID_SIDE * GRID_SIDE + 8);
63
+
64
+ // The gradient system (F, G) = (½ ∂Φ/∂s, ½ ∂Φ/∂t) and the 2×2 Jacobian
65
+ // of that gradient are inlined directly into `newton_2d`'s hot loop see
66
+ // the module preamble for the algebraic definitions of F, G, and J.
67
+
68
+ /**
69
+ * 2D Newton on (F, G) from a starting (s, t). Writes the converged (s, t) into
70
+ * `out` and returns true iff it converged inside [0, 1]² with small gradient.
71
+ */
72
+ // Tolerance for declaring the 2x2 Jacobian singular during the inlined
73
+ // Newton solve. Matches `256 * FLT_EPSILON_64` used by
74
+ // solve_linear_system_GEPP_2x2 to keep behaviour identical on edge cases.
75
+ const NEWTON_SINGULAR_TOLERANCE = 256 * 1.1102230246251565e-16;
76
+
77
+ function newton_2d(dim, s_init, t_init, out) {
78
+ let s = s_init;
79
+ let t = t_init;
80
+
81
+ // F, G at the current (s, t) — promoted out of the heap-resident
82
+ // `_newton_FG` scratch into iteration-local doubles.
83
+ let F = 0, G = 0;
84
+ let fg_valid_at_current_st = false;
85
+
86
+ for (let iter = 0; iter < NEWTON_MAX_ITER; iter++) {
87
+ // ── fused F/G + Jacobian over all dimensions ──
88
+ // A single per-iteration sweep over dims computes the gradient
89
+ // (F, G) and the symmetric Hessian (Jss, J_cross, Jtt) from one
90
+ // load of each cubic's 4 coefficients. Eagerly computing the
91
+ // Hessian costs us one extra second-derivative + 3 accumulations
92
+ // per dim on the iteration that ends up converging — cheap insurance
93
+ // for skipping a duplicate cubic-eval sweep on every non-converging
94
+ // iteration. J_st = J_ts so the symmetric cross term is computed
95
+ // once into J_cross.
96
+ F = 0;
97
+ G = 0;
98
+ let Jss = 0, J_cross = 0, Jtt = 0;
99
+
100
+ for (let d = 0; d < dim; d++) {
101
+ const base = 4 * d;
102
+ const a0 = _a_mono[base];
103
+ const a1 = _a_mono[base + 1];
104
+ const a2 = _a_mono[base + 2];
105
+ const a3 = _a_mono[base + 3];
106
+ const b0 = _b_mono[base];
107
+ const b1 = _b_mono[base + 1];
108
+ const b2 = _b_mono[base + 2];
109
+ const b3 = _b_mono[base + 3];
110
+
111
+ const A_v = a0 + s * (a1 + s * (a2 + s * a3));
112
+ const A_p = a1 + s * (2 * a2 + s * 3 * a3);
113
+ const A_pp = 2 * a2 + 6 * a3 * s;
114
+ const B_v = b0 + t * (b1 + t * (b2 + t * b3));
115
+ const B_p = b1 + t * (2 * b2 + t * 3 * b3);
116
+ const B_pp = 2 * b2 + 6 * b3 * t;
117
+
118
+ const diff = A_v - B_v;
119
+ F += diff * A_p;
120
+ G -= diff * B_p;
121
+ Jss += A_p * A_p + diff * A_pp;
122
+ J_cross += -A_p * B_p;
123
+ Jtt += B_p * B_p - diff * B_pp;
124
+ }
125
+ fg_valid_at_current_st = true;
126
+
127
+ if (Math.abs(F) < NEWTON_GRADIENT_TOLERANCE && Math.abs(G) < NEWTON_GRADIENT_TOLERANCE) {
128
+ break;
129
+ }
130
+
131
+ // ── inline solve_linear_system_GEPP_2x2 on column-major J ──
132
+ // J = [a00, a10; a01, a11] = [Jss, J_cross; J_cross, Jtt]; rhs = [F, G]
133
+ // Partial pivoting on column 0, then forward eliminate, then back-sub.
134
+ // The "exact zero" special cases handled by the full solver are skipped:
135
+ // for a smooth residual Newton, exact zeros essentially never arise
136
+ // (random + analytic inputs), and a singular Jacobian gets rejected
137
+ // below the same way the full solver would.
138
+ let a00 = Jss, a10 = J_cross, a01 = J_cross, a11 = Jtt;
139
+ let b0 = F, b1 = G;
140
+ if (Math.abs(a10) > Math.abs(a00)) {
141
+ // Row swap
142
+ let tmp = a00; a00 = a10; a10 = tmp;
143
+ tmp = a01; a01 = a11; a11 = tmp;
144
+ tmp = b0; b0 = b1; b1 = tmp;
145
+ }
146
+ if (a00 === 0) {
147
+ return false; // singular column 0
148
+ }
149
+ const f = -a10 / a00;
150
+ a11 += a01 * f;
151
+ b1 += b0 * f;
152
+ if (Math.abs(a11) < NEWTON_SINGULAR_TOLERANCE) {
153
+ return false;
154
+ }
155
+ const dt = b1 / a11;
156
+ const ds = (b0 - a01 * dt) / a00;
157
+ if (!Number.isFinite(ds) || !Number.isFinite(dt)) {
158
+ return false;
159
+ }
160
+
161
+ s -= ds;
162
+ t -= dt;
163
+
164
+ // (s, t) has just changed cached F, G no longer match.
165
+ fg_valid_at_current_st = false;
166
+
167
+ // Early bail: if (s, t) has wandered well outside [0, 1] the start is
168
+ // diverging or homed in on a critical point of the gradient system
169
+ // that lies far from the parameter square. Newton won't reel it back.
170
+ if (s < -0.5 || s > 1.5 || t < -0.5 || t > 1.5) {
171
+ return false;
172
+ }
173
+
174
+ if (Math.abs(ds) < NEWTON_STEP_TOLERANCE && Math.abs(dt) < NEWTON_STEP_TOLERANCE) {
175
+ break;
176
+ }
177
+ }
178
+
179
+ if (s < -1e-9 || s > 1 + 1e-9) return false;
180
+ if (t < -1e-9 || t > 1 + 1e-9) return false;
181
+
182
+ // Validate gradient at the final (s, t). When `fg_valid_at_current_st`
183
+ // is true we exited the loop via the gradient-tolerance break and the
184
+ // validation already passed (tolerance 1e-13 < 1e-6); otherwise we
185
+ // exited via the step-tolerance break or maxed out — re-evaluate to
186
+ // confirm.
187
+ if (!fg_valid_at_current_st) {
188
+ F = 0;
189
+ G = 0;
190
+ for (let d = 0; d < dim; d++) {
191
+ const base = 4 * d;
192
+ const a0 = _a_mono[base];
193
+ const a1 = _a_mono[base + 1];
194
+ const a2 = _a_mono[base + 2];
195
+ const a3 = _a_mono[base + 3];
196
+ const b0 = _b_mono[base];
197
+ const b1 = _b_mono[base + 1];
198
+ const b2 = _b_mono[base + 2];
199
+ const b3 = _b_mono[base + 3];
200
+
201
+ const A_v = a0 + s * (a1 + s * (a2 + s * a3));
202
+ const A_p = a1 + s * (2 * a2 + s * 3 * a3);
203
+ const B_v = b0 + t * (b1 + t * (b2 + t * b3));
204
+ const B_p = b1 + t * (2 * b2 + t * 3 * b3);
205
+
206
+ const diff = A_v - B_v;
207
+ F += diff * A_p;
208
+ G -= diff * B_p;
209
+ }
210
+ if (Math.abs(F) > 1e-6 || Math.abs(G) > 1e-6) return false;
211
+ }
212
+
213
+ out[0] = s < 0 ? 0 : (s > 1 ? 1 : s);
214
+ out[1] = t < 0 ? 0 : (t > 1 ? 1 : t);
215
+ return true;
216
+ }
217
+
218
+ // ── boundary edge: nearest point on (varying-axis) curve to a fixed N-D point ──
219
+
220
+ /**
221
+ * Minimise ‖(curve_mono...)(t) point[]‖² over t [0, 1], where the curve
222
+ * is `dim`-dimensional with monomial coefficients packed as
223
+ * `[α0_0, α1_0, α2_0, α3_0, α0_1, α1_1, ..., α3_{dim-1}]`.
224
+ *
225
+ * The squared distance is a degree-6 polynomial in t; its derivative is a
226
+ * quintic. We hand the quintic to the polynomial root finder, then test the
227
+ * roots plus the endpoints t = 0 and t = 1.
228
+ *
229
+ * Writes `[t_min, dist²]` into `out` (length ≥ 2).
230
+ */
231
+ function nearest_t_to_fixed_point(curve_mono, dim, point, point_offset, out) {
232
+ for (let i = 0; i < EDGE_QUINTIC_LEN; i++){
233
+ _edge_quintic[i] = 0;
234
+ }
235
+
236
+ for (let d = 0; d < dim; d++) {
237
+ cubic_residual_times_derivative_accumulate(
238
+ _edge_quintic, curve_mono, 4 * d, point[point_offset + d]
239
+ );
240
+ }
241
+
242
+ // Aberth iteration cap of 32 is plenty for the degree-5 quintic that
243
+ // comes out of `(p(t) - offset)·p'(t)` on a smooth Hermite cubic — the
244
+ // routine's own convergence check still fires early in the common case.
245
+ // Default cap (80) is way more than needed and is the worst-case work
246
+ // budget; lowering it is a flat ~10-20% saving on degenerate inputs.
247
+ const root_count = polynomial_real_roots_in_interval(
248
+ _edge_quintic, 5, 0, 1, _edge_roots, 0,
249
+ 32
250
+ );
251
+
252
+ let best_t = 0;
253
+ let best_d2 = Number.POSITIVE_INFINITY;
254
+
255
+ for (let i = -2; i < root_count; i++) {
256
+
257
+ const t = i === -2 ? 0 : (i === -1 ? 1 : _edge_roots[i]);
258
+ let d2 = 0;
259
+
260
+ for (let d = 0; d < dim; d++) {
261
+ const base = 4 * d;
262
+ const v = polynomial_cubic_horner_eval(curve_mono, base, t);
263
+ const dv = v - point[point_offset + d];
264
+ d2 += dv * dv;
265
+ }
266
+
267
+ if (d2 < best_d2) {
268
+ best_d2 = d2;
269
+ best_t = t;
270
+ }
271
+
272
+ }
273
+
274
+ out[0] = best_t;
275
+ out[1] = best_d2;
276
+ }
277
+
278
+ // ── public ────────────────────────────────────────────────────────────────
279
+
280
+ /**
281
+ * Upper bound on the number of (s, t) pairs the ND variant can return:
282
+ * one per starting point of the GRID_SIDE × GRID_SIDE grid Newton, plus the
283
+ * four boundary edges, plus a small slack. The corresponding result-buffer
284
+ * size in floats is `2 * ND_MAX_ROOTS`.
285
+ *
286
+ * @type {number}
287
+ */
288
+ export const ND_MAX_ROOTS = GRID_SIDE * GRID_SIDE + 8;
289
+
290
+ /**
291
+ * Critical-point enumerator for the dimension-agnostic Hermite curve-pair
292
+ * intersection / closest-approach problem (dim ≥ 2; the dispatcher routes all
293
+ * dim 2 here). Writes (s, t) pairs sequentially into `result` starting at
294
+ * `result_offset` and returns the count.
295
+ *
296
+ * What is reported (deduplicated within
297
+ * CRITICAL_POINT_DEDUPE_TOLERANCE = 1e-7 in each parameter):
298
+ * - Interior critical points of squared distance found by 2D Newton
299
+ * started from a GRID_SIDE × GRID_SIDE grid in [0,1]² and accepted
300
+ * when they converge inside [0,1]² with small gradient.
301
+ * - Four boundary closest-approach pairs, one per edge of [0,1]², each
302
+ * coming from a quintic root-find of (curve − fixed point)·curve′.
303
+ *
304
+ * Caller is responsible for evaluating both curves at each (s, t) and
305
+ * picking whichever pair(s) they care about (closest, within threshold,
306
+ * etc.).
307
+ *
308
+ * Required buffer size:
309
+ * `result.length >= result_offset + 2 * ND_MAX_ROOTS` floats (currently 178).
310
+ *
311
+ * @param {Float64Array|number[]} a length 4*dim
312
+ * @param {Float64Array|number[]} b length 4*dim
313
+ * @param {number} dim ≥ 2
314
+ * @param {Float64Array|number[]} result length >= result_offset + 2 * ND_MAX_ROOTS
315
+ * @param {number} result_offset
316
+ * @returns {number} number of (s, t) pairs written
317
+ */
318
+ export function spline3_hermite_intersection_spline3_hermite_nd(
319
+ a, b, dim,
320
+ result, result_offset
321
+ ) {
322
+ if (dim > MAX_DIM) {
323
+ throw new Error(`dim=${dim} exceeds MAX_DIM=${MAX_DIM}`);
324
+ }
325
+
326
+ // Pack monomial coefficients per-axis into the module buffers.
327
+ for (let d = 0; d < dim; d++) {
328
+ const off = 4 * d;
329
+ spline3_hermite_to_monomial(_a_mono, off, 1, a[off], a[off + 1], a[off + 2], a[off + 3]);
330
+ spline3_hermite_to_monomial(_b_mono, off, 1, b[off], b[off + 1], b[off + 2], b[off + 3]);
331
+ }
332
+
333
+ // We dedupe near-identical critical points discovered from different grid
334
+ // starts. _seen_s / _seen_t double as the "candidates written so far" and
335
+ // are mirrored into the caller's `result` buffer as we go.
336
+ let count = 0;
337
+ let write = result_offset;
338
+
339
+ const try_candidate = (s, t) => {
340
+ for (let i = 0; i < count; i++) {
341
+ if (Math.abs(_seen_s[i] - s) < CRITICAL_POINT_DEDUPE_TOLERANCE
342
+ && Math.abs(_seen_t[i] - t) < CRITICAL_POINT_DEDUPE_TOLERANCE) {
343
+ return;
344
+ }
345
+ }
346
+ _seen_s[count] = s;
347
+ _seen_t[count] = t;
348
+ result[write++] = s;
349
+ result[write++] = t;
350
+ count++;
351
+ };
352
+
353
+ // Interior critical points via Newton from a grid.
354
+ const step = 1 / (GRID_SIDE - 1);
355
+ for (let i = 0; i < GRID_SIDE; i++) {
356
+ const s_init = i * step;
357
+ for (let j = 0; j < GRID_SIDE; j++) {
358
+ const t_init = j * step;
359
+ if (newton_2d(dim, s_init, t_init, _candidate_st)) {
360
+ try_candidate(_candidate_st[0], _candidate_st[1]);
361
+ }
362
+ }
363
+ }
364
+
365
+ // Boundary edges. Each edge problem reduces to "nearest point on the
366
+ // *other* curve to a fixed point on the boundary curve".
367
+ const tmp_pt = _edge_fixed_point;
368
+
369
+ // Edge s = 0: fixed point is A(0); minimise over t on B.
370
+ for (let d = 0; d < dim; d++){
371
+ tmp_pt[d] = _a_mono[4 * d]; // α0
372
+ }
373
+ nearest_t_to_fixed_point(_b_mono, dim, tmp_pt, 0, _candidate_st);
374
+ try_candidate(0, _candidate_st[0]);
375
+
376
+ // Edge s = 1: A(1) = α0 + α1 + α2 + α3.
377
+ for (let d = 0; d < dim; d++) {
378
+ const base = 4 * d;
379
+ tmp_pt[d] = _a_mono[base] + _a_mono[base + 1] + _a_mono[base + 2] + _a_mono[base + 3];
380
+ }
381
+ nearest_t_to_fixed_point(_b_mono, dim, tmp_pt, 0, _candidate_st);
382
+ try_candidate(1, _candidate_st[0]);
383
+
384
+ // Edge t = 0: fixed point is B(0); minimise over s on A.
385
+ for (let d = 0; d < dim; d++){
386
+ tmp_pt[d] = _b_mono[4 * d];
387
+ }
388
+ nearest_t_to_fixed_point(_a_mono, dim, tmp_pt, 0, _candidate_st);
389
+ try_candidate(_candidate_st[0], 0);
390
+
391
+ // Edge t = 1.
392
+ for (let d = 0; d < dim; d++) {
393
+ const base = 4 * d;
394
+ tmp_pt[d] = _b_mono[base] + _b_mono[base + 1] + _b_mono[base + 2] + _b_mono[base + 3];
395
+ }
396
+ nearest_t_to_fixed_point(_a_mono, dim, tmp_pt, 0, _candidate_st);
397
+ try_candidate(_candidate_st[0], 1);
398
+
399
+ return count;
400
+ }