@woosh/meep-engine 2.147.0 → 2.149.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 (61) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_bridge_islands.d.ts +23 -0
  3. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_bridge_islands.d.ts.map +1 -0
  4. package/src/core/geom/3d/topology/struct/binary/io/bt_mesh_bridge_islands.js +295 -0
  5. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite.d.ts +4 -4
  6. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite.d.ts.map +1 -1
  7. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite.js +48 -52
  8. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_2d.d.ts +23 -21
  9. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_2d.d.ts.map +1 -1
  10. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_2d.js +41 -406
  11. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_nd.d.ts +5 -4
  12. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_nd.d.ts.map +1 -1
  13. package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_nd.js +400 -395
  14. package/src/engine/navigation/mesh/NavigationMesh.d.ts +6 -2
  15. package/src/engine/navigation/mesh/NavigationMesh.d.ts.map +1 -1
  16. package/src/engine/navigation/mesh/NavigationMesh.js +234 -212
  17. package/src/engine/navigation/mesh/bt_mesh_face_find_path.d.ts +7 -3
  18. package/src/engine/navigation/mesh/bt_mesh_face_find_path.d.ts.map +1 -1
  19. package/src/engine/navigation/mesh/bt_mesh_face_find_path.js +67 -73
  20. package/src/engine/navigation/mesh/build/enforce_agent_height_clearance.d.ts +16 -5
  21. package/src/engine/navigation/mesh/build/enforce_agent_height_clearance.d.ts.map +1 -1
  22. package/src/engine/navigation/mesh/build/enforce_agent_height_clearance.js +262 -147
  23. package/src/engine/navigation/mesh/build/navmesh_build_topology.d.ts.map +1 -1
  24. package/src/engine/navigation/mesh/build/navmesh_build_topology.js +33 -3
  25. package/src/engine/navigation/mesh/bvh_query_nearest_face.d.ts +4 -1
  26. package/src/engine/navigation/mesh/bvh_query_nearest_face.d.ts.map +1 -1
  27. package/src/engine/navigation/mesh/bvh_query_nearest_face.js +164 -131
  28. package/src/engine/physics/body/SolverBodyState.d.ts +142 -0
  29. package/src/engine/physics/body/SolverBodyState.d.ts.map +1 -0
  30. package/src/engine/physics/body/SolverBodyState.js +251 -0
  31. package/src/engine/physics/broadphase/generate_pairs.d.ts +2 -1
  32. package/src/engine/physics/broadphase/generate_pairs.d.ts.map +1 -1
  33. package/src/engine/physics/broadphase/generate_pairs.js +110 -108
  34. package/src/engine/physics/constraint/solve_constraints.d.ts.map +1 -1
  35. package/src/engine/physics/constraint/solve_constraints.js +691 -673
  36. package/src/engine/physics/ecs/PhysicsSystem.d.ts +21 -18
  37. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  38. package/src/engine/physics/ecs/PhysicsSystem.js +223 -91
  39. package/src/engine/physics/inertia/world_inverse_inertia.d.ts +23 -0
  40. package/src/engine/physics/inertia/world_inverse_inertia.d.ts.map +1 -1
  41. package/src/engine/physics/inertia/world_inverse_inertia.js +116 -77
  42. package/src/engine/physics/integration/integrate_position.d.ts +11 -1
  43. package/src/engine/physics/integration/integrate_position.d.ts.map +1 -1
  44. package/src/engine/physics/integration/integrate_position.js +97 -79
  45. package/src/engine/physics/integration/integrate_velocity.d.ts +12 -3
  46. package/src/engine/physics/integration/integrate_velocity.d.ts.map +1 -1
  47. package/src/engine/physics/integration/integrate_velocity.js +201 -160
  48. package/src/engine/physics/narrowphase/box_box_manifold.d.ts.map +1 -1
  49. package/src/engine/physics/narrowphase/box_box_manifold.js +750 -665
  50. package/src/engine/physics/narrowphase/box_triangle_contact.d.ts.map +1 -1
  51. package/src/engine/physics/narrowphase/box_triangle_contact.js +4 -34
  52. package/src/engine/physics/narrowphase/clip_against_axis_uv.d.ts +16 -0
  53. package/src/engine/physics/narrowphase/clip_against_axis_uv.d.ts.map +1 -0
  54. package/src/engine/physics/narrowphase/clip_against_axis_uv.js +49 -0
  55. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  56. package/src/engine/physics/narrowphase/narrowphase_step.js +24 -3
  57. package/src/engine/physics/queries/raycast.d.ts.map +1 -1
  58. package/src/engine/physics/queries/raycast.js +201 -198
  59. package/src/engine/physics/solver/solve_contacts.d.ts +2 -2
  60. package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
  61. package/src/engine/physics/solver/solve_contacts.js +1341 -1173
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "description": "Pure JavaScript game engine. Fully featured and production ready.",
7
7
  "type": "module",
