@typeonce/ritmo-math 0.8.0 → 0.10.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 +183 -0
- package/README.md +14 -4
- package/dist/Collide2.d.ts +2 -1
- package/dist/Collide2.d.ts.map +1 -1
- package/dist/Collide2.js +10 -6
- package/dist/Collide2.js.map +1 -1
- package/dist/FlowField.d.ts.map +1 -1
- package/dist/FlowField.js +2 -1
- package/dist/FlowField.js.map +1 -1
- package/dist/InputAxis.js +1 -1
- package/dist/Ops.d.ts +209 -0
- package/dist/Ops.d.ts.map +1 -0
- package/dist/Ops.js +210 -0
- package/dist/Ops.js.map +1 -0
- package/dist/Random.d.ts +63 -10
- package/dist/Random.d.ts.map +1 -1
- package/dist/Random.js +57 -32
- package/dist/Random.js.map +1 -1
- package/dist/Scalar.d.ts +7 -2
- package/dist/Scalar.d.ts.map +1 -1
- package/dist/Scalar.js +2 -1
- package/dist/Scalar.js.map +1 -1
- package/dist/SpatialGrid.d.ts +7 -2
- package/dist/SpatialGrid.d.ts.map +1 -1
- package/dist/SpatialGrid.js +35 -4
- package/dist/SpatialGrid.js.map +1 -1
- package/dist/Vec2.d.ts.map +1 -1
- package/dist/Vec2.js +8 -7
- package/dist/Vec2.js.map +1 -1
- package/dist/Vector2.d.ts +12 -0
- package/dist/Vector2.d.ts.map +1 -1
- package/dist/Vector2.js +12 -0
- package/dist/Vector2.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/fdlibm.d.ts +43 -0
- package/dist/internal/fdlibm.d.ts.map +1 -0
- package/dist/internal/fdlibm.js +1291 -0
- package/dist/internal/fdlibm.js.map +1 -0
- package/dist/internal/vector2.d.ts.map +1 -1
- package/dist/internal/vector2.js +2 -1
- package/dist/internal/vector2.js.map +1 -1
- package/package.json +3 -2
- package/src/Collide2.ts +10 -6
- package/src/FlowField.ts +2 -1
- package/src/InputAxis.ts +1 -1
- package/src/Ops.ts +232 -0
- package/src/Random.ts +104 -16
- package/src/Scalar.ts +7 -2
- package/src/SpatialGrid.ts +40 -6
- package/src/Vec2.ts +9 -7
- package/src/Vector2.ts +12 -0
- package/src/index.ts +5 -0
- package/src/internal/fdlibm.ts +1256 -0
- package/src/internal/vector2.ts +2 -1
package/src/Random.ts
CHANGED
|
@@ -3,16 +3,22 @@
|
|
|
3
3
|
* integer.
|
|
4
4
|
*
|
|
5
5
|
* Keep the state in a resource, so a run replays exactly and saves with the
|
|
6
|
-
* world:
|
|
7
|
-
*
|
|
8
|
-
* draw
|
|
6
|
+
* world: declare it with this module as its validator, start it with
|
|
7
|
+
* `seed`, and draw in systems through `draw` over its write cell, which writes
|
|
8
|
+
* every draw back (rolled back with a failed system). Two generators made
|
|
9
|
+
* from the same state draw the same sequence on every platform. `make` is for
|
|
10
|
+
* a state that lives outside the world (a level generator seeded once).
|
|
9
11
|
*
|
|
10
12
|
* @example
|
|
11
13
|
* ```ts
|
|
12
|
-
* const
|
|
13
|
-
* const
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* const Rng = Descriptor.ConstructedResource(Random)("Game/Rng")
|
|
15
|
+
* const runtime = Game.Runtime.make({ services, resources: { Rng: Random.seed(0x5eed) } })
|
|
16
|
+
*
|
|
17
|
+
* Game.System("Game/Spawn", { resources: { rng: Game.System.writeResource(Rng) } }, ({ resources }) => {
|
|
18
|
+
* const random = Random.draw(resources.rng)
|
|
19
|
+
* const angle = random.range(0, Math.PI * 2)
|
|
20
|
+
* const kind = random.pick(kinds) ?? "grunt"
|
|
21
|
+
* })
|
|
16
22
|
* ```
|
|
17
23
|
*
|
|
18
24
|
* @module Random
|
|
@@ -23,7 +29,13 @@
|
|
|
23
29
|
*
|
|
24
30
|
* @categoryDescription Construction
|
|
25
31
|
* Making a generator from a stored state.
|
|
32
|
+
*
|
|
33
|
+
* @categoryDescription Resource
|
|
34
|
+
* Keeping the state in a world resource.
|
|
26
35
|
*/
|
|
36
|
+
import type * as Brand from "@typeonce/ritmo/Brand"
|
|
37
|
+
import type { WriteCell } from "@typeonce/ritmo/Query"
|
|
38
|
+
import * as Result from "@typeonce/ritmo/Result"
|
|
27
39
|
|
|
28
40
|
/**
|
|
29
41
|
* A generator. Every draw advances `state`.
|
|
@@ -35,9 +47,9 @@ export interface Random {
|
|
|
35
47
|
readonly state: number
|
|
36
48
|
/** Uniform in [0, 1). */
|
|
37
49
|
next(): number
|
|
38
|
-
/** Uniform in [min, max). */
|
|
50
|
+
/** Uniform in [min, max), or `min` when they are equal. Throws when `min > max` or a bound is not finite. */
|
|
39
51
|
range(min: number, max: number): number
|
|
40
|
-
/** A uniform integer in [min, maxExclusive)
|
|
52
|
+
/** A uniform integer in [min, maxExclusive). Throws unless both bounds are integers with min < maxExclusive. */
|
|
41
53
|
integer(min: number, maxExclusive: number): number
|
|
42
54
|
/** A uniform element of `items`, or `undefined` when it is empty (no draw is made then). */
|
|
43
55
|
pick<A>(items: ReadonlyArray<A>): A | undefined
|
|
@@ -47,7 +59,7 @@ export interface Random {
|
|
|
47
59
|
* An element of `items` chosen with probability proportional to `weight`,
|
|
48
60
|
* or `undefined` when no element has a positive finite weight (no draw is
|
|
49
61
|
* made then). Elements with zero, negative, or non-finite weight are never
|
|
50
|
-
* chosen.
|
|
62
|
+
* chosen. `weight` is called once per element.
|
|
51
63
|
*/
|
|
52
64
|
weighted<A>(items: ReadonlyArray<A>, weight: (item: A) => number): A | undefined
|
|
53
65
|
}
|
|
@@ -70,10 +82,16 @@ class Mulberry32 implements Random {
|
|
|
70
82
|
}
|
|
71
83
|
|
|
72
84
|
range(min: number, max: number): number {
|
|
85
|
+
if (!(Number.isFinite(min) && Number.isFinite(max) && min <= max)) {
|
|
86
|
+
throw new Error(`Random.range needs finite bounds with min <= max, got [${min}, ${max})`)
|
|
87
|
+
}
|
|
73
88
|
return min + (max - min) * this.next()
|
|
74
89
|
}
|
|
75
90
|
|
|
76
91
|
integer(min: number, maxExclusive: number): number {
|
|
92
|
+
if (!(Number.isSafeInteger(min) && Number.isSafeInteger(maxExclusive) && min < maxExclusive)) {
|
|
93
|
+
throw new Error(`Random.integer needs whole bounds with min below maxExclusive, got [${min}, ${maxExclusive})`)
|
|
94
|
+
}
|
|
77
95
|
return min + Math.floor(this.next() * (maxExclusive - min))
|
|
78
96
|
}
|
|
79
97
|
|
|
@@ -93,20 +111,21 @@ class Mulberry32 implements Random {
|
|
|
93
111
|
}
|
|
94
112
|
|
|
95
113
|
weighted<A>(items: ReadonlyArray<A>, weight: (item: A) => number): A | undefined {
|
|
114
|
+
// `weight` is called once per item: it may be costly or not pure.
|
|
115
|
+
const weights = items.map(weight)
|
|
96
116
|
let total = 0
|
|
97
|
-
for (const
|
|
98
|
-
const value = weight(item)
|
|
117
|
+
for (const value of weights) {
|
|
99
118
|
if (usable(value)) total += value
|
|
100
119
|
}
|
|
101
120
|
if (!usable(total)) return undefined
|
|
102
121
|
let roll = this.next() * total
|
|
103
122
|
let last: A | undefined
|
|
104
|
-
for (
|
|
105
|
-
const value =
|
|
123
|
+
for (let index = 0; index < items.length; index++) {
|
|
124
|
+
const value = weights[index]!
|
|
106
125
|
if (!usable(value)) continue
|
|
107
|
-
last =
|
|
126
|
+
last = items[index]
|
|
108
127
|
roll -= value
|
|
109
|
-
if (roll < 0) return
|
|
128
|
+
if (roll < 0) return items[index]
|
|
110
129
|
}
|
|
111
130
|
// Rounding can leave a sliver of the roll past the last bucket.
|
|
112
131
|
return last
|
|
@@ -121,3 +140,72 @@ class Mulberry32 implements Random {
|
|
|
121
140
|
* @category Construction
|
|
122
141
|
*/
|
|
123
142
|
export const make = (state: number): Random => new Mulberry32(state)
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* A generator's state as a resource holds it: an unsigned 32-bit integer.
|
|
146
|
+
* Only `seed`, `result`, and `decode` make one, so `draw` takes the cell of a
|
|
147
|
+
* random resource and nothing else.
|
|
148
|
+
*
|
|
149
|
+
* @category Resource
|
|
150
|
+
*/
|
|
151
|
+
export type State = Brand.Branded<number, "Random/State">
|
|
152
|
+
|
|
153
|
+
/** Why a value is not a generator state. @category Resource */
|
|
154
|
+
export interface InvalidState {
|
|
155
|
+
readonly _tag: "InvalidRandomState"
|
|
156
|
+
readonly value: unknown
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const isState = (value: unknown): value is State =>
|
|
160
|
+
typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 0xffffffff
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Validates a state, for `Descriptor.ConstructedResource(Random)` and saves.
|
|
164
|
+
*
|
|
165
|
+
* @category Resource
|
|
166
|
+
*/
|
|
167
|
+
export const result = (raw: number): Result.Result<State, InvalidState> =>
|
|
168
|
+
isState(raw) ? Result.success(raw) : Result.failure({ _tag: "InvalidRandomState", value: raw })
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Validates a state read from untrusted data (a save).
|
|
172
|
+
*
|
|
173
|
+
* @category Resource
|
|
174
|
+
*/
|
|
175
|
+
export const decode = (raw: unknown): Result.Result<State, InvalidState> =>
|
|
176
|
+
isState(raw) ? Result.success(raw) : Result.failure({ _tag: "InvalidRandomState", value: raw })
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The starting state for a seed, reduced to an unsigned 32-bit integer like
|
|
180
|
+
* `make`: the value to give a random resource in `Game.Runtime.make`.
|
|
181
|
+
*
|
|
182
|
+
* @category Resource
|
|
183
|
+
*/
|
|
184
|
+
export const seed = (value: number): State => (value >>> 0) as State
|
|
185
|
+
|
|
186
|
+
class CellRandom extends Mulberry32 {
|
|
187
|
+
readonly #cell: WriteCell<State>
|
|
188
|
+
|
|
189
|
+
constructor(cell: WriteCell<State>) {
|
|
190
|
+
super(cell.get())
|
|
191
|
+
this.#cell = cell
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
override next(): number {
|
|
195
|
+
// Read the cell each time, so two generators over one cell share one sequence.
|
|
196
|
+
this.state = this.#cell.get()
|
|
197
|
+
const value = super.next()
|
|
198
|
+
this.#cell.set(this.state as State)
|
|
199
|
+
return value
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* A generator over a random resource's write cell: every draw continues from
|
|
205
|
+
* the stored state and writes the new one back, so the system needs no
|
|
206
|
+
* `set` of its own, a failed run rolls the draws back, and generators made
|
|
207
|
+
* from the same cell in one run continue one sequence.
|
|
208
|
+
*
|
|
209
|
+
* @category Resource
|
|
210
|
+
*/
|
|
211
|
+
export const draw = (cell: WriteCell<State>): Random => new CellRandom(cell)
|
package/src/Scalar.ts
CHANGED
|
@@ -41,7 +41,11 @@ import type * as Result from "@typeonce/ritmo/Result"
|
|
|
41
41
|
* Finite number brand.
|
|
42
42
|
*
|
|
43
43
|
* Values of this type are guaranteed not to be `NaN`, `Infinity`, or
|
|
44
|
-
* `-Infinity
|
|
44
|
+
* `-Infinity` when they come from a validator (`result`, `decode`). The
|
|
45
|
+
* arithmetic helpers here and in `Vector2` keep the brand without
|
|
46
|
+
* re-validating, so their results are finite only while they stay within
|
|
47
|
+
* `±Number.MAX_VALUE`: game-sized values never come close, but validate
|
|
48
|
+
* again with `result` when inputs may be that large.
|
|
45
49
|
*
|
|
46
50
|
* @category Scalar Types
|
|
47
51
|
*/
|
|
@@ -112,7 +116,8 @@ export const clamp: (value: Finite, min: Finite, max: Finite) => Finite = intern
|
|
|
112
116
|
* Interpolates between two finite values by one finite amount.
|
|
113
117
|
*
|
|
114
118
|
* The amount is not clamped. This stays a pure interpolation primitive rather
|
|
115
|
-
* than mixing interpolation with policy.
|
|
119
|
+
* than mixing interpolation with policy. The result keeps the `Finite` brand
|
|
120
|
+
* without re-validating (see `Finite`).
|
|
116
121
|
*
|
|
117
122
|
* @category Operations
|
|
118
123
|
*/
|
package/src/SpatialGrid.ts
CHANGED
|
@@ -45,17 +45,34 @@ export interface SpatialGrid {
|
|
|
45
45
|
readonly cellSize: Scalar.Positive
|
|
46
46
|
/** Removes every circle, keeping the storage for the next rebuild. */
|
|
47
47
|
clear(): void
|
|
48
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Adds circle `index` (a non-negative integer) covering `[x ± r] × [y ± r]`.
|
|
50
|
+
* A non-finite coordinate or radius throws: it would cover no cell or all of them.
|
|
51
|
+
*/
|
|
49
52
|
insert(index: number, x: number, y: number, r: number): void
|
|
50
53
|
/**
|
|
51
54
|
* Calls `visit` once for every circle whose cells overlap the box, in no
|
|
52
|
-
* particular order. `visit` must not insert into or query this grid.
|
|
55
|
+
* particular order. `visit` must not insert into or query this grid. A
|
|
56
|
+
* non-finite bound throws (an unbounded box would never finish); a box
|
|
57
|
+
* covering more cells than are occupied scans the occupied cells instead.
|
|
53
58
|
*/
|
|
54
59
|
forEachIn(minX: number, minY: number, maxX: number, maxY: number, visit: (index: number) => void): void
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
const key = (cx: number, cy: number): number => ((cx + 32768) << 16) | ((cy + 32768) & 0xffff)
|
|
58
63
|
|
|
64
|
+
// Keys repeat every 65,536 cells, so a longer span visits nothing new.
|
|
65
|
+
const AXIS_CELLS = 65_536
|
|
66
|
+
const lastCell = (first: number, last: number): number => Math.min(last, first + AXIS_CELLS - 1)
|
|
67
|
+
/** Whether a cell coordinate packed in a key falls in `[first, last]`, counting the keys' wrap-around. */
|
|
68
|
+
const coveredBy = (coordinate: number, first: number, last: number): boolean =>
|
|
69
|
+
(((coordinate - first) % AXIS_CELLS) + AXIS_CELLS) % AXIS_CELLS <= last - first
|
|
70
|
+
|
|
71
|
+
const notFinite = (operation: string, ...values: ReadonlyArray<number>): never => {
|
|
72
|
+
const value = values.find((value) => !Number.isFinite(value))
|
|
73
|
+
throw new Error(`SpatialGrid.${operation} needs finite numbers, got ${value}`)
|
|
74
|
+
}
|
|
75
|
+
|
|
59
76
|
class Grid implements SpatialGrid {
|
|
60
77
|
readonly cellSize: Scalar.Positive
|
|
61
78
|
private readonly cells = new Map<number, Array<number>>()
|
|
@@ -74,11 +91,13 @@ class Grid implements SpatialGrid {
|
|
|
74
91
|
}
|
|
75
92
|
|
|
76
93
|
insert(index: number, x: number, y: number, r: number): void {
|
|
94
|
+
// `x - x === 0` is false only for NaN and ±Infinity, without a call per number.
|
|
95
|
+
if (x - x !== 0 || y - y !== 0 || r - r !== 0) notFinite("insert", x, y, r)
|
|
77
96
|
const size = this.cellSize
|
|
78
97
|
const minX = Math.floor((x - r) / size)
|
|
79
|
-
const maxX = Math.floor((x + r) / size)
|
|
98
|
+
const maxX = lastCell(minX, Math.floor((x + r) / size))
|
|
80
99
|
const minY = Math.floor((y - r) / size)
|
|
81
|
-
const maxY = Math.floor((y + r) / size)
|
|
100
|
+
const maxY = lastCell(minY, Math.floor((y + r) / size))
|
|
82
101
|
for (let cx = minX; cx <= maxX; cx++) {
|
|
83
102
|
for (let cy = minY; cy <= maxY; cy++) {
|
|
84
103
|
const cellKey = key(cx, cy)
|
|
@@ -99,6 +118,7 @@ class Grid implements SpatialGrid {
|
|
|
99
118
|
}
|
|
100
119
|
|
|
101
120
|
forEachIn(minX: number, minY: number, maxX: number, maxY: number, visit: (index: number) => void): void {
|
|
121
|
+
if (minX - minX !== 0 || minY - minY !== 0 || maxX - maxX !== 0 || maxY - maxY !== 0) notFinite("forEachIn", minX, minY, maxX, maxY)
|
|
102
122
|
const size = this.cellSize
|
|
103
123
|
// Start the marks over before the counter leaves the stamps' range (after
|
|
104
124
|
// about two billion queries), so old marks never match a new query.
|
|
@@ -110,9 +130,23 @@ class Grid implements SpatialGrid {
|
|
|
110
130
|
const query = this.query
|
|
111
131
|
const stamp = this.stamp
|
|
112
132
|
const x0 = Math.floor(minX / size)
|
|
113
|
-
const x1 = Math.floor(maxX / size)
|
|
133
|
+
const x1 = lastCell(x0, Math.floor(maxX / size))
|
|
114
134
|
const y0 = Math.floor(minY / size)
|
|
115
|
-
const y1 = Math.floor(maxY / size)
|
|
135
|
+
const y1 = lastCell(y0, Math.floor(maxY / size))
|
|
136
|
+
if ((x1 - x0 + 1) * (y1 - y0 + 1) > this.cells.size) {
|
|
137
|
+
// A box larger than the occupied area: scan the cells that exist.
|
|
138
|
+
for (const [cellKey, cell] of this.cells) {
|
|
139
|
+
if (cell.length === 0) continue
|
|
140
|
+
if (!coveredBy(((cellKey >> 16) & 0xffff) - 32768, x0, x1) || !coveredBy((cellKey & 0xffff) - 32768, y0, y1)) continue
|
|
141
|
+
for (let offset = 0; offset < cell.length; offset++) {
|
|
142
|
+
const index = cell[offset]!
|
|
143
|
+
if (stamp[index] === query) continue
|
|
144
|
+
stamp[index] = query
|
|
145
|
+
visit(index)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return
|
|
149
|
+
}
|
|
116
150
|
for (let cx = x0; cx <= x1; cx++) {
|
|
117
151
|
for (let cy = y0; cy <= y1; cy++) {
|
|
118
152
|
const cell = this.cells.get(key(cx, cy))
|
package/src/Vec2.ts
CHANGED
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
* Immutable arithmetic, measurement, and steering helpers.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
+
import * as Ops from "./Ops.ts"
|
|
28
|
+
|
|
27
29
|
/**
|
|
28
30
|
* A two-dimensional vector in world units.
|
|
29
31
|
*
|
|
@@ -68,8 +70,8 @@ export const scale = (a: Vec2, factor: number): Vec2 => ({ x: a.x * factor, y: a
|
|
|
68
70
|
* @category Operations
|
|
69
71
|
*/
|
|
70
72
|
export const rotate = (a: Vec2, angle: number): Vec2 => {
|
|
71
|
-
const cos =
|
|
72
|
-
const sin =
|
|
73
|
+
const cos = Ops.cos(angle)
|
|
74
|
+
const sin = Ops.sin(angle)
|
|
73
75
|
return { x: a.x * cos - a.y * sin, y: a.x * sin + a.y * cos }
|
|
74
76
|
}
|
|
75
77
|
|
|
@@ -85,7 +87,7 @@ export const dot = (a: Vec2, b: Vec2): number => a.x * b.x + a.y * b.y
|
|
|
85
87
|
*
|
|
86
88
|
* @category Operations
|
|
87
89
|
*/
|
|
88
|
-
export const length = (a: Vec2): number =>
|
|
90
|
+
export const length = (a: Vec2): number => Ops.hypot(a.x, a.y)
|
|
89
91
|
|
|
90
92
|
/**
|
|
91
93
|
* Squared length of `a`, for comparisons without a square root.
|
|
@@ -99,7 +101,7 @@ export const lengthSquared = (a: Vec2): number => a.x * a.x + a.y * a.y
|
|
|
99
101
|
*
|
|
100
102
|
* @category Operations
|
|
101
103
|
*/
|
|
102
|
-
export const distance = (a: Vec2, b: Vec2): number =>
|
|
104
|
+
export const distance = (a: Vec2, b: Vec2): number => Ops.hypot(a.x - b.x, a.y - b.y)
|
|
103
105
|
|
|
104
106
|
/**
|
|
105
107
|
* Squared distance between two points, for comparisons without a square root.
|
|
@@ -120,7 +122,7 @@ export const distanceSquared = (a: Vec2, b: Vec2): number => {
|
|
|
120
122
|
* @category Operations
|
|
121
123
|
*/
|
|
122
124
|
export const normalizeOr = (a: Vec2, fallback: Vec2): Vec2 => {
|
|
123
|
-
const len =
|
|
125
|
+
const len = Ops.hypot(a.x, a.y)
|
|
124
126
|
return len < 1e-9 ? fallback : { x: a.x / len, y: a.y / len }
|
|
125
127
|
}
|
|
126
128
|
|
|
@@ -139,7 +141,7 @@ export const lerp = (a: Vec2, b: Vec2, t: number): Vec2 => ({ x: a.x + (b.x - a.
|
|
|
139
141
|
export const moveToward = (current: Vec2, target: Vec2, maxDelta: number): Vec2 => {
|
|
140
142
|
const dx = target.x - current.x
|
|
141
143
|
const dy = target.y - current.y
|
|
142
|
-
const len =
|
|
144
|
+
const len = Ops.hypot(dx, dy)
|
|
143
145
|
if (len <= maxDelta || len < 1e-9) return target
|
|
144
146
|
return { x: current.x + (dx / len) * maxDelta, y: current.y + (dy / len) * maxDelta }
|
|
145
147
|
}
|
|
@@ -150,6 +152,6 @@ export const moveToward = (current: Vec2, target: Vec2, maxDelta: number): Vec2
|
|
|
150
152
|
* @category Operations
|
|
151
153
|
*/
|
|
152
154
|
export const clampLength = (a: Vec2, max: number): Vec2 => {
|
|
153
|
-
const len =
|
|
155
|
+
const len = Ops.hypot(a.x, a.y)
|
|
154
156
|
return len <= max ? a : { x: (a.x / len) * max, y: (a.y / len) * max }
|
|
155
157
|
}
|
package/src/Vector2.ts
CHANGED
|
@@ -154,24 +154,36 @@ export const zero: () => Vector2 = internal.zero
|
|
|
154
154
|
/**
|
|
155
155
|
* Returns the sum of two branded vectors.
|
|
156
156
|
*
|
|
157
|
+
* Keeps the brands without re-validating: finite while the result stays
|
|
158
|
+
* within `±Number.MAX_VALUE` (see `Scalar.Finite`).
|
|
159
|
+
*
|
|
157
160
|
* @category Operations
|
|
158
161
|
*/
|
|
159
162
|
export const add: (left: Vector2, right: Vector2) => Vector2 = internal.add
|
|
160
163
|
/**
|
|
161
164
|
* Returns the component-wise difference between two branded vectors.
|
|
162
165
|
*
|
|
166
|
+
* Keeps the brands without re-validating: finite while the result stays
|
|
167
|
+
* within `±Number.MAX_VALUE` (see `Scalar.Finite`).
|
|
168
|
+
*
|
|
163
169
|
* @category Operations
|
|
164
170
|
*/
|
|
165
171
|
export const subtract: (left: Vector2, right: Vector2) => Vector2 = internal.subtract
|
|
166
172
|
/**
|
|
167
173
|
* Scales one vector by one validated finite scalar.
|
|
168
174
|
*
|
|
175
|
+
* Keeps the brands without re-validating: finite while the result stays
|
|
176
|
+
* within `±Number.MAX_VALUE` (see `Scalar.Finite`).
|
|
177
|
+
*
|
|
169
178
|
* @category Operations
|
|
170
179
|
*/
|
|
171
180
|
export const scale: (vector: Vector2, scalar: Scalar.Finite) => Vector2 = internal.scale
|
|
172
181
|
/**
|
|
173
182
|
* Returns the squared length of one vector.
|
|
174
183
|
*
|
|
184
|
+
* Keeps the brands without re-validating: finite while the result stays
|
|
185
|
+
* within `±Number.MAX_VALUE` (see `Scalar.Finite`).
|
|
186
|
+
*
|
|
175
187
|
* @category Operations
|
|
176
188
|
*/
|
|
177
189
|
export const lengthSquared: (vector: Vector2) => Scalar.NonNegative = internal.lengthSquared
|
package/src/index.ts
CHANGED
|
@@ -10,12 +10,17 @@
|
|
|
10
10
|
* `Box2` edges, `Collide2` swept circle collision, a `SpatialGrid` broad
|
|
11
11
|
* phase, a `FlowField` for finding the way around walls, and a deterministic
|
|
12
12
|
* `Random` whose state is one integer.
|
|
13
|
+
*
|
|
14
|
+
* `Ops` holds the math functions engines are allowed to approximate (`sin`,
|
|
15
|
+
* `cos`, `atan2`, `exp`, `pow`, `hypot`, ...) as ports that return the same
|
|
16
|
+
* bits everywhere: use them in simulation code instead of `Math`'s.
|
|
13
17
|
*/
|
|
14
18
|
export * as Aabb from "./Aabb.ts"
|
|
15
19
|
export * as Box2 from "./Box2.ts"
|
|
16
20
|
export * as Collide2 from "./Collide2.ts"
|
|
17
21
|
export * as FlowField from "./FlowField.ts"
|
|
18
22
|
export * as InputAxis from "./InputAxis.ts"
|
|
23
|
+
export * as Ops from "./Ops.ts"
|
|
19
24
|
export * as Random from "./Random.ts"
|
|
20
25
|
export * as Scalar from "./Scalar.ts"
|
|
21
26
|
export * as Size2 from "./Size2.ts"
|