@woosh/meep-engine 2.152.0 → 2.154.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 (99) hide show
  1. package/package.json +1 -1
  2. package/src/core/color/Color.d.ts +26 -6
  3. package/src/core/color/Color.d.ts.map +1 -1
  4. package/src/core/color/Color.js +38 -6
  5. package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts +112 -0
  6. package/src/core/geom/3d/shape/ConvexHullShape3D.d.ts.map +1 -0
  7. package/src/core/geom/3d/shape/ConvexHullShape3D.js +325 -0
  8. package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts +4 -0
  9. package/src/engine/graphics/ecs/trail2d/Trail2D.d.ts.map +1 -1
  10. package/src/engine/graphics/ecs/trail2d/Trail2D.js +21 -0
  11. package/src/engine/physics/PLAN.md +4 -4
  12. package/src/engine/physics/body/BodyStorage.d.ts +3 -1
  13. package/src/engine/physics/body/BodyStorage.d.ts.map +1 -1
  14. package/src/engine/physics/body/BodyStorage.js +452 -450
  15. package/src/engine/physics/body/SolverBodyState.d.ts.map +1 -1
  16. package/src/engine/physics/body/SolverBodyState.js +6 -5
  17. package/src/engine/physics/broadphase/generate_pairs.d.ts.map +1 -1
  18. package/src/engine/physics/broadphase/generate_pairs.js +9 -1
  19. package/src/engine/physics/ccd/linear_sweep.d.ts.map +1 -1
  20. package/src/engine/physics/ccd/linear_sweep.js +237 -238
  21. package/src/engine/physics/computeInterceptPoint.d.ts.map +1 -1
  22. package/src/engine/physics/computeInterceptPoint.js +8 -3
  23. package/src/engine/physics/contact/ManifoldStore.d.ts +0 -16
  24. package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
  25. package/src/engine/physics/contact/ManifoldStore.js +1 -38
  26. package/src/engine/physics/ecs/BodyKind.d.ts +3 -2
  27. package/src/engine/physics/ecs/BodyKind.d.ts.map +1 -1
  28. package/src/engine/physics/ecs/BodyKind.js +25 -24
  29. package/src/engine/physics/ecs/PhysicsEvents.d.ts +4 -5
  30. package/src/engine/physics/ecs/PhysicsEvents.d.ts.map +1 -1
  31. package/src/engine/physics/ecs/PhysicsEvents.js +15 -16
  32. package/src/engine/physics/ecs/PhysicsSystem.d.ts +5 -30
  33. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  34. package/src/engine/physics/ecs/PhysicsSystem.js +13 -45
  35. package/src/engine/physics/ecs/RigidBodySerializationAdapter.d.ts.map +1 -1
  36. package/src/engine/physics/ecs/RigidBodySerializationAdapter.js +85 -81
  37. package/src/engine/physics/ecs/is_sensor.d.ts +18 -0
  38. package/src/engine/physics/ecs/is_sensor.d.ts.map +1 -0
  39. package/src/engine/physics/ecs/is_sensor.js +27 -0
  40. package/src/engine/physics/events/ContactEventBuffer.d.ts +2 -1
  41. package/src/engine/physics/events/ContactEventBuffer.d.ts.map +1 -1
  42. package/src/engine/physics/events/ContactEventBuffer.js +84 -83
  43. package/src/engine/physics/gjk/gjk.d.ts +0 -26
  44. package/src/engine/physics/gjk/gjk.d.ts.map +1 -1
  45. package/src/engine/physics/gjk/gjk.js +3 -52
  46. package/src/engine/physics/gjk/gjk_epa_penetration.d.ts +16 -0
  47. package/src/engine/physics/gjk/gjk_epa_penetration.d.ts.map +1 -0
  48. package/src/engine/physics/gjk/gjk_epa_penetration.js +255 -0
  49. package/src/engine/physics/gjk/minkowski_support.d.ts +4 -9
  50. package/src/engine/physics/gjk/minkowski_support.d.ts.map +1 -1
  51. package/src/engine/physics/gjk/minkowski_support.js +70 -75
  52. package/src/engine/physics/gjk/mpr.d.ts +1 -1
  53. package/src/engine/physics/gjk/mpr.d.ts.map +1 -1
  54. package/src/engine/physics/gjk/mpr.js +362 -344
  55. package/src/engine/physics/island/IslandBuilder.d.ts.map +1 -1
  56. package/src/engine/physics/island/IslandBuilder.js +431 -428
  57. package/src/engine/physics/narrowphase/box_box_manifold.d.ts.map +1 -1
  58. package/src/engine/physics/narrowphase/box_box_manifold.js +4 -81
  59. package/src/engine/physics/narrowphase/box_triangle_contact.d.ts.map +1 -1
  60. package/src/engine/physics/narrowphase/box_triangle_contact.js +4 -39
  61. package/src/engine/physics/narrowphase/capsule_contacts.d.ts.map +1 -1
  62. package/src/engine/physics/narrowphase/capsule_contacts.js +459 -462
  63. package/src/engine/physics/narrowphase/clip_against_axis_uv.d.ts.map +1 -1
  64. package/src/engine/physics/narrowphase/clip_against_axis_uv.js +4 -1
  65. package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts +83 -0
  66. package/src/engine/physics/narrowphase/convex_convex_manifold.d.ts.map +1 -0
  67. package/src/engine/physics/narrowphase/convex_convex_manifold.js +425 -0
  68. package/src/engine/physics/narrowphase/convex_decomposition.d.ts +32 -0
  69. package/src/engine/physics/narrowphase/convex_decomposition.d.ts.map +1 -0
  70. package/src/engine/physics/narrowphase/convex_decomposition.js +293 -0
  71. package/src/engine/physics/narrowphase/mesh_convex_hull.d.ts +41 -0
  72. package/src/engine/physics/narrowphase/mesh_convex_hull.d.ts.map +1 -0
  73. package/src/engine/physics/narrowphase/mesh_convex_hull.js +106 -0
  74. package/src/engine/physics/narrowphase/mesh_mesh_tet_manifold.d.ts +8 -0
  75. package/src/engine/physics/narrowphase/mesh_mesh_tet_manifold.d.ts.map +1 -0
  76. package/src/engine/physics/narrowphase/mesh_mesh_tet_manifold.js +117 -0
  77. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  78. package/src/engine/physics/narrowphase/narrowphase_step.js +105 -102
  79. package/src/engine/physics/narrowphase/reduce_manifold_contacts.d.ts +29 -0
  80. package/src/engine/physics/narrowphase/reduce_manifold_contacts.d.ts.map +1 -0
  81. package/src/engine/physics/narrowphase/reduce_manifold_contacts.js +69 -0
  82. package/src/engine/physics/narrowphase/refine_ray_concave.d.ts.map +1 -1
  83. package/src/engine/physics/narrowphase/refine_ray_concave.js +152 -145
  84. package/src/engine/physics/narrowphase/sphere_box_contact.d.ts.map +1 -1
  85. package/src/engine/physics/narrowphase/sphere_box_contact.js +132 -123
  86. package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -1
  87. package/src/engine/physics/queries/overlap_shape.js +16 -17
  88. package/src/engine/physics/queries/raycast.d.ts +5 -0
  89. package/src/engine/physics/queries/raycast.d.ts.map +1 -1
  90. package/src/engine/physics/queries/raycast.js +16 -8
  91. package/src/engine/physics/queries/shape_cast.d.ts.map +1 -1
  92. package/src/engine/physics/queries/shape_cast.js +13 -7
  93. package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
  94. package/src/engine/physics/solver/solve_contacts.js +8 -11
  95. package/src/engine/physics/vehicle/RaycastVehicle.d.ts.map +1 -1
  96. package/src/engine/physics/vehicle/RaycastVehicle.js +339 -333
  97. package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts +0 -13
  98. package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts.map +0 -1
  99. package/src/engine/physics/gjk/expanding_polytope_algorithm.js +0 -399
