@typeonce/ritmo-math 0.13.0 → 0.14.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/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # @typeonce/ritmo-math
2
2
 
3
+ ## 0.14.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e169263: **`Collide2` collides circles with line segments, so walls can run at any angle.** The new `Segment2` module is the plain segment `Collide2` reads every frame (`Segment2.between(a, b)`, `closestPoint`, `overlapsCircle`), like `Box2` for boxes. A segment is a two-sided wall with round ends; both ends at one point make a round post. `Collide2.sweepCircleSegment` finds the first contact with one, next to `sweepCircleBox`. A map traced from survey data, a slanted fence, or a polygon's edges no longer need a hand-written port of `moveCircle`.
8
+
9
+ ```ts
10
+ const fence = Segment2.between({ x: 0, y: 0 }, { x: 40, y: 30 });
11
+ const moved = Collide2.moveCircle(position, delta, radius, {
12
+ boxes: rooms,
13
+ segments: [fence],
14
+ });
15
+ ```
16
+
17
+ - **Breaking: `moveCircle` and `pushOut` take the walls by kind, as `Collide2.Walls`** (`{ boxes?, segments? }`), instead of an array of boxes. An array no longer compiles (`pass walls by kind: { boxes: walls } or { segments: walls }`):
18
+
19
+ ```diff
20
+ -Collide2.moveCircle(position, delta, radius, walls)
21
+ +Collide2.moveCircle(position, delta, radius, { boxes: walls })
22
+ -Collide2.pushOut(position, radius, walls)
23
+ +Collide2.pushOut(position, radius, { boxes: walls })
24
+ ```
25
+
26
+ - **Breaking: `sweepCircleBoxes` is `sweepCircle`**, the first contact with any of the walls (line of sight with a radius of 0, as before):
27
+
28
+ ```diff
29
+ -Collide2.sweepCircleBoxes(eye, Vec2.subtract(target, eye), 0, walls) === undefined
30
+ +Collide2.sweepCircle(eye, Vec2.subtract(target, eye), 0, { boxes: walls }) === undefined
31
+ ```
32
+
33
+ Results against boxes are unchanged.
34
+
35
+ ### Patch Changes
36
+
37
+ - Updated dependencies [c3719a5]
38
+ - Updated dependencies [70d054e]
39
+ - Updated dependencies [adb4066]
40
+ - Updated dependencies [97e95fb]
41
+ - Updated dependencies [dbfbd9d]
42
+ - Updated dependencies [343660e]
43
+ - Updated dependencies [d24258e]
44
+ - Updated dependencies [cfe332a]
45
+ - Updated dependencies [8aa1e85]
46
+ - @typeonce/ritmo@0.14.0
47
+
3
48
  ## 0.13.0
4
49
 
5
50
  ### Minor Changes
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  Math for ritmo games, in two kinds:
4
4
 
5
5
  - **Validated values**: `Scalar` (`Finite`, `NonNegative`, `Positive`), `Vector2`, `Size2`, and `Aabb`. Each has constructors that validate input (`result`, `option`), a guard `is`, and one `codec`, a Standard Schema: `Descriptor.ConstructedComponent(Vector2.codec)` saves the value and checks it on restore. Inside a game's own schema, build the field from the guard (`Schema.declare(Vector2.is)` in Effect, `z.custom(Vector2.is)` in Zod). `Random.codec` does the same for a generator kept in a resource.
6
- - **Hot-path helpers** on plain numbers, for code that runs every step: `Vec2` vector arithmetic (validated `Vector2` values pass straight in), `Box2` edge boxes, `Collide2` swept circle collision that slides along walls and never tunnels through them, a `SpatialGrid` broad phase that allocates nothing per step, a `FlowField` that leads bodies around walls, a deterministic `Random` whose whole state is one integer (keep it in a resource, so runs replay), and `Ops`, math functions that return the same bits on every engine.
6
+ - **Hot-path helpers** on plain numbers, for code that runs every step: `Vec2` vector arithmetic (validated `Vector2` values pass straight in), `Box2` edge boxes, `Segment2` walls at any angle, `Collide2` swept circle collision against both that slides along walls and never tunnels through them, a `SpatialGrid` broad phase that allocates nothing per step, a `FlowField` that leads bodies around walls, a deterministic `Random` whose whole state is one integer (keep it in a resource, so runs replay), and `Ops`, math functions that return the same bits on every engine.
7
7
 
8
8
  ```ts
9
9
  import { Collide2, FlowField, Random, SpatialGrid, Vec2 } from "@typeonce/ritmo-math"
@@ -12,7 +12,7 @@ import { Collide2, FlowField, Random, SpatialGrid, Vec2 } from "@typeonce/ritmo-
12
12
  paths.setWalls(walls, radius)
13
13
  paths.setGoal(player)
14
14
  const way = paths.way(enemy) ?? Vec2.normalizeOr(Vec2.subtract(player, enemy), Vec2.zero)
15
- const moved = Collide2.moveCircle(enemy, Vec2.scale(way, speed * dt), radius, walls)
15
+ const moved = Collide2.moveCircle(enemy, Vec2.scale(way, speed * dt), radius, { boxes: walls })
16
16
  ```
17
17
 
18
18
  ```sh
@@ -1,10 +1,12 @@
1
1
  /**
2
- * Continuous collision for moving circles against static boxes and circles.
2
+ * Continuous collision for moving circles against static walls (axis-aligned
3
+ * boxes and line segments) and circles.
3
4
  *
4
- * Movement is swept, not sampled: `sweepCircleBox` finds the first time of
5
+ * Movement is swept, not sampled: `sweepCircle` finds the first time of
5
6
  * contact along the whole displacement, so fast movers (dashes, projectiles)
6
7
  * cannot skip over a wall between two steps. `moveCircle` stops at walls and
7
- * slides along them.
8
+ * slides along them. Walls are passed as {@link Walls}: `Box2` boxes for a
9
+ * grid of rooms, `Segment2` segments for walls at any angle, or both.
8
10
  *
9
11
  * Every query returns `undefined` for "no contact", never throws, and
10
12
  * allocates only its result. Inputs are expected to be finite.
@@ -12,7 +14,7 @@
12
14
  * @example
13
15
  * ```ts
14
16
  * // Move a body by its velocity this step, sliding along the walls.
15
- * const moved = Collide2.moveCircle(position, Vec2.scale(velocity, dt), radius, walls)
17
+ * const moved = Collide2.moveCircle(position, Vec2.scale(velocity, dt), radius, { boxes: walls })
16
18
  * if (moved.blocked) bumpedWall()
