@woosh/meep-engine 3.25.0 → 3.26.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 (63) hide show
  1. package/package.json +1 -1
  2. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts +59 -0
  3. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -1
  4. package/src/core/geom/3d/shape/HeightMapShape3D.js +285 -0
  5. package/src/core/geom/3d/shape/PosedShape3D.d.ts.map +1 -1
  6. package/src/core/geom/3d/shape/PosedShape3D.js +57 -0
  7. package/src/core/geom/3d/shape/TransformedShape3D.d.ts.map +1 -1
  8. package/src/core/geom/3d/shape/TransformedShape3D.js +102 -0
  9. package/src/core/geom/3d/shape/UnionShape3D.d.ts.map +1 -1
  10. package/src/core/geom/3d/shape/UnionShape3D.js +64 -0
  11. package/src/engine/physics/cloth/MEASUREMENTS.md +582 -0
  12. package/src/engine/physics/cloth/PLAN.md +330 -49
  13. package/src/engine/physics/cloth/collider/ClothColliderIndex.d.ts +84 -17
  14. package/src/engine/physics/cloth/collider/ClothColliderIndex.d.ts.map +1 -1
  15. package/src/engine/physics/cloth/collider/ClothColliderIndex.js +451 -42
  16. package/src/engine/physics/cloth/ecs/ClothInstance.d.ts +65 -7
  17. package/src/engine/physics/cloth/ecs/ClothInstance.d.ts.map +1 -1
  18. package/src/engine/physics/cloth/ecs/ClothInstance.js +72 -8
  19. package/src/engine/physics/cloth/ecs/ClothSystem.d.ts +25 -4
  20. package/src/engine/physics/cloth/ecs/ClothSystem.d.ts.map +1 -1
  21. package/src/engine/physics/cloth/ecs/ClothSystem.js +75 -8
  22. package/src/engine/physics/cloth/ecs/ClothWorld.d.ts +41 -4
  23. package/src/engine/physics/cloth/ecs/ClothWorld.d.ts.map +1 -1
  24. package/src/engine/physics/cloth/ecs/ClothWorld.js +207 -63
  25. package/src/engine/physics/cloth/ecs/WorkerClothSystem.d.ts +10 -0
  26. package/src/engine/physics/cloth/ecs/WorkerClothSystem.d.ts.map +1 -1
  27. package/src/engine/physics/cloth/ecs/WorkerClothSystem.js +17 -1
  28. package/src/engine/physics/cloth/ecs/cloth_dynamics_map.d.ts +27 -3
  29. package/src/engine/physics/cloth/ecs/cloth_dynamics_map.d.ts.map +1 -1
  30. package/src/engine/physics/cloth/ecs/cloth_dynamics_map.js +28 -3
  31. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.d.ts +75 -10
  32. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.d.ts.map +1 -1
  33. package/src/engine/physics/cloth/ecs/cloth_gather_colliders.js +131 -12
  34. package/src/engine/physics/cloth/ecs/cloth_seed_rig.d.ts +19 -0
  35. package/src/engine/physics/cloth/ecs/cloth_seed_rig.d.ts.map +1 -1
  36. package/src/engine/physics/cloth/ecs/cloth_seed_rig.js +249 -201
  37. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.d.ts +21 -0
  38. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.d.ts.map +1 -1
  39. package/src/engine/physics/cloth/ecs/cloth_seed_subtree.js +62 -1
  40. package/src/engine/physics/cloth/ecs/cloth_write_back.d.ts.map +1 -1
  41. package/src/engine/physics/cloth/ecs/cloth_write_back.js +18 -10
  42. package/src/engine/physics/cloth/playground/collide_main.js +1 -1
  43. package/src/engine/physics/cloth/solver/cloth_contact_find.d.ts +29 -8
  44. package/src/engine/physics/cloth/solver/cloth_contact_find.d.ts.map +1 -1
  45. package/src/engine/physics/cloth/solver/cloth_contact_find.js +37 -9
  46. package/src/engine/physics/cloth/solver/cloth_solver_constants.d.ts +12 -2
  47. package/src/engine/physics/cloth/solver/cloth_solver_constants.d.ts.map +1 -1
  48. package/src/engine/physics/cloth/solver/cloth_solver_constants.js +12 -2
  49. package/src/engine/physics/cloth/solver/cloth_step_instance.js +1 -1
  50. package/src/engine/physics/cloth/solver/constraint/cloth_backstop_row.d.ts +9 -2
  51. package/src/engine/physics/cloth/solver/constraint/cloth_backstop_row.d.ts.map +1 -1
  52. package/src/engine/physics/cloth/solver/constraint/cloth_backstop_row.js +9 -2
  53. package/src/engine/physics/cloth/wind/AbstractClothWind.d.ts +23 -9
  54. package/src/engine/physics/cloth/wind/AbstractClothWind.d.ts.map +1 -1
  55. package/src/engine/physics/cloth/wind/AbstractClothWind.js +23 -9
  56. package/src/engine/physics/cloth/wind/ClothAmbientWind.d.ts +7 -5
  57. package/src/engine/physics/cloth/wind/ClothAmbientWind.d.ts.map +1 -1
  58. package/src/engine/physics/cloth/wind/ClothAmbientWind.js +12 -9
  59. package/src/engine/physics/cloth/wind/ClothFluidWind.d.ts.map +1 -1
  60. package/src/engine/physics/cloth/wind/ClothFluidWind.js +5 -2
  61. package/src/engine/physics/cloth/wind/cloth_sample_air.d.ts +7 -2
  62. package/src/engine/physics/cloth/wind/cloth_sample_air.d.ts.map +1 -1
  63. package/src/engine/physics/cloth/wind/cloth_sample_air.js +12 -5
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "description": "Advanced 3D game engine",
7
7
  "type": "module",
8
8
  "author": "Alexander Goldring",
