@woosh/meep-engine 2.144.0 → 2.146.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 (60) 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/HeightMapShape3D.d.ts +33 -3
  8. package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -1
  9. package/src/core/geom/3d/shape/HeightMapShape3D.js +486 -451
  10. package/src/core/geom/3d/shape/json/shape_to_type.d.ts.map +1 -1
  11. package/src/core/geom/3d/shape/json/shape_to_type.js +3 -0
  12. package/src/core/geom/3d/shape/json/type_adapters.d.ts +15 -0
  13. package/src/core/geom/3d/shape/json/type_adapters.d.ts.map +1 -1
  14. package/src/core/geom/3d/shape/json/type_adapters.js +16 -0
  15. package/src/engine/control/first-person/DESIGN_COLLISION.md +365 -302
  16. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +1 -3
  17. package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
  18. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +12 -2
  19. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
  20. package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +7 -2
  21. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts +13 -0
  22. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts.map +1 -1
  23. package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.js +16 -2
  24. package/src/engine/control/first-person/TODO.md +13 -11
  25. package/src/engine/control/first-person/abilities/WallJump.d.ts.map +1 -1
  26. package/src/engine/control/first-person/abilities/WallJump.js +11 -3
  27. package/src/engine/control/first-person/abilities/WallRun.d.ts.map +1 -1
  28. package/src/engine/control/first-person/abilities/WallRun.js +30 -35
  29. package/src/engine/control/first-person/collision/KinematicMover.d.ts +35 -5
  30. package/src/engine/control/first-person/collision/KinematicMover.d.ts.map +1 -1
  31. package/src/engine/control/first-person/collision/KinematicMover.js +634 -424
  32. package/src/engine/control/first-person/prototype_first_person_controller.js +1003 -901
  33. package/src/engine/physics/PLAN.md +943 -767
  34. package/src/engine/physics/body/BodyStorage.d.ts +9 -0
  35. package/src/engine/physics/body/BodyStorage.d.ts.map +1 -1
  36. package/src/engine/physics/body/BodyStorage.js +23 -0
  37. package/src/engine/physics/broadphase/generate_pairs.d.ts.map +1 -1
  38. package/src/engine/physics/broadphase/generate_pairs.js +7 -0
  39. package/src/engine/physics/ccd/linear_sweep.d.ts +97 -0
  40. package/src/engine/physics/ccd/linear_sweep.d.ts.map +1 -0
  41. package/src/engine/physics/ccd/linear_sweep.js +238 -0
  42. package/src/engine/physics/ecs/PhysicsSystem.d.ts +18 -3
  43. package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
  44. package/src/engine/physics/ecs/PhysicsSystem.js +59 -8
  45. package/src/engine/physics/ecs/RigidBodyFlags.d.ts +6 -0
  46. package/src/engine/physics/ecs/RigidBodyFlags.d.ts.map +1 -1
  47. package/src/engine/physics/ecs/RigidBodyFlags.js +6 -0
  48. package/src/engine/physics/narrowphase/box_triangle_contact.js +811 -811
  49. package/src/engine/physics/narrowphase/compute_penetration.d.ts.map +1 -1
  50. package/src/engine/physics/narrowphase/compute_penetration.js +325 -323
  51. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts +27 -8
  52. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts.map +1 -1
  53. package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.js +235 -204
  54. package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
  55. package/src/engine/physics/narrowphase/narrowphase_step.js +70 -13
  56. package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -1
  57. package/src/engine/physics/queries/overlap_shape.js +185 -183
  58. package/src/engine/simulation/Ticker.d.ts +14 -0
  59. package/src/engine/simulation/Ticker.d.ts.map +1 -1
  60. package/src/engine/simulation/Ticker.js +136 -1