@@ -1,145 +1,152 @@
1
- import { v3_quat3_apply } from "../../../core/geom/vec3/v3_quat3_apply.js";
2
- import { TRIANGLE_FLOAT_STRIDE } from "./decomposition/triangle_buffer_layout.js";
3
- import { mesh_enumerate_triangles } from "./decomposition/mesh_enumerate_triangles.js";
4
- import { heightmap_enumerate_triangles } from "./decomposition/heightmap_enumerate_triangles.js";
5
-
6
- /**
7
- * # Ray vs a concave shape (mesh / heightmap)
8
- *
9
- * Refines a ray hit against a non-convex collider by triangle decomposition,
10
- * mirroring the contact narrowphase's concave path. Working in the shape's
11
- * local frame:
12
- * 1. clip the ray to the shape's local bounding box (a quick reject, and a
13
- * finite query segment even when `tMax` is unbounded);
14
- * 2. enumerate the triangles overlapping that segment's AABB
15
- * (`mesh_enumerate_triangles` / `heightmap_enumerate_triangles`);
16
- * 3. two-sided Möller–Trumbore each, keeping the nearest crossing;
17
- * 4. return its distance and the world surface normal (the triangle's
18
- * geometric normal, oriented to face the ray).
19
- *
20
- * The triangle scratch grows on saturation and is bounded by {@link MAX_TRIS}
21
- * (a ray crossing more triangles than that in one mesh is not a realistic
22
- * raycast — it would mean skewering a 65k-triangle mesh lengthwise).
23
- *
24
- * @author Alex Goldring
25
- * @copyright Company Named Limited (c) 2026
26
- */
27
-
28
- const lo = new Float64Array(3); // ray origin, shape-local
29
- const ld = new Float64Array(3); // ray direction, shape-local
30
- const bounds = new Float64Array(6); // shape local AABB
31
-
32
- const INITIAL_TRIS = 1024;
33
- const MAX_TRIS = 1 << 16;
34
- let tri_buf = new Float64Array(INITIAL_TRIS * TRIANGLE_FLOAT_STRIDE);
35
-
36
- const EPS = 1e-12;
37
- const AABB_FATTEN = 1e-5; // guards triangles whose AABB just abuts the segment
38
-
39
- /**
40
- * @param {AbstractShape3D} shape a concave shape (`is_convex === false`)
41
- * @param {{x:number,y:number,z:number}} position
42
- * @param {ArrayLike<number>} rotation body rotation quaternion (x,y,z,w)
43
- * @param {number} ox @param {number} oy @param {number} oz ray origin (world)
44
- * @param {number} dx @param {number} dy @param {number} dz ray dir (world, unit)
45
- * @param {number} tMax
46
- * @param {Float64Array|Vector3} outNormal world surface normal, written on hit
47
- * @returns {number} hit distance, or `Infinity` on miss
48
- */
49
- export function refine_ray_concave(shape, position, rotation, ox, oy, oz, dx, dy, dz, tMax, outNormal) {
50
- const qx = rotation[0], qy = rotation[1], qz = rotation[2], qw = rotation[3];
51
- // World → local (inverse body rotation; unit dir stays unit → t preserved).
52
- v3_quat3_apply(lo, 0, ox - position.x, oy - position.y, oz - position.z, -qx, -qy, -qz, qw);
53
- v3_quat3_apply(ld, 0, dx, dy, dz, -qx, -qy, -qz, qw);
54
- const lox = lo[0], loy = lo[1], loz = lo[2];
55
- const ldx = ld[0], ldy = ld[1], ldz = ld[2];
56
-
57
- // Clip the local ray to the shape's local AABB → segment [tEnter, tExit].
58
- shape.compute_bounding_box(bounds);
59
- let tEnter = 0, tExit = tMax;
60
- if (ldx > EPS || ldx < -EPS) {
61
- const inv = 1 / ldx;
62
- let t1 = (bounds[0] - lox) * inv, t2 = (bounds[3] - lox) * inv;
63
- if (t1 > t2) { const s = t1; t1 = t2; t2 = s; }
64
- if (t1 > tEnter) tEnter = t1;
65
- if (t2 < tExit) tExit = t2;
66
- } else if (lox < bounds[0] || lox > bounds[3]) return Infinity;
67
- if (ldy > EPS || ldy < -EPS) {
68
- const inv = 1 / ldy;
69
- let t1 = (bounds[1] - loy) * inv, t2 = (bounds[4] - loy) * inv;
70
- if (t1 > t2) { const s = t1; t1 = t2; t2 = s; }
71
- if (t1 > tEnter) tEnter = t1;
72
- if (t2 < tExit) tExit = t2;
73
- } else if (loy < bounds[1] || loy > bounds[4]) return Infinity;
74
- if (ldz > EPS || ldz < -EPS) {
75
- const inv = 1 / ldz;
76
- let t1 = (bounds[2] - loz) * inv, t2 = (bounds[5] - loz) * inv;
77
- if (t1 > t2) { const s = t1; t1 = t2; t2 = s; }
78
- if (t1 > tEnter) tEnter = t1;
79
- if (t2 < tExit) tExit = t2;
80
- } else if (loz < bounds[2] || loz > bounds[5]) return Infinity;
81
- if (tExit < tEnter) return Infinity; // ray misses the shape's AABB entirely
82
-
83
- // Query AABB = bbox of the clipped local segment.
84
- const sax = lox + ldx * tEnter, say = loy + ldy * tEnter, saz = loz + ldz * tEnter;
85
- const sbx = lox + ldx * tExit, sby = loy + ldy * tExit, sbz = loz + ldz * tExit;
86
- const qminx = (sax < sbx ? sax : sbx) - AABB_FATTEN;
87
- const qminy = (say < sby ? say : sby) - AABB_FATTEN;
88
- const qminz = (saz < sbz ? saz : sbz) - AABB_FATTEN;
89
- const qmaxx = (sax > sbx ? sax : sbx) + AABB_FATTEN;
90
- const qmaxy = (say > sby ? say : sby) + AABB_FATTEN;
91
- const qmaxz = (saz > sbz ? saz : sbz) + AABB_FATTEN;
92
-
93
- const enumerate = shape.isHeightMapShape3D === true ? heightmap_enumerate_triangles : mesh_enumerate_triangles;
94
- let cap = (tri_buf.length / TRIANGLE_FLOAT_STRIDE) | 0;
95
- let count = enumerate(tri_buf, 0, shape, qminx, qminy, qminz, qmaxx, qmaxy, qmaxz);
96
- while (count === cap && cap < MAX_TRIS) {
97
- cap = cap * 2 < MAX_TRIS ? cap * 2 : MAX_TRIS;
98
- tri_buf = new Float64Array(cap * TRIANGLE_FLOAT_STRIDE);
99
- count = enumerate(tri_buf, 0, shape, qminx, qminy, qminz, qmaxx, qmaxy, qmaxz);
100
- }
101
-
102
- // Two-sided Möller–Trumbore over the candidates; keep the nearest.
103
- let best = tMax;
104
- let found = false;
105
- let nx = 0, ny = 0, nz = 0;
106
- for (let i = 0; i < count; i++) {
107
- const o = i * TRIANGLE_FLOAT_STRIDE;
108
- const ax = tri_buf[o], ay = tri_buf[o + 1], az = tri_buf[o + 2];
109
- const e1x = tri_buf[o + 3] - ax, e1y = tri_buf[o + 4] - ay, e1z = tri_buf[o + 5] - az;
110
- const e2x = tri_buf[o + 6] - ax, e2y = tri_buf[o + 7] - ay, e2z = tri_buf[o + 8] - az;
111
-
112
- const px = ldy * e2z - ldz * e2y, py = ldz * e2x - ldx * e2z, pz = ldx * e2y - ldy * e2x;
113
- const det = e1x * px + e1y * py + e1z * pz;
114
- if (det < EPS && det > -EPS) continue; // ray parallel to the triangle
115
- const invDet = 1 / det;
116
-
117
- const tvx = lox - ax, tvy = loy - ay, tvz = loz - az;
118
- const u = (tvx * px + tvy * py + tvz * pz) * invDet;
119
- if (u < 0 || u > 1) continue;
120
-
121
- const wx = tvy * e1z - tvz * e1y, wy = tvz * e1x - tvx * e1z, wz = tvx * e1y - tvy * e1x;
122
- const v = (ldx * wx + ldy * wy + ldz * wz) * invDet;
123
- if (v < 0 || u + v > 1) continue;
124
-
125
- const t = (e2x * wx + e2y * wy + e2z * wz) * invDet;
126
- if (t < 0 || t >= best) continue;
127
-
128
- best = t;
129
- found = true;
130
- // Geometric normal e1 × e2 (local, unnormalised).
131
- nx = e1y * e2z - e1z * e2y;
132
- ny = e1z * e2x - e1x * e2z;
133
- nz = e1x * e2y - e1y * e2x;
134
- }
135
-
136
- if (!found) return Infinity;
137
-
138
- // Normalise, orient to face the ray, then rotate the local normal to world.
139
- let nl = Math.sqrt(nx * nx + ny * ny + nz * nz);
140
- if (nl === 0) nl = 1;
141
- nx /= nl; ny /= nl; nz /= nl;
142
- if (nx * ldx + ny * ldy + nz * ldz > 0) { nx = -nx; ny = -ny; nz = -nz; }
143
- v3_quat3_apply(outNormal, 0, nx, ny, nz, qx, qy, qz, qw);
144
- return best;
145
- }
1
+ import { v3_quat3_apply } from "../../../core/geom/vec3/v3_quat3_apply.js";
2
+ import { TRIANGLE_FLOAT_STRIDE } from "./decomposition/triangle_buffer_layout.js";
3
+ import { mesh_enumerate_triangles } from "./decomposition/mesh_enumerate_triangles.js";
4
+ import { heightmap_enumerate_triangles } from "./decomposition/heightmap_enumerate_triangles.js";
5
+
6
+ /**
7
+ * # Ray vs a concave shape (mesh / heightmap)
8
+ *
9
+ * Refines a ray hit against a non-convex collider by triangle decomposition,
10
+ * mirroring the contact narrowphase's concave path. Working in the shape's
11
+ * local frame:
12
+ * 1. clip the ray to the shape's local bounding box (a quick reject, and a
13
+ * finite query segment even when `tMax` is unbounded);
14
+ * 2. enumerate the triangles overlapping that segment's AABB
15
+ * (`mesh_enumerate_triangles` / `heightmap_enumerate_triangles`);
16
+ * 3. two-sided Möller–Trumbore each, keeping the nearest crossing;
17
+ * 4. return its distance and the world surface normal (the triangle's
18
+ * geometric normal, oriented to face the ray).
19
+ *
20
+ * The triangle scratch grows on saturation and is bounded by {@link MAX_TRIS}
21
+ * (a ray crossing more triangles than that in one mesh is not a realistic
22
+ * raycast — it would mean skewering a 65k-triangle mesh lengthwise).
23
+ *
24
+ * @author Alex Goldring
25
+ * @copyright Company Named Limited (c) 2026
26
+ */
27
+
28
+ const lo = new Float64Array(3); // ray origin, shape-local
29
+ const ld = new Float64Array(3); // ray direction, shape-local
30
+ const bounds = new Float64Array(6); // shape local AABB
31
+
32
+ const INITIAL_TRIS = 1024;
33
+ const MAX_TRIS = 1 << 16;
34
+ let tri_buf = new Float64Array(INITIAL_TRIS * TRIANGLE_FLOAT_STRIDE);
35
+
36
+ const EPS = 1e-12;
37
+ const AABB_FATTEN = 1e-5; // guards triangles whose AABB just abuts the segment
38
+
39
+ /**
40
+ * @param {AbstractShape3D} shape a concave shape (`is_convex === false`)
41
+ * @param {{x:number,y:number,z:number}} position
42
+ * @param {ArrayLike<number>} rotation body rotation quaternion (x,y,z,w)
43
+ * @param {number} ox @param {number} oy @param {number} oz ray origin (world)
44
+ * @param {number} dx @param {number} dy @param {number} dz ray dir (world, unit)
45
+ * @param {number} tMax
46
+ * @param {Float64Array|Vector3} outNormal world surface normal, written on hit
47
+ * @returns {number} hit distance, or `Infinity` on miss
48
+ */
49
+ export function refine_ray_concave(shape, position, rotation, ox, oy, oz, dx, dy, dz, tMax, outNormal) {
50
+ const qx = rotation[0], qy = rotation[1], qz = rotation[2], qw = rotation[3];
51
+ // World → local (inverse body rotation; unit dir stays unit → t preserved).
52
+ v3_quat3_apply(lo, 0, ox - position.x, oy - position.y, oz - position.z, -qx, -qy, -qz, qw);
53
+ v3_quat3_apply(ld, 0, dx, dy, dz, -qx, -qy, -qz, qw);
54
+ const lox = lo[0], loy = lo[1], loz = lo[2];
55
+ const ldx = ld[0], ldy = ld[1], ldz = ld[2];
56
+
57
+ // Clip the local ray to the shape's local AABB → segment [tEnter, tExit].
58
+ shape.compute_bounding_box(bounds);
59
+ let tEnter = 0, tExit = tMax;
60
+ if (ldx > EPS || ldx < -EPS) {
61
+ const inv = 1 / ldx;
62
+ let t1 = (bounds[0] - lox) * inv, t2 = (bounds[3] - lox) * inv;
63
+ if (t1 > t2) { const s = t1; t1 = t2; t2 = s; }
64
+ if (t1 > tEnter) tEnter = t1;
65
+ if (t2 < tExit) tExit = t2;
66
+ } else if (lox < bounds[0] || lox > bounds[3]) return Infinity;
67
+ if (ldy > EPS || ldy < -EPS) {
68
+ const inv = 1 / ldy;
69
+ let t1 = (bounds[1] - loy) * inv, t2 = (bounds[4] - loy) * inv;
70
+ if (t1 > t2) { const s = t1; t1 = t2; t2 = s; }
71
+ if (t1 > tEnter) tEnter = t1;
72
+ if (t2 < tExit) tExit = t2;
73
+ } else if (loy < bounds[1] || loy > bounds[4]) return Infinity;
74
+ if (ldz > EPS || ldz < -EPS) {
75
+ const inv = 1 / ldz;
76
+ let t1 = (bounds[2] - loz) * inv, t2 = (bounds[5] - loz) * inv;
77
+ if (t1 > t2) { const s = t1; t1 = t2; t2 = s; }
78
+ if (t1 > tEnter) tEnter = t1;
79
+ if (t2 < tExit) tExit = t2;
80
+ } else if (loz < bounds[2] || loz > bounds[5]) return Infinity;
81
+ if (tExit < tEnter) return Infinity; // ray misses the shape's AABB entirely
82
+
83
+ // Query AABB = bbox of the clipped local segment.
84
+ const sax = lox + ldx * tEnter, say = loy + ldy * tEnter, saz = loz + ldz * tEnter;
85
+ const sbx = lox + ldx * tExit, sby = loy + ldy * tExit, sbz = loz + ldz * tExit;
86
+ const qminx = (sax < sbx ? sax : sbx) - AABB_FATTEN;
87
+ const qminy = (say < sby ? say : sby) - AABB_FATTEN;
88
+ const qminz = (saz < sbz ? saz : sbz) - AABB_FATTEN;
89
+ const qmaxx = (sax > sbx ? sax : sbx) + AABB_FATTEN;
90
+ const qmaxy = (say > sby ? say : sby) + AABB_FATTEN;
91
+ const qmaxz = (saz > sbz ? saz : sbz) + AABB_FATTEN;
92
+
93
+ const enumerate = shape.isHeightMapShape3D === true ? heightmap_enumerate_triangles : mesh_enumerate_triangles;
94
+ let cap = (tri_buf.length / TRIANGLE_FLOAT_STRIDE) | 0;
95
+ // The enumerators return the TRUE overlap count, which can EXCEED the buffer
96
+ // capacity (writes past the end are silently dropped). So the grow trigger is
97
+ // `count > cap` (not `=== cap`): regrow whenever the true count overflowed.
98
+ let count = enumerate(tri_buf, 0, shape, qminx, qminy, qminz, qmaxx, qmaxy, qmaxz);
99
+ while (count > cap && cap < MAX_TRIS) {
100
+ cap = cap * 2 < MAX_TRIS ? cap * 2 : MAX_TRIS;
101
+ tri_buf = new Float64Array(cap * TRIANGLE_FLOAT_STRIDE);
102
+ count = enumerate(tri_buf, 0, shape, qminx, qminy, qminz, qmaxx, qmaxy, qmaxz);
103
+ }
104
+ // If the true count still exceeds MAX_TRIS, only `cap` triangles were written;
105
+ // clamp so the consume loop never reads undefined past the buffer (which would
106
+ // poison `best` with NaN). Dropping the overflow is the documented MAX_TRIS bound.
107
+ const n_tris = count < cap ? count : cap;
108
+
109
+ // Two-sided Möller–Trumbore over the candidates; keep the nearest.
110
+ let best = tMax;
111
+ let found = false;
112
+ let nx = 0, ny = 0, nz = 0;
113
+ for (let i = 0; i < n_tris; i++) {
114
+ const o = i * TRIANGLE_FLOAT_STRIDE;
115
+ const ax = tri_buf[o], ay = tri_buf[o + 1], az = tri_buf[o + 2];
116
+ const e1x = tri_buf[o + 3] - ax, e1y = tri_buf[o + 4] - ay, e1z = tri_buf[o + 5] - az;
117
+ const e2x = tri_buf[o + 6] - ax, e2y = tri_buf[o + 7] - ay, e2z = tri_buf[o + 8] - az;
118
+
119
+ const px = ldy * e2z - ldz * e2y, py = ldz * e2x - ldx * e2z, pz = ldx * e2y - ldy * e2x;
120
+ const det = e1x * px + e1y * py + e1z * pz;
121
+ if (det < EPS && det > -EPS) continue; // ray parallel to the triangle
122
+ const invDet = 1 / det;
123
+
124
+ const tvx = lox - ax, tvy = loy - ay, tvz = loz - az;
125
+ const u = (tvx * px + tvy * py + tvz * pz) * invDet;
126
+ if (u < 0 || u > 1) continue;
127
+
128
+ const wx = tvy * e1z - tvz * e1y, wy = tvz * e1x - tvx * e1z, wz = tvx * e1y - tvy * e1x;
129
+ const v = (ldx * wx + ldy * wy + ldz * wz) * invDet;
130
+ if (v < 0 || u + v > 1) continue;
131
+
132
+ const t = (e2x * wx + e2y * wy + e2z * wz) * invDet;
133
+ if (t < 0 || t >= best) continue;
134
+
135
+ best = t;
136
+ found = true;
137
+ // Geometric normal e1 × e2 (local, unnormalised).
138
+ nx = e1y * e2z - e1z * e2y;
139
+ ny = e1z * e2x - e1x * e2z;
140
+ nz = e1x * e2y - e1y * e2x;
141
+ }
142
+
143
+ if (!found) return Infinity;
144
+
145
+ // Normalise, orient to face the ray, then rotate the local normal to world.
146
+ let nl = Math.sqrt(nx * nx + ny * ny + nz * nz);
147
+ if (nl === 0) nl = 1;
148
+ nx /= nl; ny /= nl; nz /= nl;
149
+ if (nx * ldx + ny * ldy + nz * ldz > 0) { nx = -nx; ny = -ny; nz = -nz; }
150
+ v3_quat3_apply(outNormal, 0, nx, ny, nz, qx, qy, qz, qw);
151
+ return best;
152
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"sphere_box_contact.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/sphere_box_contact.js"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wCAjBW,MAAM,EAAE,GAAC,YAAY,MACrB,MAAM,MACN,MAAM,MACN,MAAM,UACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,GACJ,OAAO,CA8EnB"}
1
+ {"version":3,"file":"sphere_box_contact.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/narrowphase/sphere_box_contact.js"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wCAjBW,MAAM,EAAE,GAAC,YAAY,MACrB,MAAM,MACN,MAAM,MACN,MAAM,UACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,OACN,MAAM,MACN,MAAM,MACN,MAAM,MACN,MAAM,GACJ,OAAO,CAuFnB"}
@@ -1,123 +1,132 @@
1
- import { v3_quat3_apply } from "../../../core/geom/vec3/v3_quat3_apply.js";
2
- import { v3_quat3_apply_inverse } from "../../../core/geom/vec3/v3_quat3_apply_inverse.js";
3
-
4
- /**
5
- * Scratch for the world→box-local rotation of the sphere centre at the
6
- * top of {@link sphere_box_contact}. 3 floats; allocation-free.
7
- * @type {Float64Array}
8
- */
9
- const scratch_local = new Float64Array(3);
10
-
11
- /**
12
- * Closed-form contact generation for a unit sphere vs. an oriented box.
13
- *
14
- * Algorithm: transform the sphere centre into the box's body frame, clamp it
15
- * to the box's half-extents to get the closest surface point in body space,
16
- * measure the distance, and (when overlapping) rotate the resulting normal
17
- * and surface point back into world.
18
- *
19
- * Singular cases:
20
- * - If the sphere centre is inside the box (distance == 0 in body frame),
21
- * the closest-face vector is undefined; we pick the smallest-overlap face
22
- * direction as a deterministic tie-break.
23
- *
24
- * Output convention (mirrors {@link sphere_sphere_contact}): the normal in
25
- * `out[0..2]` points from the box surface toward the sphere centre; `out[3]`
26
- * is the (positive) penetration depth; `out[4..6]` is the world-space contact
27
- * point on the sphere's surface; `out[7..9]` is the world-space contact point
28
- * on the box's surface.
29
- *
30
- * @param {number[]|Float64Array} out length >= 10
31
- * @param {number} sx sphere centre x
32
- * @param {number} sy
33
- * @param {number} sz
34
- * @param {number} radius
35
- * @param {number} bx box centre x
36
- * @param {number} by
37
- * @param {number} bz
38
- * @param {number} bqx box rotation quaternion x
39
- * @param {number} bqy
40
- * @param {number} bqz
41
- * @param {number} bqw
42
- * @param {number} hx box half-extent x (body frame)
43
- * @param {number} hy
44
- * @param {number} hz
45
- * @returns {boolean} true if overlap
46
- */
47
- export function sphere_box_contact(
48
- out,
49
- sx, sy, sz, radius,
50
- bx, by, bz,
51
- bqx, bqy, bqz, bqw,
52
- hx, hy, hz
53
- ) {
54
- // Step 1: bring the sphere centre into box-local space via the conjugate
55
- // quaternion. v_local = q* · (s - b) · q
56
- v3_quat3_apply_inverse(scratch_local, 0, sx - bx, sy - by, sz - bz, bqx, bqy, bqz, bqw);
57
- const lx = scratch_local[0];
58
- const ly = scratch_local[1];
59
- const lz = scratch_local[2];
60
-
61
- // Step 2: closest point on the box to the sphere centre, in body frame.
62
- const clx = lx < -hx ? -hx : (lx > hx ? hx : lx);
63
- const cly = ly < -hy ? -hy : (ly > hy ? hy : ly);
64
- const clz = lz < -hz ? -hz : (lz > hz ? hz : lz);
65
-
66
- const inside = clx === lx && cly === ly && clz === lz;
67
-
68
- let nlx, nly, nlz, dist;
69
- if (!inside) {
70
- // Centre is outside the box: normal is from clamped point toward centre.
71
- const ex = lx - clx, ey = ly - cly, ez = lz - clz;
72
- const dist_sqr = ex * ex + ey * ey + ez * ez;
73
- if (dist_sqr >= radius * radius) {
74
- return false;
75
- }
76
- dist = Math.sqrt(dist_sqr);
77
- if (dist > 0) {
78
- const inv = 1 / dist;
79
- nlx = ex * inv; nly = ey * inv; nlz = ez * inv;
80
- } else {
81
- // Centre lies exactly on a face pick +X as a tie-break.
82
- nlx = 1; nly = 0; nlz = 0;
83
- }
84
- } else {
85
- // Centre is inside the box. Closest face is the one we're nearest to.
86
- // Compute the per-axis "distance to face" (positive); pick min.
87
- const dx_face = hx - Math.abs(lx);
88
- const dy_face = hy - Math.abs(ly);
89
- const dz_face = hz - Math.abs(lz);
90
-
91
- // Pick smallest depth-to-face; ties broken X > Y > Z (deterministic).
92
- if (dx_face <= dy_face && dx_face <= dz_face) {
93
- nlx = lx >= 0 ? 1 : -1; nly = 0; nlz = 0;
94
- dist = -dx_face;
95
- } else if (dy_face <= dz_face) {
96
- nlx = 0; nly = ly >= 0 ? 1 : -1; nlz = 0;
97
- dist = -dy_face;
98
- } else {
99
- nlx = 0; nly = 0; nlz = lz >= 0 ? 1 : -1;
100
- dist = -dz_face;
101
- }
102
- }
103
-
104
- // Step 3: rotate normal and surface point back to world. v_world = q · v · q*
105
-
106
- // World normal (box → sphere).
107
- v3_quat3_apply(out, 0, nlx, nly, nlz, bqx, bqy, bqz, bqw);
108
- const nx = out[0], ny = out[1], nz = out[2];
109
- out[3] = radius - dist;
110
-
111
- // World contact on sphere = sphere_center - normal * radius.
112
- out[4] = sx - nx * radius;
113
- out[5] = sy - ny * radius;
114
- out[6] = sz - nz * radius;
115
-
116
- // World contact on box: rotate the local-space clamped point to world, plus box centre.
117
- v3_quat3_apply(out, 7, clx, cly, clz, bqx, bqy, bqz, bqw);
118
- out[7] += bx;
119
- out[8] += by;
120
- out[9] += bz;
121
-
122
- return true;
123
- }
1
+ import { v3_quat3_apply } from "../../../core/geom/vec3/v3_quat3_apply.js";
2
+ import { v3_quat3_apply_inverse } from "../../../core/geom/vec3/v3_quat3_apply_inverse.js";
3
+
4
+ /**
5
+ * Scratch for the world→box-local rotation of the sphere centre at the
6
+ * top of {@link sphere_box_contact}. 3 floats; allocation-free.
7
+ * @type {Float64Array}
8
+ */
9
+ const scratch_local = new Float64Array(3);
10
+
11
+ /**
12
+ * Closed-form contact generation for a unit sphere vs. an oriented box.
13
+ *
14
+ * Algorithm: transform the sphere centre into the box's body frame, clamp it
15
+ * to the box's half-extents to get the closest surface point in body space,
16
+ * measure the distance, and (when overlapping) rotate the resulting normal
17
+ * and surface point back into world.
18
+ *
19
+ * Singular cases:
20
+ * - If the sphere centre is inside the box (distance == 0 in body frame),
21
+ * the closest-face vector is undefined; we pick the smallest-overlap face
22
+ * direction as a deterministic tie-break.
23
+ *
24
+ * Output convention (mirrors {@link sphere_sphere_contact}): the normal in
25
+ * `out[0..2]` points from the box surface toward the sphere centre; `out[3]`
26
+ * is the (positive) penetration depth; `out[4..6]` is the world-space contact
27
+ * point on the sphere's surface; `out[7..9]` is the world-space contact point
28
+ * on the box's surface.
29
+ *
30
+ * @param {number[]|Float64Array} out length >= 10
31
+ * @param {number} sx sphere centre x
32
+ * @param {number} sy
33
+ * @param {number} sz
34
+ * @param {number} radius
35
+ * @param {number} bx box centre x
36
+ * @param {number} by
37
+ * @param {number} bz
38
+ * @param {number} bqx box rotation quaternion x
39
+ * @param {number} bqy
40
+ * @param {number} bqz
41
+ * @param {number} bqw
42
+ * @param {number} hx box half-extent x (body frame)
43
+ * @param {number} hy
44
+ * @param {number} hz
45
+ * @returns {boolean} true if overlap
46
+ */
47
+ export function sphere_box_contact(
48
+ out,
49
+ sx, sy, sz, radius,
50
+ bx, by, bz,
51
+ bqx, bqy, bqz, bqw,
52
+ hx, hy, hz
53
+ ) {
54
+ // Step 1: bring the sphere centre into box-local space via the conjugate
55
+ // quaternion. v_local = q* · (s - b) · q
56
+ v3_quat3_apply_inverse(scratch_local, 0, sx - bx, sy - by, sz - bz, bqx, bqy, bqz, bqw);
57
+ const lx = scratch_local[0];
58
+ const ly = scratch_local[1];
59
+ const lz = scratch_local[2];
60
+
61
+ // Step 2: closest point on the box to the sphere centre, in body frame.
62
+ // `let` because the interior branch snaps the point onto the chosen face
63
+ // (when the centre is inside the box the clamp leaves it AT the centre).
64
+ let clx = lx < -hx ? -hx : (lx > hx ? hx : lx);
65
+ let cly = ly < -hy ? -hy : (ly > hy ? hy : ly);
66
+ let clz = lz < -hz ? -hz : (lz > hz ? hz : lz);
67
+
68
+ const inside = clx === lx && cly === ly && clz === lz;
69
+
70
+ let nlx, nly, nlz, dist;
71
+ if (!inside) {
72
+ // Centre is outside the box: normal is from clamped point toward centre.
73
+ const ex = lx - clx, ey = ly - cly, ez = lz - clz;
74
+ const dist_sqr = ex * ex + ey * ey + ez * ez;
75
+ if (dist_sqr >= radius * radius) {
76
+ return false;
77
+ }
78
+ dist = Math.sqrt(dist_sqr);
79
+ if (dist > 0) {
80
+ const inv = 1 / dist;
81
+ nlx = ex * inv; nly = ey * inv; nlz = ez * inv;
82
+ } else {
83
+ // Centre lies exactly on a face — pick +X as a tie-break.
84
+ nlx = 1; nly = 0; nlz = 0;
85
+ }
86
+ } else {
87
+ // Centre is inside the box. Closest face is the one we're nearest to.
88
+ // Compute the per-axis "distance to face" (positive); pick min.
89
+ const dx_face = hx - Math.abs(lx);
90
+ const dy_face = hy - Math.abs(ly);
91
+ const dz_face = hz - Math.abs(lz);
92
+
93
+ // Pick smallest depth-to-face; ties broken X > Y > Z (deterministic).
94
+ // Snap the clamped point onto the chosen face so the world box contact
95
+ // point (out[7..9], written below) lies on the box SURFACE and is
96
+ // consistent with the reported normal/depth not left at the interior
97
+ // sphere centre (which the clamp produced for an inside centre).
98
+ if (dx_face <= dy_face && dx_face <= dz_face) {
99
+ nlx = lx >= 0 ? 1 : -1; nly = 0; nlz = 0;
100
+ dist = -dx_face;
101
+ clx = lx >= 0 ? hx : -hx;
102
+ } else if (dy_face <= dz_face) {
103
+ nlx = 0; nly = ly >= 0 ? 1 : -1; nlz = 0;
104
+ dist = -dy_face;
105
+ cly = ly >= 0 ? hy : -hy;
106
+ } else {
107
+ nlx = 0; nly = 0; nlz = lz >= 0 ? 1 : -1;
108
+ dist = -dz_face;
109
+ clz = lz >= 0 ? hz : -hz;
110
+ }
111
+ }
112
+
113
+ // Step 3: rotate normal and surface point back to world. v_world = q · v · q*
114
+
115
+ // World normal (box → sphere).
116
+ v3_quat3_apply(out, 0, nlx, nly, nlz, bqx, bqy, bqz, bqw);
117
+ const nx = out[0], ny = out[1], nz = out[2];
118
+ out[3] = radius - dist;
119
+
120
+ // World contact on sphere = sphere_center - normal * radius.
121
+ out[4] = sx - nx * radius;
122
+ out[5] = sy - ny * radius;
123
+ out[6] = sz - nz * radius;
124
+
125
+ // World contact on box: rotate the local-space clamped point to world, plus box centre.
126
+ v3_quat3_apply(out, 7, clx, cly, clz, bqx, bqy, bqz, bqw);
127
+ out[7] += bx;
128
+ out[8] += by;
129
+ out[9] += bz;
130
+
131
+ return true;
132
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"overlap_shape.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/queries/overlap_shape.js"],"names":[],"mappings":"AA8CA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,uFAZW;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,YAE5B;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,UAErC,WAAW,GAAC,MAAM,EAAE,iBAEpB,MAAM,oBACE,MAAM,yBAAsB,OAAO,GAEzC,MAAM,CAqGlB"}
1
+ {"version":3,"file":"overlap_shape.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/queries/overlap_shape.js"],"names":[],"mappings":"AA8CA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,uFAZW;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,YAE5B;IAAC,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAC;IAAA,CAAC,EAAC,MAAM,CAAA;CAAC,UAErC,WAAW,GAAC,MAAM,EAAE,iBAEpB,MAAM,oBACE,MAAM,yBAAsB,OAAO,GAEzC,MAAM,CAoGlB"}
@@ -100,13 +100,19 @@ export function overlap_shape(system, shape, position, rotation, output, output_
100
100
  );
