@woosh/meep-engine 2.143.0 → 2.145.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 (56) hide show
  1. package/package.json +1 -1
  2. package/src/core/bvh2/bvh3/BVH.d.ts.map +1 -1
  3. package/src/core/bvh2/bvh3/BVH.js +158 -4
  4. package/src/core/geom/3d/shape/CylinderShape3D.d.ts +56 -0
  5. package/src/core/geom/3d/shape/CylinderShape3D.d.ts.map +1 -0
  6. package/src/core/geom/3d/shape/CylinderShape3D.js +223 -0
  7. package/src/core/geom/3d/shape/PointShape3D.d.ts +1 -0
  8. package/src/core/geom/3d/shape/PointShape3D.d.ts.map +1 -1
  9. package/src/core/geom/3d/shape/PointShape3D.js +11 -0
  10. package/src/core/geom/3d/shape/SphereShape3D.d.ts +1 -0
  11. package/src/core/geom/3d/shape/SphereShape3D.d.ts.map +1 -1
  12. package/src/core/geom/3d/shape/SphereShape3D.js +4 -0
  13. package/src/core/geom/3d/shape/json/shape_to_type.d.ts.map +1 -1
  14. package/src/core/geom/3d/shape/json/shape_to_type.js +3 -0
  15. package/src/core/geom/3d/shape/json/type_adapters.d.ts +15 -0
  16. package/src/core/geom/3d/shape/json/type_adapters.d.ts.map +1 -1
  17. package/src/core/geom/3d/shape/json/type_adapters.js +16 -0
  18. package/src/engine/control/first-person/DESIGN_COLLISION.md +314 -217
  19. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts +104 -58
  20. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts.map +1 -1
  21. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.js +1828 -1789
  22. package/src/engine/control/first-person/TODO.md +17 -32
  23. package/src/engine/control/first-person/abilities/WallRun.d.ts.map +1 -1
  24. package/src/engine/control/first-person/abilities/WallRun.js +18 -35
  25. package/src/engine/control/first-person/collision/KinematicMover.d.ts +206 -0
  26. package/src/engine/control/first-person/collision/KinematicMover.d.ts.map +1 -0
  27. package/src/engine/control/first-person/collision/KinematicMover.js +592 -0
  28. package/src/engine/control/first-person/prototype_first_person_controller.js +65 -0
  29. package/src/engine/graphics/render/buffer/simple-fx/ao/SAOShader.js +18 -9
  30. package/src/engine/physics/PLAN.md +145 -41
  31. package/src/engine/physics/contact/ManifoldStore.d.ts +28 -2
  32. package/src/engine/physics/contact/ManifoldStore.d.ts.map +1 -1
  33. package/src/engine/physics/contact/ManifoldStore.js +37 -3
  34. package/src/engine/physics/contact/combine_material.d.ts +30 -0
  35. package/src/engine/physics/contact/combine_material.d.ts.map +1 -0
  36. package/src/engine/physics/contact/combine_material.js +35 -0
  37. package/src/engine/physics/ecs/Collider.d.ts +15 -0
  38. package/src/engine/physics/ecs/Collider.d.ts.map +1 -1
  39. package/src/engine/physics/ecs/Collider.js +34 -0
  40. package/src/engine/physics/ecs/Joint.d.ts +18 -0
  41. package/src/engine/physics/ecs/Joint.d.ts.map +1 -1
  42. package/src/engine/physics/ecs/Joint.js +70 -0
  43. package/src/engine/physics/ecs/PhysicsSystem.d.ts +9 -4
  44. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  45. package/src/engine/physics/ecs/PhysicsSystem.js +9 -4
  46. package/src/engine/physics/ecs/RigidBody.d.ts +15 -0
  47. package/src/engine/physics/ecs/RigidBody.d.ts.map +1 -1
  48. package/src/engine/physics/ecs/RigidBody.js +46 -0
  49. package/src/engine/physics/narrowphase/compute_penetration.d.ts +41 -41
  50. package/src/engine/physics/narrowphase/compute_penetration.d.ts.map +1 -1
  51. package/src/engine/physics/narrowphase/compute_penetration.js +96 -169
  52. package/src/engine/physics/narrowphase/narrowphase_step.d.ts +52 -0
  53. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  54. package/src/engine/physics/narrowphase/narrowphase_step.js +130 -3
  55. package/src/engine/physics/solver/solve_contacts.d.ts.map +1 -1
  56. package/src/engine/physics/solver/solve_contacts.js +10 -21
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "description": "Pure JavaScript game engine. Fully featured and production ready.",
7
7
  "type": "module",
8
8
  "author": "Alexander Goldring",
9
- "version": "2.143.0",
9
+ "version": "2.145.0",
10
10
  "main": "build/meep.module.js",
11
11
  "module": "build/meep.module.js",
