@codexo/exojs 0.6.11 → 0.6.12
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/CHANGELOG.md +40 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/math/index.d.ts +1 -0
- package/dist/esm/math/swept-collision.d.ts +90 -0
- package/dist/esm/math/swept-collision.js +255 -0
- package/dist/esm/math/swept-collision.js.map +1 -0
- package/dist/exo.esm.js +252 -1
- package/dist/exo.esm.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,46 @@ All notable changes to ExoJS are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.6.12] - 2026-05-02
|
|
8
|
+
|
|
9
|
+
Adds swept (continuous) collision detection. Pure-math addition —
|
|
10
|
+
prevents fast-moving shapes from tunneling through stationary
|
|
11
|
+
colliders during a single frame's update.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`sweepRectangle(moving, deltaX, deltaY, target)`** — swept AABB
|
|
16
|
+
vs AABB via the slab method. Returns `SweptHit | null` with time
|
|
17
|
+
of impact `t ∈ [0..1]`, contact position `(x, y)`, and surface
|
|
18
|
+
normal `(normalX, normalY)`. Handles already-overlapping case
|
|
19
|
+
(returns `t = 0` with deepest-penetration axis as normal).
|
|
20
|
+
- **`sweepCircleVsCircle(moving, deltaX, deltaY, target)`** —
|
|
21
|
+
closed-form quadratic solution.
|
|
22
|
+
- **`sweepCircleVsRectangle(moving, deltaX, deltaY, target)`** —
|
|
23
|
+
v1 uses the simple expanded-AABB fallback (rectangle expanded
|
|
24
|
+
by circle radius, treated as AABB swept against zero-sized
|
|
25
|
+
moving circle). Over-collides slightly at corners — true
|
|
26
|
+
Minkowski corner rounding is V2.
|
|
27
|
+
- **`sweepRectangleAgainst(moving, dx, dy, targets)`** /
|
|
28
|
+
**`sweepCircleAgainst(moving, dx, dy, targets)`** — earliest
|
|
29
|
+
hit against an array of static colliders. Broad-phase swept-AABB
|
|
30
|
+
early-out per target.
|
|
31
|
+
- **`substepSweep(fromX, fromY, deltaX, deltaY, maxStepSize)`** —
|
|
32
|
+
generator that yields `(x, y, t)` snapshots along a movement
|
|
33
|
+
vector at fixed intervals. Use this for arbitrary shape pairs
|
|
34
|
+
that lack a closed-form swept test: iterate, place shape at
|
|
35
|
+
each snapshot, run discrete intersection.
|
|
36
|
+
- **`SweptHit` interface** exported.
|
|
37
|
+
|
|
38
|
+
### Notes
|
|
39
|
+
|
|
40
|
+
- Pure math only — no Scene / RenderNode / Physics integration. User
|
|
41
|
+
code calls these in their game's update step.
|
|
42
|
+
- v1 covers the common cases (AABB + Circle). Polygon-vs-anything
|
|
43
|
+
swept tests are V2 (use `substepSweep` as a fallback for now).
|
|
44
|
+
- Returns the hit; does NOT compute response. Sliding / bouncing /
|
|
45
|
+
velocity adjustment is the caller's responsibility.
|
|
46
|
+
|
|
7
47
|
## [0.6.11] - 2026-05-02
|
|
8
48
|
|
|
9
49
|
Adds a fluent-builder Tween / Animation system. Pure addition — no
|
package/dist/esm/index.js
CHANGED
|
@@ -58,6 +58,7 @@ export { Random } from './math/Random.js';
|
|
|
58
58
|
export { Segment } from './math/Segment.js';
|
|
59
59
|
export { Size } from './math/Size.js';
|
|
60
60
|
export { PolarVector } from './math/PolarVector.js';
|
|
61
|
+
export { substepSweep, sweepCircleAgainst, sweepCircleVsCircle, sweepCircleVsRectangle, sweepRectangle, sweepRectangleAgainst } from './math/swept-collision.js';
|
|
61
62
|
export { ColorAffector } from './particles/affectors/ColorAffector.js';
|
|
62
63
|
export { ForceAffector } from './particles/affectors/ForceAffector.js';
|
|
63
64
|
export { ScaleAffector } from './particles/affectors/ScaleAffector.js';
|
package/dist/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/esm/math/index.d.ts
CHANGED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Rectangle } from './Rectangle';
|
|
2
|
+
import type { CircleLike } from './CircleLike';
|
|
3
|
+
/**
|
|
4
|
+
* Result of a swept-collision query. The moving shape's reference point
|
|
5
|
+
* is at `(x, y)` at impact, having travelled fraction `t` ∈ [0..1] of
|
|
6
|
+
* the requested move (where 0 = no movement, 1 = full move). The
|
|
7
|
+
* `(normalX, normalY)` vector is the contact normal pointing AWAY from
|
|
8
|
+
* the target (suitable for sliding response: project the remaining
|
|
9
|
+
* velocity onto the perpendicular).
|
|
10
|
+
*/
|
|
11
|
+
export interface SweptHit {
|
|
12
|
+
readonly t: number;
|
|
13
|
+
readonly x: number;
|
|
14
|
+
readonly y: number;
|
|
15
|
+
readonly normalX: number;
|
|
16
|
+
readonly normalY: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Swept axis-aligned box vs. axis-aligned box.
|
|
20
|
+
*
|
|
21
|
+
* Uses the separating-axis slab method: for each axis we compute the entry
|
|
22
|
+
* and exit times of the moving box's slab vs the static box's slab, then
|
|
23
|
+
* combine. `t` is the fraction of the requested move at which first contact
|
|
24
|
+
* occurs (0 = already overlapping at start, 1 = just barely reaches).
|
|
25
|
+
*
|
|
26
|
+
* Already-overlapping case (tEntry < 0 overall): returns `t = 0` with the
|
|
27
|
+
* normal of the deepest-penetration axis, allowing callers to handle the
|
|
28
|
+
* "I'm already inside" situation without a separate discrete test.
|
|
29
|
+
*/
|
|
30
|
+
export declare function sweepRectangle(moving: Rectangle, deltaX: number, deltaY: number, target: Rectangle): SweptHit | null;
|
|
31
|
+
/**
|
|
32
|
+
* Swept circle vs. axis-aligned box.
|
|
33
|
+
*
|
|
34
|
+
* **V1 implementation** uses the simple Minkowski expansion fallback:
|
|
35
|
+
* the target rectangle is expanded by `circle.radius` on all sides, then
|
|
36
|
+
* `sweepRectangle` is run treating the circle centre as a zero-sized moving
|
|
37
|
+
* box. This over-collides at rectangle corners (the circle collides with the
|
|
38
|
+
* expanded-rect's flat face when geometrically it should curve around the
|
|
39
|
+
* corner), producing slightly early hits in corner-quadrant trajectories —
|
|
40
|
+
* a known and acceptable accuracy trade-off for V1.
|
|
41
|
+
*
|
|
42
|
+
* TODO (V2): Replace with the full Minkowski rounded-rectangle formulation
|
|
43
|
+
* that handles the four corner quadrants with per-corner circle-vs-circle
|
|
44
|
+
* sub-tests.
|
|
45
|
+
*/
|
|
46
|
+
export declare function sweepCircleVsRectangle(moving: CircleLike, deltaX: number, deltaY: number, target: Rectangle): SweptHit | null;
|
|
47
|
+
/**
|
|
48
|
+
* Swept circle vs. stationary circle.
|
|
49
|
+
*
|
|
50
|
+
* Solves `|(moving.centre + delta*t) − target.centre|² = (r1+r2)²` for t,
|
|
51
|
+
* yielding a quadratic. Returns the smaller root if it is in [0, 1].
|
|
52
|
+
*
|
|
53
|
+
* Already-overlapping case: returns `{ t: 0 }` with the normal pointing from
|
|
54
|
+
* target → moving (or an arbitrary normal if both centres coincide).
|
|
55
|
+
*/
|
|
56
|
+
export declare function sweepCircleVsCircle(moving: CircleLike, deltaX: number, deltaY: number, target: CircleLike): SweptHit | null;
|
|
57
|
+
/**
|
|
58
|
+
* Returns the earliest `SweptHit` against an array of rectangle targets, or
|
|
59
|
+
* `null` if none are hit.
|
|
60
|
+
*
|
|
61
|
+
* Optimisation: before testing each target individually the swept AABB of the
|
|
62
|
+
* moving rectangle is computed once; targets whose AABB does not overlap the
|
|
63
|
+
* swept AABB are skipped.
|
|
64
|
+
*/
|
|
65
|
+
export declare function sweepRectangleAgainst(moving: Rectangle, deltaX: number, deltaY: number, targets: ReadonlyArray<Rectangle>): SweptHit | null;
|
|
66
|
+
/**
|
|
67
|
+
* Returns the earliest `SweptHit` against an array of circle targets, or
|
|
68
|
+
* `null` if none are hit.
|
|
69
|
+
*
|
|
70
|
+
* Optimisation: the swept AABB of the moving circle is computed once and used
|
|
71
|
+
* to skip targets that cannot possibly be reached.
|
|
72
|
+
*/
|
|
73
|
+
export declare function sweepCircleAgainst(moving: CircleLike, deltaX: number, deltaY: number, targets: ReadonlyArray<CircleLike>): SweptHit | null;
|
|
74
|
+
/**
|
|
75
|
+
* Generator that yields evenly-spaced position snapshots along a movement
|
|
76
|
+
* vector so the caller can run their own discrete intersection check at each
|
|
77
|
+
* step. Useful for arbitrary shape pairs that lack a closed-form swept test.
|
|
78
|
+
*
|
|
79
|
+
* `maxStepSize` controls the step granularity — smaller values produce more
|
|
80
|
+
* accurate detection but more iterations. Use the smallest dimension of the
|
|
81
|
+
* smaller collider as a sensible default.
|
|
82
|
+
*
|
|
83
|
+
* Always yields at least 2 snapshots (t=0 and t=1), even for zero-length
|
|
84
|
+
* deltas.
|
|
85
|
+
*/
|
|
86
|
+
export declare function substepSweep(fromX: number, fromY: number, deltaX: number, deltaY: number, maxStepSize: number): IterableIterator<{
|
|
87
|
+
x: number;
|
|
88
|
+
y: number;
|
|
89
|
+
t: number;
|
|
90
|
+
}>;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { Rectangle } from './Rectangle.js';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// sweepRectangle — AABB vs AABB slab method
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
/**
|
|
7
|
+
* Swept axis-aligned box vs. axis-aligned box.
|
|
8
|
+
*
|
|
9
|
+
* Uses the separating-axis slab method: for each axis we compute the entry
|
|
10
|
+
* and exit times of the moving box's slab vs the static box's slab, then
|
|
11
|
+
* combine. `t` is the fraction of the requested move at which first contact
|
|
12
|
+
* occurs (0 = already overlapping at start, 1 = just barely reaches).
|
|
13
|
+
*
|
|
14
|
+
* Already-overlapping case (tEntry < 0 overall): returns `t = 0` with the
|
|
15
|
+
* normal of the deepest-penetration axis, allowing callers to handle the
|
|
16
|
+
* "I'm already inside" situation without a separate discrete test.
|
|
17
|
+
*/
|
|
18
|
+
function sweepRectangle(moving, deltaX, deltaY, target) {
|
|
19
|
+
const movMinX = moving.x;
|
|
20
|
+
const movMaxX = moving.x + moving.width;
|
|
21
|
+
const movMinY = moving.y;
|
|
22
|
+
const movMaxY = moving.y + moving.height;
|
|
23
|
+
const tarMinX = target.x;
|
|
24
|
+
const tarMaxX = target.x + target.width;
|
|
25
|
+
const tarMinY = target.y;
|
|
26
|
+
const tarMaxY = target.y + target.height;
|
|
27
|
+
// X axis
|
|
28
|
+
let tEntryX = -Infinity;
|
|
29
|
+
let tExitX = Infinity;
|
|
30
|
+
if (deltaX > 0) {
|
|
31
|
+
tEntryX = (tarMinX - movMaxX) / deltaX;
|
|
32
|
+
tExitX = (tarMaxX - movMinX) / deltaX;
|
|
33
|
+
}
|
|
34
|
+
else if (deltaX < 0) {
|
|
35
|
+
tEntryX = (tarMaxX - movMinX) / deltaX;
|
|
36
|
+
tExitX = (tarMinX - movMaxX) / deltaX;
|
|
37
|
+
}
|
|
38
|
+
else if (movMaxX <= tarMinX || movMinX >= tarMaxX) {
|
|
39
|
+
// No movement on X and no static overlap — can never collide
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
// Y axis
|
|
43
|
+
let tEntryY = -Infinity;
|
|
44
|
+
let tExitY = Infinity;
|
|
45
|
+
if (deltaY > 0) {
|
|
46
|
+
tEntryY = (tarMinY - movMaxY) / deltaY;
|
|
47
|
+
tExitY = (tarMaxY - movMinY) / deltaY;
|
|
48
|
+
}
|
|
49
|
+
else if (deltaY < 0) {
|
|
50
|
+
tEntryY = (tarMaxY - movMinY) / deltaY;
|
|
51
|
+
tExitY = (tarMinY - movMaxY) / deltaY;
|
|
52
|
+
}
|
|
53
|
+
else if (movMaxY <= tarMinY || movMinY >= tarMaxY) {
|
|
54
|
+
// No movement on Y and no static overlap — can never collide
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const tEntry = Math.max(tEntryX, tEntryY);
|
|
58
|
+
const tExit = Math.min(tExitX, tExitY);
|
|
59
|
+
// No overlap window
|
|
60
|
+
if (tEntry > tExit || tExit < 0 || tEntry > 1) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const t = Math.max(0, tEntry);
|
|
64
|
+
const hitX = moving.x + deltaX * t;
|
|
65
|
+
const hitY = moving.y + deltaY * t;
|
|
66
|
+
// Normal is on the axis whose slab entry was latest.
|
|
67
|
+
// Already-overlapping: use the deepest-penetration axis normal.
|
|
68
|
+
let normalX = 0;
|
|
69
|
+
let normalY = 0;
|
|
70
|
+
if (tEntry <= 0) {
|
|
71
|
+
// Already overlapping — pick the axis with least penetration
|
|
72
|
+
const overlapX = Math.min(movMaxX - tarMinX, tarMaxX - movMinX);
|
|
73
|
+
const overlapY = Math.min(movMaxY - tarMinY, tarMaxY - movMinY);
|
|
74
|
+
if (overlapX < overlapY) {
|
|
75
|
+
normalX = movMinX < tarMinX ? -1 : 1;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
normalY = movMinY < tarMinY ? -1 : 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else if (tEntryX > tEntryY) {
|
|
82
|
+
// X axis had the latest entry
|
|
83
|
+
normalX = deltaX > 0 ? -1 : 1;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// Y axis had the latest entry
|
|
87
|
+
normalY = deltaY > 0 ? -1 : 1;
|
|
88
|
+
}
|
|
89
|
+
return { t, x: hitX, y: hitY, normalX, normalY };
|
|
90
|
+
}
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// sweepCircleVsRectangle — expanded-AABB simple fallback (V1)
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
/**
|
|
95
|
+
* Swept circle vs. axis-aligned box.
|
|
96
|
+
*
|
|
97
|
+
* **V1 implementation** uses the simple Minkowski expansion fallback:
|
|
98
|
+
* the target rectangle is expanded by `circle.radius` on all sides, then
|
|
99
|
+
* `sweepRectangle` is run treating the circle centre as a zero-sized moving
|
|
100
|
+
* box. This over-collides at rectangle corners (the circle collides with the
|
|
101
|
+
* expanded-rect's flat face when geometrically it should curve around the
|
|
102
|
+
* corner), producing slightly early hits in corner-quadrant trajectories —
|
|
103
|
+
* a known and acceptable accuracy trade-off for V1.
|
|
104
|
+
*
|
|
105
|
+
* TODO (V2): Replace with the full Minkowski rounded-rectangle formulation
|
|
106
|
+
* that handles the four corner quadrants with per-corner circle-vs-circle
|
|
107
|
+
* sub-tests.
|
|
108
|
+
*/
|
|
109
|
+
function sweepCircleVsRectangle(moving, deltaX, deltaY, target) {
|
|
110
|
+
const r = moving.radius;
|
|
111
|
+
// Expanded target: grow each side by the circle radius
|
|
112
|
+
const expanded = new Rectangle(target.x - r, target.y - r, target.width + r * 2, target.height + r * 2);
|
|
113
|
+
// Treat the circle centre as a zero-sized moving box
|
|
114
|
+
const centreBox = new Rectangle(moving.x, moving.y, 0, 0);
|
|
115
|
+
return sweepRectangle(centreBox, deltaX, deltaY, expanded);
|
|
116
|
+
}
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// sweepCircleVsCircle — quadratic equation
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
/**
|
|
121
|
+
* Swept circle vs. stationary circle.
|
|
122
|
+
*
|
|
123
|
+
* Solves `|(moving.centre + delta*t) − target.centre|² = (r1+r2)²` for t,
|
|
124
|
+
* yielding a quadratic. Returns the smaller root if it is in [0, 1].
|
|
125
|
+
*
|
|
126
|
+
* Already-overlapping case: returns `{ t: 0 }` with the normal pointing from
|
|
127
|
+
* target → moving (or an arbitrary normal if both centres coincide).
|
|
128
|
+
*/
|
|
129
|
+
function sweepCircleVsCircle(moving, deltaX, deltaY, target) {
|
|
130
|
+
const dx = moving.x - target.x;
|
|
131
|
+
const dy = moving.y - target.y;
|
|
132
|
+
const r = moving.radius + target.radius;
|
|
133
|
+
const a = deltaX * deltaX + deltaY * deltaY;
|
|
134
|
+
const b = 2 * (dx * deltaX + dy * deltaY);
|
|
135
|
+
const c = dx * dx + dy * dy - r * r;
|
|
136
|
+
// Already overlapping at start
|
|
137
|
+
if (c <= 0) {
|
|
138
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
139
|
+
const normalX = dist > 0 ? dx / dist : 1;
|
|
140
|
+
const normalY = dist > 0 ? dy / dist : 0;
|
|
141
|
+
return { t: 0, x: moving.x, y: moving.y, normalX, normalY };
|
|
142
|
+
}
|
|
143
|
+
// No movement
|
|
144
|
+
if (a === 0) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
const disc = b * b - 4 * a * c;
|
|
148
|
+
if (disc < 0) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
const t = (-b - Math.sqrt(disc)) / (2 * a);
|
|
152
|
+
if (t < 0 || t > 1) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
const hitX = moving.x + deltaX * t;
|
|
156
|
+
const hitY = moving.y + deltaY * t;
|
|
157
|
+
// Normal points from target centre → hit circle centre
|
|
158
|
+
const normalX = (hitX - target.x) / r;
|
|
159
|
+
const normalY = (hitY - target.y) / r;
|
|
160
|
+
return { t, x: hitX, y: hitY, normalX, normalY };
|
|
161
|
+
}
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// Batch helpers — sweep a shape against multiple targets
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
/**
|
|
166
|
+
* Returns the earliest `SweptHit` against an array of rectangle targets, or
|
|
167
|
+
* `null` if none are hit.
|
|
168
|
+
*
|
|
169
|
+
* Optimisation: before testing each target individually the swept AABB of the
|
|
170
|
+
* moving rectangle is computed once; targets whose AABB does not overlap the
|
|
171
|
+
* swept AABB are skipped.
|
|
172
|
+
*/
|
|
173
|
+
function sweepRectangleAgainst(moving, deltaX, deltaY, targets) {
|
|
174
|
+
if (targets.length === 0) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
// Swept AABB of the moving rectangle (broad-phase skip)
|
|
178
|
+
const sweptMinX = Math.min(moving.x, moving.x + deltaX);
|
|
179
|
+
const sweptMaxX = Math.max(moving.x + moving.width, moving.x + moving.width + deltaX);
|
|
180
|
+
const sweptMinY = Math.min(moving.y, moving.y + deltaY);
|
|
181
|
+
const sweptMaxY = Math.max(moving.y + moving.height, moving.y + moving.height + deltaY);
|
|
182
|
+
let earliest = null;
|
|
183
|
+
for (const target of targets) {
|
|
184
|
+
// Broad-phase: skip if swept AABB doesn't overlap target AABB
|
|
185
|
+
if (sweptMaxX <= target.x
|
|
186
|
+
|| sweptMinX >= target.x + target.width
|
|
187
|
+
|| sweptMaxY <= target.y
|
|
188
|
+
|| sweptMinY >= target.y + target.height) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const hit = sweepRectangle(moving, deltaX, deltaY, target);
|
|
192
|
+
if (hit !== null && (earliest === null || hit.t < earliest.t)) {
|
|
193
|
+
earliest = hit;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return earliest;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Returns the earliest `SweptHit` against an array of circle targets, or
|
|
200
|
+
* `null` if none are hit.
|
|
201
|
+
*
|
|
202
|
+
* Optimisation: the swept AABB of the moving circle is computed once and used
|
|
203
|
+
* to skip targets that cannot possibly be reached.
|
|
204
|
+
*/
|
|
205
|
+
function sweepCircleAgainst(moving, deltaX, deltaY, targets) {
|
|
206
|
+
if (targets.length === 0) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
// Swept AABB of the moving circle
|
|
210
|
+
const sweptMinX = Math.min(moving.x, moving.x + deltaX) - moving.radius;
|
|
211
|
+
const sweptMaxX = Math.max(moving.x, moving.x + deltaX) + moving.radius;
|
|
212
|
+
const sweptMinY = Math.min(moving.y, moving.y + deltaY) - moving.radius;
|
|
213
|
+
const sweptMaxY = Math.max(moving.y, moving.y + deltaY) + moving.radius;
|
|
214
|
+
let earliest = null;
|
|
215
|
+
for (const target of targets) {
|
|
216
|
+
// Broad-phase: skip if swept AABB doesn't overlap target's AABB
|
|
217
|
+
if (sweptMaxX <= target.x - target.radius
|
|
218
|
+
|| sweptMinX >= target.x + target.radius
|
|
219
|
+
|| sweptMaxY <= target.y - target.radius
|
|
220
|
+
|| sweptMinY >= target.y + target.radius) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const hit = sweepCircleVsCircle(moving, deltaX, deltaY, target);
|
|
224
|
+
if (hit !== null && (earliest === null || hit.t < earliest.t)) {
|
|
225
|
+
earliest = hit;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return earliest;
|
|
229
|
+
}
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// substepSweep — generic fallback iterator
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
/**
|
|
234
|
+
* Generator that yields evenly-spaced position snapshots along a movement
|
|
235
|
+
* vector so the caller can run their own discrete intersection check at each
|
|
236
|
+
* step. Useful for arbitrary shape pairs that lack a closed-form swept test.
|
|
237
|
+
*
|
|
238
|
+
* `maxStepSize` controls the step granularity — smaller values produce more
|
|
239
|
+
* accurate detection but more iterations. Use the smallest dimension of the
|
|
240
|
+
* smaller collider as a sensible default.
|
|
241
|
+
*
|
|
242
|
+
* Always yields at least 2 snapshots (t=0 and t=1), even for zero-length
|
|
243
|
+
* deltas.
|
|
244
|
+
*/
|
|
245
|
+
function* substepSweep(fromX, fromY, deltaX, deltaY, maxStepSize) {
|
|
246
|
+
const length = Math.hypot(deltaX, deltaY);
|
|
247
|
+
const stepCount = Math.max(1, Math.ceil(length / maxStepSize));
|
|
248
|
+
for (let i = 0; i <= stepCount; i++) {
|
|
249
|
+
const t = i / stepCount;
|
|
250
|
+
yield { x: fromX + deltaX * t, y: fromY + deltaY * t, t };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export { substepSweep, sweepCircleAgainst, sweepCircleVsCircle, sweepCircleVsRectangle, sweepRectangle, sweepRectangleAgainst };
|
|
255
|
+
//# sourceMappingURL=swept-collision.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"swept-collision.js","sources":["../../../../src/math/swept-collision.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
|