17
19
  * ```
18
20
  *
@@ -29,6 +31,7 @@
29
31
  * Moving a circle through walls with sliding.
30
32
  */
31
33
  import type { Box2 } from "./Box2.ts";
34
+ import type { Segment2 } from "./Segment2.ts";
32
35
  import type { Vec2 } from "./Vec2.ts";
33
36
  /**
34
37
  * A contact along a sweep.
@@ -38,10 +41,22 @@ import type { Vec2 } from "./Vec2.ts";
38
41
  export interface Hit {
39
42
  /** Fraction of the displacement travelled before contact, in [0, 1]. */
40
43
  readonly time: number;
41
- /** Unit contact normal, pointing away from the box. */
44
+ /** Unit contact normal, pointing away from the wall. */
42
45
  readonly nx: number;
43
46
  readonly ny: number;
44
47
  }
48
+ /**
49
+ * The static walls a circle collides with: axis-aligned `boxes`, line
50
+ * `segments`, or both. A missing list is no walls of that kind.
51
+ *
52
+ * @category Collision Types
53
+ */
54
+ export interface Walls {
55
+ readonly boxes?: ReadonlyArray<Box2>;
56
+ readonly segments?: ReadonlyArray<Segment2>;
57
+ /** Not a setting: an array of walls is passed by kind. */
58
+ readonly length?: "pass walls by kind: { boxes: walls } or { segments: walls }";
59
+ }
45
60
  /**
46
61
  * Where a moved circle ended up.
47
62
  *
@@ -79,7 +94,18 @@ export declare const rayCircle: (ox: number, oy: number, dx: number, dy: number,
79
94
  */
80
95
  export declare const sweepCircleBox: (origin: Vec2, delta: Vec2, radius: number, box: Box2) => Hit | undefined;
81
96
  /**
82
- * First contact of a moving circle with any of `boxes`, or `undefined`.
97
+ * First contact of a circle of `radius` at `origin` moving by `delta` with
98
+ * `segment`, or `undefined`. Contact is computed against the segment
99
+ * thickened by the radius with round ends (the exact Minkowski sum), so the
100
+ * circle slides along it and around its ends. A circle that starts exactly
101
+ * touching it and moves into it hits at time 0. A circle that starts
102
+ * overlapping the segment is not reported; resolve it with `pushOut` first.
103
+ *
104
+ * @category Queries
105
+ */
106
+ export declare const sweepCircleSegment: (origin: Vec2, delta: Vec2, radius: number, segment: Segment2) => Hit | undefined;
107
+ /**
108
+ * First contact of a moving circle with any of `walls`, or `undefined`.
83
109
  *
84
110
  * With a `radius` of 0 the circle is a point and the sweep is a segment, so
85
111
  * this is also line of sight: `undefined` means nothing blocks the line from
@@ -88,12 +114,12 @@ export declare const sweepCircleBox: (origin: Vec2, delta: Vec2, radius: number,
88
114
  *
89
115
  * @example
90
116
  * ```ts
