time for a snapshot
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
# Step 05 — Sweeping in 1D (entry & exit time)
|
||||
|
||||
This is the seed of the whole engine. Get this one *in your bones* and the scary
|
||||
2D `sweptAABB` becomes "do this twice and combine."
|
||||
|
||||
## Concept
|
||||
|
||||
Forget 2D. Forget boxes. We have:
|
||||
|
||||
- a **point** sitting at position `p` on a number line,
|
||||
- moving with velocity `v` — meaning over this one frame it travels a total of
|
||||
`v` units (so at fraction `t` of the frame, it's at `p + v*t`, for `t` from 0 to 1),
|
||||
- and a static **interval** `[min, max]` on that same line.
|
||||
|
||||
Question: **during this frame, for which `t` is the point inside `[min, max]`?**
|
||||
|
||||
### The slab math
|
||||
|
||||
The point reaches `min` when `p + v*t = min`, i.e. `t = (min - p) / v`.
|
||||
Likewise it reaches `max` at `t = (max - p) / v`.
|
||||
```
|
||||
t1 = (min - p) / v
|
||||
t2 = (max - p) / v
|
||||
```
|
||||
|
||||
If `v` is **negative** (moving left), the point hits `max` *before* `min`, so
|
||||
`t1 > t2`. We always want `entry` to be the smaller and `exit` the larger, so
|
||||
**swap them if they're out of order**. Then:
|
||||
|
||||
- `entry` = the time the point *enters* the interval,
|
||||
- `exit` = the time it *leaves*.
|
||||
|
||||
> These can be negative or greater than 1 — that just means the crossing happens
|
||||
> before this frame started or after it ends. Don't clamp here; the caller (step
|
||||
> 06/08) decides whether `entry` falls within `[0, 1]`. Keeping the raw numbers
|
||||
> is what lets us combine axes later.
|
||||
|
||||
### The `v == 0` edge case
|
||||
|
||||
If the point isn't moving (`v == 0`), it never *crosses* an edge — dividing by
|
||||
zero is meaningless. Instead: it's either already inside the interval for the
|
||||
whole frame, or never. So:
|
||||
|
||||
- if `min <= p <= max`: it's inside the entire time → `entry = -Infinity`,
|
||||
`exit = +Infinity`.
|
||||
- otherwise: it never overlaps → return `null`.
|
||||
|
||||
(Those infinities are deliberate: in 2D they let a non-moving axis say "I'm not
|
||||
the axis that limits the collision," without breaking the `max`/`min` combine
|
||||
step. You'll see why in step 06.)
|
||||
|
||||
## Task
|
||||
|
||||
Implement `sweepInterval(p, v, min, max)` in `sweep1d.ts`. Return
|
||||
`{ entry, exit }`, or `null` only in the not-moving-and-outside case.
|
||||
|
||||
```sh
|
||||
bun test workshop/steps/05-sweep-1d
|
||||
```
|
||||
@@ -0,0 +1,29 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { sweepInterval } from "./sweep1d.ts";
|
||||
|
||||
test("moving right into the interval", () => {
|
||||
// point at 0, moves +10 this frame, interval [5, 8]
|
||||
// enters at (5-0)/10 = 0.5, exits at (8-0)/10 = 0.8
|
||||
const s = sweepInterval(0, 10, 5, 8)!;
|
||||
expect(s.entry).toBeCloseTo(0.5);
|
||||
expect(s.exit).toBeCloseTo(0.8);
|
||||
});
|
||||
|
||||
test("moving left: entry/exit are swapped into order", () => {
|
||||
// point at 10, moves -10, interval [5, 8]
|
||||
// crosses 8 at t=0.2, crosses 5 at t=0.5 -> entry 0.2, exit 0.5
|
||||
const s = sweepInterval(10, -10, 5, 8)!;
|
||||
expect(s.entry).toBeCloseTo(0.2);
|
||||
expect(s.exit).toBeCloseTo(0.5);
|
||||
expect(s.entry).toBeLessThanOrEqual(s.exit);
|
||||
});
|
||||
|
||||
test("not moving but already inside -> infinite span", () => {
|
||||
const s = sweepInterval(6, 0, 5, 8)!;
|
||||
expect(s.entry).toBe(-Infinity);
|
||||
expect(s.exit).toBe(Infinity);
|
||||
});
|
||||
|
||||
test("not moving and outside -> null", () => {
|
||||
expect(sweepInterval(2, 0, 5, 8)).toBeNull();
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
export type Span = { entry: number; exit: number };
|
||||
|
||||
/**
|
||||
* When is a point at `p`, moving by `v` over the frame, inside [min, max]?
|
||||
*
|
||||
* Returns the entry/exit times (t, where the position is p + v*t).
|
||||
* `entry` is always <= `exit`. Values may be < 0 or > 1.
|
||||
*
|
||||
* Special case: if v === 0, return {entry:-Infinity, exit:Infinity} when the
|
||||
* point is already inside [min, max], otherwise return null.
|
||||
*/
|
||||
export function sweepInterval(
|
||||
p: number,
|
||||
v: number,
|
||||
min: number,
|
||||
max: number,
|
||||
): Span | null {
|
||||
if (v === 0) {
|
||||
return isInBetween(p, min, max)
|
||||
? {
|
||||
entry: -Infinity,
|
||||
exit: +Infinity,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
let t1 = (min - p) / v;
|
||||
let t2 = (max - p) / v;
|
||||
|
||||
if (t1 > t2) {
|
||||
[t1, t2] = [t2, t1];
|
||||
}
|
||||
|
||||
return {
|
||||
entry: t1,
|
||||
exit: t2,
|
||||
};
|
||||
}
|
||||
|
||||
function isInBetween(p: number, min: number, max: number): boolean {
|
||||
return min <= p && p <= max;
|
||||
}
|
||||
Reference in New Issue
Block a user