@woosh/meep-engine 2.139.0 → 2.140.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/package.json +1 -1
  2. package/src/core/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.d.ts +3 -3
  3. package/src/core/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.d.ts.map +1 -1
  4. package/src/core/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.js +4 -4
  5. package/src/{engine/physics/broadphase/aabb_transform_oriented.d.ts → core/geom/3d/aabb/aabb3_transform_oriented.d.ts} +2 -2
  6. package/src/core/geom/3d/aabb/aabb3_transform_oriented.d.ts.map +1 -0
  7. package/src/{engine/physics/broadphase/aabb_transform_oriented.js → core/geom/3d/aabb/aabb3_transform_oriented.js} +1 -1
  8. package/src/core/geom/3d/quaternion/quat3_to_matrix3.d.ts +54 -0
  9. package/src/core/geom/3d/quaternion/quat3_to_matrix3.d.ts.map +1 -0
  10. package/src/core/geom/3d/quaternion/quat3_to_matrix3.js +69 -0
  11. package/src/core/geom/3d/shape/AbstractShape3D.d.ts +24 -2
  12. package/src/core/geom/3d/shape/AbstractShape3D.d.ts.map +1 -1
  13. package/src/core/geom/3d/shape/AbstractShape3D.js +24 -1
  14. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts +148 -0
  15. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -0
  16. package/src/core/geom/3d/shape/HeightMapShape3D.js +451 -0
  17. package/src/core/geom/3d/shape/MeshShape3D.d.ts +210 -0
  18. package/src/core/geom/3d/shape/MeshShape3D.d.ts.map +1 -0
  19. package/src/core/geom/3d/shape/MeshShape3D.js +593 -0
  20. package/src/core/geom/3d/shape/TransformedShape3D.d.ts.map +1 -1
  21. package/src/core/geom/3d/shape/TransformedShape3D.js +46 -2
  22. package/src/core/geom/3d/shape/Triangle3D.d.ts +95 -0
  23. package/src/core/geom/3d/shape/Triangle3D.d.ts.map +1 -0
  24. package/src/core/geom/3d/shape/Triangle3D.js +318 -0
  25. package/src/core/geom/3d/shape/UnionShape3D.js +13 -0
  26. package/src/core/geom/3d/shape/shape_mesh_from_geometry.d.ts +30 -0
  27. package/src/core/geom/3d/shape/shape_mesh_from_geometry.d.ts.map +1 -0
  28. package/src/core/geom/3d/shape/shape_mesh_from_geometry.js +64 -0
  29. package/src/core/geom/3d/tetrahedra/prototype_tetrahedrize_mesh.js +9 -11
  30. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.d.ts +28 -0
  31. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.d.ts.map +1 -0
  32. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_build_vertex_to_tets_map.js +48 -0
  33. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_improve_quality.d.ts.map +1 -1
  34. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_improve_quality.js +40 -18
  35. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.d.ts +9 -5
  36. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.d.ts.map +1 -1
  37. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_smooth_vertex.js +38 -10
  38. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.d.ts +14 -5
  39. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.d.ts.map +1 -1
  40. package/src/core/geom/3d/tetrahedra/tetrahedral_mesh_vertex_is_boundary.js +47 -5
  41. package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.d.ts +19 -0
  42. package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.d.ts.map +1 -1
  43. package/src/core/geom/3d/topology/struct/binary/BinaryElementPool.js +75 -13
  44. package/src/core/geom/3d/triangle/v3_compute_triangle_normal.d.ts +2 -2
  45. package/src/core/geom/3d/triangle/v3_compute_triangle_normal.d.ts.map +1 -1
  46. package/src/core/geom/3d/triangle/v3_compute_triangle_normal.js +1 -1
  47. package/src/core/geom/vec3/v3_dot_array_array.d.ts +3 -3
  48. package/src/core/geom/vec3/v3_dot_array_array.d.ts.map +1 -1
  49. package/src/core/geom/vec3/v3_dot_array_array.js +2 -2
  50. package/src/core/geom/vec3/v3_negate_array.d.ts +3 -3
  51. package/src/core/geom/vec3/v3_negate_array.d.ts.map +1 -1
  52. package/src/core/geom/vec3/v3_negate_array.js +2 -2
  53. package/src/core/geom/vec3/v3_quat3_apply.d.ts +29 -0
  54. package/src/core/geom/vec3/v3_quat3_apply.d.ts.map +1 -0
  55. package/src/core/geom/vec3/v3_quat3_apply.js +39 -0
  56. package/src/core/geom/vec3/v3_quat3_apply_inverse.d.ts +30 -0
  57. package/src/core/geom/vec3/v3_quat3_apply_inverse.d.ts.map +1 -0
  58. package/src/core/geom/vec3/v3_quat3_apply_inverse.js +41 -0
  59. package/src/core/geom/vec3/v3_triple_cross_product.d.ts +32 -0
  60. package/src/core/geom/vec3/v3_triple_cross_product.d.ts.map +1 -0
  61. package/src/core/geom/vec3/v3_triple_cross_product.js +45 -0
  62. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +16 -3
  63. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
  64. package/src/engine/control/first-person/FirstPersonPlayerController.js +211 -211
  65. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +72 -8
  66. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
  67. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +37 -5
  68. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts +101 -3
  69. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts.map +1 -1
  70. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.js +1789 -1416
  71. package/src/engine/control/first-person/TODO.md +173 -127
  72. package/src/engine/control/first-person/abilities/Slide.d.ts.map +1 -1
  73. package/src/engine/control/first-person/abilities/Slide.js +9 -1
  74. package/src/engine/control/first-person/prototype_first_person_controller.js +88 -2
  75. package/src/engine/control/first-person/test/buildTestPlayer.d.ts.map +1 -1
  76. package/src/engine/control/first-person/test/buildTestPlayer.js +9 -1
  77. package/src/engine/graphics/geometry/CapsuleGeometry.d.ts +42 -0
  78. package/src/engine/graphics/geometry/CapsuleGeometry.d.ts.map +1 -0
  79. package/src/engine/graphics/geometry/CapsuleGeometry.js +171 -0
  80. package/src/engine/physics/BULLET_REVIEW.md +945 -0
  81. package/src/engine/physics/CANNON_REVIEW.md +1300 -0
  82. package/src/engine/physics/JOLT_REVIEW.md +913 -0
  83. package/src/engine/physics/PLAN.md +461 -236
  84. package/src/engine/physics/RAPIER_REVIEW.md +934 -0
  85. package/src/engine/physics/REVIEW_001_ACTION_PLAN.md +642 -0
  86. package/src/engine/physics/broadphase/compute_fat_world_aabb.js +2 -2
  87. package/src/engine/physics/contact/ManifoldStore.d.ts +83 -10
  88. package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
  89. package/src/engine/physics/contact/ManifoldStore.js +608 -499
  90. package/src/engine/physics/ecs/ColliderObserverSystem.d.ts +2 -2
  91. package/src/engine/physics/ecs/ColliderObserverSystem.d.ts.map +1 -1
  92. package/src/engine/physics/ecs/PhysicsSystem.d.ts +128 -20
  93. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  94. package/src/engine/physics/ecs/PhysicsSystem.js +1301 -1159
  95. package/src/engine/physics/fluid/FluidSimulator.d.ts.map +1 -1
  96. package/src/engine/physics/fluid/FluidSimulator.js +2 -1
  97. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts +28 -6
  98. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.d.ts.map +1 -1
  99. package/src/engine/physics/fluid/solver/v3_grid_subtract_pressure_gradient.js +39 -17
  100. package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts +6 -6
  101. package/src/engine/physics/gjk/expanding_polytope_algorithm.d.ts.map +1 -1
  102. package/src/engine/physics/gjk/expanding_polytope_algorithm.js +68 -22
  103. package/src/engine/physics/gjk/gjk.d.ts +28 -2
  104. package/src/engine/physics/gjk/gjk.d.ts.map +1 -1
  105. package/src/engine/physics/gjk/gjk.js +421 -378
  106. package/src/engine/physics/gjk/minkowski_support.d.ts +37 -0
  107. package/src/engine/physics/gjk/minkowski_support.d.ts.map +1 -0
  108. package/src/engine/physics/gjk/minkowski_support.js +75 -0
  109. package/src/engine/physics/gjk/mpr.d.ts +56 -0
  110. package/src/engine/physics/gjk/mpr.d.ts.map +1 -0
  111. package/src/engine/physics/gjk/mpr.js +344 -0
  112. package/src/engine/physics/inertia/world_inverse_inertia.d.ts +20 -5
  113. package/src/engine/physics/inertia/world_inverse_inertia.d.ts.map +1 -1
  114. package/src/engine/physics/inertia/world_inverse_inertia.js +36 -38
  115. package/src/engine/physics/integration/integrate_position.d.ts +25 -7
  116. package/src/engine/physics/integration/integrate_position.d.ts.map +1 -1
  117. package/src/engine/physics/integration/integrate_position.js +43 -12
  118. package/src/engine/physics/integration/integrate_velocity.d.ts +30 -0
  119. package/src/engine/physics/integration/integrate_velocity.d.ts.map +1 -1
  120. package/src/engine/physics/integration/integrate_velocity.js +82 -1
  121. package/src/engine/physics/narrowphase/PosedShape.d.ts +0 -8
  122. package/src/engine/physics/narrowphase/PosedShape.d.ts.map +1 -1
  123. package/src/engine/physics/narrowphase/PosedShape.js +28 -30
  124. package/src/engine/physics/narrowphase/box_box_manifold.d.ts.map +1 -1
  125. package/src/engine/physics/narrowphase/box_box_manifold.js +113 -17
  126. package/src/engine/physics/narrowphase/box_triangle_contact.d.ts +30 -0
  127. package/src/engine/physics/narrowphase/box_triangle_contact.d.ts.map +1 -0
  128. package/src/engine/physics/narrowphase/box_triangle_contact.js +811 -0
  129. package/src/engine/physics/narrowphase/capsule_contacts.d.ts.map +1 -1
  130. package/src/engine/physics/narrowphase/capsule_contacts.js +10 -56
  131. package/src/engine/physics/narrowphase/capsule_triangle_contact.d.ts +71 -0
  132. package/src/engine/physics/narrowphase/capsule_triangle_contact.d.ts.map +1 -0
  133. package/src/engine/physics/narrowphase/capsule_triangle_contact.js +375 -0
  134. package/src/engine/physics/narrowphase/compute_penetration.d.ts +91 -0
  135. package/src/engine/physics/narrowphase/compute_penetration.d.ts.map +1 -0
  136. package/src/engine/physics/narrowphase/compute_penetration.js +396 -0
  137. package/src/engine/physics/narrowphase/decomposition/aabb_world_to_local.d.ts +35 -0
  138. package/src/engine/physics/narrowphase/decomposition/aabb_world_to_local.d.ts.map +1 -0
  139. package/src/engine/physics/narrowphase/decomposition/aabb_world_to_local.js +80 -0
  140. package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.d.ts +31 -0
  141. package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.d.ts.map +1 -0
  142. package/src/engine/physics/narrowphase/decomposition/decompose_to_triangles.js +55 -0
  143. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts +42 -0
  144. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts.map +1 -0
  145. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.js +204 -0
  146. package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.d.ts +42 -0
  147. package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.d.ts.map +1 -0
  148. package/src/engine/physics/narrowphase/decomposition/mesh_enumerate_triangles.js +94 -0
  149. package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.d.ts +37 -0
  150. package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.d.ts.map +1 -0
  151. package/src/engine/physics/narrowphase/decomposition/triangle_buffer_layout.js +37 -0
  152. package/src/engine/physics/narrowphase/narrowphase_step.d.ts +8 -2
  153. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  154. package/src/engine/physics/narrowphase/narrowphase_step.js +1422 -382
  155. package/src/engine/physics/narrowphase/sphere_box_contact.d.ts.map +1 -1
  156. package/src/engine/physics/narrowphase/sphere_box_contact.js +16 -23
  157. package/src/engine/physics/narrowphase/sphere_triangle_contact.d.ts +48 -0
  158. package/src/engine/physics/narrowphase/sphere_triangle_contact.d.ts.map +1 -0
  159. package/src/engine/physics/narrowphase/sphere_triangle_contact.js +143 -0
  160. package/src/engine/physics/queries/overlap_shape.d.ts +51 -0
  161. package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -0
  162. package/src/engine/physics/queries/overlap_shape.js +183 -0
  163. package/src/engine/physics/queries/shape_cast.d.ts +56 -0
  164. package/src/engine/physics/queries/shape_cast.d.ts.map +1 -0
  165. package/src/engine/physics/queries/shape_cast.js +387 -0
  166. package/src/engine/physics/solver/solve_contacts.d.ts +116 -30
  167. package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
  168. package/src/engine/physics/solver/solve_contacts.js +641 -223
  169. package/src/engine/physics/broadphase/aabb_transform_oriented.d.ts.map +0 -1
  170. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_unmasked_legacy.d.ts +0 -20
  171. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_unmasked_legacy.d.ts.map +0 -1
  172. package/src/engine/physics/fluid/solver/v3_grid_solve_pressure_unmasked_legacy.js +0 -83