8
8
  "author": "Alexander Goldring",
9
- "version": "2.147.0",
9
+ "version": "2.149.0",
10
10
  "main": "build/meep.module.js",
11
11
  "module": "build/meep.module.js",
12
12
  "exports": {
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Connect islands that are not topologically joined but lie within a small step (vertical) and/or gap
3
+ * (lateral) of one another, so an agent can path across stairs and small breaks in the floor.
4
+ *
5
+ * The two unused params on the build path (`agent_max_step_height`, `agent_max_step_distance`) drive this.
6
+ * Boundary edges of distinct islands whose endpoints can be paired within the thresholds are stitched
7
+ * with a bridge quad (two triangles). Connectivity is realised by rebuilding the mesh from its triangle
8
+ * soup plus the bridge quads and re-running the same vertex-merge / edge-fuse the build already uses -
9
+ * the bridge corners are copied verbatim from existing boundary vertices, so they fuse onto the islands.
10
+ *
11
+ * Mutates `mesh` in place. Triangulated input is assumed (as produced by the navmesh build pipeline).
12
+ *
13
+ * @param {BinaryTopology} mesh
14
+ * @param {{max_step_height:number, max_step_distance:number, up:Vector3}} options
15
+ * @returns {number} number of bridges added
16
+ */
17
+ export function bt_mesh_bridge_islands(mesh: BinaryTopology, { max_step_height, max_step_distance, up }: {
18
+ max_step_height: number;
19
+ max_step_distance: number;
20
+ up: Vector3;
21
+ }): number;
22
+ import { BinaryTopology } from "../BinaryTopology.js";
23
+ //# sourceMappingURL=bt_mesh_bridge_islands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bt_mesh_bridge_islands.d.ts","sourceRoot":"","sources":["../../../../../../../../../src/core/geom/3d/topology/struct/binary/io/bt_mesh_bridge_islands.js"],"names":[],"mappings":"AAoHA;;;;;;;;;;;;;;;GAeG;AACH,6CAJW,cAAc,8CACd;IAAC,eAAe,EAAC,MAAM,CAAC;IAAC,iBAAiB,EAAC,MAAM,CAAC;IAAC,EAAE,UAAQ;CAAC,GAC5D,MAAM,CAoKlB;+BAtS8B,sBAAsB"}
@@ -0,0 +1,295 @@
1
+ import { BinaryTopology } from "../BinaryTopology.js";
2
+ import { bt_mesh_compute_face_islands } from "../query/bt_mesh_compute_face_islands.js";
3
+ import { bt_query_edge_is_boundary } from "../query/bt_query_edge_is_boundary.js";
4
+ import { bt_mesh_from_unindexed_geometry } from "./bt_mesh_from_unindexed_geometry.js";
5
+ import { bt_mesh_fuse_duplicate_edges } from "./edge/bt_mesh_fuse_duplicate_edges.js";
6
+ import { bt_merge_verts_by_distance } from "./vertex/bt_merge_verts_by_distance.js";
7
+
8
+ /**
9
+ * Distance under which rebuilt vertices are fused, matching the navmesh build pipeline. Bridge corner
10
+ * coordinates are copied verbatim from existing boundary vertices, so they fuse exactly.
11
+ * @type {number}
12
+ */
13
+ const VERTEX_MERGE_DISTANCE = 1e-6;
14
+
15
+ /**
16
+ * Tolerance added to the step thresholds so exactly-on-the-limit connections (e.g. a perfectly aligned
17
+ * step with zero lateral gap, tested against max_step_distance = 0) are accepted.
18
+ * @type {number}
19
+ */
20
+ const THRESHOLD_EPSILON = 1e-6;
21
+
22
+ /**
23
+ * Decompose the vector (px,py,pz) - (qx,qy,qz) into components along and perpendicular to `up` and,
24
+ * if it is within the step thresholds, return the perpendicular (lateral) distance; otherwise -1.
25
+ */
26
+ function connection_lateral_distance(
27
+ px, py, pz,
28
+ qx, qy, qz,
29
+ up_x, up_y, up_z,
30
+ max_step_height, max_step_distance
31
+ ) {
32
+ const dx = px - qx;
33
+ const dy = py - qy;
34
+ const dz = pz - qz;
35
+
36
+ const along_up = dx * up_x + dy * up_y + dz * up_z;
37
+ const vertical = Math.abs(along_up);
38
+ const lateral = Math.sqrt(Math.max(0, (dx * dx + dy * dy + dz * dz) - along_up * along_up));
39
+
40
+ if (vertical <= max_step_height + THRESHOLD_EPSILON && lateral <= max_step_distance + THRESHOLD_EPSILON) {
41
+ return lateral;
42
+ }
43
+
44
+ return -1;
45
+ }
46
+
47
+ /**
48
+ * Evaluate whether two boundary edges can be bridged and, if so, which endpoint pairing to use.
49
+ * Writes the cheaper valid pairing into `out_best` as `[pairing, cost]` (pairing 1 = a1<->b1 / a2<->b2,
50
+ * pairing 2 = a1<->b2 / a2<->b1) and returns true; returns false (leaving `out_best` untouched) when
51
+ * neither pairing fits the thresholds. `out_best` is supplied by the caller to avoid per-pair allocation.
52
+ *
53
+ * @returns {boolean}
54
+ */
55
+ function evaluate_pair(a, b, up_x, up_y, up_z, max_step_height, max_step_distance, out_best) {
56
+ // pairing 1: a.p1 <-> b.p1, a.p2 <-> b.p2
57
+ const c1_1 = connection_lateral_distance(b.x1, b.y1, b.z1, a.x1, a.y1, a.z1, up_x, up_y, up_z, max_step_height, max_step_distance);
58
+ const c1_2 = connection_lateral_distance(b.x2, b.y2, b.z2, a.x2, a.y2, a.z2, up_x, up_y, up_z, max_step_height, max_step_distance);
59
+
60
+ // pairing 2: a.p1 <-> b.p2, a.p2 <-> b.p1
61
+ const c2_1 = connection_lateral_distance(b.x2, b.y2, b.z2, a.x1, a.y1, a.z1, up_x, up_y, up_z, max_step_height, max_step_distance);
62
+ const c2_2 = connection_lateral_distance(b.x1, b.y1, b.z1, a.x2, a.y2, a.z2, up_x, up_y, up_z, max_step_height, max_step_distance);
63
+
64
+ let found = false;
65
+ let pairing = 0;
66
+ let cost = Infinity;
67
+
68
+ if (c1_1 >= 0 && c1_2 >= 0) {
69
+ pairing = 1;
70
+ cost = c1_1 + c1_2;
71
+ found = true;
72
+ }
73
+
74
+ if (c2_1 >= 0 && c2_2 >= 0) {
75
+ const cost_2 = c2_1 + c2_2;
76
+ if (!found || cost_2 < cost) {
77
+ pairing = 2;
78
+ cost = cost_2;
79
+ found = true;
80
+ }
81
+ }
82
+
83
+ if (found) {
84
+ out_best[0] = pairing;
85
+ out_best[1] = cost;
86
+ }
87
+
88
+ return found;
89
+ }
90
+
91
+ /**
92
+ * Append a triangle to the soup, choosing the winding so its normal points along `up` (keeps bridge
93
+ * faces walkable / consistently oriented with the floor islands they connect).
94
+ */
95
+ function push_triangle_facing_up(
96
+ soup,
97
+ p0x, p0y, p0z,
98
+ p1x, p1y, p1z,
99
+ p2x, p2y, p2z,
100
+ up_x, up_y, up_z
101
+ ) {
102
+ const e1x = p1x - p0x, e1y = p1y - p0y, e1z = p1z - p0z;
103
+ const e2x = p2x - p0x, e2y = p2y - p0y, e2z = p2z - p0z;
104
+
105
+ const nx = e1y * e2z - e1z * e2y;
106
+ const ny = e1z * e2x - e1x * e2z;
107
+ const nz = e1x * e2y - e1y * e2x;
108
+
109
+ if (nx * up_x + ny * up_y + nz * up_z >= 0) {
110
+ soup.push(p0x, p0y, p0z, p1x, p1y, p1z, p2x, p2y, p2z);
111
+ } else {
112
+ // flip winding so the normal faces up (preserves the undirected edges, only reverses direction)
113
+ soup.push(p0x, p0y, p0z, p2x, p2y, p2z, p1x, p1y, p1z);
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Connect islands that are not topologically joined but lie within a small step (vertical) and/or gap
119
+ * (lateral) of one another, so an agent can path across stairs and small breaks in the floor.
120
+ *
121
+ * The two unused params on the build path (`agent_max_step_height`, `agent_max_step_distance`) drive this.
122
+ * Boundary edges of distinct islands whose endpoints can be paired within the thresholds are stitched
123
+ * with a bridge quad (two triangles). Connectivity is realised by rebuilding the mesh from its triangle
124
+ * soup plus the bridge quads and re-running the same vertex-merge / edge-fuse the build already uses -
125
+ * the bridge corners are copied verbatim from existing boundary vertices, so they fuse onto the islands.
126
+ *
127
+ * Mutates `mesh` in place. Triangulated input is assumed (as produced by the navmesh build pipeline).
128
+ *
129
+ * @param {BinaryTopology} mesh
130
+ * @param {{max_step_height:number, max_step_distance:number, up:Vector3}} options
131
+ * @returns {number} number of bridges added
132
+ */
133
+ export function bt_mesh_bridge_islands(mesh, { max_step_height, max_step_distance, up }) {
134
+ const islands = bt_mesh_compute_face_islands(mesh);
135
+
136
+ if (islands.length < 2) {
137
+ // a single (or empty) connected component - nothing to bridge
138
+ return 0;
139
+ }
140
+
141
+ // normalize up so the along/perpendicular split uses world distances
142
+ let up_x = up.x;
143
+ let up_y = up.y;
144
+ let up_z = up.z;
145
+
146
+ const up_length = Math.sqrt(up_x * up_x + up_y * up_y + up_z * up_z);
147
+ up_x /= up_length;
148
+ up_y /= up_length;
149
+ up_z /= up_length;
150
+
151
+ // face -> island index
152
+ const island_of_face = new Map();
153
+ for (let i = 0; i < islands.length; i++) {
154
+ const faces = islands[i];
155
+ for (let k = 0; k < faces.length; k++) {
156
+ island_of_face.set(faces[k], i);
157
+ }
158
+ }
159
+
160
+ // collect boundary edges, tagged with their island and endpoint coordinates
161
+ const boundary = [];
162
+ const edge_count = mesh.edges.size;
163
+ const coord_1 = [0, 0, 0];
164
+ const coord_2 = [0, 0, 0];
165
+
166
+ for (let e = 0; e < edge_count; e++) {
167
+ if (!mesh.edges.is_allocated(e)) {
168
+ continue;
169
+ }
170
+
171
+ if (!bt_query_edge_is_boundary(mesh, e)) {
172
+ continue;
173
+ }
174
+
175
+ const loop = mesh.edge_read_loop(e);
176
+ const face = mesh.loop_read_face(loop);
177
+ const island = island_of_face.get(face);
178
+
179
+ mesh.vertex_read_coordinate(coord_1, 0, mesh.edge_read_vertex1(e));
180
+ mesh.vertex_read_coordinate(coord_2, 0, mesh.edge_read_vertex2(e));
181
+
182
+ boundary.push({
183
+ island,
184
+ x1: coord_1[0], y1: coord_1[1], z1: coord_1[2],
185
+ x2: coord_2[0], y2: coord_2[1], z2: coord_2[2],
186
+ });
187
+ }
188
+
189
+ // for each boundary edge, find its cheapest bridgeable partner in another island
190
+ const bridges = [];
191
+ const bridged_pairs = new Set();
192
+
193
+ // reused [pairing, cost] scratch, written by evaluate_pair, so the inner loop allocates nothing
194
+ const pair_result = [0, 0];
195
+
196
+ for (let i = 0; i < boundary.length; i++) {
197
+ let best_j = -1;
198
+ let best_cost = Infinity;
199
+ let best_pairing = 0;
200
+
201
+ for (let j = 0; j < boundary.length; j++) {
202
+ if (boundary[j].island === boundary[i].island) {
203
+ continue;
204
+ }
205
+
206
+ if (evaluate_pair(boundary[i], boundary[j], up_x, up_y, up_z, max_step_height, max_step_distance, pair_result)) {
207
+ const cost = pair_result[1];
208
+
209
+ if (cost < best_cost) {
210
+ best_cost = cost;
211
+ best_j = j;
212
+ best_pairing = pair_result[0];
213
+ }
214
+ }
215
+ }
216
+
217
+ if (best_j === -1) {
218
+ continue;
219
+ }
220
+
221
+ const key = i < best_j ? `${i}_${best_j}` : `${best_j}_${i}`;
222
+ if (bridged_pairs.has(key)) {
223
+ continue;
224
+ }
225
+ bridged_pairs.add(key);
226
+
227
+ bridges.push({ a: boundary[i], b: boundary[best_j], pairing: best_pairing });
228
+ }
229
+
230
+ if (bridges.length === 0) {
231
+ return 0;
232
+ }
233
+
234
+ // extract the current mesh as a triangle soup
235
+ const soup = [];
236
+ const face_count = mesh.faces.size;
237
+ const coord = [0, 0, 0];
238
+
239
+ for (let f = 0; f < face_count; f++) {
240
+ if (!mesh.faces.is_allocated(f)) {
241
+ continue;
242
+ }
243
+
244
+ const loop_start = mesh.face_read_loop(f);
245
+ let loop = loop_start;
246
+
247
+ do {
248
+ mesh.vertex_read_coordinate(coord, 0, mesh.loop_read_vertex(loop));
249
+ soup.push(coord[0], coord[1], coord[2]);
250
+ loop = mesh.loop_read_next(loop);
251
+ } while (loop !== loop_start);
252
+ }
253
+
254
+ // append the bridge quads (two triangles each)
255
+ for (let i = 0; i < bridges.length; i++) {
256
+ const { a, b, pairing } = bridges[i];
257
+
258
+ // corner0/corner1 are A's boundary edge; corner2/corner3 are B's, ordered so that the quad's
259
+ // first triangle carries A's edge and the second carries B's edge (so both fuse onto their island)
260
+ const b2x = pairing === 1 ? b.x2 : b.x1;
261
+ const b2y = pairing === 1 ? b.y2 : b.y1;
262
+ const b2z = pairing === 1 ? b.z2 : b.z1;
263
+
264
+ const b3x = pairing === 1 ? b.x1 : b.x2;
265
+ const b3y = pairing === 1 ? b.y1 : b.y2;
266
+ const b3z = pairing === 1 ? b.z1 : b.z2;
267
+
268
+ push_triangle_facing_up(
269
+ soup,
270
+ a.x1, a.y1, a.z1,
271
+ a.x2, a.y2, a.z2,
272
+ b2x, b2y, b2z,
273
+ up_x, up_y, up_z
274
+ );
275
+
276
+ push_triangle_facing_up(
277
+ soup,
278
+ a.x1, a.y1, a.z1,
279
+ b2x, b2y, b2z,
280
+ b3x, b3y, b3z,
281
+ up_x, up_y, up_z
282
+ );
283
+ }
284
+
285
+ // rebuild topology from the augmented soup; the same merge/fuse the initial build uses re-establishes
286
+ // connectivity, joining the bridge triangles onto the island boundaries whose vertices they share
287
+ const rebuilt = new BinaryTopology();
288
+ bt_mesh_from_unindexed_geometry(rebuilt, soup);
289
+ bt_merge_verts_by_distance(rebuilt, VERTEX_MERGE_DISTANCE);
290
+ bt_mesh_fuse_duplicate_edges(rebuilt);
291
+
292
+ mesh.copy(rebuilt);
293
+
294
+ return bridges.length;
295
+ }
@@ -13,10 +13,10 @@
13
13
  * of candidate critical points (see the variant docs for what each reports
14
14
  * and how many entries the result buffer needs):
15
15
  * - 1D: range overlap or extrema-based closest pair → 1 pair.
16
- * - 2D: (3,3)/(3,3) Bezout interior intersections + boundary minima → up
17
- * to 13 pairs (~26 floats).
18
- * - ND (≥ 3): grid-Newton interior critical points + boundary minima → up
19
- * to 89 pairs (~178 floats).
16
+ * - ND (≥ 2): grid-Newton interior critical points + boundary minima → up
17
+ * to 89 pairs (~178 floats). dim === 2 uses this path too; the former
18
+ * bespoke 2D Bezout specialization was removed because it missed interior
19
+ * non-intersection closest approaches.
20
20
  *
21
21
  * Required buffer size: caller must reserve enough space for the worst
22
22
  * case of the dim it intends to call with. Reserving ≥ 178 floats past
@@ -1 +1 @@
1
- {"version":3,"file":"spline3_hermite_intersection_spline3_hermite.d.ts","sourceRoot":"","sources":["../../../../../src/core/math/spline/spline3_hermite_intersection_spline3_hermite.js"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,gEAPW,YAAY,GAAC,MAAM,EAAE,KACrB,YAAY,GAAC,MAAM,EAAE,OACrB,MAAM,UACN,YAAY,GAAC,MAAM,EAAE,iBACrB,MAAM,GACJ,MAAM,CAiBlB"}
1
+ {"version":3,"file":"spline3_hermite_intersection_spline3_hermite.d.ts","sourceRoot":"","sources":["../../../../../src/core/math/spline/spline3_hermite_intersection_spline3_hermite.js"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,gEAPW,YAAY,GAAC,MAAM,EAAE,KACrB,YAAY,GAAC,MAAM,EAAE,OACrB,MAAM,UACN,YAAY,GAAC,MAAM,EAAE,iBACrB,MAAM,GACJ,MAAM,CAclB"}
@@ -1,52 +1,48 @@
1
- import { assert } from "../../assert.js";
2
- import { spline3_hermite_intersection_spline3_hermite_1d } from "./spline3_hermite_intersection_spline3_hermite_1d.js";
3
- import { spline3_hermite_intersection_spline3_hermite_2d } from "./spline3_hermite_intersection_spline3_hermite_2d.js";
4
- import { spline3_hermite_intersection_spline3_hermite_nd } from "./spline3_hermite_intersection_spline3_hermite_nd.js";
5
-
6
- /**
7
- * Critical points of squared distance between two cubic Hermite curves over
8
- * the [0,1]² parameter square. Writes (s, t) pairs sequentially into
9
- * `result` starting at `result_offset` and returns the count of pairs
10
- * written. Caller is responsible for evaluating both curves at each pair
11
- * and computing distance (e.g. to pick the nearest root).
12
- *
13
- * Coefficient layout for `a` and `b`: per-dimension grouped quads,
14
- * `[p0_0, p1_0, m0_0, m1_0, p0_1, p1_1, m0_1, m1_1, ..., m1_{dim-1}]`,
15
- * length `4 * dim`. dim ≥ 1.
16
- *
17
- * Internally dispatched by dimension; each path enumerates different sets
18
- * of candidate critical points (see the variant docs for what each reports
19
- * and how many entries the result buffer needs):
20
- * - 1D: range overlap or extrema-based closest pair1 pair.
21
- * - 2D: (3,3)/(3,3) Bezout interior intersections + boundary minima up
22
- * to 13 pairs (~26 floats).
23
- * - ND (≥ 3): grid-Newton interior critical points + boundary minima → up
24
- * to 89 pairs (~178 floats).
25
- *
26
- * Required buffer size: caller must reserve enough space for the worst
27
- * case of the dim it intends to call with. Reserving ≥ 178 floats past
28
- * `result_offset` covers all dims.
29
- *
30
- * @param {Float64Array|number[]} a length 4*dim
31
- * @param {Float64Array|number[]} b length 4*dim
32
- * @param {number} dim
33
- * @param {Float64Array|number[]} result
34
- * @param {number} result_offset
35
- * @returns {number} number of (s, t) pairs written
36
- */
37
- export function spline3_hermite_intersection_spline3_hermite(
38
- a, b, dim,
39
- result, result_offset
40
- ) {
41
- assert.greaterThanOrEqual(dim, 1, 'dim');
42
-
43
- if (dim === 1) {
44
- return spline3_hermite_intersection_spline3_hermite_1d(a, b, result, result_offset);
45
- }
46
-
47
- if (dim === 2) {
48
- return spline3_hermite_intersection_spline3_hermite_2d(a, b, result, result_offset);
49
- }
50
-
51
- return spline3_hermite_intersection_spline3_hermite_nd(a, b, dim, result, result_offset);
52
- }
1
+ import { assert } from "../../assert.js";
2
+ import { spline3_hermite_intersection_spline3_hermite_1d } from "./spline3_hermite_intersection_spline3_hermite_1d.js";
3
+ import { spline3_hermite_intersection_spline3_hermite_nd } from "./spline3_hermite_intersection_spline3_hermite_nd.js";
4
+
5
+ /**
6
+ * Critical points of squared distance between two cubic Hermite curves over
7
+ * the [0,1]² parameter square. Writes (s, t) pairs sequentially into
8
+ * `result` starting at `result_offset` and returns the count of pairs
9
+ * written. Caller is responsible for evaluating both curves at each pair
10
+ * and computing distance (e.g. to pick the nearest root).
11
+ *
12
+ * Coefficient layout for `a` and `b`: per-dimension grouped quads,
13
+ * `[p0_0, p1_0, m0_0, m1_0, p0_1, p1_1, m0_1, m1_1, ..., m1_{dim-1}]`,
14
+ * length `4 * dim`. dim 1.
15
+ *
16
+ * Internally dispatched by dimension; each path enumerates different sets
17
+ * of candidate critical points (see the variant docs for what each reports
18
+ * and how many entries the result buffer needs):
19
+ * - 1D: range overlap or extrema-based closest pair → 1 pair.
20
+ * - ND (≥ 2): grid-Newton interior critical points + boundary minima up
21
+ * to 89 pairs (~178 floats). dim === 2 uses this path too; the former
22
+ * bespoke 2D Bezout specialization was removed because it missed interior
23
+ * non-intersection closest approaches.
24
+ *
25
+ * Required buffer size: caller must reserve enough space for the worst
26
+ * case of the dim it intends to call with. Reserving ≥ 178 floats past
27
+ * `result_offset` covers all dims.
28
+ *
29
+ * @param {Float64Array|number[]} a length 4*dim
30
+ * @param {Float64Array|number[]} b length 4*dim
31
+ * @param {number} dim
32
+ * @param {Float64Array|number[]} result
33
+ * @param {number} result_offset
34
+ * @returns {number} number of (s, t) pairs written
35
+ */
36
+ export function spline3_hermite_intersection_spline3_hermite(
37
+ a, b, dim,
38
+ result, result_offset
39
+ ) {
40
+ assert.greaterThanOrEqual(dim, 1, 'dim');
41
+
42
+ if (dim === 1) {
43
+ return spline3_hermite_intersection_spline3_hermite_1d(a, b, result, result_offset);
44
+ }
45
+
46
+ // dim >= 2 (including 2D) goes through the dimension-agnostic ND path.
47
+ return spline3_hermite_intersection_spline3_hermite_nd(a, b, dim, result, result_offset);
48
+ }
@@ -1,31 +1,33 @@
1
1
  /**
2
- * Critical-point enumerator for the 2D Hermite curve-pair intersection
3
- * problem. Writes (s, t) pairs sequentially into `result` starting at
4
- * `result_offset` and returns the count.
2
+ * @deprecated The dedicated 2D Bezout specialization has been removed. It
3
+ * enumerated interior *intersections* (via the (3,3)/(3,3) Bezout resultant)
4
+ * plus the four boundary-edge minima only — it never enumerated interior
5
+ * *non-intersection* critical points, so it silently missed the closest
6
+ * approach of two non-intersecting cubic arcs whenever that approach is
7
+ * interior-to-interior (wrong on ~27% of random curve pairs, by as much as
8
+ * several units of squared distance). Its premise — "no interior intersection
9
+ * ⇒ the global minimum lies on the boundary" — is false for cubic arcs.
5
10
  *
6
- * What is reported:
7
- * - All interior true intersections found by the Bezout resultant (up to 9).
8
- * - Four boundary closest-approach pairs, one per edge of [0,1]²:
9
- * (0, t*) with t* minimising ‖A(0) B(t)‖²
10
- * (1, t*) with t* minimising ‖A(1) B(t)‖²
11
- * (s*, 0) with s* minimising ‖A(s) − B(0)‖²
12
- * (s*, 1) with s* minimising ‖A(s) − B(1)‖²
13
- * Each `nearest_t_on_curve` call internally tests endpoints, so corners
14
- * are folded into these as appropriate.
11
+ * This symbol is retained only so existing callers keep working; it now
12
+ * forwards to the dimension-agnostic
13
+ * {@link spline3_hermite_intersection_spline3_hermite_nd} path, which
14
+ * enumerates interior critical points (grid-seeded 2D Newton) in addition to
15
+ * the boundary minima and is correct for the full closest-approach problem.
15
16
  *
16
- * Caller is responsible for evaluating both curves at each (s, t) and picking
17
- * whichever pair(s) they care about (closest, within threshold, etc.).
17
+ * Prefer the dimension dispatcher
18
+ * {@link spline3_hermite_intersection_spline3_hermite} or
19
+ * `spline3_hermite_intersection_spline3_hermite_nd` directly. Note the buffer
20
+ * requirement is now the ND worst case: `result.length >= result_offset +
21
+ * 2 * ND_MAX_ROOTS` floats (currently 178), not the old 2D bound.
18
22
  *
19
- * Typical buffer size: at most 9 interior + 4 boundary = 13 pairs →
20
- * `result.length >= result_offset + 26`. in degenerate cases up to 89 pairs.
21
- *
22
- * Edge case: if the Bezout resultant is numerically degenerate (one of the
23
- * input curves is constant on an axis, etc.), the routine forwards to the
24
- * dimension-agnostic ND path.
23
+ * Critical-point enumerator for the 2D Hermite curve-pair problem. Writes
24
+ * (s, t) pairs sequentially into `result` starting at `result_offset` and
25
+ * returns the count of pairs written. The caller evaluates both curves at each
26
+ * pair and picks the one(s) it cares about (closest, within threshold, etc.).
25
27
  *
26
28
  * @param {Float64Array|number[]} a length 8: [a_p0_x, a_p1_x, a_m0_x, a_m1_x, a_p0_y, a_p1_y, a_m0_y, a_m1_y]
27
29
  * @param {Float64Array|number[]} b length 8 (same layout)
28
- * @param {Float64Array|number[]} result length >= result_offset + 198
30
+ * @param {Float64Array|number[]} result length >= result_offset + 2 * ND_MAX_ROOTS
29
31
  * @param {number} result_offset
30
32
  * @returns {number} number of (s, t) pairs written
31
33
  */
@@ -1 +1 @@
1
- {"version":3,"file":"spline3_hermite_intersection_spline3_hermite_2d.d.ts","sourceRoot":"","sources":["../../../../../src/core/math/spline/spline3_hermite_intersection_spline3_hermite_2d.js"],"names":[],"mappings":"AA+SA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,mEANW,YAAY,GAAC,MAAM,EAAE,KACrB,YAAY,GAAC,MAAM,EAAE,UACrB,YAAY,GAAC,MAAM,EAAE,iBACrB,MAAM,GACJ,MAAM,CAyElB"}
1
+ {"version":3,"file":"spline3_hermite_intersection_spline3_hermite_2d.d.ts","sourceRoot":"","sources":["../../../../../src/core/math/spline/spline3_hermite_intersection_spline3_hermite_2d.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,mEANW,YAAY,GAAC,MAAM,EAAE,KACrB,YAAY,GAAC,MAAM,EAAE,UACrB,YAAY,GAAC,MAAM,EAAE,iBACrB,MAAM,GACJ,MAAM,CAOlB"}