101
101
 
102
102
  // ── 2. Gather broadphase candidates ─────────────────────────────
103
- const n_static = bvh_query_user_data_overlaps_aabb(
104
- scratch_candidates, 0, system.staticBvh, world_aabb
105
- );
106
- const n_dynamic = bvh_query_user_data_overlaps_aabb(
107
- scratch_candidates, n_static, system.dynamicBvh, world_aabb
108
- );
109
- const n_total = n_static + n_dynamic;
103
+ // The BVH query returns the TRUE overlap count and silently drops writes past
104
+ // the buffer end, so an undersized buffer would leave the read loop pulling
105
+ // `undefined` (garbage body ids) and silently MISS real overlaps. Grow by
106
+ // doubling and re-query until both static + dynamic fit (queries are
107
+ // deterministic, so this converges in one resize).
108
+ let n_static, n_dynamic, n_total;
109
+ for (;;) {
110
+ n_static = bvh_query_user_data_overlaps_aabb(scratch_candidates, 0, system.staticBvh, world_aabb);
111
+ n_dynamic = bvh_query_user_data_overlaps_aabb(scratch_candidates, n_static, system.dynamicBvh, world_aabb);
112
+ n_total = n_static + n_dynamic;
113
+ if (n_total <= scratch_candidates.length) break;
114
+ scratch_candidates = new Uint32Array(Math.max(n_total, scratch_candidates.length * 2));
115
+ }
110
116
  if (n_total === 0) return 0;
