@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.
- package/package.json +1 -1
- package/src/core/bvh2/bvh3/BVH.d.ts.map +1 -1
- package/src/core/bvh2/bvh3/BVH.js +158 -4
- package/src/core/geom/3d/shape/CylinderShape3D.d.ts +56 -0
- package/src/core/geom/3d/shape/CylinderShape3D.d.ts.map +1 -0
- package/src/core/geom/3d/shape/CylinderShape3D.js +223 -0
- package/src/core/geom/3d/shape/HeightMapShape3D.d.ts +33 -3
- package/src/core/geom/3d/shape/HeightMapShape3D.d.ts.map +1 -1
- package/src/core/geom/3d/shape/HeightMapShape3D.js +486 -451
- package/src/core/geom/3d/shape/json/shape_to_type.d.ts.map +1 -1
- package/src/core/geom/3d/shape/json/shape_to_type.js +3 -0
- package/src/core/geom/3d/shape/json/type_adapters.d.ts +15 -0
- package/src/core/geom/3d/shape/json/type_adapters.d.ts.map +1 -1
- package/src/core/geom/3d/shape/json/type_adapters.js +16 -0
- package/src/engine/control/first-person/DESIGN_COLLISION.md +365 -302
- package/src/engine/control/first-person/FirstPersonPlayerController.d.ts +1 -3
- package/src/engine/control/first-person/FirstPersonPlayerController.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts +12 -2
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerConfig.js +7 -2
- package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts +13 -0
- package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.d.ts.map +1 -1
- package/src/engine/control/first-person/FirstPersonPlayerControllerSystem.js +16 -2
- package/src/engine/control/first-person/TODO.md +13 -11
- package/src/engine/control/first-person/abilities/WallJump.d.ts.map +1 -1
- package/src/engine/control/first-person/abilities/WallJump.js +11 -3
- package/src/engine/control/first-person/abilities/WallRun.d.ts.map +1 -1
- package/src/engine/control/first-person/abilities/WallRun.js +30 -35
- package/src/engine/control/first-person/collision/KinematicMover.d.ts +35 -5
- package/src/engine/control/first-person/collision/KinematicMover.d.ts.map +1 -1
- package/src/engine/control/first-person/collision/KinematicMover.js +634 -424
- package/src/engine/control/first-person/prototype_first_person_controller.js +1003 -901
- package/src/engine/physics/PLAN.md +943 -767
- package/src/engine/physics/body/BodyStorage.d.ts +9 -0
- package/src/engine/physics/body/BodyStorage.d.ts.map +1 -1
- package/src/engine/physics/body/BodyStorage.js +23 -0
- package/src/engine/physics/broadphase/generate_pairs.d.ts.map +1 -1
- package/src/engine/physics/broadphase/generate_pairs.js +7 -0
- package/src/engine/physics/ccd/linear_sweep.d.ts +97 -0
- package/src/engine/physics/ccd/linear_sweep.d.ts.map +1 -0
- package/src/engine/physics/ccd/linear_sweep.js +238 -0
- package/src/engine/physics/ecs/PhysicsSystem.d.ts +18 -3
- package/src/engine/physics/ecs/PhysicsSystem.d.ts.map +1 -1
- package/src/engine/physics/ecs/PhysicsSystem.js +59 -8
- package/src/engine/physics/ecs/RigidBodyFlags.d.ts +6 -0
- package/src/engine/physics/ecs/RigidBodyFlags.d.ts.map +1 -1
- package/src/engine/physics/ecs/RigidBodyFlags.js +6 -0
- package/src/engine/physics/narrowphase/box_triangle_contact.js +811 -811
- package/src/engine/physics/narrowphase/compute_penetration.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/compute_penetration.js +325 -323
- package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts +27 -8
- package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/decomposition/heightmap_enumerate_triangles.js +235 -204
- package/src/engine/physics/narrowphase/narrowphase_step.d.ts.map +1 -1
- package/src/engine/physics/narrowphase/narrowphase_step.js +70 -13
- package/src/engine/physics/queries/overlap_shape.d.ts.map +1 -1
- package/src/engine/physics/queries/overlap_shape.js +185 -183
- package/src/engine/simulation/Ticker.d.ts +14 -0
- package/src/engine/simulation/Ticker.d.ts.map +1 -1
- 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":"
|
|
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
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* @param {
|
|
74
|
-
*
|
|
75
|
-
* @param {{x:number,y:number,z:number
|
|
76
|
-
*
|
|
77
|
-
* @param {
|
|
78
|
-
*
|
|
79
|
-
* @param {number}
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* @
|
|
83
|
-
* @
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
candidate_posed.
|
|
160
|
-
candidate_posed.
|
|
161
|
-
candidate_posed.
|
|
162
|
-
candidate_posed.
|
|
163
|
-
candidate_posed.
|
|
164
|
-
candidate_posed.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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;
|
|
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;
|