time for a snapshot

This commit is contained in:
Schluffe
2026-07-11 17:48:28 +02:00
parent 135b7c3042
commit 7262752e16
47 changed files with 1712 additions and 61 deletions
+59
View File
@@ -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();
});
+42
View File
@@ -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;
}