91
- * const sees = Collide2.sweepCircleBoxes(eye, Vec2.subtract(target, eye), 0, walls) === undefined
117
+ * const sees = Collide2.sweepCircle(eye, Vec2.subtract(target, eye), 0, { boxes: walls }) === undefined
92
118
  * ```
93
119
  *
94
120
  * @category Queries
95
121
  */
96
- export declare const sweepCircleBoxes: (origin: Vec2, delta: Vec2, radius: number, boxes: ReadonlyArray<Box2>) => Hit | undefined;
122
+ export declare const sweepCircle: (origin: Vec2, delta: Vec2, radius: number, walls: Walls) => Hit | undefined;
97
123
  /**
98
124
  * First time in [0, 1] at which a circle of `radius` moving from `origin` by
99
125
  * `delta` touches a circle of `targetRadius` at `target`, or `undefined`. A
@@ -103,20 +129,28 @@ export declare const sweepCircleBoxes: (origin: Vec2, delta: Vec2, radius: numbe
103
129
  */
104
130
  export declare const sweepCircleCircle: (origin: Vec2, delta: Vec2, radius: number, target: Vec2, targetRadius: number) => number | undefined;
105
131
  /**
106
- * Pushes a circle out of every box it overlaps, in order, leaving `skin`
107
- * between it and each wall. Returns the corrected center.
132
+ * Pushes a circle out of every wall it overlaps, boxes first and then
133
+ * segments, each in order, leaving `skin` between it and each wall. Returns
134
+ * the corrected center. A center inside a box leaves through the nearest
135
+ * side; a center exactly on a segment leaves along the segment's left normal.
108
136
  *
109
137
  * @category Movement
110
138
  */
111
- export declare const pushOut: (center: Vec2, radius: number, boxes: ReadonlyArray<Box2>, skin?: number) => Vec2;
139
+ export declare const pushOut: (center: Vec2, radius: number, walls: Walls, skin?: number) => Vec2;
112
140
  /**
113
141
  * Moves a circle by `delta`, stopping at walls and sliding along them (up to
114
142
  * four contacts per call). The whole path is swept, so no displacement can
115
143
  * tunnel through a wall. A circle that starts inside a wall is pushed out
116
- * first. A circle meeting a box's corner dead-on goes around it rather than
117
- * stopping against it.
144
+ * first. A circle meeting a box's corner or a segment's end dead-on goes
145
+ * around it rather than stopping against it.
146
+ *
147
+ * @example
148
+ * ```ts
149
+ * // A map traced from survey data: every wall is a segment.
150
+ * const moved = Collide2.moveCircle(position, delta, radius, { segments: nearbyWalls })
151
+ * ```
118
152
  *
119
153
  * @category Movement
120
154
  */
121
- export declare const moveCircle: (start: Vec2, delta: Vec2, radius: number, boxes: ReadonlyArray<Box2>, skin?: number) => MoveResult;
155
+ export declare const moveCircle: (start: Vec2, delta: Vec2, radius: number, walls: Walls, skin?: number) => MoveResult;
122
156
  //# sourceMappingURL=Collide2.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Collide2.d.ts","sourceRoot":"","sources":["../src/Collide2.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAErC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAErC;;;;GAIG;AACH,MAAM,WAAW,GAAG;IAClB,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CACpB;AAED;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAA;IACvB,oCAAoC;IACpC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAC1B;AAED;;;;;GAKG;AACH,eAAO,MAAM,WAAW,OAAO,CAAA;AAE/B;;;;;;GAMG;AACH,eAAO,MAAM,SAAS,OAAQ,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,KAAK,MAAM,KAAG,MAAM,GAAG,SAWtH,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,WAAY,IAAI,SAAS,IAAI,UAAU,MAAM,OAAO,IAAI,KAAG,GAAG,GAAG,SAyE3F,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,gBAAgB,WAAY,IAAI,SAAS,IAAI,UAAU,MAAM,SAAS,aAAa,CAAC,IAAI,CAAC,KAAG,GAAG,GAAG,SAO9G,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,WACpB,IAAI,SACL,IAAI,UACH,MAAM,UACN,IAAI,gBACE,MAAM,KACnB,MAAM,GAAG,SAMX,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,OAAO,WAAY,IAAI,UAAU,MAAM,SAAS,aAAa,CAAC,IAAI,CAAC,SAAQ,MAAM,KAAiB,IA4B9G,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,UAAU,UAAW,IAAI,SAAS,IAAI,UAAU,MAAM,SAAS,aAAa,CAAC,IAAI,CAAC,SAAQ,MAAM,KAAiB,UAoC7H,CAAA"}
1
+ {"version":3,"file":"Collide2.d.ts","sourceRoot":"","sources":["../src/Collide2.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAErC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAC7C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAErC;;;;GAIG;AACH,MAAM,WAAW,GAAG;IAClB,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,wDAAwD;IACxD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CACpB;AAED;;;;;GAKG;AACH,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC,IAAI,CAAC,CAAA;IACpC,QAAQ,CAAC,QAAQ,CAAC,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAA;IAC3C,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,CAAC,EAAE,6DAA6D,CAAA;CAChF;AAKD;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAA;IACvB,oCAAoC;IACpC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAC1B;AAED;;;;;GAKG;AACH,eAAO,MAAM,WAAW,OAAO,CAAA;AAE/B;;;;;;GAMG;AACH,eAAO,MAAM,SAAS,OAAQ,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,KAAK,MAAM,KAAG,MAAM,GAAG,SAWtH,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,WAAY,IAAI,SAAS,IAAI,UAAU,MAAM,OAAO,IAAI,KAAG,GAAG,GAAG,SAyE3F,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,WAAY,IAAI,SAAS,IAAI,UAAU,MAAM,WAAW,QAAQ,KAAG,GAAG,GAAG,SA0CvG,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,WAAW,WAAY,IAAI,SAAS,IAAI,UAAU,MAAM,SAAS,KAAK,KAAG,GAAG,GAAG,SAW3F,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,WACpB,IAAI,SACL,IAAI,UACH,MAAM,UACN,IAAI,gBACE,MAAM,KACnB,MAAM,GAAG,SAMX,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,OAAO,WAAY,IAAI,UAAU,MAAM,SAAS,KAAK,SAAQ,MAAM,KAAiB,IAqDhG,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,UAAU,UAAW,IAAI,SAAS,IAAI,UAAU,MAAM,SAAS,KAAK,SAAQ,MAAM,KAAiB,UAyD/G,CAAA"}
package/dist/Collide2.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import * as Ops from "./Ops.js";
2
+ const noBoxes = [];
3
+ const noSegments = [];
2
4
  /**
3
5
  * The distance `pushOut` and `moveCircle` keep from walls after a contact
4
6
  * (in world units), so the next sweep starts outside the wall.
@@ -120,7 +122,64 @@ export const sweepCircleBox = (origin, delta, radius, box) => {
120
122
  return { time: enter, nx, ny };
121
123
  };
122
124
  /**
123
- * First contact of a moving circle with any of `boxes`, or `undefined`.
125
+ * First contact of a circle of `radius` at `origin` moving by `delta` with
126
+ * `segment`, or `undefined`. Contact is computed against the segment
127
+ * thickened by the radius with round ends (the exact Minkowski sum), so the
128
+ * circle slides along it and around its ends. A circle that starts exactly
129
+ * touching it and moves into it hits at time 0. A circle that starts
130
+ * overlapping the segment is not reported; resolve it with `pushOut` first.
131
+ *
132
+ * @category Queries
133
+ */
134
+ export const sweepCircleSegment = (origin, delta, radius, segment) => {
135
+ const ex = segment.bx - segment.ax;
136
+ const ey = segment.by - segment.ay;
137
+ const lengthSquared = ex * ex + ey * ey;
138
+ if (lengthSquared > 1e-12) {
139
+ // The flat side facing the circle, `radius` away from the segment's line.
140
+ const length = Math.sqrt(lengthSquared);
141
+ let nx = -ey / length;
142
+ let ny = ex / length;
143
+ let side = (origin.x - segment.ax) * nx + (origin.y - segment.ay) * ny;
144
+ if (side < 0) {
145
+ nx = -nx;
146
+ ny = -ny;
147
+ side = -side;
148
+ }
149
+ if (side < radius) {
150
+ // Beside the line: overlapping when level with the segment, otherwise
151
+ // only its ends are in the way.
152
+ const along = ((origin.x - segment.ax) * ex + (origin.y - segment.ay) * ey) / lengthSquared;
153
+ if (along >= 0 && along <= 1)
154
+ return undefined;
155
+ }
156
+ else {
157
+ const into = delta.x * nx + delta.y * ny;
158
+ if (into < 0) {
159
+ // `0 +` turns a -0 (touching at the start) into 0.
160
+ const time = 0 + (radius - side) / into;
161
+ if (time <= 1) {
162
+ const along = ((origin.x + delta.x * time - segment.ax) * ex + (origin.y + delta.y * time - segment.ay) * ey) / lengthSquared;
163
+ // The thickened segment is convex, so entering through its flat side is the first contact.
164
+ if (along >= 0 && along <= 1)
165
+ return { time, nx, ny };
166
+ }
167
+ }
168
+ }
169
+ }
170
+ const atA = rayCircle(origin.x, origin.y, delta.x, delta.y, segment.ax, segment.ay, radius);
171
+ const atB = lengthSquared > 1e-12 ? rayCircle(origin.x, origin.y, delta.x, delta.y, segment.bx, segment.by, radius) : undefined;
172
+ if (atA === undefined && atB === undefined)
173
+ return undefined;
174
+ const fromA = atB === undefined || (atA !== undefined && atA <= atB);
175
+ const time = fromA ? atA : atB;
176
+ const cx = origin.x + delta.x * time - (fromA ? segment.ax : segment.bx);
177
+ const cy = origin.y + delta.y * time - (fromA ? segment.ay : segment.by);
178
+ const length = Ops.hypot(cx, cy) || 1;
179
+ return { time, nx: cx / length, ny: cy / length };
180
+ };
181
+ /**
182
+ * First contact of a moving circle with any of `walls`, or `undefined`.
124
183
  *
125
184
  * With a `radius` of 0 the circle is a point and the sweep is a segment, so
126
185
  * this is also line of sight: `undefined` means nothing blocks the line from
@@ -129,18 +188,23 @@ export const sweepCircleBox = (origin, delta, radius, box) => {
129
188
  *
130
189
  * @example
131
190
  * ```ts