9
- "version": "3.25.0",
9
+ "version": "3.26.0",
10
10
  "exports": {
11
11
  "./build/*": "./build/*",
12
12
  "./src/*": "./src/*",
@@ -158,6 +158,65 @@ export class HeightMapShape3D extends AbstractShape3D {
158
158
  * site is forced to handle heightmaps explicitly.
159
159
  */
160
160
  support(result: any, result_offset: any, direction_x: any, direction_y: any, direction_z: any): void;
161
+ /**
162
+ * Nearest ray hit against the height-field SURFACE, in the shape's body-local
163
+ * frame. Writes `[px, py, pz, nx, ny, nz]` into `hit` and returns true; a miss
164
+ * returns false and leaves `hit` untouched.
165
+ *
166
+ * ## What is hit
167
+ *
168
+ * The surface, not the solid. The class models a closed volume (floor at h=0,
169
+ * height field above), but a ray meets only the height field — there is no
170
+ * floor quad and no side skirt, so a ray that passes under the terrain or
171
+ * enters through the footprint edge below the surface reports a miss. That is
172
+ * the same body {@link heightmap_enumerate_triangles} hands the contact
173
+ * narrowphase, and the same body the terrain renderer draws; a raycast that
174
+ * answered for the skirt would disagree with both. Mirrors
175
+ * {@link MeshShape3D#raycast}, which is likewise surface-only.
176
+ *
177
+ * Precisely the same triangles, too: the traversal reads its corner heights at
178
+ * the identical `i / seg_u` / `j / seg_v` UVs through the identical Catmull-Rom
179
+ * filter, and splits each sub-cell on the same lower-left diagonal
180
+ * (`(A,D,B)`, `(D,C,B)`), so a cast and a contact resolve against a surface
181
+ * that agrees bit for bit — {@link tessellation} included.
182
+ *
183
+ * NORMAL: the triangle's geometric normal, flipped to face the incoming ray
184
+ * (the shared {@link tri3_raycast} convention). A cast from below the terrain
185
+ * therefore reports a downward normal, not the surface's true upward one.
186
+ *
187
+ * A hit landing exactly ON a sub-cell boundary gets the normal of one of the
188
+ * facets meeting there, and which one follows from traversal order: this walks
189
+ * the grid along the ray, the narrowphase enumerates it row-major, so on a grid
190
+ * line — and especially a grid corner, where four facets meet — the two can
191
+ * name different facets. The hit POINT still agrees; a faceted surface simply
192
+ * has no single normal at a seam. Measure-zero, and called out because it is
193
+ * the only place the two answers part company.
194
+ *
195
+ * `size.y` does NOT bound the query. Sampler values are not clamped to it (see
196
+ * the class docstring), so clipping the ray to the height range that
197
+ * {@link compute_bounding_box} reports could reject real surface standing above
198
+ * it; only the footprint is clipped against.
199
+ *
200
+ * ## Cost
201
+ *
202
+ * A 2-D DDA over the `(W−1)·N × (H−1)·N` sub-cell grid, two triangles per cell,
203
+ * stopping at the first cell that hits — cells are visited in increasing ray
204
+ * order and a sub-cell's triangles project exactly onto its own footprint
205
+ * rectangle, so nothing behind can be nearer. O(cells the ray crosses), not
206
+ * O(cells in the ray's bounding box): a long diagonal cast costs the grid's
207
+ * diagonal rather than its area.
208
+ *
209
+ * Four Catmull-Rom evaluations per cell, uncached — deliberately unlike
210
+ * {@link heightmap_enumerate_triangles}, whose corner-row cache exists because
211
+ * the contact narrowphase re-samples the same rect every substep. A ray walks
212
+ * a diagonal, visits each cell once, and stops early; there is nothing to
213
+ * amortise against.
214
+ *
215
+ * @param {Float32Array|number[]} hit out: [px, py, pz, nx, ny, nz]
216
+ * @param {Ray3} ray body-local ray (unit direction; `ray[6]` = tMax)
217
+ * @returns {boolean} whether anything was hit
218
+ */
219
+ raycast(hit: Float32Array | number[], ray: Ray3): boolean;
161
220
  sample_random_point_in_volume(result: any, result_offset: any, random: any): void;
162
221
  /**
163
222
  * @param {HeightMapShape3D} other
@@ -1 +1 @@
1
- {"version":3,"file":"HeightMapShape3D.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/3d/shape/HeightMapShape3D.js"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH;IA4DI;;;;;;;;;;;OAWG;IACH,wCATW,MAAM,UACN,MAAM,UACN,MAAM,gBACN,OAAO,iBACP,MAAM,GAGJ,gBAAgB,CAqB5B;IA1FD;;;;;OAKG;IACH,sBAFU,OAAO,CAEkB;IAEnC;;;;;;;OAOG;IACH,eAFU,OAAO,CAEW;IAE5B;;;;;OAKG;IACH,SAFU,YAAY,IAAI,CAEX;IAEf;;;;;;;;;;;;;;;;OAgBG;IACH,cAFU,MAAM,CAEC;IAEjB;;;;;OAKG;IACH,eAA6B;IAG7B,6BAA2B;IAE3B,6BAA2B;IAC3B,6BAA2B;IAmC3B;;;;;;;;;OASG;IACH,sBA+CC;IAED;;;;;;OAMG;IACH,yBAJW,MAAM,OACN,MAAM,GACJ,MAAM,CAIlB;IAED;;;;;;;;OAQG;IACH,8BALW,MAAM,MACN,MAAM,MACN,MAAM,GACJ,MAAM,CAelB;IAED,wCA8BC;IAED,oCA2BC;IAED;;;;;;;;OAQG;IACH,6CAmBC;IAED;;;;OAIG;IACH,4DA2BC;IAED;;;;;;;;;OASG;IACH,qGAEC;IAED,kFAiBC;IA6CD;;;OAGG;IACH,cAHW,gBAAgB,GACd,OAAO,CAUnB;IAWL;;;;;;OAMG;IACH,6BAFU,OAAO,CAE4B;CAT5C;gCAhc+B,sBAAsB;wBAD9B,kBAAkB"}
1
+ {"version":3,"file":"HeightMapShape3D.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/3d/shape/HeightMapShape3D.js"],"names":[],"mappings":"AAoBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH;IA4DI;;;;;;;;;;;OAWG;IACH,wCATW,MAAM,UACN,MAAM,UACN,MAAM,gBACN,OAAO,iBACP,MAAM,GAGJ,gBAAgB,CAqB5B;IA1FD;;;;;OAKG;IACH,sBAFU,OAAO,CAEkB;IAEnC;;;;;;;OAOG;IACH,eAFU,OAAO,CAEW;IAE5B;;;;;OAKG;IACH,SAFU,YAAY,IAAI,CAEX;IAEf;;;;;;;;;;;;;;;;OAgBG;IACH,cAFU,MAAM,CAEC;IAEjB;;;;;OAKG;IACH,eAA6B;IAG7B,6BAA2B;IAE3B,6BAA2B;IAC3B,6BAA2B;IAmC3B;;;;;;;;;OASG;IACH,sBA+CC;IAED;;;;;;OAMG;IACH,yBAJW,MAAM,OACN,MAAM,GACJ,MAAM,CAIlB;IAED;;;;;;;;OAQG;IACH,8BALW,MAAM,MACN,MAAM,MACN,MAAM,GACJ,MAAM,CAelB;IAED,wCA8BC;IAED,oCA2BC;IAED;;;;;;;;OAQG;IACH,6CAmBC;IAED;;;;OAIG;IACH,4DA2BC;IAED;;;;;;;;;OASG;IACH,qGAEC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAyDG;IACH,aAJW,YAAY,GAAC,MAAM,EAAE,cAEnB,OAAO,CAyNnB;IAED,kFAiBC;IA6CD;;;OAGG;IACH,cAHW,gBAAgB,GACd,OAAO,CAUnB;IAWL;;;;;;OAMG;IACH,6BAFU,OAAO,CAE4B;CAT5C;gCA5tB+B,sBAAsB;wBAD9B,kBAAkB"}
@@ -4,10 +4,20 @@ import { assert } from "../../../assert.js";
4
4
  import { clamp } from "../../../math/clamp.js";
5
5
  import { optional_equality } from "../../../model/object/optional_equality.js";
6
6
  import { optional_hash } from "../../../model/object/optional_hash.js";
7
+ import { tri3_raycast } from "../triangle/tri3_raycast.js";
7
8
  import { v3_length } from "../../vec3/v3_length.js";
8
9
  import { Vector3 } from "../../Vector3.js";
9
10
  import { AbstractShape3D } from "./AbstractShape3D.js";
10
11
 
12
+ /**
13
+ * Below this, a ray's footprint-plane velocity component counts as zero in
14
+ * {@link HeightMapShape3D#raycast}'s slab clip — the reciprocal a smaller value
15
+ * would produce is not a usable slab distance. Matches the concave narrowphase's
16
+ * `refine_ray_concave`, so the two agree on rays that graze the footprint edge.
17
+ * @type {number}
18
+ */
19
+ const RAY_SLAB_EPSILON = 1e-12;
20
+
11
21
  /**
12
22
  * Heightmap shape, intended primarily for terrain.
13
23
  *
@@ -371,6 +381,281 @@ export class HeightMapShape3D extends AbstractShape3D {
371
381
  throw new Error("HeightMapShape3D.support: heightmaps are non-convex; the narrowphase must dispatch grid-traversal instead.");
372
382
  }
373
383
 
384
+ /**
385
+ * Nearest ray hit against the height-field SURFACE, in the shape's body-local
386
+ * frame. Writes `[px, py, pz, nx, ny, nz]` into `hit` and returns true; a miss
387
+ * returns false and leaves `hit` untouched.
388
+ *
389
+ * ## What is hit
390
+ *
391
+ * The surface, not the solid. The class models a closed volume (floor at h=0,
392
+ * height field above), but a ray meets only the height field — there is no
393
+ * floor quad and no side skirt, so a ray that passes under the terrain or
394
+ * enters through the footprint edge below the surface reports a miss. That is
395
+ * the same body {@link heightmap_enumerate_triangles} hands the contact
396
+ * narrowphase, and the same body the terrain renderer draws; a raycast that
397
+ * answered for the skirt would disagree with both. Mirrors
398
+ * {@link MeshShape3D#raycast}, which is likewise surface-only.
399
+ *
400
+ * Precisely the same triangles, too: the traversal reads its corner heights at
401
+ * the identical `i / seg_u` / `j / seg_v` UVs through the identical Catmull-Rom
402
+ * filter, and splits each sub-cell on the same lower-left diagonal
403
+ * (`(A,D,B)`, `(D,C,B)`), so a cast and a contact resolve against a surface
404
+ * that agrees bit for bit — {@link tessellation} included.
405
+ *
406
+ * NORMAL: the triangle's geometric normal, flipped to face the incoming ray
407
+ * (the shared {@link tri3_raycast} convention). A cast from below the terrain
408
+ * therefore reports a downward normal, not the surface's true upward one.
409
+ *
410
+ * A hit landing exactly ON a sub-cell boundary gets the normal of one of the
411
+ * facets meeting there, and which one follows from traversal order: this walks
412
+ * the grid along the ray, the narrowphase enumerates it row-major, so on a grid
413
+ * line — and especially a grid corner, where four facets meet — the two can
414
+ * name different facets. The hit POINT still agrees; a faceted surface simply
415
+ * has no single normal at a seam. Measure-zero, and called out because it is
416
+ * the only place the two answers part company.
417
+ *
418
+ * `size.y` does NOT bound the query. Sampler values are not clamped to it (see
419
+ * the class docstring), so clipping the ray to the height range that
420
+ * {@link compute_bounding_box} reports could reject real surface standing above
421
+ * it; only the footprint is clipped against.
422
+ *
423
+ * ## Cost
424
+ *
425
+ * A 2-D DDA over the `(W−1)·N × (H−1)·N` sub-cell grid, two triangles per cell,
426
+ * stopping at the first cell that hits — cells are visited in increasing ray
427
+ * order and a sub-cell's triangles project exactly onto its own footprint
428
+ * rectangle, so nothing behind can be nearer. O(cells the ray crosses), not
429
+ * O(cells in the ray's bounding box): a long diagonal cast costs the grid's
430
+ * diagonal rather than its area.
431
+ *
432
+ * Four Catmull-Rom evaluations per cell, uncached — deliberately unlike
433
+ * {@link heightmap_enumerate_triangles}, whose corner-row cache exists because
434
+ * the contact narrowphase re-samples the same rect every substep. A ray walks
435
+ * a diagonal, visits each cell once, and stops early; there is nothing to
436
+ * amortise against.
437
+ *
438
+ * @param {Float32Array|number[]} hit out: [px, py, pz, nx, ny, nz]
439
+ * @param {Ray3} ray body-local ray (unit direction; `ray[6]` = tMax)
440
+ * @returns {boolean} whether anything was hit
441
+ */
442
+ raycast(hit, ray) {
443
+ const sampler = this.sampler;
444
+
445
+ if (sampler === null) {
446
+ return false;
447
+ }
448
+
449
+ const size_x = this.size[0];
450
+ const size_z = this.size[2];
451
+
452
+ if (size_x <= 0 || size_z <= 0) {
453
+ return false;
454
+ }
455
+
456
+ // The collision grid heightmap_enumerate_triangles builds. A sampler too
457
+ // thin to hold one cell on an axis (width or height < 2), or tessellation
458
+ // 0, collapses it — no triangles exist, so nothing can be hit.
459
+ const seg_u = (sampler.width - 1) * this.tessellation;
460
+ const seg_v = (sampler.height - 1) * this.tessellation;
461
+
462
+ if (seg_u < 1 || seg_v < 1) {
463
+ return false;
464
+ }
465
+
466
+ this._ensure_basis();
467
+
468
+ const b = this._basis;
469
+
470
+ const o_x = ray[0], o_y = ray[1], o_z = ray[2];
471
+ const d_x = ray[3], d_y = ray[4], d_z = ray[5];
472
+
473
+ // Body → heightmap-local footprint coordinates. The basis is orthonormal,
474
+ // so the direction stays unit and `t` is the same distance in both frames;
475
+ // the height component is never needed, the triangles carry it.
476
+ const o_u = b[0] * o_x + b[1] * o_y + b[2] * o_z;
477
+ const o_v = b[3] * o_x + b[4] * o_y + b[5] * o_z;
478
+ const d_u = b[0] * d_x + b[1] * d_y + b[2] * d_z;
479
+ const d_v = b[3] * d_x + b[4] * d_y + b[5] * d_z;
480
+
481
+ const half_u = size_x * 0.5;
482
+ const half_v = size_z * 0.5;
483
+
484
+ const t_max = ray[6];
485
+
486
+ // ── Clip to the footprint slabs → the segment the DDA walks ──────────
487
+ let t_enter = 0;
488
+ let t_exit = t_max;
489
+
490
+ if (d_u > RAY_SLAB_EPSILON || d_u < -RAY_SLAB_EPSILON) {
491
+ const inv = 1 / d_u;
492
+ let t0 = (-half_u - o_u) * inv;
493
+ let t1 = (half_u - o_u) * inv;
494
+ if (t0 > t1) { const s = t0; t0 = t1; t1 = s; }
495
+ if (t0 > t_enter) t_enter = t0;
496
+ if (t1 < t_exit) t_exit = t1;
497
+ } else if (o_u < -half_u || o_u > half_u) {
498
+ return false;
499
+ }
500
+
501
+ if (d_v > RAY_SLAB_EPSILON || d_v < -RAY_SLAB_EPSILON) {
502
+ const inv = 1 / d_v;
503
+ let t0 = (-half_v - o_v) * inv;
504
+ let t1 = (half_v - o_v) * inv;
505
+ if (t0 > t1) { const s = t0; t0 = t1; t1 = s; }
506
+ if (t0 > t_enter) t_enter = t0;
507
+ if (t1 < t_exit) t_exit = t1;
508
+ } else if (o_v < -half_v || o_v > half_v) {
509
+ return false;
510
+ }
511
+
512
+ if (t_exit < t_enter) {
513
+ return false;
514
+ }
515
+
516
+ // ── Amanatides–Woo setup on the sub-cell grid ────────────────────────
517
+ //
518
+ // Grid coordinates run [0, seg_u] × [0, seg_v] across the footprint. A
519
+ // ray with no footprint-plane velocity (cast straight down the
520
+ // orientation axis) keeps both rates at 0, both t_next at Infinity, and
521
+ // the loop tests its single cell and stops.
522
+ const per_cell_u = seg_u / size_x;
523
+ const per_cell_v = seg_v / size_z;
524
+
525
+ const rate_u = d_u * per_cell_u;
526
+ const rate_v = d_v * per_cell_v;
527
+
528
+ const g_u = (o_u + d_u * t_enter + half_u) * per_cell_u;
529
+ const g_v = (o_v + d_v * t_enter + half_v) * per_cell_v;
530
+
531
+ // Clamped because the slab clip lands the entry point ON the boundary,
532
+ // where rounding can put it a ULP outside the grid.
533
+ let i = Math.floor(g_u);
534
+ let j = Math.floor(g_v);
535
+
536
+ if (i < 0) i = 0; else if (i > seg_u - 1) i = seg_u - 1;
537
+ if (j < 0) j = 0; else if (j > seg_v - 1) j = seg_v - 1;
538
+
539
+ let step_i = 0, t_delta_u = Infinity, t_next_u = Infinity;
540
+ if (rate_u > 0) {
541
+ step_i = 1;
542
+ t_delta_u = 1 / rate_u;
543
+ t_next_u = t_enter + (i + 1 - g_u) * t_delta_u;
544
+ } else if (rate_u < 0) {
545
+ step_i = -1;
546
+ t_delta_u = -1 / rate_u;
547
+ t_next_u = t_enter + (i - g_u) / rate_u;
548
+ }
549
+
550
+ let step_j = 0, t_delta_v = Infinity, t_next_v = Infinity;
551
+ if (rate_v > 0) {
552
+ step_j = 1;
553
+ t_delta_v = 1 / rate_v;
554
+ t_next_v = t_enter + (j + 1 - g_v) * t_delta_v;
555
+ } else if (rate_v < 0) {
556
+ step_j = -1;
557
+ t_delta_v = -1 / rate_v;
558
+ t_next_v = t_enter + (j - g_v) / rate_v;
559
+ }
560
+
561
+ const inv_seg_u = 1 / seg_u;
562
+ const inv_seg_v = 1 / seg_v;
563
+
564
+ const ux = b[0], uy = b[1], uz = b[2];
565
+ const vx = b[3], vy = b[4], vz = b[5];
566
+ const nx = b[6], ny = b[7], nz = b[8];
567
+
568
+ // Every iteration but the last ends by crossing one grid line, and a
569
+ // straight line crosses fewer than seg_u + seg_v of them (a corner costs
570
+ // two iterations to move diagonally — one per axis — but no extra lines).
571
+ // The walk normally ends on t_exit; this is the bound that holds when a
572
+ // near-denormal rate makes t_exit unreachable.
573
+ const max_steps = seg_u + seg_v + 2;
574
+
575
+ for (let step = 0; step < max_steps; step++) {
576
+ // ── The sub-cell's two triangles, built exactly as the enumerator
577
+ // builds them: corners (A,B,C,D) = (i,j), (i+1,j), (i+1,j+1),
578
+ // (i,j+1), split (A,D,B) and (D,C,B).
579
+ const u01_lo = i * inv_seg_u;
580
+ const u01_hi = (i + 1) * inv_seg_u;
581
+ const v01_lo = j * inv_seg_v;
582
+ const v01_hi = (j + 1) * inv_seg_v;
583
+
584
+ const u_lo = -half_u + size_x * u01_lo;
585
+ const u_hi = -half_u + size_x * u01_hi;
586
+ const v_lo = -half_v + size_z * v01_lo;
587
+ const v_hi = -half_v + size_z * v01_hi;
588
+
589
+ const hA = sampler.sampleChannelCatmullRomUV(u01_lo, v01_lo, 0);
590
+ const hB = sampler.sampleChannelCatmullRomUV(u01_hi, v01_lo, 0);
591
+ const hC = sampler.sampleChannelCatmullRomUV(u01_hi, v01_hi, 0);
592
+ const hD = sampler.sampleChannelCatmullRomUV(u01_lo, v01_hi, 0);
593
+
594
+ const Ax = ux * u_lo + vx * v_lo + nx * hA;
595
+ const Ay = uy * u_lo + vy * v_lo + ny * hA;
596
+ const Az = uz * u_lo + vz * v_lo + nz * hA;
597
+
598
+ const Bx = ux * u_hi + vx * v_lo + nx * hB;
599
+ const By = uy * u_hi + vy * v_lo + ny * hB;
600
+ const Bz = uz * u_hi + vz * v_lo + nz * hB;
601
+
602
+ const Cx = ux * u_hi + vx * v_hi + nx * hC;
603
+ const Cy = uy * u_hi + vy * v_hi + ny * hC;
604
+ const Cz = uz * u_hi + vz * v_hi + nz * hC;
605
+
606
+ const Dx = ux * u_lo + vx * v_hi + nx * hD;
607
+ const Dy = uy * u_lo + vy * v_hi + ny * hD;
608
+ const Dz = uz * u_lo + vz * v_hi + nz * hD;
609
+
610
+ // tri3_raycast writes hit[3..5] only for a hit inside the budget it
611
+ // was given, so handing the second triangle the first's distance
612
+ // leaves the nearer one's normal standing. Both can claim a point on
613
+ // the shared D–B diagonal, and this keeps the second where the
614
+ // narrowphase's copy of the same test keeps the first — the seam case
615
+ // the docstring calls out.
616
+ let best = tri3_raycast(
617
+ hit, 3, o_x, o_y, o_z, d_x, d_y, d_z, t_max,
618
+ Ax, Ay, Az, Dx, Dy, Dz, Bx, By, Bz
619
+ );
620
+
621
+ const t_second = tri3_raycast(
622
+ hit, 3, o_x, o_y, o_z, d_x, d_y, d_z, best < t_max ? best : t_max,
623
+ Dx, Dy, Dz, Cx, Cy, Cz, Bx, By, Bz
624
+ );
625
+
626
+ if (t_second < best) {
627
+ best = t_second;
628
+ }
629
+
630
+ if (best !== Infinity) {
631
+ hit[0] = o_x + d_x * best;
632
+ hit[1] = o_y + d_y * best;
633
+ hit[2] = o_z + d_z * best;
634
+
635
+ return true;
636
+ }
637
+
638
+ // ── Advance to the next cell along the ray ───────────────────────
639
+ if (t_next_u < t_next_v) {
640
+ if (t_next_u > t_exit) break;
641
+ i += step_i;
642
+ if (i < 0 || i >= seg_u) break;
643
+ t_next_u += t_delta_u;
644
+ } else if (t_next_v !== Infinity) {
645
+ if (t_next_v > t_exit) break;
646
+ j += step_j;
647
+ if (j < 0 || j >= seg_v) break;
648
+ t_next_v += t_delta_v;
649
+ } else {
650
+ // Both rates are zero: the ray runs along the orientation axis and
651
+ // never leaves the cell it started in.
652
+ break;
653
+ }
654
+ }
655
+
656
+ return false;
657
+ }
658
+
374
659
  sample_random_point_in_volume(result, result_offset, random) {
375
660
  const u01 = random();
376
661
  const v01 = random();
@@ -1 +1 @@
1
- {"version":3,"file":"PosedShape3D.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/3d/shape/PosedShape3D.js"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;;;GAmBG;AACH;IAEI,mCAAmC;IACnC,OADW,eAAe,GAAC,IAAI,CAClB;IAEb,WAAO;IACP,WAAO;IACP,WAAO;IACP,WAAO;IACP,WAAO;IACP,WAAO;IACP,WAAO;IAEP;;;;;;;OAOG;IACH,aAJW,eAAe,YACf,UAAQ;QAAC,CAAC,EAAC,MAAM,CAAC;QAAA,CAAC,EAAC,MAAM,CAAC;QAAA,CAAC,EAAC,MAAM,CAAA;KAAC,YACpC,aAAW;QAAC,CAAC,EAAC,MAAM,CAAC;QAAA,CAAC,EAAC,MAAM,CAAC;QAAA,CAAC,EAAC,MAAM,CAAC;QAAA,CAAC,EAAC,MAAM,CAAA;KAAC,QAe1D;IAED;;;;;;;;;;;;;;;OAeG;IACH,wBATW,eAAe,MACf,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,QAMhB;IAyED,wCASC;IA4BD;;;;;;;OAOG;IACH,gCAHW,MAAM,EAAE,GAAC,YAAY,GAAC,YAAY,GAChC,MAAM,CAKlB;IAED;;;;;OAKG;IACH,sBAHW,MAAM,EAAE,GAAC,YAAY,GAAC,YAAY,GAChC,OAAO,CAKnB;IAGL;;;;OAIG;IACH,yBAFU,OAAO,CAEoB;;CAPpC;gCAtN+B,sBAAsB"}
1
+ {"version":3,"file":"PosedShape3D.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/3d/shape/PosedShape3D.js"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;;;;;GAmBG;AACH;IAEI,mCAAmC;IACnC,OADW,eAAe,GAAC,IAAI,CAClB;IAEb,WAAO;IACP,WAAO;IACP,WAAO;IACP,WAAO;IACP,WAAO;IACP,WAAO;IACP,WAAO;IAEP;;;;;;;OAOG;IACH,aAJW,eAAe,YACf,UAAQ;QAAC,CAAC,EAAC,MAAM,CAAC;QAAA,CAAC,EAAC,MAAM,CAAC;QAAA,CAAC,EAAC,MAAM,CAAA;KAAC,YACpC,aAAW;QAAC,CAAC,EAAC,MAAM,CAAC;QAAA,CAAC,EAAC,MAAM,CAAC;QAAA,CAAC,EAAC,MAAM,CAAC;QAAA,CAAC,EAAC,MAAM,CAAA;KAAC,QAe1D;IAED;;;;;;;;;;;;;;;OAeG;IACH,wBATW,eAAe,MACf,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,QAMhB;IA+HD,wCASC;IA4BD;;;;;;;OAOG;IACH,gCAHW,MAAM,EAAE,GAAC,YAAY,GAAC,YAAY,GAChC,MAAM,CAKlB;IAED;;;;;OAKG;IACH,sBAHW,MAAM,EAAE,GAAC,YAAY,GAAC,YAAY,GAChC,OAAO,CAKnB;IAGL;;;;OAIG;IACH,yBAFU,OAAO,CAEoB;;CAPpC;gCA5Q+B,sBAAsB"}
@@ -1,5 +1,8 @@
1
1
  import { assert } from "../../../assert.js";
2
+ import { v3_quaternion_apply } from "../../vec3/v3_quaternion_apply.js";
3
+ import { v3_quaternion_apply_inverse } from "../../vec3/v3_quaternion_apply_inverse.js";
2
4
  import { aabb3_transform_oriented } from "../aabb/aabb3_transform_oriented.js";
5
+ import { Ray3 } from "../ray/Ray3.js";
3
6
  import { AbstractShape3D } from "./AbstractShape3D.js";
4
7
 
5
8
  const scratch_local = new Float64Array(3);
@@ -154,6 +157,60 @@ export class PosedShape3D extends AbstractShape3D {
154
157
  result[result_offset + 2] = tsz * qw - tsw * qz - tsx * qy + tsy * qx + this.pz;
155
158
  }
156
159
 
160
+ /**
161
+ * Ray ↔ posed shape, with the ray given in the WRAPPER's frame. The inverse pose carries it into
162
+ * the subject's frame (translate by −position, rotate by the conjugate quaternion), the subject
163
+ * answers there, and the answer is rotated back — the point re-translated, the normal not.
164
+ *
165
+ * A rigid pose is an isometry, so the crossing changes nothing about the ray's parametrisation:
166
+ * the rotated direction is still unit, `tMax` goes through untouched, and the distance the subject
167
+ * measures IS the distance here. The normal's ORIENTATION survives for the same reason — a rotation
168
+ * preserves the angle between normal and direction, so a subject that turned its normal to meet the
169
+ * ray has turned this one to meet it too, and there is nothing to re-check.
170
+ *
171
+ * `hit` is handed straight to the subject rather than buffered and copied back: on a hit its
172
+ * contents are mapped in place, and on a miss the subject leaves it untouched — which is the
173
+ * convention this class owes its own caller, so it is passed on intact.
174
+ *
175
+ * The physics ray query does not come through here; it uses `refine_ray_posed`, which performs the
176
+ * same mapping on loose scalars. That path reports a distance rather than a point, so it never
177
+ * rotates anything but the normal.
178
+ *
179
+ * @param {Float32Array|number[]} hit out: [px, py, pz, nx, ny, nz]
180
+ * @param {Ray3} ray wrapper-frame ray (origin in [0..2], unit direction in [3..5], tMax in [6])
181
+ * @returns {boolean} whether anything was hit
182
+ */
183
+ raycast(hit, ray) {
184
+ const shape = this.shape;
185
+
186
+ if (shape === null) {
187
+ return false;
188
+ }
189
+
190
+ const qx = this.qx, qy = this.qy, qz = this.qz, qw = this.qw;
191
+
192
+ // Allocated per call rather than kept as a module scratch: a posed shape may wrap another
193
+ // wrapper, and every level of that recursion needs its own sub-ray alive across the call below.
194
+ const local_ray = new Ray3();
195
+
196
+ v3_quaternion_apply_inverse(local_ray, 0, ray[0] - this.px, ray[1] - this.py, ray[2] - this.pz, qx, qy, qz, qw);
197
+ v3_quaternion_apply_inverse(local_ray, 3, ray[3], ray[4], ray[5], qx, qy, qz, qw);
198
+ local_ray[6] = ray[6];
199
+
200
+ if (!shape.raycast(hit, local_ray)) {
201
+ return false;
202
+ }
203
+
204
+ v3_quaternion_apply(hit, 0, hit[0], hit[1], hit[2], qx, qy, qz, qw);
205
+ v3_quaternion_apply(hit, 3, hit[3], hit[4], hit[5], qx, qy, qz, qw);
206
+
207
+ hit[0] += this.px;
208
+ hit[1] += this.py;
209
+ hit[2] += this.pz;
210
+
211
+ return true;
212
+ }
213
+
157
214
  compute_bounding_box(result) {
158
215
  this.shape.compute_bounding_box(scratch_local_aabb);
159
216
  aabb3_transform_oriented(
@@ -1 +1 @@
1
- {"version":3,"file":"TransformedShape3D.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/3d/shape/TransformedShape3D.js"],"names":[],"mappings":"AAwBA;IAwCI;;;;;OAKG;IACH,wBAHW,eAAe,MADf,MAAM,EAAE,GAAC,YAAY,OAAK,GAExB,kBAAkB,CAS9B;IAED;;;;;OAKG;IACH,uCAJW,eAAe,eACf,MAAM,EAAE,SACR,MAAM,EAAE,sBAiBlB;IAED;;;;OAIG;IACH,iCAHW,eAAe,eACf,MAAM,EAAE,sBAIlB;IAED;;;;;OAKG;IACH,2BAHW,eAAe,SACf,MAAM,EAAE,GAAC,YAAY,OAAK,GAFxB,kBAAkB,CAM9B;IA9FD;;;;OAIG;IACH,iBAAgC;IAEhC;;;;OAIG;IACH,yBAAwC;IAExC;;;;OAIG;IACH,kBAAiB;IAEjB;;;OAGG;IACH,+BAEC;IAqED;;;OAGG;IACH,0CAIC;IAED,yCAEC;IAED;;;;;;;OAOG;IACH,6BAEC;IAUD,wCAKC;IAED,mEAyDC;IAED,6CAEC;IAED,oCAKC;IAED,kFAIC;IAED,qGAwCC;IAED;;;;OAIG;IACH,cAHW,kBAAkB,GAChB,OAAO,CAOnB;IAQL;;;;;OAKG;IACH,+BAFU,OAAO,CAEgC;CARhD;gCA9R+B,sBAAsB"}
1
+ {"version":3,"file":"TransformedShape3D.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/3d/shape/TransformedShape3D.js"],"names":[],"mappings":"AAyBA;IAwCI;;;;;OAKG;IACH,wBAHW,eAAe,MADf,MAAM,EAAE,GAAC,YAAY,OAAK,GAExB,kBAAkB,CAS9B;IAED;;;;;OAKG;IACH,uCAJW,eAAe,eACf,MAAM,EAAE,SACR,MAAM,EAAE,sBAiBlB;IAED;;;;OAIG;IACH,iCAHW,eAAe,eACf,MAAM,EAAE,sBAIlB;IAED;;;;;OAKG;IACH,2BAHW,eAAe,SACf,MAAM,EAAE,GAAC,YAAY,OAAK,GAFxB,kBAAkB,CAM9B;IA9FD;;;;OAIG;IACH,iBAAgC;IAEhC;;;;OAIG;IACH,yBAAwC;IAExC;;;;OAIG;IACH,kBAAiB;IAEjB;;;OAGG;IACH,+BAEC;IAqED;;;OAGG;IACH,0CAIC;IAED,yCAEC;IAED;;;;;;;OAOG;IACH,6BAEC;IAUD,wCAKC;IAED,mEAyDC;IAED,6CAEC;IAED,oCAKC;IAED,kFAIC;IAED,qGAwCC;IAuGD;;;;OAIG;IACH,cAHW,kBAAkB,GAChB,OAAO,CAOnB;IAQL;;;;;OAKG;IACH,+BAFU,OAAO,CAEgC;CARhD;gCAnY+B,sBAAsB"}
@@ -8,6 +8,7 @@ import { aabb3_matrix4_project } from "../aabb/aabb3_matrix4_project.js";
8
8
  import { m4_extract_scale } from "../mat4/m4_extract_scale.js";
9
9
  import { m4_invert } from "../mat4/m4_invert.js";
10
10
  import { m4_make_scale } from "../mat4/m4_make_scale.js";
11
+ import { Ray3 } from "../ray/Ray3.js";
11
12
  import { AbstractShape3D } from "./AbstractShape3D.js";
12
13
 
13
14
  /**
@@ -278,6 +279,107 @@ export class TransformedShape3D extends AbstractShape3D {
278
279
  v3_matrix4_multiply(result, result_offset, scratch_v3_0, 0, this.__matrix);
279
280
  }
280
281
 
282
+ /**
283
+ * Ray ↔ transformed shape, with the ray given in the WRAPPER's frame. The cached inverse matrix
284
+ * carries the ray into the subject's frame, the subject answers there, and the answer comes back
285
+ * by two different maps: the point through the forward matrix, the normal through the inverse
286
+ * TRANSPOSE.
287
+ *
288
+ * Three things an affine transform does to a ray that a rigid pose ({@link PosedShape3D#raycast})
289
+ * does not:
290
+ *
291
+ * - DISTANCE. The mapped direction is no longer unit — a scaled shape stretches travel along the
292
+ * ray — so it is renormalised and the stretch factor is carried on `tMax` INTO the subject.
293
+ * Nothing carries it back out, because the hit point is mapped by the matrix rather than
294
+ * rebuilt from a distance, and an affine map takes the subject's point on its ray to this
295
+ * ray's point at the matching parameter. So a caller recovering `t` as `(p − origin)·direction`
296
+ * still gets the distance in THIS frame, which is what the family contract promises.
297
+ * - NORMAL. It maps by (M⁻¹)ᵀ rather than by M — that is what keeps a normal perpendicular to
298
+ * its surface under non-uniform scale — and is renormalised afterwards, because that map does
299
+ * not preserve length. Done inline against the cached {@link inverse_transform}, not through
300
+ * `v3_matrix4_rotate_normal`, which would invert the matrix again on every ray.
301
+ * - ORIENTATION. Nothing to fix, as it happens: `n_here · d_here = scale · (n_subject ·
302
+ * d_subject)` with `scale > 0`, so a normal the subject turned to meet its ray meets this one
303
+ * too. True even of a mirroring matrix — its determinant is negative, but that scale, a vector
304
+ * length, is not.
305
+ *
306
+ * A transform that was never successfully inverted — a singular matrix, or a shape whose
307
+ * `transform` was never set — leaves the cached inverse zeroed, and the mapped direction with it;
308
+ * that is reported as a miss rather than propagated onward as NaN. Note the narrower case this
309
+ * does NOT rescue: {@link m4_invert} leaves a PREVIOUS inverse in place when it refuses, so a
310
+ * shape handed a singular transform after a good one goes on casting against the older one. That
311
+ * hazard belongs to every method here that reads the cached inverse, not to this one.
312
+ *
313
+ * The physics ray query does not come through here; it uses `refine_ray_transformed`, which does
314
+ * the same mapping on loose scalars and, reporting a distance rather than a point, divides that
315
+ * distance by the stretch factor instead of pushing a point through the matrix.
316
+ *
317
+ * @param {Float32Array|number[]} hit out: [px, py, pz, nx, ny, nz]
318
+ * @param {Ray3} ray wrapper-frame ray (origin in [0..2], unit direction in [3..5], tMax in [6])
319
+ * @returns {boolean} whether anything was hit
320
+ */
321
+ raycast(hit, ray) {
322
+ const subject = this.__subject;
323
+
324
+ if (subject === null) {
325
+ return false;
326
+ }
327
+
328
+ const inverse = this.__inverse_matrix;
329
+
330
+ // Direction takes the linear part only, so it is written out rather than run through
331
+ // v3_matrix4_multiply — and its length is needed anyway.
332
+ const dx = ray[3], dy = ray[4], dz = ray[5];
333
+
334
+ const sdx = inverse[0] * dx + inverse[4] * dy + inverse[8] * dz;
335
+ const sdy = inverse[1] * dx + inverse[5] * dy + inverse[9] * dz;
336
+ const sdz = inverse[2] * dx + inverse[6] * dy + inverse[10] * dz;
337
+
338
+ // How much longer one unit of travel here is in the subject's frame.
339
+ const scale = Math.sqrt(sdx * sdx + sdy * sdy + sdz * sdz);
340
+
341
+ if (!(scale > 0)) {
342
+ // Singular (or never-inverted) transform: the direction has no image to cast along.
343
+ return false;
344
+ }
345
+
346
+ const inv_scale = 1 / scale;
347
+
348
+ // Allocated per call rather than kept as a module scratch: a transformed shape may wrap another
349
+ // wrapper, and every level of that recursion needs its own sub-ray alive across the call below.
350
+ const subject_ray = new Ray3();
351
+
352
+ v3_matrix4_multiply(subject_ray, 0, ray, 0, inverse);
353
+
354
+ subject_ray[3] = sdx * inv_scale;
355
+ subject_ray[4] = sdy * inv_scale;
356
+ subject_ray[5] = sdz * inv_scale;
357
+ subject_ray[6] = ray[6] * scale;
358
+
359
+ if (!subject.raycast(hit, subject_ray)) {
360
+ return false;
361
+ }
362
+
363
+ // Point: the full forward transform, in place. v3_matrix4_multiply reads all three components
364
+ // before writing any, so `hit` may be both its source and its destination.
365
+ v3_matrix4_multiply(hit, 0, hit, 0, this.__matrix);
366
+
367
+ // Normal: (M⁻¹)ᵀ · n, then renormalised.
368
+ const nx = hit[3], ny = hit[4], nz = hit[5];
369
+
370
+ const wx = inverse[0] * nx + inverse[1] * ny + inverse[2] * nz;
371
+ const wy = inverse[4] * nx + inverse[5] * ny + inverse[6] * nz;
372
+ const wz = inverse[8] * nx + inverse[9] * ny + inverse[10] * nz;
373
+
374
+ const inv_length = 1 / Math.sqrt(wx * wx + wy * wy + wz * wz);
375
+
376
+ hit[3] = wx * inv_length;
377
+ hit[4] = wy * inv_length;
378
+ hit[5] = wz * inv_length;
379
+
380
+ return true;
381
+ }
382
+
281
383
  /**
282
384
  *
283
385
  * @param {TransformedShape3D} other
@@ -1 +1 @@
1
- {"version":3,"file":"UnionShape3D.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/3d/shape/UnionShape3D.js"],"names":[],"mappings":"AA2BA;IAkII;;;;OAIG;IACH,sBAHW,eAAe,EAAE,GACf,YAAY,CAYxB;IAhJD;;;OAGG;IACH,UAFU,eAAe,EAAE,CAEb;IAEd;;;;;OAKG;IACH,kCAA+B;IAE/B,qBAAkB;IAClB,qBAAmB;IACnB,uBAAqB;IAErB,wCAgCC;IAED;;;OAGG;IACH,mCAkBC;IAED;;;OAGG;IACH,2CA+CC;IA2DD,mEAEC;IAED,6CAaC;IAED,oCAYC;IAED,kFAoDC;IAED,qGAyBC;IAED;;;;OAIG;IACH,cAHW,YAAY,GACV,OAAO,CAKnB;IA6BL;;;;OAIG;IACH,yBAFU,OAAO,CAEoB;CApBpC;gCArV+B,sBAAsB"}
1
+ {"version":3,"file":"UnionShape3D.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/3d/shape/UnionShape3D.js"],"names":[],"mappings":"AA4BA;IAkII;;;;OAIG;IACH,sBAHW,eAAe,EAAE,GACf,YAAY,CAYxB;IAhJD;;;OAGG;IACH,UAFU,eAAe,EAAE,CAEb;IAEd;;;;;OAKG;IACH,kCAA+B;IAE/B,qBAAkB;IAClB,qBAAmB;IACnB,uBAAqB;IAErB,wCAgCC;IAED;;;OAGG;IACH,mCAkBC;IAED;;;OAGG;IACH,2CA+CC;IA2DD,mEAEC;IAED,6CAaC;IAED,oCAYC;IAED,kFAoDC;IAED,qGAyBC;IAiED;;;;OAIG;IACH,cAHW,YAAY,GACV,OAAO,CAKnB;IA6BL;;;;OAIG;IACH,yBAFU,OAAO,CAEoB;CApBpC;gCApZ+B,sBAAsB"}