111
117
 
112
118
  // ── 3. Set up query PosedShape (constant across candidates) ─────
@@ -154,16 +160,9 @@ export function overlap_shape(system, shape, position, rotation, output, output_
154
160
  concave_query_aabb[3], concave_query_aabb[4], concave_query_aabb[5]
155
161
  );
156
162
 
157
- // Re-pose candidate as the concave body, rebinding the
158
- // flyweight triangle per iteration.
159
- candidate_posed.shape = triangle_shape;
160
- candidate_posed.px = candidate_tr.position.x;
161
- candidate_posed.py = candidate_tr.position.y;
162
- candidate_posed.pz = candidate_tr.position.z;
163
- candidate_posed.qx = candidate_tr.rotation.x;
164
- candidate_posed.qy = candidate_tr.rotation.y;
165
- candidate_posed.qz = candidate_tr.rotation.z;
166
- candidate_posed.qw = candidate_tr.rotation.w;
163
+ // Re-pose candidate as the concave body (the flyweight triangle is
164
+ // rebound per iteration below; setup only stores the shape reference).
165
+ candidate_posed.setup(triangle_shape, candidate_tr.position, candidate_tr.rotation);
167
166
 
168
167
  for (let t = 0; t < tri_count; t++) {
169
168
  triangle_shape.bind(triangle_buffer, t * TRIANGLE_FLOAT_STRIDE);