132
- * const sees = Collide2.sweepCircleBoxes(eye, Vec2.subtract(target, eye), 0, walls) === undefined
191
+ * const sees = Collide2.sweepCircle(eye, Vec2.subtract(target, eye), 0, { boxes: walls }) === undefined
133
192
  * ```
134
193
  *
135
194
  * @category Queries
136
195
  */
137
- export const sweepCircleBoxes = (origin, delta, radius, boxes) => {
196
+ export const sweepCircle = (origin, delta, radius, walls) => {
138
197
  let first;
139
- for (const box of boxes) {
198
+ for (const box of walls.boxes ?? noBoxes) {
140
199
  const hit = sweepCircleBox(origin, delta, radius, box);
141
200
  if (hit !== undefined && (first === undefined || hit.time < first.time))
142
201
  first = hit;
143
202
  }
203
+ for (const segment of walls.segments ?? noSegments) {
204
+ const hit = sweepCircleSegment(origin, delta, radius, segment);
205
+ if (hit !== undefined && (first === undefined || hit.time < first.time))
206
+ first = hit;
207
+ }
144
208
  return first;
145
209
  };
146
210
  /**
@@ -159,15 +223,17 @@ export const sweepCircleCircle = (origin, delta, radius, target, targetRadius) =
159
223
  return rayCircle(origin.x, origin.y, delta.x, delta.y, target.x, target.y, r);
160
224
  };
161
225
  /**
162
- * Pushes a circle out of every box it overlaps, in order, leaving `skin`
163
- * between it and each wall. Returns the corrected center.
226
+ * Pushes a circle out of every wall it overlaps, boxes first and then
227
+ * segments, each in order, leaving `skin` between it and each wall. Returns
228
+ * the corrected center. A center inside a box leaves through the nearest
229
+ * side; a center exactly on a segment leaves along the segment's left normal.
164
230
  *
165
231
  * @category Movement
166
232
  */
167
- export const pushOut = (center, radius, boxes, skin = defaultSkin) => {
233
+ export const pushOut = (center, radius, walls, skin = defaultSkin) => {
168
234
  let x = center.x;
169
235
  let y = center.y;
170
- for (const box of boxes) {
236
+ for (const box of walls.boxes ?? noBoxes) {
171
237
  const closestX = Math.max(box.minX, Math.min(x, box.maxX));
172
238
  const closestY = Math.max(box.minY, Math.min(y, box.maxY));
173
239
  const dx = x - closestX;
@@ -197,26 +263,83 @@ export const pushOut = (center, radius, boxes, skin = defaultSkin) => {
197
263
  y = box.maxY + radius + skin;
198
264
  }
199
265
  }
266
+ for (const segment of walls.segments ?? noSegments) {
267
+ const ex = segment.bx - segment.ax;
268
+ const ey = segment.by - segment.ay;
269
+ const lengthSquared = ex * ex + ey * ey;
270
+ const along = lengthSquared > 1e-12 ? Math.max(0, Math.min(1, ((x - segment.ax) * ex + (y - segment.ay) * ey) / lengthSquared)) : 0;
271
+ const closestX = segment.ax + ex * along;
272
+ const closestY = segment.ay + ey * along;
273
+ const dx = x - closestX;
274
+ const dy = y - closestY;
275
+ const distanceSquared = dx * dx + dy * dy;
276
+ if (distanceSquared >= radius * radius)
277
+ continue;
278
+ if (distanceSquared > 1e-12) {
279
+ const distance = Math.sqrt(distanceSquared);
280
+ x = closestX + (dx / distance) * (radius + skin);
281
+ y = closestY + (dy / distance) * (radius + skin);
282
+ }
283
+ else if (lengthSquared > 1e-12) {
284
+ // Center on the segment: leave along its left normal.
285
+ const length = Math.sqrt(lengthSquared);
286
+ x = closestX - (ey / length) * (radius + skin);
287
+ y = closestY + (ex / length) * (radius + skin);
288
+ }
289
+ else {
290
+ // Center on a round post: leave along +x.
291
+ x = closestX + radius + skin;
292
+ }
293
+ }
200
294
  return { x, y };
201
295
  };
202
296
  /**
203
297
  * Moves a circle by `delta`, stopping at walls and sliding along them (up to
204
298
  * four contacts per call). The whole path is swept, so no displacement can
205
299
  * tunnel through a wall. A circle that starts inside a wall is pushed out
206
- * first. A circle meeting a box's corner dead-on goes around it rather than
207
- * stopping against it.
300
+ * first. A circle meeting a box's corner or a segment's end dead-on goes
301
+ * around it rather than stopping against it.
302
+ *
303
+ * @example
304
+ * ```ts
305
+ * // A map traced from survey data: every wall is a segment.
306
+ * const moved = Collide2.moveCircle(position, delta, radius, { segments: nearbyWalls })
307
+ * ```
208
308
  *
209
309
  * @category Movement
210
310
  */
211
- export const moveCircle = (start, delta, radius, boxes, skin = defaultSkin) => {
212
- let position = pushOut(start, radius, boxes, skin);
311
+ export const moveCircle = (start, delta, radius, walls, skin = defaultSkin) => {
312
+ let position = pushOut(start, radius, walls, skin);
213
313
  let rx = delta.x;
214
314
  let ry = delta.y;
215
315
  let blocked = false;
216
316
  for (let iteration = 0; iteration < 4; iteration++) {
217
317
  if (rx * rx + ry * ry < 1e-12)
218
318
  break;
219
- const hit = sweepCircleBoxes(position, { x: rx, y: ry }, radius, boxes);
319
+ const step = { x: rx, y: ry };
320
+ // The first contact, and whether it is round (a box's corner or a
321
+ // segment's end) rather than a flat side.
322
+ let hit;
323
+ let round = false;
324
+ for (const box of walls.boxes ?? noBoxes) {
325
+ const found = sweepCircleBox(position, step, radius, box);
326
+ if (found === undefined || (hit !== undefined && found.time >= hit.time))
327
+ continue;
328
+ hit = found;
329
+ round = found.nx !== 0 && found.ny !== 0;
330
+ }
331
+ for (const segment of walls.segments ?? noSegments) {
332
+ const found = sweepCircleSegment(position, step, radius, segment);
333
+ if (found === undefined || (hit !== undefined && found.time >= hit.time))
334
+ continue;
335
+ hit = found;
336
+ const ex = segment.bx - segment.ax;
337
+ const ey = segment.by - segment.ay;
338
+ const lengthSquared = ex * ex + ey * ey;
339
+ const along = found.nx * ex + found.ny * ey;
340
+ // A flat side's normal is perpendicular to the segment.
341
+ round = lengthSquared < 1e-12 || along * along > 1e-18 * lengthSquared;
342
+ }
220
343
  if (hit === undefined) {
221
344
  position = { x: position.x + rx, y: position.y + ry };
222
345
  break;
@@ -234,10 +357,10 @@ export const moveCircle = (start, delta, radius, boxes, skin = defaultSkin) => {
234
357
  if (into < 0) {
235
358
  rx -= into * hit.nx;
236
359
  ry -= into * hit.ny;
237
- // Dead-on into a rounded corner (as an exact diagonal meets an
238
- // axis-aligned box) nothing is left to slide along: go around the
239
- // corner instead of stopping against it.
240
- if (hit.nx !== 0 && hit.ny !== 0 && rx * rx + ry * ry < 1e-12 * into * into) {
360
+ // Dead-on into a rounded corner or end (as an exact diagonal meets an
361
+ // axis-aligned box) nothing is left to slide along: go around it
362
+ // instead of stopping against it.
363
+ if (round && rx * rx + ry * ry < 1e-12 * into * into) {
241
364
  const side = rx * -hit.ny + ry * hit.nx < 0 ? -1 : 1;
242
365
  rx = -hit.ny * -into * side;
243
366
  ry = hit.nx * -into * side;
@@ -1 +1 @@
1
- {"version":3,"file":"Collide2.js","sourceRoot":"","sources":["../src/Collide2.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Collide2.js","sourceRoot":"","sources":["../src/Collide2.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Plain line segments stored by their ends, for hot-path collision against
3
+ * walls that are not axis-aligned (a map traced from survey data, a slanted
4
+ * fence, a polygon's edges).
5
+ *
6
+ * Like `Box2`, it is the plain form the collision helpers (`Collide2`) read
7
+ * every frame, built once from whatever the game stores. A wall is
8
+ * two-sided: a circle is kept off it on both sides and around both ends.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * const fence = Segment2.between({ x: 0, y: 0 }, { x: 40, y: 30 })
13
+ * const moved = Collide2.moveCircle(position, delta, radius, { segments: [fence] })
14
+ * ```
15
+ *
16
+ * @module Segment2
17
+ * @docGroup helpers
18
+ *
19
+ * @categoryDescription Segment Types
20
+ * The structural segment shape.
21
+ *
22
+ * @categoryDescription Operations
23
+ * Building segments and measuring them against points and circles.
24
+ */
25
+ import type { Vec2 } from "./Vec2.ts";
26
+ /**
27
+ * The segment from (`ax`, `ay`) to (`bx`, `by`). Both ends may be the same
28
+ * point, which is then a round post.
29
+ *
30
+ * @category Segment Types
31
+ */
32
+ export interface Segment2 {
33
+ readonly ax: number;
34
+ readonly ay: number;
35
+ readonly bx: number;
36
+ readonly by: number;
37
+ }
38
+ /**
39
+ * The segment from `a` to `b`.
40
+ *
41
+ * @category Operations
42
+ */
43
+ export declare const between: (a: Vec2, b: Vec2) => Segment2;
44
+ /**
45
+ * The point of the segment nearest to `point`.
46
+ *
47
+ * @category Operations
48
+ */
49
+ export declare const closestPoint: (segment: Segment2, point: Vec2) => Vec2;
50
+ /**
51
+ * Whether a circle overlaps (or touches) the segment.
52
+ *
53
+ * @category Operations
54
+ */
55
+ export declare const overlapsCircle: (segment: Segment2, center: Vec2, radius: number) => boolean;
56
+ //# sourceMappingURL=Segment2.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Segment2.d.ts","sourceRoot":"","sources":["../src/Segment2.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAErC;;;;;GAKG;AACH,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CACpB;AAED;;;;GAIG;AACH,eAAO,MAAM,OAAO,MAAO,IAAI,KAAK,IAAI,KAAG,QAAoD,CAAA;AAE/F;;;;GAIG;AACH,eAAO,MAAM,YAAY,YAAa,QAAQ,SAAS,IAAI,KAAG,IAO7D,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,cAAc,YAAa,QAAQ,UAAU,IAAI,UAAU,MAAM,KAAG,OAKhF,CAAA"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The segment from `a` to `b`.
3
+ *
4
+ * @category Operations
5
+ */
6
+ export const between = (a, b) => ({ ax: a.x, ay: a.y, bx: b.x, by: b.y });
7
+ /**
8
+ * The point of the segment nearest to `point`.
9
+ *
10
+ * @category Operations
11
+ */
12
+ export const closestPoint = (segment, point) => {
13
+ const ex = segment.bx - segment.ax;
14
+ const ey = segment.by - segment.ay;
15
+ const lengthSquared = ex * ex + ey * ey;
16
+ if (lengthSquared < 1e-12)
17
+ return { x: segment.ax, y: segment.ay };
18
+ const along = Math.max(0, Math.min(1, ((point.x - segment.ax) * ex + (point.y - segment.ay) * ey) / lengthSquared));
19
+ return { x: segment.ax + ex * along, y: segment.ay + ey * along };
20
+ };
21
+ /**
22
+ * Whether a circle overlaps (or touches) the segment.
23
+ *
24
+ * @category Operations
25
+ */
26
+ export const overlapsCircle = (segment, center, radius) => {
27
+ const nearest = closestPoint(segment, center);
28
+ const dx = center.x - nearest.x;
29
+ const dy = center.y - nearest.y;
30
+ return dx * dx + dy * dy <= radius * radius;
31
+ };
32
+ //# sourceMappingURL=Segment2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Segment2.js","sourceRoot":"","sources":["../src/Segment2.ts"],"names":[],"mappings":"AAuCA;;;;GAIG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,CAAO,EAAE,CAAO,EAAY,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;AAE/F;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,OAAiB,EAAE,KAAW,EAAQ,EAAE;IACnE,MAAM,EAAE,GAAG,OAAO,CAAC,EAAE,GAAG,OAAO,CAAC,EAAE,CAAA;IAClC,MAAM,EAAE,GAAG,OAAO,CAAC,EAAE,GAAG,OAAO,CAAC,EAAE,CAAA;IAClC,MAAM,aAAa,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAA;IACvC,IAAI,aAAa,GAAG,KAAK;QAAE,OAAO,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,EAAE,CAAA;IAClE,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,aAAa,CAAC,CAAC,CAAA;IACnH,OAAO,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,GAAG,KAAK,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,GAAG,KAAK,EAAE,CAAA;AACnE,CAAC,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,OAAiB,EAAE,MAAY,EAAE,MAAc,EAAW,EAAE;IACzF,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;IAC7C,MAAM,EAAE,GAAG,MAAM,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAA;IAC/B,MAAM,EAAE,GAAG,MAAM,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAA;IAC/B,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,MAAM,GAAG,MAAM,CAAA;AAC7C,CAAC,CAAA"}
package/dist/index.d.ts CHANGED
@@ -7,7 +7,8 @@
7
7
  * `ConstructedResource(Random.codec)` and are validated once at the boundary. `InputAxis` normalizes directional input.