@@ -1 +1 @@
1
- {"version":3,"file":"overlap_shape.d.ts","sourceRoot":"","sources":["../../../../../src/engine/physics/queries/overlap_shape.js"],"names":[],"mappings":"AA4CA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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,CAqGlB"}
@@ -1,183 +1,185 @@
1
- import { bvh_query_user_data_overlaps_aabb } from "../../../core/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.js";
2
- import { returnTrue } from "../../../core/function/returnTrue.js";
3
- import { aabb3_transform_oriented } from "../../../core/geom/3d/aabb/aabb3_transform_oriented.js";
4
- import { Triangle3D } from "../../../core/geom/3d/shape/Triangle3D.js";
5
- import { body_id_index } from "../body/BodyStorage.js";
6
- import { gjk } from "../gjk/gjk.js";
7
- import { aabb_world_to_local } from "../narrowphase/decomposition/aabb_world_to_local.js";
8
- import { decompose_to_triangles } from "../narrowphase/decomposition/decompose_to_triangles.js";
9
- import { TRIANGLE_FLOAT_STRIDE } from "../narrowphase/decomposition/triangle_buffer_layout.js";
10
- import { PosedShape } from "../narrowphase/PosedShape.js";
11
-
12
- /**
13
- * Scratch state — module-scoped to avoid per-query allocation. Safe
14
- * because PhysicsSystem queries run on the main thread, sequentially.
15
- */
16
- const local_aabb = new Float64Array(6);
17
- const world_aabb = new Float64Array(6);
18
- const concave_query_aabb = new Float64Array(6);
19
- const simplex_buf = new Float64Array(12);
20
-
21
- const query_posed = new PosedShape();
22
- const candidate_posed = new PosedShape();
23
- const triangle_shape = new Triangle3D();
24
-
25
- /**
26
- * Maximum triangles a concave candidate can emit per overlap pair.
27
- * Same rationale as the narrowphase's `MAX_TRIANGLES_PER_PAIR`: the
28
- * broadphase has already bounded the query AABB to the query shape's
29
- * envelope, so a single candidate typically yields tens of triangles.
30
- * Excess triangles are dropped by the enumerator's bounds check —
31
- * worst case is a missed overlap on a far edge of the candidate's
32
- * geometry, recovered next query.
33
- * @type {number}
34
- */
35
- const MAX_TRIANGLES_PER_PAIR = 1024;
36
-
37
- const triangle_buffer = new Float64Array(MAX_TRIANGLES_PER_PAIR * TRIANGLE_FLOAT_STRIDE);
38
-
39
- /**
40
- * Broadphase candidate buffer. Grows by doubling on overflow.
41
- * @type {Uint32Array}
42
- */
43
- let scratch_candidates = new Uint32Array(64);
44
-
45
- /**
46
- * Test what bodies overlap a convex shape placed at a given pose. Each
47
- * overlapping body's `body_id` is written to `output` starting at
48
- * `output_offset`; the function returns the number of body ids written.
49
- *
50
- * Use case: speculative physics queries for kinematic / character
51
- * controllers. An external system can ask "would my body collide with
52
- * anything if I moved it here?" without committing a tick of
53
- * simulation. The output is a flat list of body ids so the caller can
54
- * decide what to do per hit (skip, push, slide, etc.).
55
- *
56
- * The pipeline mirrors the narrowphase pair test:
57
- * 1. Build the query shape's world AABB.
58
- * 2. Pull candidates from both broadphase trees that overlap that AABB.
59
- * 3. For each candidate, run GJK in world frame. Convex candidates
60
- * go through one GJK call; concave candidates (heightmap / mesh)
61
- * go through the per-triangle decomposition path.
62
- * 4. Apply the optional `filter` callback (same signature as in
63
- * raycast / shapeCast) before the GJK test — early-out on bodies
64
- * the caller already wants to skip (themselves, allies, etc.).
65
- *
66
- * The query shape must be convex (`is_convex === true`). Concave shapes
67
- * are typically static terrain and not used as kinematic query
68
- * probes; rejecting them avoids the M×N triangle-pair cost.
69
- *
70
- * @param {PhysicsSystem} system
71
- * @param {AbstractShape3D} shape query shape, convex; expressed in
72
- * its own local frame
73
- * @param {{x:number,y:number,z:number}} position world position of the
74
- * query shape
75
- * @param {{x:number,y:number,z:number,w:number}} rotation world rotation
76
- * of the query shape (unit quaternion)
77
- * @param {Uint32Array|number[]} output buffer to write body_ids into.
78
- * Caller is responsible for sizing it; ids past its end are dropped.
79
- * @param {number} output_offset float-index in output to start writing at
80
- * @param {(entity:number, collider:Collider)=>boolean} [filter]
81
- * defaults to {@link returnTrue} (accept every candidate)
82
- * @returns {number} number of overlapping bodies written
83
- * @throws {Error} if `shape.is_convex === false`
84
- */
85
- export function overlap_shape(system, shape, position, rotation, output, output_offset, filter = returnTrue) {
86
- if (shape.is_convex === false) {
87
- throw new Error(`overlap_shape: query shape must be convex; received \`${shape.constructor.name}\` (is_convex=false)`);
88
- }
89
-
90
- // ── 1. Query shape's world AABB ─────────────────────────────────
91
- shape.compute_bounding_box(local_aabb);
92
- aabb3_transform_oriented(
93
- world_aabb, 0,
94
- local_aabb[0], local_aabb[1], local_aabb[2],
95
- local_aabb[3], local_aabb[4], local_aabb[5],
96
- position.x, position.y, position.z,
97
- rotation.x, rotation.y, rotation.z, rotation.w
98
- );
99
-
100
- // ── 2. Gather broadphase candidates ─────────────────────────────
101
- const n_static = bvh_query_user_data_overlaps_aabb(
102
- scratch_candidates, 0, system.staticBvh, world_aabb
103
- );
104
- const n_dynamic = bvh_query_user_data_overlaps_aabb(
105
- scratch_candidates, n_static, system.dynamicBvh, world_aabb
106
- );
107
- const n_total = n_static + n_dynamic;
108
- if (n_total === 0) return 0;
109
-
110
- // ── 3. Set up query PosedShape (constant across candidates) ─────
111
- query_posed.setup(shape, position, rotation);
112
-
113
- // ── 4. Per-candidate narrowphase ────────────────────────────────
114
- const output_capacity = output.length - output_offset;
115
- let count = 0;
116
- let cursor = output_offset;
117
-
118
- for (let i = 0; i < n_total; i++) {
119
- if (count >= output_capacity) break;
120
-
121
- const body_id = scratch_candidates[i];
122
- const body_idx = body_id_index(body_id);
123
-
124
- const entity = system.entityOf(body_id);
125
- if (entity < 0) continue;
126
-
127
- const collider = system.__primary_collider(body_idx);
128
- if (collider === null) continue;
129
- if (!filter(entity, collider)) continue;
130
-
131
- const candidate_tr = system.__transforms[body_idx];
132
-
133
- let overlaps = false;
134
-
135
- if (collider.shape.is_convex !== false) {
136
- candidate_posed.setup(collider.shape, candidate_tr.position, candidate_tr.rotation);
137
- overlaps = gjk(simplex_buf, query_posed, candidate_posed);
138
- } else {
139
- // Concave candidate: project the query's world AABB into
140
- // the candidate's body-local frame, decompose to triangles,
141
- // run per-triangle GJK until one overlap is found.
142
- aabb_world_to_local(
143
- concave_query_aabb, 0,
144
- world_aabb,
145
- candidate_tr.position.x, candidate_tr.position.y, candidate_tr.position.z,
146
- candidate_tr.rotation.x, candidate_tr.rotation.y, candidate_tr.rotation.z, candidate_tr.rotation.w
147
- );
148
-
149
- const tri_count = decompose_to_triangles(
150
- triangle_buffer, 0, collider.shape,
151
- concave_query_aabb[0], concave_query_aabb[1], concave_query_aabb[2],
152
- concave_query_aabb[3], concave_query_aabb[4], concave_query_aabb[5]
153
- );
154
-
155
- // Re-pose candidate as the concave body, rebinding the
156
- // flyweight triangle per iteration.
157
- candidate_posed.shape = triangle_shape;
158
- candidate_posed.px = candidate_tr.position.x;
159
- candidate_posed.py = candidate_tr.position.y;
160
- candidate_posed.pz = candidate_tr.position.z;
161
- candidate_posed.qx = candidate_tr.rotation.x;
162
- candidate_posed.qy = candidate_tr.rotation.y;
163
- candidate_posed.qz = candidate_tr.rotation.z;
164
- candidate_posed.qw = candidate_tr.rotation.w;
165
-
166
- for (let t = 0; t < tri_count; t++) {
167
- triangle_shape.bind(triangle_buffer, t * TRIANGLE_FLOAT_STRIDE);
168
- if (gjk(simplex_buf, query_posed, candidate_posed)) {
169
- overlaps = true;
170
- break;
171
- }
172
- }
173
- }
174
-
175
- if (overlaps) {
176
- output[cursor] = body_id;
177
- cursor++;
178
- count++;
179
- }
180
- }
181
-
182
- return count;
183
- }
1
+ import { bvh_query_user_data_overlaps_aabb } from "../../../core/bvh2/bvh3/query/bvh_query_user_data_overlaps_aabb.js";
2
+ import { returnTrue } from "../../../core/function/returnTrue.js";
3
+ import { aabb3_transform_oriented } from "../../../core/geom/3d/aabb/aabb3_transform_oriented.js";
4
+ import { Triangle3D } from "../../../core/geom/3d/shape/Triangle3D.js";
5
+ import { body_id_index } from "../body/BodyStorage.js";
6
+ import { gjk } from "../gjk/gjk.js";
7
+ import { aabb_world_to_local } from "../narrowphase/decomposition/aabb_world_to_local.js";
8
+ import { decompose_to_triangles } from "../narrowphase/decomposition/decompose_to_triangles.js";
9
+ import { TRIANGLE_FLOAT_STRIDE } from "../narrowphase/decomposition/triangle_buffer_layout.js";
10
+ import { PosedShape } from "../narrowphase/PosedShape.js";
11
+
12
+ /**
13
+ * Scratch state — module-scoped to avoid per-query allocation. Safe
14
+ * because PhysicsSystem queries run on the main thread, sequentially.
15
+ */
16
+ const local_aabb = new Float64Array(6);
17
+ const world_aabb = new Float64Array(6);
18
+ const concave_query_aabb = new Float64Array(6);
19
+ const simplex_buf = new Float64Array(12);
20
+
21
+ const query_posed = new PosedShape();
22
+ const candidate_posed = new PosedShape();
23
+ const triangle_shape = new Triangle3D();
24
+
25
+ /**
26
+ * Maximum triangles a concave candidate can emit per overlap pair.
27
+ * Same rationale as the narrowphase's `MAX_TRIANGLES_PER_PAIR`: the
28
+ * broadphase has already bounded the query AABB to the query shape's
29
+ * envelope, so a single candidate typically yields tens of triangles
30
+ * (a heightmap's count scales O(N²) with its `tessellation`, still
31
+ * inside the buffer for a bounded query at moderate tessellation).
32
+ * Excess triangles are dropped by the enumerator's bounds check —
33
+ * worst case is a missed overlap on a far edge of the candidate's
34
+ * geometry, recovered next query.
35
+ * @type {number}
36
+ */
37
+ const MAX_TRIANGLES_PER_PAIR = 1024;
38
+
39
+ const triangle_buffer = new Float64Array(MAX_TRIANGLES_PER_PAIR * TRIANGLE_FLOAT_STRIDE);
40
+
41
+ /**
42
+ * Broadphase candidate buffer. Grows by doubling on overflow.
43
+ * @type {Uint32Array}
44
+ */
45
+ let scratch_candidates = new Uint32Array(64);
46
+
47
+ /**
48
+ * Test what bodies overlap a convex shape placed at a given pose. Each
49
+ * overlapping body's `body_id` is written to `output` starting at
50
+ * `output_offset`; the function returns the number of body ids written.
51
+ *
52
+ * Use case: speculative physics queries for kinematic / character
53
+ * controllers. An external system can ask "would my body collide with
54
+ * anything if I moved it here?" without committing a tick of
55
+ * simulation. The output is a flat list of body ids so the caller can
56
+ * decide what to do per hit (skip, push, slide, etc.).
57
+ *
58
+ * The pipeline mirrors the narrowphase pair test:
59
+ * 1. Build the query shape's world AABB.
60
+ * 2. Pull candidates from both broadphase trees that overlap that AABB.
61
+ * 3. For each candidate, run GJK in world frame. Convex candidates
62
+ * go through one GJK call; concave candidates (heightmap / mesh)
63
+ * go through the per-triangle decomposition path.
64
+ * 4. Apply the optional `filter` callback (same signature as in
65
+ * raycast / shapeCast) before the GJK test — early-out on bodies
66
+ * the caller already wants to skip (themselves, allies, etc.).
67
+ *
68
+ * The query shape must be convex (`is_convex === true`). Concave shapes
69
+ * are typically static terrain and not used as kinematic query
70
+ * probes; rejecting them avoids the M×N triangle-pair cost.
71
+ *
72
+ * @param {PhysicsSystem} system
73
+ * @param {AbstractShape3D} shape query shape, convex; expressed in
74
+ * its own local frame
75
+ * @param {{x:number,y:number,z:number}} position world position of the
76
+ * query shape
77
+ * @param {{x:number,y:number,z:number,w:number}} rotation world rotation
78
+ * of the query shape (unit quaternion)
79
+ * @param {Uint32Array|number[]} output buffer to write body_ids into.
80
+ * Caller is responsible for sizing it; ids past its end are dropped.
81
+ * @param {number} output_offset float-index in output to start writing at
82
+ * @param {(entity:number, collider:Collider)=>boolean} [filter]
83
+ * defaults to {@link returnTrue} (accept every candidate)
84
+ * @returns {number} number of overlapping bodies written
85
+ * @throws {Error} if `shape.is_convex === false`
86
+ */
87
+ export function overlap_shape(system, shape, position, rotation, output, output_offset, filter = returnTrue) {
88
+ if (shape.is_convex === false) {
89
+ throw new Error(`overlap_shape: query shape must be convex; received \`${shape.constructor.name}\` (is_convex=false)`);
90
+ }
91
+
92
+ // ── 1. Query shape's world AABB ─────────────────────────────────
93
+ shape.compute_bounding_box(local_aabb);
94
+ aabb3_transform_oriented(
95
+ world_aabb, 0,
96
+ local_aabb[0], local_aabb[1], local_aabb[2],
97
+ local_aabb[3], local_aabb[4], local_aabb[5],
98
+ position.x, position.y, position.z,
99
+ rotation.x, rotation.y, rotation.z, rotation.w
100
+ );
101
+
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;
110
+ if (n_total === 0) return 0;
111
+
112
+ // ── 3. Set up query PosedShape (constant across candidates) ─────
113
+ query_posed.setup(shape, position, rotation);
114
+
115
+ // ── 4. Per-candidate narrowphase ────────────────────────────────
116
+ const output_capacity = output.length - output_offset;
117
+ let count = 0;
118
+ let cursor = output_offset;
119
+
120
+ for (let i = 0; i < n_total; i++) {
121
+ if (count >= output_capacity) break;
122
+
123
+ const body_id = scratch_candidates[i];
124
+ const body_idx = body_id_index(body_id);
125
+
126
+ const entity = system.entityOf(body_id);
127
+ if (entity < 0) continue;
128
+
129
+ const collider = system.__primary_collider(body_idx);
130
+ if (collider === null) continue;
131
+ if (!filter(entity, collider)) continue;
132
+
133
+ const candidate_tr = system.__transforms[body_idx];
134
+
135
+ let overlaps = false;
136
+
137
+ if (collider.shape.is_convex !== false) {
138
+ candidate_posed.setup(collider.shape, candidate_tr.position, candidate_tr.rotation);
139
+ overlaps = gjk(simplex_buf, query_posed, candidate_posed);
140
+ } else {
141
+ // Concave candidate: project the query's world AABB into
142
+ // the candidate's body-local frame, decompose to triangles,
143
+ // run per-triangle GJK until one overlap is found.
144
+ aabb_world_to_local(
145
+ concave_query_aabb, 0,
146
+ world_aabb,
147
+ candidate_tr.position.x, candidate_tr.position.y, candidate_tr.position.z,
148
+ candidate_tr.rotation.x, candidate_tr.rotation.y, candidate_tr.rotation.z, candidate_tr.rotation.w
149
+ );
150
+
151
+ const tri_count = decompose_to_triangles(
152
+ triangle_buffer, 0, collider.shape,
153
+ concave_query_aabb[0], concave_query_aabb[1], concave_query_aabb[2],
154
+ concave_query_aabb[3], concave_query_aabb[4], concave_query_aabb[5]
155
+ );
156
+
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;
167
+
168
+ for (let t = 0; t < tri_count; t++) {
169
+ triangle_shape.bind(triangle_buffer, t * TRIANGLE_FLOAT_STRIDE);
170
+ if (gjk(simplex_buf, query_posed, candidate_posed)) {
171
+ overlaps = true;
172
+ break;
173
+ }
174
+ }
175
+ }
176
+
177
+ if (overlaps) {
178
+ output[cursor] = body_id;
179
+ cursor++;
180
+ count++;
181
+ }
182
+ }
183
+
184
+ return count;
185
+ }
@@ -20,6 +20,20 @@ declare class Ticker {
20
20
  * @type {Clock}
21
21
  */
22
22
  readonly clock: Clock;
23
+ /**
24
+ * When `true`, ticking is suspended while the host document/tab is in the
25
+ * background (hidden, or stored in the back/forward cache) and resumes from
26
+ * the current moment once it returns to the foreground.
27
+ *
28
+ * This avoids dispatching a flood of catch-up ticks (or one huge delta) for
29
+ * the time spent suspended, which would otherwise destabilise the simulation.
30
+ *
31
+ * Has no effect in environments without document/visibility support (e.g.
32
+ * Node.js), where the relevant lifecycle events simply never fire.
33
+ *
34
+ * @type {boolean}
35
+ */
36
+ suspend_in_background: boolean;
23
37
  /**
24
38
  * Dispatches time delta in seconds since last tick
25
39
  * @readonly
@@ -1 +1 @@
1
- {"version":3,"file":"Ticker.d.ts","sourceRoot":"","sources":["../../../../src/engine/simulation/Ticker.js"],"names":[],"mappings":";AAIA;;;;;;;;;;;;;;GAcG;AACH;IACI;;;OAGG;IACH,gBAFU,KAAK,CAEK;IAiBpB;;;;OAIG;IACH,iBAFU,OAAO,MAAM,CAAC,CAEF;IAUtB;;;OAGG;IACH,uBAFW,MAAM,QAgDhB;IAED,cAOC;IAED,eAQC;IAED,aAOC;;CACJ;kBArIiB,aAAa;mBADZ,oCAAoC"}
1
+ {"version":3,"file":"Ticker.d.ts","sourceRoot":"","sources":["../../../../src/engine/simulation/Ticker.js"],"names":[],"mappings":";AAIA;;;;;;;;;;;;;;GAcG;AACH;IACI;;;OAGG;IACH,gBAFU,KAAK,CAEK;IAEpB;;;;;;;;;;;;OAYG;IACH,uBAFU,OAAO,CAEY;IAwB7B;;;;OAIG;IACH,iBAFU,OAAO,MAAM,CAAC,CAEF;IA+GtB;;;OAGG;IACH,uBAFW,MAAM,QAyDhB;IAED,cAOC;IAED,eAQC;IAED,aAUC;;CACJ;kBA5QiB,aAAa;mBADZ,oCAAoC"}
@@ -24,12 +24,34 @@ class Ticker {
24
24
  */
25
25
  clock = new Clock();
26
26
 
27
+ /**
28
+ * When `true`, ticking is suspended while the host document/tab is in the
29
+ * background (hidden, or stored in the back/forward cache) and resumes from
30
+ * the current moment once it returns to the foreground.
31
+ *
32
+ * This avoids dispatching a flood of catch-up ticks (or one huge delta) for
33
+ * the time spent suspended, which would otherwise destabilise the simulation.
34
+ *
35
+ * Has no effect in environments without document/visibility support (e.g.
36
+ * Node.js), where the relevant lifecycle events simply never fire.
37
+ *
38
+ * @type {boolean}
39
+ */
40
+ suspend_in_background = true;
41
+
27
42
  /**
28
43
  * @private
29
44
  * @type {boolean}
30
45
  */
31
46
  #isRunning = false;
32
47
 
48
+ /**
49
+ * Whether ticking is currently suspended because the host page is in the background
50
+ * @private
51
+ * @type {boolean}
52
+ */
53
+ #isSuspended = false;
54
+
33
55
  #animationFrameHandle = -1;
34
56
  #timeoutHandle = -1;
35
57
 
@@ -54,6 +76,107 @@ class Ticker {
54
76
  return this.#timeoutHandle !== -1 && this.#animationFrameHandle !== -1;
55
77
  }
56
78
 
79
+ /**
80
+ * Stop dispatching ticks because the host page moved to the background.
81
+ * The clock is paused so no simulation time accrues while suspended. The
82
+ * update loop itself keeps cycling so it can pick back up cleanly, but
83
+ * {@link Ticker#onTick} stays silent until the page returns to the foreground.
84
+ * @private
85
+ */
86
+ #suspend() {
87
+ if (!this.suspend_in_background) {
88
+ return;
89
+ }
90
+
91
+ if (!this.#isRunning) {
92
+ // not actively ticking, nothing to suspend
93
+ return;
94
+ }
95
+
96
+ if (this.#isSuspended) {
97
+ return;
98
+ }
99
+
100
+ this.#isSuspended = true;
101
+
102
+ this.clock.pause();
103
+ }
104
+
105
+ /**
106
+ * Resume after the host page returned to the foreground.
107
+ *
108
+ * The clock is always restarted here (it was paused while suspended), with the
109
+ * delta accumulated for the suspended period discarded so that time continues
110
+ * from the current moment rather than catching up all at once. Whether ticks
111
+ * actually flow afterwards is still governed by the running flag, so a manual
112
+ * {@link Ticker#pause} taken during suspension is preserved.
113
+ * @private
114
+ */
115
+ #resume() {
116
+ if (!this.#isSuspended) {
117
+ return;
118
+ }
119
+
120
+ this.#isSuspended = false;
121
+
122
+ this.clock.getDelta(); //discard time spent suspended
123
+ this.clock.start();
124
+ }
125
+
126
+ #handleVisibilityChange = () => {
127
+ const doc = globalThis.document;
128
+
129
+ if (doc === undefined) {
130
+ return;
131
+ }
132
+
133
+ if (doc.visibilityState === 'hidden') {
134
+ this.#suspend();
135
+ } else {
136
+ this.#resume();
137
+ }
138
+ };
139
+
140
+ #handlePageHide = () => {
141
+ this.#suspend();
142
+ };
143
+
144
+ #handlePageShow = () => {
145
+ this.#resume();
146
+ };
147
+
148
+ /**
149
+ * Subscribe to page lifecycle events where the host environment provides them.
150
+ * Degrades gracefully where there is no DOM event target (e.g. Node.js).
151
+ * @private
152
+ */
153
+ #addLifecycleListeners() {
154
+ const doc = globalThis.document;
155
+ if (doc !== undefined && typeof doc.addEventListener === 'function') {
156
+ doc.addEventListener('visibilitychange', this.#handleVisibilityChange);
157
+ }
158
+
159
+ if (typeof globalThis.addEventListener === 'function') {
160
+ globalThis.addEventListener('pagehide', this.#handlePageHide);
161
+ globalThis.addEventListener('pageshow', this.#handlePageShow);
162
+ }
163
+ }
164
+
165
+ /**
166
+ * @private
167
+ */
168
+ #removeLifecycleListeners() {
169
+ const doc = globalThis.document;
170
+ if (doc !== undefined && typeof doc.removeEventListener === 'function') {
171
+ doc.removeEventListener('visibilitychange', this.#handleVisibilityChange);
172
+ }
173
+
174
+ if (typeof globalThis.removeEventListener === 'function') {
175
+ globalThis.removeEventListener('pagehide', this.#handlePageHide);
176
+ globalThis.removeEventListener('pageshow', this.#handlePageShow);
177
+ }
178
+ }
179
+
57
180
  /**
58
181
  *
59
182
  * @param {number} [maxTimeout]
@@ -67,9 +190,10 @@ class Ticker {
67
190
  }
68
191
 
69
192
  this.#isRunning = true;
193
+ this.#isSuspended = false;
70
194
 
71
195
  const update = () => {
72
- if (!this.#isRunning) {
196
+ if (!this.#isRunning || this.#isSuspended) {
73
197
  return;
74
198
  }
75
199
 
@@ -103,7 +227,15 @@ class Ticker {
103
227
  this.clock.getDelta(); //purge delta
104
228
  this.clock.start();
105
229
 
230
+ this.#addLifecycleListeners();
231
+
106
232
  requestAnimationFrame(animationFrameCallback);
233
+
234
+ //if we are starting while the page is already in the background, suspend right away
235
+ const doc = globalThis.document;
236
+ if (doc !== undefined && doc.visibilityState === 'hidden') {
237
+ this.#suspend();
238
+ }
107
239
  }
108
240
 
109
241
  pause() {
@@ -126,9 +258,12 @@ class Ticker {
126
258
  }
127
259
 
128
260
  stop() {
261
+ this.#removeLifecycleListeners();
262
+
129
263
  clearTimeout(this.#timeoutHandle);
130
264
  cancelAnimationFrame(this.#animationFrameHandle);
131
265
  this.#isRunning = false;
266
+ this.#isSuspended = false;
132
267
 
133
268
  this.#timeoutHandle = -1;
134
269
  this.#animationFrameHandle = -1;