@@ -0,0 +1,451 @@
1
+ import { assert } from "../../../assert.js";
2
+ import { clamp } from "../../../math/clamp.js";
3
+ import { v3_length } from "../../vec3/v3_length.js";
4
+ import { Vector3 } from "../../Vector3.js";
5
+ import { AbstractShape3D } from "./AbstractShape3D.js";
6
+
7
+ /**
8
+ * Heightmap shape, intended primarily for terrain.
9
+ *
10
+ * The shape is a closed solid bounded below by the plane perpendicular to
11
+ * {@link orientation} (the "floor") and above by a height-field surface
12
+ * defined by a {@link Sampler2D}. Heights are sampled with Catmull-Rom
13
+ * filtering ({@link Sampler2D#sampleChannelCatmullRomUV}), matching what
14
+ * the terrain system uses for geometry construction.
15
+ *
16
+ * Local frame layout:
17
+ * - The orientation vector defines the local "up" axis (unit).
18
+ * - An orthonormal basis (u, v, n=orientation) is built from it.
19
+ * - Footprint extends along the basis-u axis over [-size.x/2, +size.x/2]
20
+ * and along the basis-v axis over [-size.z/2, +size.z/2].
21
+ * - The surface height (along orientation) at heightmap-UV (u01, v01) is
22
+ * `sampler.sampleChannelCatmullRomUV(u01, v01, 0)`.
23
+ * - The solid volume occupies `h ∈ [0, sampledHeight(u01, v01)]` along the
24
+ * orientation axis, with `0` being the floor at body-local origin.
25
+ * - `size.y` is the maximum height value of the heightfield (used for the
26
+ * bounding box). Sampler values are NOT clamped to it.
27
+ *
28
+ * NON-CONVEX. {@link support} throws — GJK/EPA cannot be run against a
29
+ * heightmap directly. The physics narrowphase must dispatch a dedicated
30
+ * grid-traversal path when one of the colliders is a heightmap.
31
+ *
32
+ * @author Alex Goldring
33
+ * @copyright Company Named Limited (c) 2026
34
+ */
35
+ export class HeightMapShape3D extends AbstractShape3D {
36
+ constructor() {
37
+ super();
38
+
39
+ /**
40
+ * Unit vector defining the local "up" axis (the direction the
41
+ * heightmap's surface faces). Default is +Y.
42
+ * @readonly
43
+ * @type {Vector3}
44
+ */
45
+ this.orientation = new Vector3(0, 1, 0);
46
+
47
+ /**
48
+ * Bounding-box extents in the heightmap-local (u, height, v) frame.
49
+ * size.x — footprint extent along basis-u (perpendicular to orientation)
50
+ * size.y — maximum heightmap height (extent along orientation)
51
+ * size.z — footprint extent along basis-v (perpendicular to orientation)
52
+ * @readonly
53
+ * @type {Vector3}
54
+ */
55
+ this.size = new Vector3(1, 1, 1);
56
+
57
+ /**
58
+ * Sampler holding height values. Float32 backing recommended so the
59
+ * Terrain system's height texture plugs in directly. Single-channel
60
+ * sampler is the common case; only channel 0 is read.
61
+ * @type {Sampler2D | null}
62
+ */
63
+ this.sampler = null;
64
+
65
+ /**
66
+ * Cached orthonormal basis [u_x,u_y,u_z, v_x,v_y,v_z, n_x,n_y,n_z]
67
+ * built from {@link orientation}. Updated lazily by {@link _ensure_basis}.
68
+ * @private
69
+ * @type {Float64Array}
70
+ */
71
+ this._basis = new Float64Array(9);
72
+
73
+ // last-seen orientation components, used to detect dirty basis
74
+ this._basis_orientation_x = NaN;
75
+ this._basis_orientation_y = NaN;
76
+ this._basis_orientation_z = NaN;
77
+ }
78
+
79
+ /**
80
+ * Convenience constructor.
81
+ * @param {Sampler2D} sampler
82
+ * @param {number} size_x footprint extent along basis-u
83
+ * @param {number} size_y maximum heightmap height (along orientation)
84
+ * @param {number} size_z footprint extent along basis-v
85
+ * @param {Vector3} [orientation] defaults to +Y
86
+ * @returns {HeightMapShape3D}
87
+ */
88
+ static from(sampler, size_x, size_y, size_z, orientation) {
89
+ assert.isNumber(size_x, "size_x");
90
+ assert.isNumber(size_y, "size_y");
91
+ assert.isNumber(size_z, "size_z");
92
+ assert.greaterThanOrEqual(size_x, 0, "size_x");
93
+ assert.greaterThanOrEqual(size_y, 0, "size_y");
94
+ assert.greaterThanOrEqual(size_z, 0, "size_z");
95
+
96
+ const r = new HeightMapShape3D();
97
+ r.sampler = sampler;
98
+ r.size.set(size_x, size_y, size_z);
99
+
100
+ if (orientation !== undefined) {
101
+ r.orientation.set(orientation.x, orientation.y, orientation.z);
102
+ }
103
+
104
+ return r;
105
+ }
106
+
107
+ /**
108
+ * Recompute the orthonormal basis (u, v, n) if {@link orientation} changed.
109
+ *
110
+ * Construction chosen so that the default orientation +Y produces the
111
+ * intuitive mapping `u = +X, v = +Z, n = +Y` — i.e. size.x runs along
112
+ * body X, size.z along body Z. We project body +X onto the plane
113
+ * perpendicular to n (the orientation), with a fall-back to projecting
114
+ * body +Z when n is too close to colinear with +X.
115
+ * @private
116
+ */
117
+ _ensure_basis() {
118
+ const nx = this.orientation[0];
119
+ const ny = this.orientation[1];
120
+ const nz = this.orientation[2];
121
+
122
+ if (
123
+ nx === this._basis_orientation_x
124
+ && ny === this._basis_orientation_y
125
+ && nz === this._basis_orientation_z
126
+ ) {
127
+ return;
128
+ }
129
+
130
+ // tangent u = normalize(project body +X onto plane perpendicular to n)
131
+ // fall back to body +Z if n is too colinear with +X
132
+ let u_x, u_y, u_z;
133
+
134
+ if (Math.abs(nx) < 0.9) {
135
+ // u = (+X) - (n . +X) * n = (1 - nx*nx, -nx*ny, -nx*nz)
136
+ u_x = 1 - nx * nx;
137
+ u_y = -nx * ny;
138
+ u_z = -nx * nz;
139
+ } else {
140
+ // u = (+Z) - (n . +Z) * n = (-nz*nx, -nz*ny, 1 - nz*nz)
141
+ u_x = -nz * nx;
142
+ u_y = -nz * ny;
143
+ u_z = 1 - nz * nz;
144
+ }
145
+
146
+ const u_inv = 1 / v3_length(u_x, u_y, u_z);
147
+ u_x *= u_inv;
148
+ u_y *= u_inv;
149
+ u_z *= u_inv;
150
+
151
+ // v = u × n (for n=+Y, u=+X this gives v=+Z, the intuitive choice)
152
+ const v_x = u_y * nz - u_z * ny;
153
+ const v_y = u_z * nx - u_x * nz;
154
+ const v_z = u_x * ny - u_y * nx;
155
+
156
+ const b = this._basis;
157
+ b[0] = u_x; b[1] = u_y; b[2] = u_z;
158
+ b[3] = v_x; b[4] = v_y; b[5] = v_z;
159
+ b[6] = nx; b[7] = ny; b[8] = nz;
160
+
161
+ this._basis_orientation_x = nx;
162
+ this._basis_orientation_y = ny;
163
+ this._basis_orientation_z = nz;
164
+ }
165
+
166
+ /**
167
+ * Sample the surface height at heightmap UV coordinates.
168
+ * Uses Catmull-Rom filtering to match the terrain system's geometry construction.
169
+ * @param {number} u01 horizontal UV, in [0, 1]
170
+ * @param {number} v01 vertical UV, in [0, 1]
171
+ * @returns {number} height along orientation axis
172
+ */
173
+ sample_height_at_uv(u01, v01) {
174
+ return this.sampler.sampleChannelCatmullRomUV(u01, v01, 0);
175
+ }
176
+
177
+ /**
178
+ * Project a body-local position to the surface and sample the height there.
179
+ * The returned height is in the orientation-axis direction.
180
+ * Positions outside the footprint sample the clamped UV (sampler clamps).
181
+ * @param {number} px body-local x
182
+ * @param {number} py body-local y
183
+ * @param {number} pz body-local z
184
+ * @returns {number}
185
+ */
186
+ sample_height_at_position(px, py, pz) {
187
+ this._ensure_basis();
188
+
189
+ const b = this._basis;
190
+
191
+ // project onto basis u and basis v
192
+ const u_coord = b[0] * px + b[1] * py + b[2] * pz;
193
+ const v_coord = b[3] * px + b[4] * py + b[5] * pz;
194
+
195
+ const u01 = u_coord / this.size[0] + 0.5;
196
+ const v01 = v_coord / this.size[2] + 0.5;
197
+
198
+ return this.sample_height_at_uv(u01, v01);
199
+ }
200
+
201
+ compute_bounding_box(result) {
202
+ this._ensure_basis();
203
+
204
+ const b = this._basis;
205
+
206
+ const sx = this.size[0];
207
+ const sy = this.size[1];
208
+ const sz = this.size[2];
209
+
210
+ const half_u = sx * 0.5;
211
+ const half_v = sz * 0.5;
212
+
213
+ // For each body axis k (0=x, 1=y, 2=z):
214
+ // body[k] = b[0+k]*u + b[3+k]*v + b[6+k]*h
215
+ //
216
+ // The (u,v) footprint contribution is symmetric (u ∈ [-half_u, +half_u], v ∈ [-half_v, +half_v])
217
+ // The height contribution is asymmetric (h ∈ [0, sy])
218
+ for (let k = 0; k < 3; k++) {
219
+ const cu = b[k];
220
+ const cv = b[3 + k];
221
+ const ch = b[6 + k];
222
+
223
+ const uv_extent = Math.abs(cu) * half_u + Math.abs(cv) * half_v;
224
+
225
+ const h_lo = ch < 0 ? ch * sy : 0;
226
+ const h_hi = ch > 0 ? ch * sy : 0;
227
+
228
+ result[k] = -uv_extent + h_lo;
229
+ result[k + 3] = uv_extent + h_hi;
230
+ }
231
+ }
232
+
233
+ contains_point(point) {
234
+ const px = point[0];
235
+ const py = point[1];
236
+ const pz = point[2];
237
+
238
+ this._ensure_basis();
239
+
240
+ const b = this._basis;
241
+
242
+ // project into heightmap-local frame (u, v, h)
243
+ const u_coord = b[0] * px + b[1] * py + b[2] * pz;
244
+ const v_coord = b[3] * px + b[4] * py + b[5] * pz;
245
+ const h_coord = b[6] * px + b[7] * py + b[8] * pz;
246
+
247
+ const half_u = this.size[0] * 0.5;
248
+ const half_v = this.size[2] * 0.5;
249
+
250
+ if (u_coord <= -half_u || u_coord >= half_u) return false;
251
+ if (v_coord <= -half_v || v_coord >= half_v) return false;
252
+ if (h_coord <= 0 || h_coord >= this.size[1]) return false;
253
+
254
+ const u01 = u_coord / this.size[0] + 0.5;
255
+ const v01 = v_coord / this.size[2] + 0.5;
256
+
257
+ const surface_h = this.sample_height_at_uv(u01, v01);
258
+
259
+ return h_coord < surface_h;
260
+ }
261
+
262
+ /**
263
+ * Approximate signed distance: difference between the point's height
264
+ * (along orientation) and the surface height sampled at the point's
265
+ * footprint UV. POSITIVE = above surface, NEGATIVE = below.
266
+ *
267
+ * Locally correct when the surface is flat; biased when the surface
268
+ * has significant slope. Sufficient for cling/raycast queries that
269
+ * walk down to the surface.
270
+ */
271
+ signed_distance_at_point(point) {
272
+ const px = point[0];
273
+ const py = point[1];
274
+ const pz = point[2];
275
+
276
+ this._ensure_basis();
277
+
278
+ const b = this._basis;
279
+
280
+ const u_coord = b[0] * px + b[1] * py + b[2] * pz;
281
+ const v_coord = b[3] * px + b[4] * py + b[5] * pz;
282
+ const h_coord = b[6] * px + b[7] * py + b[8] * pz;
283
+
284
+ const u01 = u_coord / this.size[0] + 0.5;
285
+ const v01 = v_coord / this.size[2] + 0.5;
286
+
287
+ const surface_h = this.sample_height_at_uv(u01, v01);
288
+
289
+ return h_coord - surface_h;
290
+ }
291
+
292
+ /**
293
+ * Project a reference point onto the surface along the orientation axis.
294
+ * The footprint UV is clamped, so points outside the footprint produce
295
+ * the nearest edge-of-footprint surface sample (approximate).
296
+ */
297
+ nearest_point_on_surface(result, reference) {
298
+ const rx = reference[0];
299
+ const ry = reference[1];
300
+ const rz = reference[2];
301
+
302
+ this._ensure_basis();
303
+
304
+ const b = this._basis;
305
+
306
+ const u_coord = b[0] * rx + b[1] * ry + b[2] * rz;
307
+ const v_coord = b[3] * rx + b[4] * ry + b[5] * rz;
308
+
309
+ const half_u = this.size[0] * 0.5;
310
+ const half_v = this.size[2] * 0.5;
311
+
312
+ const u_clamped = clamp(u_coord, -half_u, half_u);
313
+ const v_clamped = clamp(v_coord, -half_v, half_v);
314
+
315
+ const u01 = u_clamped / this.size[0] + 0.5;
316
+ const v01 = v_clamped / this.size[2] + 0.5;
317
+
318
+ const surface_h = this.sample_height_at_uv(u01, v01);
319
+
320
+ // compose body-local point: u_axis*u_clamped + v_axis*v_clamped + n_axis*surface_h
321
+ result[0] = b[0] * u_clamped + b[3] * v_clamped + b[6] * surface_h;
322
+ result[1] = b[1] * u_clamped + b[4] * v_clamped + b[7] * surface_h;
323
+ result[2] = b[2] * u_clamped + b[5] * v_clamped + b[8] * surface_h;
324
+ }
325
+
326
+ /**
327
+ * Heightmaps are non-convex; GJK/EPA cannot work against them directly.
328
+ * The physics narrowphase must dispatch a grid-traversal path that
329
+ * decomposes the heightmap into per-cell triangle pairs and tests each
330
+ * against the other shape (analogous to Bullet's btHeightfieldTerrainShape
331
+ * × btConcaveShape interface).
332
+ *
333
+ * This throws rather than returning a degenerate result so the call
334
+ * site is forced to handle heightmaps explicitly.
335
+ */
336
+ support(result, result_offset, direction_x, direction_y, direction_z) {
337
+ throw new Error("HeightMapShape3D.support: heightmaps are non-convex; the narrowphase must dispatch grid-traversal instead.");
338
+ }
339
+
340
+ sample_random_point_in_volume(result, result_offset, random) {
341
+ const u01 = random();
342
+ const v01 = random();
343
+
344
+ const u_coord = (u01 - 0.5) * this.size[0];
345
+ const v_coord = (v01 - 0.5) * this.size[2];
346
+
347
+ const surface_h = this.sample_height_at_uv(u01, v01);
348
+ const h_coord = random() * surface_h;
349
+
350
+ this._ensure_basis();
351
+
352
+ const b = this._basis;
353
+
354
+ result[result_offset] = b[0] * u_coord + b[3] * v_coord + b[6] * h_coord;
355
+ result[result_offset + 1] = b[1] * u_coord + b[4] * v_coord + b[7] * h_coord;
356
+ result[result_offset + 2] = b[2] * u_coord + b[5] * v_coord + b[8] * h_coord;
357
+ }
358
+
359
+ /**
360
+ * Sum of sampler heights × per-cell footprint area. This is the
361
+ * piecewise-constant approximation of the integral ∫h(u,v) dA over
362
+ * the footprint — exact when h is constant per cell, biased when
363
+ * h is smooth.
364
+ */
365
+ get volume() {
366
+ const sampler = this.sampler;
367
+
368
+ if (sampler === null) {
369
+ return 0;
370
+ }
371
+
372
+ const w = sampler.width;
373
+ const h = sampler.height;
374
+
375
+ if (w === 0 || h === 0) {
376
+ return 0;
377
+ }
378
+
379
+ const cell_area = (this.size[0] / w) * (this.size[2] / h);
380
+
381
+ let total = 0;
382
+
383
+ for (let y = 0; y < h; y++) {
384
+ for (let x = 0; x < w; x++) {
385
+ total += sampler.readChannel(x, y, 0);
386
+ }
387
+ }
388
+
389
+ return total * cell_area;
390
+ }
391
+
392
+ /**
393
+ * Returns just the footprint area. A true heightmap surface area
394
+ * requires integrating sqrt(1 + (∂h/∂u)² + (∂h/∂v)²) over the grid;
395
+ * the footprint area is a lower bound and is sufficient for the
396
+ * physics inertia-tensor seam (heightmaps are static anyway).
397
+ */
398
+ get surface_area() {
399
+ return this.size[0] * this.size[2];
400
+ }
401
+
402
+ /**
403
+ * @param {HeightMapShape3D} other
404
+ * @returns {boolean}
405
+ */
406
+ equals(other) {
407
+ if (!super.equals(other)) return false;
408
+
409
+ if (!this.orientation.equals(other.orientation)) return false;
410
+ if (!this.size.equals(other.size)) return false;
411
+
412
+ // strict identity is enough for sampler equality in the common case;
413
+ // fall through to value equality so two shapes built from independent
414
+ // but identical sampler instances still compare equal
415
+ if (this.sampler === other.sampler) return true;
416
+
417
+ if (this.sampler === null || other.sampler === null) return false;
418
+
419
+ return this.sampler.equals(other.sampler);
420
+ }
421
+
422
+ hash() {
423
+ const a = this.orientation.hash();
424
+ const b = this.size.hash();
425
+ const c = this.sampler !== null ? this.sampler.hash() : 0;
426
+
427
+ let h = (a * 31 + b) | 0;
428
+ h = (h * 31 + c) | 0;
429
+
430
+ return h;
431
+ }
432
+ }
433
+
434
+ /**
435
+ * Fast type-check marker, matching the pattern on every other concrete
436
+ * AbstractShape3D subclass. The physics narrowphase reads this to dispatch
437
+ * the heightmap-vs-X grid-traversal path.
438
+ * @readonly
439
+ * @type {boolean}
440
+ */
441
+ HeightMapShape3D.prototype.isHeightMapShape3D = true;
442
+
443
+ /**
444
+ * Heightmaps are non-convex: the solid volume bounded by an arbitrary
445
+ * height-field has valleys and overhangs that break GJK's convex-Minkowski
446
+ * precondition. The narrowphase must use grid traversal + per-triangle
447
+ * GJK instead of feeding this shape's {@link support} into pair tests.
448
+ * @readonly
449
+ * @type {boolean}
450
+ */
451
+ HeightMapShape3D.prototype.is_convex = false;
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Arbitrary triangle-mesh collider.
3
+ *
4
+ * The shape carries two synchronized representations:
5
+ * - flat (`positions`, `indices`) surface — used by GJK's support
6
+ * function and the per-triangle distance queries;
7
+ * - a {@link TetrahedralMesh} of the *interior* (built from the surface
8
+ * by `compute_tetrahedral_mesh_from_surface`) plus a {@link BVH}
9
+ * keyed by tet AABB — used by `contains_point` and any future
10
+ * volume-aware query.
11
+ *
12
+ * The tet decomposition gives us convex primitives. Two consequences:
13
+ * 1. `contains_point` is a point-in-AABB BVH query followed by a
14
+ * per-tet `orient3d` test. Critically this works for **disconnected
15
+ * meshes** (two cubes welded into one buffer; a torus and a sphere
16
+ * handed to the same shape). A walking query started from one
17
+ * component can't reach the others — that was the previous
18
+ * implementation's silent bug.
19
+ * 2. Per-point queries are O(log N_tets) on average — the BVH's
20
+ * traversal short-circuits cleanly when the query point is far
21
+ * from the mesh.
22
+ *
23
+ * Construct via {@link shape_mesh_from_geometry} rather than `new
24
+ * MeshShape3D()` directly — the factory compacts the tet mesh, builds
25
+ * the BVH, and caches the bbox / volume / surface area.
26
+ *
27
+ * Narrowphase routing:
28
+ * - GJK + EPA for the general case, using {@link support}.
29
+ * - The support function currently returns the deepest tet-mesh
30
+ * vertex, which gives GJK the *convex hull* of the mesh — not the
31
+ * true non-convex surface. For convex authored geometry that's
32
+ * fine; for highly concave shapes (a torus's hole, an L-bracket,
33
+ * two disconnected components) the convex-hull approximation
34
+ * overstates the collision volume. The accurate fix is a per-tet
35
+ * GJK loop in the narrowphase, which would consume `tet_mesh` and
36
+ * `tet_positions` directly — see PLAN.md's mesh-vs-convex
37
+ * closed-form item.
38
+ *
39
+ * @author Alex Goldring
40
+ * @copyright Company Named Limited (c) 2026
41
+ */
42
+ export class MeshShape3D extends AbstractShape3D {
43
+ /**
44
+ * Surface vertex positions, flat `(x, y, z)` per vertex. Authored
45
+ * by the factory; mutate at your own risk (the cached bbox /
46
+ * volume / surface area / BVH will go stale).
47
+ * @type {Float32Array}
48
+ */
49
+ positions: Float32Array;
50
+ /**
51
+ * Surface triangle indices, three uint32 per face referring to
52
+ * {@link positions}. Same mutate-at-your-own-risk caveat.
53
+ * @type {Uint32Array}
54
+ */
55
+ indices: Uint32Array;
56
+ /**
57
+ * Tetrahedral decomposition of the interior. The factory
58
+ * compacts the mesh before storing (low-index slots that the
59
+ * carve pass freed get refilled), so iteration via
60
+ * `tet_id in [0, tet_mesh.count)` is safe — no need for
61
+ * `forEach` or `exists` filtering on the hot path.
62
+ * @type {TetrahedralMesh}
63
+ */
64
+ tet_mesh: TetrahedralMesh;
65
+ /**
66
+ * Flat positions array for {@link tet_mesh}. Includes the
67
+ * surface vertices and any hole-closing centroids added during
68
+ * tetrahedralisation. For closed airtight meshes this equals
69
+ * {@link positions} element-wise; for meshes with boundary holes
70
+ * (rare in authored content, common in mis-exported assets) the
71
+ * trailing slots carry the closure centroids.
72
+ * @type {Float32Array}
73
+ */
74
+ tet_positions: Float32Array;
75
+ /**
76
+ * BVH over the tet mesh, keyed by tet index. Built by the
77
+ * factory after compacting the tet mesh, so leaf user_data
78
+ * values are valid tet indices that can be passed straight to
79
+ * `tet_mesh.getVertexIndex`.
80
+ * @type {BVH}
81
+ */
82
+ tet_bvh: BVH;
83
+ /**
84
+ * Cached axis-aligned bounding box, layout `[minX, minY, minZ,
85
+ * maxX, maxY, maxZ]`.
86
+ * @private
87
+ * @type {Float64Array}
88
+ */
89
+ private __bbox;
90
+ /**
91
+ * Cached total volume in the shape's local frame, computed from
92
+ * the tetrahedral decomposition at construction.
93
+ * @private
94
+ * @type {number}
95
+ */
96
+ private __volume;
97
+ /**
98
+ * Cached total surface area, sum of triangle areas.
99
+ * @private
100
+ * @type {number}
101
+ */
102
+ private __surface_area;
103
+ compute_bounding_box(result: any): void;
104
+ /**
105
+ * Refresh the cached bounding box, volume, and surface area from the
106
+ * current `positions` / `indices` / `tet_mesh`. Called by
107
+ * {@link shape_mesh_from_geometry} at construction; call again after
108
+ * mutating the underlying data.
109
+ */
110
+ recompute_cached(): void;
111
+ /**
112
+ * Support function: returns the deepest tet-mesh vertex along the
113
+ * supplied direction. This gives GJK the **convex hull** of the
114
+ * mesh — fine for convex authored geometry, an overestimate for
115
+ * concave shapes (a torus's hole or two disconnected components
116
+ * read as one filled blob through this function).
117
+ *
118
+ * Iterates `tet_positions` rather than the original surface
119
+ * `positions` so any hole-closing centroid that the
120
+ * tetrahedralisation added shows up. For airtight closed meshes the
121
+ * two arrays match element-wise.
122
+ *
123
+ * Future work: an accurate non-convex `support` would have to be
124
+ * per-tet (each tet is convex; the shape is the union). The
125
+ * narrowphase would then iterate tets via the BVH. See PLAN.md.
126
+ *
127
+ * @param {number[]|Float32Array} result
128
+ * @param {number} result_offset
129
+ * @param {number} direction_x
130
+ * @param {number} direction_y
131
+ * @param {number} direction_z
132
+ */
133
+ support(result: number[] | Float32Array, result_offset: number, direction_x: number, direction_y: number, direction_z: number): void;
134
+ /**
135
+ * Inside test via the tet mesh. Queries the tet BVH for the leaves
136
+ * whose AABB contains the point, then runs an `orient3d`-based
137
+ * point-in-tet test on each candidate. Returns `true` on the first
138
+ * hit.
139
+ *
140
+ * Crucially this works for **disconnected meshes** — a single
141
+ * `MeshShape3D` holding two cubes welded into one buffer, or a
142
+ * torus with a separate sphere, will correctly classify points
143
+ * inside either component. The previous implementation walked tet
144
+ * neighbours starting from a single seed, which trapped the query
145
+ * in the seed's connected component and produced silent false
146
+ * negatives in the other components.
147
+ *
148
+ * @param {number[]|Float32Array} point
149
+ * @returns {boolean}
150
+ */
151
+ contains_point(point: number[] | Float32Array): boolean;
152
+ /**
153
+ * Closest world-space point on the surface to `reference`. Linear
154
+ * scan over triangles using the barycentric closest-point helper.
155
+ * For mesh sizes where this becomes a bottleneck, a triangle BVH
156
+ * would be the next acceleration (sibling to {@link tet_bvh} but
157
+ * keyed on surface triangles).
158
+ *
159
+ * @param {number[]|Float32Array} result
160
+ * @param {number[]|Float32Array} reference
161
+ */
162
+ nearest_point_on_surface(result: number[] | Float32Array, reference: number[] | Float32Array): void;
163
+ /**
164
+ * Signed distance to the surface: positive outside the mesh,
165
+ * negative inside. Linear scan over triangles for nearest unsigned
166
+ * distance, then the BVH-backed {@link contains_point} flips the
167
+ * sign for interior points.
168
+ *
169
+ * @param {number[]|Float32Array} point
170
+ * @returns {number}
171
+ */
172
+ signed_distance_at_point(point: number[] | Float32Array): number;
173
+ /**
174
+ * Reject-sample inside the bounding box until a point lands in the
175
+ * mesh volume. For convex or near-convex shapes this terminates in
176
+ * 1–3 attempts; for thin or pinched shapes the worst-case cost can
177
+ * be high. Falls back to the bbox centre after 256 misses.
178
+ *
179
+ * @param {number[]|Float32Array} result
180
+ * @param {number} result_offset
181
+ * @param {function():number} random
182
+ */
183
+ sample_random_point_in_volume(result: number[] | Float32Array, result_offset: number, random: () => number): void;
184
+ /**
185
+ * Numerical-gradient SDF gradient.
186
+ *
187
+ * @param {number[]|Float32Array} result
188
+ * @param {number[]|Float32Array} point
189
+ * @returns {number}
190
+ */
191
+ signed_distance_gradient_at_point(result: number[] | Float32Array, point: number[] | Float32Array): number;
192
+ /**
193
+ * Topology equality: same vertex count, same triangle count, and
194
+ * positions / indices match element-by-element.
195
+ *
196
+ * @param {MeshShape3D} other
197
+ * @returns {boolean}
198
+ */
199
+ equals(other: MeshShape3D): boolean;
200
+ /**
201
+ * Fast type-check marker.
202
+ * @readonly
203
+ * @type {boolean}
204
+ */
205
+ readonly isMeshShape3D: boolean;
206
+ }
207
+ import { AbstractShape3D } from "./AbstractShape3D.js";
208
+ import { TetrahedralMesh } from "../tetrahedra/TetrahedralMesh.js";
209
+ import { BVH } from "../../../bvh2/bvh3/BVH.js";
210
+ //# sourceMappingURL=MeshShape3D.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MeshShape3D.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/3d/shape/MeshShape3D.js"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH;IAKQ;;;;;OAKG;IACH,WAFU,YAAY,CAEc;IAEpC;;;;OAIG;IACH,SAFU,WAAW,CAEY;IAEjC;;;;;;;OAOG;IACH,UAFU,eAAe,CAEY;IAErC;;;;;;;;OAQG;IACH,eAFU,YAAY,CAEkB;IAExC;;;;;;OAMG;IACH,SAFU,GAAG,CAEW;IAExB;;;;;OAKG;IACH,eAAkD;IAElD;;;;;OAKG;IACH,iBAAiB;IAEjB;;;;OAIG;IACH,uBAAuB;IAW3B,wCAIC;IAED;;;;;OAKG;IACH,yBAmDC;IAED;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,gBANW,MAAM,EAAE,GAAC,YAAY,iBACrB,MAAM,eACN,MAAM,eACN,MAAM,eACN,MAAM,QA0BhB;IAED;;;;;;;;;;;;;;;;OAgBG;IACH,sBAHW,MAAM,EAAE,GAAC,YAAY,GACnB,OAAO,CAkCnB;IAED;;;;;;;;;OASG;IACH,iCAHW,MAAM,EAAE,GAAC,YAAY,aACrB,MAAM,EAAE,GAAC,YAAY,QA4C/B;IAED;;;;;;;;OAQG;IACH,gCAHW,MAAM,EAAE,GAAC,YAAY,GACnB,MAAM,CAsClB;IAED;;;;;;;;;OASG;IACH,sCAJW,MAAM,EAAE,GAAC,YAAY,iBACrB,MAAM,gBACK,MAAM,QAuB3B;IAED;;;;;;OAMG;IACH,0CAJW,MAAM,EAAE,GAAC,YAAY,SACrB,MAAM,EAAE,GAAC,YAAY,GACnB,MAAM,CAiBlB;IAED;;;;;;OAMG;IACH,cAHW,WAAW,GACT,OAAO,CAanB;IAkBL;;;;OAIG;IACH,wBAFU,OAAO,CAEkB;CAPlC;gCAje+B,sBAAsB;gCADtB,kCAAkC;oBAJ9C,2BAA2B"}