8
8
  *
9
9
  * Hot-path helpers work on plain numbers every frame: `Vec2` arithmetic,
10
- * `Box2` edges, `Collide2` swept circle collision, a `SpatialGrid` broad
10
+ * `Box2` edges, `Segment2` walls at any angle, `Collide2` swept circle
11
+ * collision against both, a `SpatialGrid` broad
11
12
  * phase, a `FlowField` for finding the way around walls, and a deterministic
12
13
  * `Random` whose state is one integer.
13
14
  *
@@ -23,6 +24,7 @@ export * as InputAxis from "./InputAxis.ts";
23
24
  export * as Ops from "./Ops.ts";
24
25
  export * as Random from "./Random.ts";
25
26
  export * as Scalar from "./Scalar.ts";
27
+ export * as Segment2 from "./Segment2.ts";
26
28
  export * as Size2 from "./Size2.ts";
27
29
  export * as SpatialGrid from "./SpatialGrid.ts";
28
30
  export * as Vec2 from "./Vec2.ts";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AACjC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AACjC,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAA;AACzC,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAA;AAC3C,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAA;AAC3C,OAAO,KAAK,GAAG,MAAM,UAAU,CAAA;AAC/B,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AACrC,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AACrC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AACnC,OAAO,KAAK,WAAW,MAAM,kBAAkB,CAAA;AAC/C,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AACjC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AACjC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AACjC,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAA;AACzC,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAA;AAC3C,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAA;AAC3C,OAAO,KAAK,GAAG,MAAM,UAAU,CAAA;AAC/B,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AACrC,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AACrC,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAA;AACzC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AACnC,OAAO,KAAK,WAAW,MAAM,kBAAkB,CAAA;AAC/C,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AACjC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA"}
package/dist/index.js CHANGED
@@ -7,7 +7,8 @@
7
7
  * `ConstructedResource(Random.codec)` and are validated once at the boundary. `InputAxis` normalizes directional input.