12
12
  "exports": {
@@ -1 +1 @@
1
- {"version":3,"file":"BVH.d.ts","sourceRoot":"","sources":["../../../../../src/core/bvh2/bvh3/BVH.js"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"BVH.d.ts","sourceRoot":"","sources":["../../../../../src/core/bvh2/bvh3/BVH.js"],"names":[],"mappings":"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"}
@@ -800,7 +800,14 @@ export class BVH {
800
800
  const uint32 = this.__data_uint32;
801
801
 
802
802
  while (index !== NULL_NODE) {
803
- index = this.balance(index);
803
+ // SAH-reducing rotation (Box2D v3 style) — keeps query cost near the
804
+ // SAH optimum. {@link balance_height} (pure height-AVL) is kept as
805
+ // the alternative. The tree shape this produces differs from the
806
+ // height-balanced one, which (because the contact solver's order
807
+ // follows broadphase traversal order) shifts Gauss-Seidel
808
+ // convergence on near-aligned stacks — see the BVH balance note in
809
+ // engine/physics/PLAN.md.
810
+ index = this.balance_rotate(index);
804
811
 
805
812
  const node_address = index * ELEMENT_WORD_COUNT;
806
813
 
@@ -875,13 +882,18 @@ export class BVH {
875
882
  }
876
883
 
877
884
  /**
878
- * Perform a left or right rotation if node A is imbalanced.
879
- * Returns the new root index.
885
+ * **Height-balancing** rotation (Box2D v2 `b2DynamicTree::Balance`): if the
886
+ * two subtrees of node A differ in height by more than one, rotate the
887
+ * taller grandchild up. Keeps the tree height ≤ O(log N) but is chosen
888
+ * purely on *height* — it does not consider surface area, so the result is
889
+ * height-balanced yet not SAH-balanced (query cost can stay 1.3–2× above an
890
+ * SAH-optimal tree). See {@link balance_rotate} for the SAH-driven variant.
891
+ * Returns the (possibly new) subtree-root index.
880
892
  * @param {number} iA
881
893
  * @returns {number}
882
894
  * @private
883
895
  */
884
- balance(iA) {
896
+ balance_height(iA) {
885
897
  assert.notEqual(iA, NULL_NODE, 'input is a null node');
886
898
 
887
899
  //b2TreeNode* A = m_nodes + iA;
@@ -1079,6 +1091,148 @@ export class BVH {
1079
1091
  return iA;
1080
1092
  }
1081
1093
 
1094
+ /**
1095
+ * **SAH-reducing** rotation (Kensler 2008 / Box2D v3 `b2RotateNodes`). For
1096
+ * node A with children B, C, consider the four ways to swap a child of A
1097
+ * with a grandchild (a child of the other child); each rotation re-forms
1098
+ * exactly one internal node and changes the tree's total surface-area
1099
+ * (SAH) cost by `area(old child) − area(new child)`. Apply the swap with
1100
+ * the largest positive reduction, or none if no swap helps.
1101
+ *
1102
+ * Unlike {@link balance_height} the subtree root A never moves (so the
1103
+ * return value is always `iA`), and the criterion is *surface area*, not
1104
+ * height — keeping query cost near the SAH optimum. Height is not an
1105
+ * explicit invariant here, but SAH-good trees stay well-shaped for
1106
+ * spatially-distributed data (this is what modern Box2D ships).
1107
+ *
1108
+ * Deterministic: a pure function of the current tree state.
1109
+ *
1110
+ * @param {number} iA
1111
+ * @returns {number} always `iA` (the subtree root is unchanged)
1112
+ * @private
1113
+ */
1114
+ balance_rotate(iA) {
1115
+ assert.notEqual(iA, NULL_NODE, 'input is a null node');
1116
+
1117
+ const uint32 = this.__data_uint32;
1118
+ const a_addr = iA * ELEMENT_WORD_COUNT;
1119
+
1120
+ if (this.node_is_leaf(iA) || uint32[a_addr + COLUMN_HEIGHT] < 2) {
1121
+ return iA;
1122
+ }
1123
+
1124
+ const iB = uint32[a_addr + COLUMN_CHILD_1];
1125
+ const iC = uint32[a_addr + COLUMN_CHILD_2];
1126
+
1127
+ const b_leaf = this.node_is_leaf(iB);
1128
+ const c_leaf = this.node_is_leaf(iC);
1129
+
1130
+ // Largest beneficial SAH reduction found so far. Strictly positive, so a
1131
+ // zero-improvement (or worse) configuration leaves the tree untouched —
1132
+ // no churn, and deterministic.
1133
+ let best_improvement = 0;
1134
+ let best_rotation = 0; // 1..4, 0 = keep as-is
1135
+
1136
+ let iD = NULL_NODE, iE = NULL_NODE, iF = NULL_NODE, iG = NULL_NODE;
1137
+
1138
+ if (!b_leaf) {
1139
+ // B is internal with children D, E. Moving C down into B (and a
1140
+ // grandchild D/E up to A) re-forms B; the only changed area is B's.
1141
+ const area_b = this.node_get_surface_area(iB);
1142
+ iD = uint32[iB * ELEMENT_WORD_COUNT + COLUMN_CHILD_1];
1143
+ iE = uint32[iB * ELEMENT_WORD_COUNT + COLUMN_CHILD_2];
1144
+
1145
+ // Rotation 1 — swap C and D → B = (C, E), A = (B, D).
1146
+ const imp1 = area_b - this.node_get_combined_surface_area(iC, iE);
1147
+ if (imp1 > best_improvement) { best_improvement = imp1; best_rotation = 1; }
1148
+
1149
+ // Rotation 2 — swap C and E → B = (D, C), A = (B, E).
1150
+ const imp2 = area_b - this.node_get_combined_surface_area(iD, iC);
1151
+ if (imp2 > best_improvement) { best_improvement = imp2; best_rotation = 2; }
1152
+ }
1153
+
1154
+ if (!c_leaf) {
1155
+ // C is internal with children F, G. Symmetric: re-forms C.
1156
+ const area_c = this.node_get_surface_area(iC);
1157
+ iF = uint32[iC * ELEMENT_WORD_COUNT + COLUMN_CHILD_1];
1158
+ iG = uint32[iC * ELEMENT_WORD_COUNT + COLUMN_CHILD_2];
1159
+
1160
+ // Rotation 3 — swap B and F → C = (B, G), A = (F, C).
1161
+ const imp3 = area_c - this.node_get_combined_surface_area(iB, iG);
1162
+ if (imp3 > best_improvement) { best_improvement = imp3; best_rotation = 3; }
1163
+
1164
+ // Rotation 4 — swap B and G → C = (F, B), A = (G, C).
1165
+ const imp4 = area_c - this.node_get_combined_surface_area(iF, iB);
1166
+ if (imp4 > best_improvement) { best_improvement = imp4; best_rotation = 4; }
1167
+ }
1168
+
1169
+ if (best_rotation === 0) {
1170
+ return iA; // no beneficial rotation
1171
+ }
1172
+
1173
+ // Apply the chosen rotation: re-wire one child↔grandchild pair, then
1174
+ // refresh the re-formed internal node's AABB + height. A's AABB / height
1175
+ // are recomputed by the caller (bubble_up_update) after we return, but
1176
+ // we also set A's height here so the subtree is self-consistent.
1177
+ switch (best_rotation) {
1178
+ case 1: { // swap C and D: B = (C, E), A = (B, D)
1179
+ uint32[a_addr + COLUMN_CHILD_2] = iD;
1180
+ uint32[iB * ELEMENT_WORD_COUNT + COLUMN_CHILD_1] = iC;
1181
+ uint32[iD * ELEMENT_WORD_COUNT + COLUMN_PARENT] = iA;
1182
+ uint32[iC * ELEMENT_WORD_COUNT + COLUMN_PARENT] = iB;
1183
+ this.node_set_combined_aabb(iB, iC, iE);
1184
+ uint32[iB * ELEMENT_WORD_COUNT + COLUMN_HEIGHT] = 1 + max2(
1185
+ uint32[iC * ELEMENT_WORD_COUNT + COLUMN_HEIGHT],
1186
+ uint32[iE * ELEMENT_WORD_COUNT + COLUMN_HEIGHT]
1187
+ );
1188
+ break;
1189
+ }
1190
+ case 2: { // swap C and E: B = (D, C), A = (B, E)
1191
+ uint32[a_addr + COLUMN_CHILD_2] = iE;
1192
+ uint32[iB * ELEMENT_WORD_COUNT + COLUMN_CHILD_2] = iC;
1193
+ uint32[iE * ELEMENT_WORD_COUNT + COLUMN_PARENT] = iA;
1194
+ uint32[iC * ELEMENT_WORD_COUNT + COLUMN_PARENT] = iB;
1195
+ this.node_set_combined_aabb(iB, iD, iC);
1196
+ uint32[iB * ELEMENT_WORD_COUNT + COLUMN_HEIGHT] = 1 + max2(
1197
+ uint32[iD * ELEMENT_WORD_COUNT + COLUMN_HEIGHT],
1198
+ uint32[iC * ELEMENT_WORD_COUNT + COLUMN_HEIGHT]
1199
+ );
1200
+ break;
1201
+ }
1202
+ case 3: { // swap B and F: C = (B, G), A = (F, C)
1203
+ uint32[a_addr + COLUMN_CHILD_1] = iF;
1204
+ uint32[iC * ELEMENT_WORD_COUNT + COLUMN_CHILD_1] = iB;
1205
+ uint32[iF * ELEMENT_WORD_COUNT + COLUMN_PARENT] = iA;
1206
+ uint32[iB * ELEMENT_WORD_COUNT + COLUMN_PARENT] = iC;
1207
+ this.node_set_combined_aabb(iC, iB, iG);
1208
+ uint32[iC * ELEMENT_WORD_COUNT + COLUMN_HEIGHT] = 1 + max2(
1209
+ uint32[iB * ELEMENT_WORD_COUNT + COLUMN_HEIGHT],
1210
+ uint32[iG * ELEMENT_WORD_COUNT + COLUMN_HEIGHT]
1211
+ );
1212
+ break;
1213
+ }
1214
+ case 4: { // swap B and G: C = (F, B), A = (G, C)
1215
+ uint32[a_addr + COLUMN_CHILD_1] = iG;
1216
+ uint32[iC * ELEMENT_WORD_COUNT + COLUMN_CHILD_2] = iB;
1217
+ uint32[iG * ELEMENT_WORD_COUNT + COLUMN_PARENT] = iA;
1218
+ uint32[iB * ELEMENT_WORD_COUNT + COLUMN_PARENT] = iC;
1219
+ this.node_set_combined_aabb(iC, iF, iB);
1220
+ uint32[iC * ELEMENT_WORD_COUNT + COLUMN_HEIGHT] = 1 + max2(
1221
+ uint32[iF * ELEMENT_WORD_COUNT + COLUMN_HEIGHT],
1222
+ uint32[iB * ELEMENT_WORD_COUNT + COLUMN_HEIGHT]
1223
+ );
1224
+ break;
1225
+ }
1226
+ }
1227
+
1228
+ uint32[a_addr + COLUMN_HEIGHT] = 1 + max2(
1229
+ uint32[uint32[a_addr + COLUMN_CHILD_1] * ELEMENT_WORD_COUNT + COLUMN_HEIGHT],
1230
+ uint32[uint32[a_addr + COLUMN_CHILD_2] * ELEMENT_WORD_COUNT + COLUMN_HEIGHT]
1231
+ );
1232
+
1233
+ return iA;
1234
+ }
1235
+
1082
1236
  /**
1083
1237
  * Utility method for assigning both children at once
1084
1238
  * Children must be valid nodes (non-null)
@@ -0,0 +1,56 @@
1
+ /**
2
+ * A solid right circular cylinder aligned with the Y axis, centred at the local
3
+ * origin. A disk of {@link radius} extruded along Y for {@link height}, so it
4
+ * spans `y ∈ [-height/2, +height/2]` with **flat** circular caps (the capsule's
5
+ * flat-cap sibling — same radius/height parametrisation, hemispherical caps
6
+ * replaced by disks).
7
+ *
8
+ * Convex, so the physics narrowphase handles it through the GJK + EPA fallback
9
+ * via {@link support} — there is no closed-form cylinder-vs-X contact path yet
10
+ * (a future refinement; the curved side is the usual smooth-support EPA case).
11
+ *
12
+ * @author Alex Goldring
13
+ * @copyright Company Named Limited (c) 2026
14
+ */
15
+ export class CylinderShape3D extends AbstractShape3D {
16
+ /**
17
+ * @param {number} radius
18
+ * @param {number} height
19
+ * @returns {CylinderShape3D}
20
+ */
21
+ static from(radius: number, height: number): CylinderShape3D;
22
+ /**
23
+ * Radius of the circular cross-section.
24
+ * @type {number}
25
+ */
26
+ radius: number;
27
+ /**
28
+ * Full height along the Y axis (total extent — the cylinder spans
29
+ * `[-height/2, +height/2]`).
30
+ * @type {number}
31
+ */
32
+ height: number;
33
+ compute_bounding_box(result: any): void;
34
+ support(result: any, result_offset: any, direction_x: any, direction_y: any, direction_z: any): void;
35
+ signed_distance_at_point(point: any): number;
36
+ signed_distance_gradient_at_point(result: any, point: any): number;
37
+ contains_point(point: any): boolean;
38
+ nearest_point_on_surface(result: any, reference: any): void;
39
+ sample_random_point_in_volume(result: any, result_offset: any, random: any): void;
40
+ /**
41
+ * @param {CylinderShape3D} other
42
+ * @returns {boolean}
43
+ */
44
+ equals(other: CylinderShape3D): boolean;
45
+ /**
46
+ * Fast type-check marker, mirroring `isSphereShape3D` / `isBoxShape3D` /
47
+ * `isCapsuleShape3D`. Reserved for a future closed-form cylinder narrowphase
48
+ * dispatch; until then the cylinder routes through GJK + EPA like any other
49
+ * convex shape (no marker check needed for that path).
50
+ * @readonly
51
+ * @type {boolean}
52
+ */
53
+ readonly isCylinderShape3D: boolean;
54
+ }
55
+ import { AbstractShape3D } from "./AbstractShape3D.js";
56
+ //# sourceMappingURL=CylinderShape3D.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CylinderShape3D.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/3d/shape/CylinderShape3D.js"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;GAaG;AACH;IAkBI;;;;OAIG;IACH,oBAJW,MAAM,UACN,MAAM,GACJ,eAAe,CAa3B;IA9BG;;;OAGG;IACH,QAFU,MAAM,CAEC;IAEjB;;;;OAIG;IACH,QAFU,MAAM,CAED;IAgCnB,wCAUC;IAED,qGAqBC;IAED,6CAoBC;IAED,mEAEC;IAED,oCASC;IAED,4DA0CC;IAED,kFAOC;IAED;;;OAGG;IACH,cAHW,eAAe,GACb,OAAO,CAMnB;IAUL;;;;;;;OAOG;IACH,4BAFU,OAAO,CAE0B;CAV1C;gCA/M+B,sBAAsB"}
@@ -0,0 +1,223 @@
1
+ import { assert } from "../../../assert.js";
2
+ import { clamp } from "../../../math/clamp.js";
3
+ import { sign_not_zero } from "../../../math/sign_not_zero.js";
4
+ import { computeHashFloat } from "../../../primitives/numbers/computeHashFloat.js";
5
+ import { randomPointInCircle } from "../../random/randomPointInCircle.js";
6
+ import { AbstractShape3D } from "./AbstractShape3D.js";
7
+ import { compute_signed_distance_gradient_by_sampling } from "./util/compute_signed_distance_gradient_by_sampling.js";
8
+
9
+ const scratch_v3 = [0, 0, 0];
10
+
11
+ /**
12
+ * A solid right circular cylinder aligned with the Y axis, centred at the local
13
+ * origin. A disk of {@link radius} extruded along Y for {@link height}, so it
14
+ * spans `y ∈ [-height/2, +height/2]` with **flat** circular caps (the capsule's
15
+ * flat-cap sibling — same radius/height parametrisation, hemispherical caps
16
+ * replaced by disks).
17
+ *
18
+ * Convex, so the physics narrowphase handles it through the GJK + EPA fallback
19
+ * via {@link support} — there is no closed-form cylinder-vs-X contact path yet
20
+ * (a future refinement; the curved side is the usual smooth-support EPA case).
21
+ *
22
+ * @author Alex Goldring
23
+ * @copyright Company Named Limited (c) 2026
24
+ */
25
+ export class CylinderShape3D extends AbstractShape3D {
26
+ constructor() {
27
+ super();
28
+
29
+ /**
30
+ * Radius of the circular cross-section.
31
+ * @type {number}
32
+ */
33
+ this.radius = 0.5;
34
+
35
+ /**
36
+ * Full height along the Y axis (total extent — the cylinder spans
37
+ * `[-height/2, +height/2]`).
38
+ * @type {number}
39
+ */
40
+ this.height = 1;
41
+ }
42
+
43
+ /**
44
+ * @param {number} radius
45
+ * @param {number} height
46
+ * @returns {CylinderShape3D}
47
+ */
48
+ static from(radius, height) {
49
+ assert.isNumber(radius, 'radius');
50
+ assert.isNumber(height, 'height');
51
+ assert.greaterThanOrEqual(radius, 0, 'radius');
52
+ assert.greaterThanOrEqual(height, 0, 'height');
53
+
54
+ const r = new CylinderShape3D();
55
+ r.radius = radius;
56
+ r.height = height;
57
+
58
+ return r;
59
+ }
60
+
61
+ get volume() {
62
+ const r = this.radius;
63
+ return Math.PI * r * r * this.height;
64
+ }
65
+
66
+ get surface_area() {
67
+ const r = this.radius;
68
+ // two circular caps + the lateral (side) surface
69
+ return 2 * Math.PI * r * r + 2 * Math.PI * r * this.height;
70
+ }
71
+
72
+ compute_bounding_box(result) {
73
+ const r = this.radius;
74
+ const half_h = this.height / 2;
75
+
76
+ result[0] = -r;
77
+ result[1] = -half_h;
78
+ result[2] = -r;
79
+ result[3] = r;
80
+ result[4] = half_h;
81
+ result[5] = r;
82
+ }
83
+
84
+ support(result, result_offset, direction_x, direction_y, direction_z) {
85
+ const r = this.radius;
86
+ const half_h = this.height / 2;
87
+
88
+ // Radial: farthest point on the cross-section disk along (dx, dz). For a
89
+ // purely axial query (rl ≈ 0) any point on the cap face is extremal —
90
+ // the cap centre (0,0) is on the (flat) surface, so it's a valid support.
91
+ const rl = Math.sqrt(direction_x * direction_x + direction_z * direction_z);
92
+ let sx = 0, sz = 0;
93
+ if (rl > 0) {
94
+ const inv = r / rl;
95
+ sx = direction_x * inv;
96
+ sz = direction_z * inv;
97
+ }
98
+
99
+ // Axial: the cap on the +direction.y side.
100
+ const sy = sign_not_zero(direction_y) * half_h;
101
+
102
+ result[result_offset] = sx;
103
+ result[result_offset + 1] = sy;
104
+ result[result_offset + 2] = sz;
105
+ }
106
+
107
+ signed_distance_at_point(point) {
108
+ const px = point[0];
109
+ const py = point[1];
110
+ const pz = point[2];
111
+
112
+ const half_h = this.height / 2;
113
+
114
+ // Inigo Quilez capped-cylinder SDF, axis = Y. `d_radial` / `d_axial` are
115
+ // the signed distances outside the infinite cylinder and the slab.
116
+ const d_radial = Math.sqrt(px * px + pz * pz) - this.radius;
117
+ const d_axial = Math.abs(py) - half_h;
118
+
119
+ const ext_r = d_radial > 0 ? d_radial : 0;
120
+ const ext_a = d_axial > 0 ? d_axial : 0;
121
+ const outside = Math.sqrt(ext_r * ext_r + ext_a * ext_a);
122
+
123
+ const m = d_radial > d_axial ? d_radial : d_axial;
124
+ const inside = m < 0 ? m : 0;
125
+
126
+ return outside + inside;
127
+ }
128
+
129
+ signed_distance_gradient_at_point(result, point) {
130
+ return compute_signed_distance_gradient_by_sampling(result, this, point);
131
+ }
132
+
133
+ contains_point(point) {
134
+ const px = point[0];
135
+ const py = point[1];
136
+ const pz = point[2];
137
+
138
+ const half_h = this.height / 2;
139
+ const r = this.radius;
140
+
141
+ return (px * px + pz * pz) < r * r && (py < half_h) && (py > -half_h);
142
+ }
143
+
144
+ nearest_point_on_surface(result, reference) {
145
+ const rx = reference[0];
146
+ const ry = reference[1];
147
+ const rz = reference[2];
148
+
149
+ const r = this.radius;
150
+ const half_h = this.height / 2;
151
+
152
+ const rho = Math.sqrt(rx * rx + rz * rz);
153
+ let dirx, dirz;
154
+ if (rho > 0) {
155
+ dirx = rx / rho;
156
+ dirz = rz / rho;
157
+ } else {
158
+ // On the axis — radial direction is undefined; pick +X.
159
+ dirx = 1;
160
+ dirz = 0;
161
+ }
162
+
163
+ // Three candidate projections; the nearest to the reference is the
164
+ // closest surface point (correct for points inside and outside).
165
+ // 1. side — radial clamped to r, y clamped to the band
166
+ // 2. top cap — y = +half_h, radial clamped to r
167
+ // 3. bot cap — y = -half_h, radial clamped to r
168
+ const cy = clamp(ry, -half_h, half_h);
169
+ const s1x = r * dirx, s1y = cy, s1z = r * dirz;
170
+
171
+ const rc = rho < r ? rho : r;
172
+ const s2x = rc * dirx, s2y = half_h, s2z = rc * dirz;
173
+ const s3x = rc * dirx, s3y = -half_h, s3z = rc * dirz;
174
+
175
+ const d1 = (s1x - rx) * (s1x - rx) + (s1y - ry) * (s1y - ry) + (s1z - rz) * (s1z - rz);
176
+ const d2 = (s2x - rx) * (s2x - rx) + (s2y - ry) * (s2y - ry) + (s2z - rz) * (s2z - rz);
177
+ const d3 = (s3x - rx) * (s3x - rx) + (s3y - ry) * (s3y - ry) + (s3z - rz) * (s3z - rz);
178
+
179
+ if (d1 <= d2 && d1 <= d3) {
180
+ result[0] = s1x; result[1] = s1y; result[2] = s1z;
181
+ } else if (d2 <= d3) {
182
+ result[0] = s2x; result[1] = s2y; result[2] = s2z;
183
+ } else {
184
+ result[0] = s3x; result[1] = s3y; result[2] = s3z;
185
+ }
186
+ }
187
+
188
+ sample_random_point_in_volume(result, result_offset, random) {
189
+ // Uniform in the disk (sqrt-weighted radius) × uniform in y.
190
+ randomPointInCircle(random, scratch_v3, 0);
191
+
192
+ result[result_offset] = scratch_v3[0] * this.radius;
193
+ result[result_offset + 1] = (random() - 0.5) * this.height;
194
+ result[result_offset + 2] = scratch_v3[1] * this.radius;
195
+ }
196
+
197
+ /**
198
+ * @param {CylinderShape3D} other
199
+ * @returns {boolean}
200
+ */
201
+ equals(other) {
202
+ return super.equals(other)
203
+ && this.radius === other.radius
204
+ && this.height === other.height;
205
+ }
206
+
207
+ hash() {
208
+ const a = computeHashFloat(this.radius);
209
+ const b = computeHashFloat(this.height);
210
+
211
+ return ((a << 5) - a + b) | 0;
212
+ }
213
+ }
214
+
215
+ /**
216
+ * Fast type-check marker, mirroring `isSphereShape3D` / `isBoxShape3D` /
217
+ * `isCapsuleShape3D`. Reserved for a future closed-form cylinder narrowphase
218
+ * dispatch; until then the cylinder routes through GJK + EPA like any other
219
+ * convex shape (no marker check needed for that path).
220
+ * @readonly
221
+ * @type {boolean}
222
+ */
223
+ CylinderShape3D.prototype.isCylinderShape3D = true;
@@ -9,6 +9,7 @@ export class PointShape3D extends AbstractShape3D {
9
9
  nearest_point_on_surface(result: any, reference: any): void;
10
10
  sample_random_point_in_volume(result: any, result_offset: any, random: any): void;
11
11
  support(result: any, result_offset: any, direction_x: any, direction_y: any, direction_z: any): void;
12
+ equals(other: any): boolean;
12
13
  }
13
14
  import { AbstractShape3D } from "./AbstractShape3D.js";
14
15
  //# sourceMappingURL=PointShape3D.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"PointShape3D.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/3d/shape/PointShape3D.js"],"names":[],"mappings":"AAGA;;GAEG;AACH;IA4CI,8BAAqC;IAnCrC,wCAOC;IAED,6CAEC;IAED,oCAEC;IAED,4DAIC;IAED,kFAIC;IAED,qGAIC;CAGJ;gCAlD+B,sBAAsB"}
1
+ {"version":3,"file":"PointShape3D.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/3d/shape/PointShape3D.js"],"names":[],"mappings":"AAGA;;GAEG;AACH;IAuDI,8BAAqC;IA9CrC,wCAOC;IAED,6CAEC;IAED,oCAEC;IAED,4DAIC;IAED,kFAIC;IAED,qGAIC;IAED,4BAGC;CASJ;gCA7D+B,sBAAsB"}
@@ -48,5 +48,16 @@ export class PointShape3D extends AbstractShape3D {
48
48
  result[result_offset + 2] = 0;
49
49
  }
50
50
 
51
+ equals(other) {
52
+ // Parameterless: identity is the type alone (a point has no fields).
53
+ return super.equals(other);
54
+ }
55
+
56
+ hash() {
57
+ // Constant — every PointShape3D is identical. Distinct from the
58
+ // base-class 0 so it doesn't collide with a hypothetical default shape.
59
+ return 0x504f494e; // "POIN"
60
+ }
61
+
51
62
  static INSTANCE = new PointShape3D();
52
63
  }
@@ -32,6 +32,7 @@ export class SphereShape3D extends AbstractShape3D {
32
32
  signed_distance_at_point(point: any): number;
33
33
  contains_point(point: any): boolean;
34
34
  sample_random_point_in_volume(result: any, result_offset: any, random: any): void;
35
+ equals(other: any): boolean;
35
36
  /**
36
37
  * Fast type-check marker. Lets the physics narrowphase short-circuit
37
38
  * sphere-involved pairs to closed-form solvers (reading `radius`) rather than
@@ -1 +1 @@
1
- {"version":3,"file":"SphereShape3D.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/3d/shape/SphereShape3D.js"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;GAcG;AACH;IAUI;;;;OAIG;IACH,oBAHW,MAAM,GACJ,aAAa,CAMzB;IAhBG;;;OAGG;IACH,QAFU,MAAM,CAED;IAwBnB,qGAQC;IAED,wCAQC;IAED,4DAaC;IAED,mEAEC;IAED,6CAEC;IAED,oCAOC;IAED,kFAMC;IAOL;;;;;;;;OAQG;IACH,0BAFU,OAAO,CAEsB;CAXtC;gCAhH+B,sBAAsB"}
1
+ {"version":3,"file":"SphereShape3D.d.ts","sourceRoot":"","sources":["../../../../../../src/core/geom/3d/shape/SphereShape3D.js"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;GAcG;AACH;IAUI;;;;OAIG;IACH,oBAHW,MAAM,GACJ,aAAa,CAMzB;IAhBG;;;OAGG;IACH,QAFU,MAAM,CAED;IAwBnB,qGAQC;IAED,wCAQC;IAED,4DAaC;IAED,mEAEC;IAED,6CAEC;IAED,oCAOC;IAED,kFAMC;IAED,4BAEC;IAOL;;;;;;;;OAQG;IACH,0BAFU,OAAO,CAEsB;CAXtC;gCApH+B,sBAAsB"}
@@ -110,6 +110,10 @@ export class SphereShape3D extends AbstractShape3D {
110
110
  result[result_offset + 2] *= r;
111
111
  }
112
112
 
113
+ equals(other) {
114
+ return super.equals(other) && this.radius === other.radius;
115
+ }
116
+
113
117
  hash() {
114
118
  return computeHashFloat(this.radius);
115
119
  }
@@ -1 +1 @@
1
- {"version":3,"file":"shape_to_type.d.ts","sourceRoot":"","sources":["../../../../../../../src/core/geom/3d/shape/json/shape_to_type.js"],"names":[],"mappings":"AAMA;;;;GAIG;AACH,uDAFa,MAAM,CAkBlB"}
1
+ {"version":3,"file":"shape_to_type.d.ts","sourceRoot":"","sources":["../../../../../../../src/core/geom/3d/shape/json/shape_to_type.js"],"names":[],"mappings":"AAOA;;;;GAIG;AACH,uDAFa,MAAM,CAoBlB"}
@@ -1,4 +1,5 @@
1
1
  import { CapsuleShape3D } from "../CapsuleShape3D.js";
2
+ import { CylinderShape3D } from "../CylinderShape3D.js";
2
3
  import { SphereShape3D } from "../SphereShape3D.js";
3
4
  import { TransformedShape3D } from "../TransformedShape3D.js";
4
5
  import { UnionShape3D } from "../UnionShape3D.js";
@@ -22,6 +23,8 @@ export function shape_to_type(shape) {
22
23
  return 'transform';
23
24
  } else if (shape instanceof CapsuleShape3D) {
24
25
  return 'capsule';
26
+ } else if (shape instanceof CylinderShape3D) {
27
+ return 'cylinder';
25
28
  } else {
26
29
  throw new Error('Unsupported shape');
27
30
  }
@@ -30,6 +30,20 @@ export namespace type_adapters {
30
30
  height: number;
31
31
  };
32
32
  }
33
+ namespace cylinder {
34
+ function read({ radius, height }: {
35
+ radius: any;
36
+ height: any;
37
+ }): CylinderShape3D;
38
+ /**
39
+ *
40
+ * @param {CylinderShape3D} object
41
+ */
42
+ function write(object: CylinderShape3D): {
43
+ radius: number;
44
+ height: number;
45
+ };
46
+ }
33
47
  namespace transform {
34
48
  function read({ transform, subject }: {
35
49
  transform: any;
@@ -60,6 +74,7 @@ export namespace type_adapters {
60
74
  import { UnitCubeShape3D } from "../UnitCubeShape3D.js";
61
75
  import { SphereShape3D } from "../SphereShape3D.js";
62
76
  import { CapsuleShape3D } from "../CapsuleShape3D.js";
77
+ import { CylinderShape3D } from "../CylinderShape3D.js";
63
78
  import { TransformedShape3D } from "../TransformedShape3D.js";
64
79
  import { UnionShape3D } from "../UnionShape3D.js";
65
80
  //# sourceMappingURL=type_adapters.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"type_adapters.d.ts","sourceRoot":"","sources":["../../../../../../../src/core/geom/3d/shape/json/type_adapters.js"],"names":[],"mappings":";;QAWQ,iCAEC;QACD,qBAEC;;;QAGD;;0BAQC;QACD;;WAEG;QACH;;;;UAIC;;;QAGD;;;2BAEC;QACD;;;WAGG;QACH;;;UAKC;;;QAGD;;;+BAIC;QACD;;;WAGG;QACH;;;UAKC;;;QAGD;;yBAIC;QACD;;;WAGG;QACH;;UAIC;;;gCA/EuB,uBAAuB;8BAHzB,qBAAqB;+BADpB,sBAAsB;mCAElB,0BAA0B;6BAChC,oBAAoB"}
1
+ {"version":3,"file":"type_adapters.d.ts","sourceRoot":"","sources":["../../../../../../../src/core/geom/3d/shape/json/type_adapters.js"],"names":[],"mappings":";;QAYQ,iCAEC;QACD,qBAEC;;;QAGD;;0BAQC;QACD;;WAEG;QACH;;;;UAIC;;;QAGD;;;2BAEC;QACD;;;WAGG;QACH;;;UAKC;;;QAGD;;;4BAEC;QACD;;;WAGG;QACH;;;UAKC;;;QAGD;;;+BAIC;QACD;;;WAGG;QACH;;;UAKC;;;QAGD;;yBAIC;QACD;;;WAGG;QACH;;UAIC;;;gCA9FuB,uBAAuB;8BAHzB,qBAAqB;+BAFpB,sBAAsB;gCACrB,uBAAuB;mCAEpB,0BAA0B;6BAChC,oBAAoB"}
@@ -1,4 +1,5 @@
1
1
  import { CapsuleShape3D } from "../CapsuleShape3D.js";
2
+ import { CylinderShape3D } from "../CylinderShape3D.js";
2
3
  import { SphereShape3D } from "../SphereShape3D.js";
3
4
  import { TransformedShape3D } from "../TransformedShape3D.js";
4
5
  import { UnionShape3D } from "../UnionShape3D.js";
@@ -50,6 +51,21 @@ export const type_adapters = {
50
51
  };
51
52
  }
52
53
  },
54
+ 'cylinder': {
55
+ read({ radius, height }) {
56
+ return CylinderShape3D.from(radius, height);
57
+ },
58
+ /**
59
+ *
60
+ * @param {CylinderShape3D} object
61
+ */
62
+ write(object) {
63
+ return {
64
+ radius: object.radius,
65
+ height: object.height
66
+ };
67
+ }
68
+ },
53
69
  'transform': {
54
70
  read({ transform, subject }) {
55
71
  const subject_shape = shape_from_json(subject);