8
8
  *
9
9
  * Hot-path helpers work on plain numbers every frame: `Vec2` arithmetic,
10
- * `Box2` edges, `Collide2` swept circle collision, a `SpatialGrid` broad
10
+ * `Box2` edges, `Segment2` walls at any angle, `Collide2` swept circle
11
+ * collision against both, a `SpatialGrid` broad
11
12
  * phase, a `FlowField` for finding the way around walls, and a deterministic
12
13
  * `Random` whose state is one integer.
13
14
  *
@@ -23,6 +24,7 @@ export * as InputAxis from "./InputAxis.js";
23
24
  export * as Ops from "./Ops.js";
24
25
  export * as Random from "./Random.js";
25
26
  export * as Scalar from "./Scalar.js";
27
+ export * as Segment2 from "./Segment2.js";
26
28
  export * as Size2 from "./Size2.js";
27
29
  export * as SpatialGrid from "./SpatialGrid.js";
28
30
  export * as Vec2 from "./Vec2.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AACjC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AACjC,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAA;AACzC,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAA;AAC3C,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAA;AAC3C,OAAO,KAAK,GAAG,MAAM,UAAU,CAAA;AAC/B,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AACrC,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AACrC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AACnC,OAAO,KAAK,WAAW,MAAM,kBAAkB,CAAA;AAC/C,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AACjC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AACjC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AACjC,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAA;AACzC,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAA;AAC3C,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAA;AAC3C,OAAO,KAAK,GAAG,MAAM,UAAU,CAAA;AAC/B,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AACrC,OAAO,KAAK,MAAM,MAAM,aAAa,CAAA;AACrC,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAA;AACzC,OAAO,KAAK,KAAK,MAAM,YAAY,CAAA;AACnC,OAAO,KAAK,WAAW,MAAM,kBAAkB,CAAA;AAC/C,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AACjC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeonce/ritmo-math",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Validated math values and hot-path game helpers (collision, spatial grid, flow field, random) for Ritmo games",
5
5
  "author": "Sandro Maglione",
6
6
  "license": "MIT",
@@ -47,7 +47,7 @@
47
47
  "provenance": true
48
48
  },
49
49
  "dependencies": {
50
- "@typeonce/ritmo": "^0.13.0"
50
+ "@typeonce/ritmo": "^0.14.0"
51
51
  },
52
52
  "engines": {
53
53
  "node": ">=20"
package/src/Collide2.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  /**
2
- * Continuous collision for moving circles against static boxes and circles.
2
+ * Continuous collision for moving circles against static walls (axis-aligned
3
+ * boxes and line segments) and circles.
3
4
  *
4
- * Movement is swept, not sampled: `sweepCircleBox` finds the first time of
5
+ * Movement is swept, not sampled: `sweepCircle` finds the first time of
5
6
  * contact along the whole displacement, so fast movers (dashes, projectiles)
6
7
  * cannot skip over a wall between two steps. `moveCircle` stops at walls and
7
- * slides along them.
8
+ * slides along them. Walls are passed as {@link Walls}: `Box2` boxes for a
9
+ * grid of rooms, `Segment2` segments for walls at any angle, or both.
8
10
  *
9
11
  * Every query returns `undefined` for "no contact", never throws, and
10
12
  * allocates only its result. Inputs are expected to be finite.
@@ -12,7 +14,7 @@
12
14
  * @example
13
15
  * ```ts
14
16
  * // Move a body by its velocity this step, sliding along the walls.
15
- * const moved = Collide2.moveCircle(position, Vec2.scale(velocity, dt), radius, walls)
17
+ * const moved = Collide2.moveCircle(position, Vec2.scale(velocity, dt), radius, { boxes: walls })
16
18
  * if (moved.blocked) bumpedWall()
17
19
  * ```
18
20
  *
@@ -30,6 +32,7 @@
30
32
  */
31
33
  import type { Box2 } from "./Box2.ts"
32
34
  import * as Ops from "./Ops.ts"
35
+ import type { Segment2 } from "./Segment2.ts"
33
36
  import type { Vec2 } from "./Vec2.ts"
34
37
 
35
38
  /**
@@ -40,11 +43,27 @@ import type { Vec2 } from "./Vec2.ts"
40
43
  export interface Hit {
41
44
  /** Fraction of the displacement travelled before contact, in [0, 1]. */
42
45
  readonly time: number
43
- /** Unit contact normal, pointing away from the box. */
46
+ /** Unit contact normal, pointing away from the wall. */
44
47
  readonly nx: number
45
48
  readonly ny: number
46
49
  }
47
50
 
51
+ /**
52
+ * The static walls a circle collides with: axis-aligned `boxes`, line
53
+ * `segments`, or both. A missing list is no walls of that kind.
54
+ *
55
+ * @category Collision Types
56
+ */
57
+ export interface Walls {
58
+ readonly boxes?: ReadonlyArray<Box2>
59
+ readonly segments?: ReadonlyArray<Segment2>
60
+ /** Not a setting: an array of walls is passed by kind. */
61
+ readonly length?: "pass walls by kind: { boxes: walls } or { segments: walls }"
62
+ }
63
+
64
+ const noBoxes: ReadonlyArray<Box2> = []
65
+ const noSegments: ReadonlyArray<Segment2> = []
66
+
48
67
  /**
49
68
  * Where a moved circle ended up.
50
69
  *
@@ -170,7 +189,61 @@ export const sweepCircleBox = (origin: Vec2, delta: Vec2, radius: number, box: B
170
189
  }
171
190
 
172
191
  /**
173
- * First contact of a moving circle with any of `boxes`, or `undefined`.
192
+ * First contact of a circle of `radius` at `origin` moving by `delta` with
193
+ * `segment`, or `undefined`. Contact is computed against the segment
194
+ * thickened by the radius with round ends (the exact Minkowski sum), so the
195
+ * circle slides along it and around its ends. A circle that starts exactly
196
+ * touching it and moves into it hits at time 0. A circle that starts
197
+ * overlapping the segment is not reported; resolve it with `pushOut` first.
198
+ *
199
+ * @category Queries
200
+ */
201
+ export const sweepCircleSegment = (origin: Vec2, delta: Vec2, radius: number, segment: Segment2): Hit | undefined => {
202
+ const ex = segment.bx - segment.ax
203
+ const ey = segment.by - segment.ay
204
+ const lengthSquared = ex * ex + ey * ey
205
+ if (lengthSquared > 1e-12) {
206
+ // The flat side facing the circle, `radius` away from the segment's line.
207
+ const length = Math.sqrt(lengthSquared)
208
+ let nx = -ey / length
209
+ let ny = ex / length
210
+ let side = (origin.x - segment.ax) * nx + (origin.y - segment.ay) * ny
211
+ if (side < 0) {
212
+ nx = -nx
213
+ ny = -ny
214
+ side = -side
215
+ }
216
+ if (side < radius) {
217
+ // Beside the line: overlapping when level with the segment, otherwise
218
+ // only its ends are in the way.
219
+ const along = ((origin.x - segment.ax) * ex + (origin.y - segment.ay) * ey) / lengthSquared
220
+ if (along >= 0 && along <= 1) return undefined
221
+ } else {
222
+ const into = delta.x * nx + delta.y * ny
223
+ if (into < 0) {
224
+ // `0 +` turns a -0 (touching at the start) into 0.
225
+ const time = 0 + (radius - side) / into
226
+ if (time <= 1) {
227
+ const along = ((origin.x + delta.x * time - segment.ax) * ex + (origin.y + delta.y * time - segment.ay) * ey) / lengthSquared
228
+ // The thickened segment is convex, so entering through its flat side is the first contact.
229
+ if (along >= 0 && along <= 1) return { time, nx, ny }
230
+ }
231
+ }
232
+ }
233
+ }
234
+ const atA = rayCircle(origin.x, origin.y, delta.x, delta.y, segment.ax, segment.ay, radius)
235
+ const atB = lengthSquared > 1e-12 ? rayCircle(origin.x, origin.y, delta.x, delta.y, segment.bx, segment.by, radius) : undefined
236
+ if (atA === undefined && atB === undefined) return undefined
237
+ const fromA = atB === undefined || (atA !== undefined && atA <= atB)
238
+ const time = fromA ? atA! : atB
239
+ const cx = origin.x + delta.x * time - (fromA ? segment.ax : segment.bx)
240
+ const cy = origin.y + delta.y * time - (fromA ? segment.ay : segment.by)
241
+ const length = Ops.hypot(cx, cy) || 1
242
+ return { time, nx: cx / length, ny: cy / length }
243
+ }
244
+
245
+ /**
246
+ * First contact of a moving circle with any of `walls`, or `undefined`.
174
247
  *
175
248
  * With a `radius` of 0 the circle is a point and the sweep is a segment, so
176
249
  * this is also line of sight: `undefined` means nothing blocks the line from
@@ -179,17 +252,21 @@ export const sweepCircleBox = (origin: Vec2, delta: Vec2, radius: number, box: B
179
252
  *
180
253
  * @example
181
254
  * ```ts
182
- * const sees = Collide2.sweepCircleBoxes(eye, Vec2.subtract(target, eye), 0, walls) === undefined
255
+ * const sees = Collide2.sweepCircle(eye, Vec2.subtract(target, eye), 0, { boxes: walls }) === undefined
183
256
  * ```
184
257
  *
185
258
  * @category Queries
186
259
  */
187
- export const sweepCircleBoxes = (origin: Vec2, delta: Vec2, radius: number, boxes: ReadonlyArray<Box2>): Hit | undefined => {
260
+ export const sweepCircle = (origin: Vec2, delta: Vec2, radius: number, walls: Walls): Hit | undefined => {
188
261
  let first: Hit | undefined
189
- for (const box of boxes) {
262
+ for (const box of walls.boxes ?? noBoxes) {
190
263
  const hit = sweepCircleBox(origin, delta, radius, box)
191
264
  if (hit !== undefined && (first === undefined || hit.time < first.time)) first = hit
192
265
  }
266
+ for (const segment of walls.segments ?? noSegments) {
267
+ const hit = sweepCircleSegment(origin, delta, radius, segment)
268
+ if (hit !== undefined && (first === undefined || hit.time < first.time)) first = hit
269
+ }
193
270
  return first
194
271
  }
195
272
 
@@ -215,15 +292,17 @@ export const sweepCircleCircle = (
215
292
  }
216
293
 
217
294
  /**
218
- * Pushes a circle out of every box it overlaps, in order, leaving `skin`
219
- * between it and each wall. Returns the corrected center.
295
+ * Pushes a circle out of every wall it overlaps, boxes first and then
296
+ * segments, each in order, leaving `skin` between it and each wall. Returns
297
+ * the corrected center. A center inside a box leaves through the nearest
298
+ * side; a center exactly on a segment leaves along the segment's left normal.
220
299
  *
221
300
  * @category Movement
222
301
  */
223
- export const pushOut = (center: Vec2, radius: number, boxes: ReadonlyArray<Box2>, skin: number = defaultSkin): Vec2 => {
302
+ export const pushOut = (center: Vec2, radius: number, walls: Walls, skin: number = defaultSkin): Vec2 => {
224
303
  let x = center.x
225
304
  let y = center.y
226
- for (const box of boxes) {
305
+ for (const box of walls.boxes ?? noBoxes) {
227
306
  const closestX = Math.max(box.minX, Math.min(x, box.maxX))
228
307
  const closestY = Math.max(box.minY, Math.min(y, box.maxY))
229
308
  const dx = x - closestX
@@ -247,6 +326,31 @@ export const pushOut = (center: Vec2, radius: number, boxes: ReadonlyArray<Box2>
247
326
  else y = box.maxY + radius + skin
248
327
  }
249
328
  }
329
+ for (const segment of walls.segments ?? noSegments) {
330
+ const ex = segment.bx - segment.ax
331
+ const ey = segment.by - segment.ay
332
+ const lengthSquared = ex * ex + ey * ey
333
+ const along = lengthSquared > 1e-12 ? Math.max(0, Math.min(1, ((x - segment.ax) * ex + (y - segment.ay) * ey) / lengthSquared)) : 0
334
+ const closestX = segment.ax + ex * along
335
+ const closestY = segment.ay + ey * along
336
+ const dx = x - closestX
337
+ const dy = y - closestY
338
+ const distanceSquared = dx * dx + dy * dy
339
+ if (distanceSquared >= radius * radius) continue
340
+ if (distanceSquared > 1e-12) {
341
+ const distance = Math.sqrt(distanceSquared)
342
+ x = closestX + (dx / distance) * (radius + skin)
343
+ y = closestY + (dy / distance) * (radius + skin)
344
+ } else if (lengthSquared > 1e-12) {
345
+ // Center on the segment: leave along its left normal.
346
+ const length = Math.sqrt(lengthSquared)
347
+ x = closestX - (ey / length) * (radius + skin)
348
+ y = closestY + (ex / length) * (radius + skin)
349
+ } else {
350
+ // Center on a round post: leave along +x.
351
+ x = closestX + radius + skin
352
+ }
353
+ }
250
354
  return { x, y }
251
355
  }
252
356
 
@@ -254,19 +358,46 @@ export const pushOut = (center: Vec2, radius: number, boxes: ReadonlyArray<Box2>
254
358
  * Moves a circle by `delta`, stopping at walls and sliding along them (up to
255
359
  * four contacts per call). The whole path is swept, so no displacement can
256
360
  * tunnel through a wall. A circle that starts inside a wall is pushed out
257
- * first. A circle meeting a box's corner dead-on goes around it rather than
258
- * stopping against it.
361
+ * first. A circle meeting a box's corner or a segment's end dead-on goes
362
+ * around it rather than stopping against it.
363
+ *
364
+ * @example
365
+ * ```ts
366
+ * // A map traced from survey data: every wall is a segment.
367
+ * const moved = Collide2.moveCircle(position, delta, radius, { segments: nearbyWalls })
368
+ * ```
259
369
  *
260
370
  * @category Movement
261
371
  */
262
- export const moveCircle = (start: Vec2, delta: Vec2, radius: number, boxes: ReadonlyArray<Box2>, skin: number = defaultSkin): MoveResult => {
263
- let position = pushOut(start, radius, boxes, skin)
372
+ export const moveCircle = (start: Vec2, delta: Vec2, radius: number, walls: Walls, skin: number = defaultSkin): MoveResult => {
373
+ let position = pushOut(start, radius, walls, skin)
264
374
  let rx = delta.x
265
375
  let ry = delta.y
266
376
  let blocked = false
267
377
  for (let iteration = 0; iteration < 4; iteration++) {
268
378
  if (rx * rx + ry * ry < 1e-12) break
269
- const hit = sweepCircleBoxes(position, { x: rx, y: ry }, radius, boxes)
379
+ const step = { x: rx, y: ry }
380
+ // The first contact, and whether it is round (a box's corner or a
381
+ // segment's end) rather than a flat side.
382
+ let hit: Hit | undefined
383
+ let round = false
384
+ for (const box of walls.boxes ?? noBoxes) {
385
+ const found = sweepCircleBox(position, step, radius, box)
386
+ if (found === undefined || (hit !== undefined && found.time >= hit.time)) continue
387
+ hit = found
388
+ round = found.nx !== 0 && found.ny !== 0
389
+ }
390
+ for (const segment of walls.segments ?? noSegments) {
391
+ const found = sweepCircleSegment(position, step, radius, segment)
392
+ if (found === undefined || (hit !== undefined && found.time >= hit.time)) continue
393
+ hit = found
394
+ const ex = segment.bx - segment.ax
395
+ const ey = segment.by - segment.ay
396
+ const lengthSquared = ex * ex + ey * ey
397
+ const along = found.nx * ex + found.ny * ey
398
+ // A flat side's normal is perpendicular to the segment.
399
+ round = lengthSquared < 1e-12 || along * along > 1e-18 * lengthSquared
400
+ }
270
401
  if (hit === undefined) {
271
402
  position = { x: position.x + rx, y: position.y + ry }
272
403
  break
@@ -284,10 +415,10 @@ export const moveCircle = (start: Vec2, delta: Vec2, radius: number, boxes: Read
284
415
  if (into < 0) {
285
416
  rx -= into * hit.nx
286
417
  ry -= into * hit.ny
287
- // Dead-on into a rounded corner (as an exact diagonal meets an
288
- // axis-aligned box) nothing is left to slide along: go around the
289
- // corner instead of stopping against it.
290
- if (hit.nx !== 0 && hit.ny !== 0 && rx * rx + ry * ry < 1e-12 * into * into) {
418
+ // Dead-on into a rounded corner or end (as an exact diagonal meets an
419
+ // axis-aligned box) nothing is left to slide along: go around it
420
+ // instead of stopping against it.
421
+ if (round && rx * rx + ry * ry < 1e-12 * into * into) {
291
422
  const side = rx * -hit.ny + ry * hit.nx < 0 ? -1 : 1
292
423
  rx = -hit.ny * -into * side
293
424
  ry = hit.nx * -into * side
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Plain line segments stored by their ends, for hot-path collision against
3
+ * walls that are not axis-aligned (a map traced from survey data, a slanted
4
+ * fence, a polygon's edges).
5
+ *
6
+ * Like `Box2`, it is the plain form the collision helpers (`Collide2`) read
7
+ * every frame, built once from whatever the game stores. A wall is
8
+ * two-sided: a circle is kept off it on both sides and around both ends.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * const fence = Segment2.between({ x: 0, y: 0 }, { x: 40, y: 30 })
13
+ * const moved = Collide2.moveCircle(position, delta, radius, { segments: [fence] })
14
+ * ```
15
+ *
16
+ * @module Segment2
17
+ * @docGroup helpers
18
+ *
19
+ * @categoryDescription Segment Types
20
+ * The structural segment shape.
21
+ *
22
+ * @categoryDescription Operations
23
+ * Building segments and measuring them against points and circles.
24
+ */
25
+ import type { Vec2 } from "./Vec2.ts"
26
+
27
+ /**
28
+ * The segment from (`ax`, `ay`) to (`bx`, `by`). Both ends may be the same
29
+ * point, which is then a round post.
30
+ *
31
+ * @category Segment Types
32
+ */
33
+ export interface Segment2 {
34
+ readonly ax: number
35
+ readonly ay: number
36
+ readonly bx: number
37
+ readonly by: number
38
+ }
39
+
40
+ /**
41
+ * The segment from `a` to `b`.
42
+ *
43
+ * @category Operations
44
+ */
45
+ export const between = (a: Vec2, b: Vec2): Segment2 => ({ ax: a.x, ay: a.y, bx: b.x, by: b.y })
46
+
47
+ /**
48
+ * The point of the segment nearest to `point`.
49
+ *
50
+ * @category Operations
51
+ */
52
+ export const closestPoint = (segment: Segment2, point: Vec2): Vec2 => {
53
+ const ex = segment.bx - segment.ax
54
+ const ey = segment.by - segment.ay
55
+ const lengthSquared = ex * ex + ey * ey
56
+ if (lengthSquared < 1e-12) return { x: segment.ax, y: segment.ay }
57
+ const along = Math.max(0, Math.min(1, ((point.x - segment.ax) * ex + (point.y - segment.ay) * ey) / lengthSquared))
58
+ return { x: segment.ax + ex * along, y: segment.ay + ey * along }
59
+ }
60
+
61
+ /**
62
+ * Whether a circle overlaps (or touches) the segment.
63
+ *
64
+ * @category Operations
65
+ */
66
+ export const overlapsCircle = (segment: Segment2, center: Vec2, radius: number): boolean => {
67
+ const nearest = closestPoint(segment, center)
68
+ const dx = center.x - nearest.x
69
+ const dy = center.y - nearest.y
70
+ return dx * dx + dy * dy <= radius * radius
71
+ }
package/src/index.ts CHANGED
@@ -7,7 +7,8 @@
7
7
  * `ConstructedResource(Random.codec)` and are validated once at the boundary. `InputAxis` normalizes directional input.
8
8
  *
9
9
  * Hot-path helpers work on plain numbers every frame: `Vec2` arithmetic,
10
- * `Box2` edges, `Collide2` swept circle collision, a `SpatialGrid` broad
10
+ * `Box2` edges, `Segment2` walls at any angle, `Collide2` swept circle
11
+ * collision against both, a `SpatialGrid` broad
11
12
  * phase, a `FlowField` for finding the way around walls, and a deterministic
12
13
  * `Random` whose state is one integer.
13
14
  *
@@ -23,6 +24,7 @@ export * as InputAxis from "./InputAxis.ts"
23
24
  export * as Ops from "./Ops.ts"
24
25
  export * as Random from "./Random.ts"
25
26
  export * as Scalar from "./Scalar.ts"
27
+ export * as Segment2 from "./Segment2.ts"
26
28
  export * as Size2 from "./Size2.ts"
27
29
  export * as SpatialGrid from "./SpatialGrid.ts"
28
30
  export * as Vec2 from "./Vec2.ts"