@typeonce/ritmo-math 0.11.0 → 0.13.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +1 -1
  3. package/dist/Aabb.d.ts +15 -15
  4. package/dist/Aabb.d.ts.map +1 -1
  5. package/dist/Aabb.js +13 -5
  6. package/dist/Aabb.js.map +1 -1
  7. package/dist/Random.d.ts +16 -6
  8. package/dist/Random.d.ts.map +1 -1
  9. package/dist/Random.js +13 -3
  10. package/dist/Random.js.map +1 -1
  11. package/dist/Scalar.d.ts +26 -7
  12. package/dist/Scalar.d.ts.map +1 -1
  13. package/dist/Scalar.js +6 -3
  14. package/dist/Scalar.js.map +1 -1
  15. package/dist/Size2.d.ts +15 -15
  16. package/dist/Size2.d.ts.map +1 -1
  17. package/dist/Size2.js +13 -5
  18. package/dist/Size2.js.map +1 -1
  19. package/dist/Vector2.d.ts +15 -15
  20. package/dist/Vector2.d.ts.map +1 -1
  21. package/dist/Vector2.js +13 -5
  22. package/dist/Vector2.js.map +1 -1
  23. package/dist/index.d.ts +3 -3
  24. package/dist/index.js +3 -3
  25. package/dist/internal/aabb.d.ts +9 -2
  26. package/dist/internal/aabb.d.ts.map +1 -1
  27. package/dist/internal/aabb.js +3 -1
  28. package/dist/internal/aabb.js.map +1 -1
  29. package/dist/internal/codec.d.ts +9 -0
  30. package/dist/internal/codec.d.ts.map +1 -0
  31. package/dist/internal/codec.js +16 -0
  32. package/dist/internal/codec.js.map +1 -0
  33. package/dist/internal/scalar.d.ts +3 -4
  34. package/dist/internal/scalar.d.ts.map +1 -1
  35. package/dist/internal/scalar.js +19 -12
  36. package/dist/internal/scalar.js.map +1 -1
  37. package/dist/internal/size2.d.ts +9 -2
  38. package/dist/internal/size2.d.ts.map +1 -1
  39. package/dist/internal/size2.js +3 -1
  40. package/dist/internal/size2.js.map +1 -1
  41. package/dist/internal/vector2.d.ts +9 -2
  42. package/dist/internal/vector2.d.ts.map +1 -1
  43. package/dist/internal/vector2.js +3 -1
  44. package/dist/internal/vector2.js.map +1 -1
  45. package/package.json +2 -2
  46. package/src/Aabb.ts +15 -16
  47. package/src/Random.ts +21 -7
  48. package/src/Scalar.ts +27 -7
  49. package/src/Size2.ts +15 -16
  50. package/src/Vector2.ts +15 -16
  51. package/src/index.ts +3 -3
  52. package/src/internal/aabb.ts +8 -2
  53. package/src/internal/codec.ts +21 -0
  54. package/src/internal/scalar.ts +24 -13
  55. package/src/internal/size2.ts +8 -2
  56. package/src/internal/vector2.ts +8 -2
package/src/Scalar.ts CHANGED
@@ -36,13 +36,14 @@
36
36
  */
37
37
  import type * as Brand from "@typeonce/ritmo/Brand"
38
38
  import * as internal from "./internal/scalar.ts"
39
+ import type { StandardSchema } from "@typeonce/ritmo/Descriptor"
39
40
  import type * as Result from "@typeonce/ritmo/Result"
40
41
 
41
42
  /**
42
43
  * Finite number brand.
43
44
  *
44
45
  * Values of this type are guaranteed not to be `NaN`, `Infinity`, or
45
- * `-Infinity` when they come from a validator (`result`, `decode`). The
46
+ * `-Infinity` when they come from a constructor (`result`, `option`) or a `codec`. The
46
47
  * arithmetic helpers here and in `Vector2` keep the brand without
47
48
  * re-validating, so their results are finite only while they stay within
48
49
  * `±Number.MAX_VALUE`: game-sized values never come close, but validate
@@ -85,27 +86,46 @@ export type InterpolationError = {
85
86
  readonly end: number
86
87
  }
87
88
 
89
+ /**
90
+ * The constructors, guard, and schema of one scalar brand.
91
+ *
92
+ * @category Constructors
93
+ */
94
+ export interface Constructor<Value> {
95
+ /** Validates one number and returns an explicit success/failure result. */
96
+ readonly result: (raw: number) => Result.Result<Value, Error>
97
+ /** Validates one number and returns `null` on failure. */
98
+ readonly option: (raw: number) => Value | null
99
+ /** Whether any value is a number of this brand. */
100
+ readonly is: (raw: unknown) => raw is Value
101
+ /** The Standard Schema for `Descriptor.ConstructedComponent(...)`: the saved form is the number itself. */
102
+ readonly codec: StandardSchema<number, Value>
103
+ }
104
+
88
105
  /**
89
106
  * Constructor for {@link Finite}: `result` (explicit failure), `option`
90
- * (`null` on failure), and `is` (a type guard).
107
+ * (`null` on failure), `is` (a type guard), and `codec`, for
108
+ * `Descriptor.ConstructedComponent(Scalar.Finite.codec)`.
91
109
  *
92
110
  * @category Constructors
93
111
  */
94
- export const Finite = internal.Finite
112
+ export const Finite: Constructor<Finite> = internal.Finite
95
113
  /**
96
114
  * Constructor for {@link NonNegative}: `result` (explicit failure), `option`
97
- * (`null` on failure), and `is` (a type guard).
115
+ * (`null` on failure), `is` (a type guard), and `codec`, for
116
+ * `Descriptor.ConstructedComponent(Scalar.NonNegative.codec)`.
98
117
  *
99
118
  * @category Constructors
100
119
  */
101
- export const NonNegative = internal.NonNegative
120
+ export const NonNegative: Constructor<NonNegative> = internal.NonNegative
102
121
  /**
103
122
  * Constructor for {@link Positive}: `result` (explicit failure), `option`
104
- * (`null` on failure), and `is` (a type guard).
123
+ * (`null` on failure), `is` (a type guard), and `codec`, for
124
+ * `Descriptor.ConstructedComponent(Scalar.Positive.codec)`.
105
125
  *
106
126
  * @category Constructors
107
127
  */
108
- export const Positive = internal.Positive
128
+ export const Positive: Constructor<Positive> = internal.Positive
109
129
 
110
130
  /**
111
131
  * Clamps one finite value between two finite bounds.
package/src/Size2.ts CHANGED
@@ -31,6 +31,7 @@
31
31
  * Pure readers and raw views over already-validated size values.
32
32
  */
33
33
  import type * as Brand from "@typeonce/ritmo/Brand"
34
+ import type { StandardSchema } from "@typeonce/ritmo/Descriptor"
34
35
  import * as internal from "./internal/size2.ts"
35
36
  import type * as Result from "@typeonce/ritmo/Result"
36
37
  import type * as Scalar from "./Scalar.ts"
@@ -67,16 +68,6 @@ export type Error = {
67
68
  readonly input: Raw
68
69
  }
69
70
 
70
- /**
71
- * Failure produced by `decode` when the input does not have the raw shape.
72
- *
73
- * @category Size Types
74
- */
75
- export type DecodeError = {
76
- readonly tag: "Size2/Malformed"
77
- readonly input: unknown
78
- }
79
-
80
71
  /**
81
72
  * Validates one raw size and returns an explicit result.
82
73
  *
@@ -84,13 +75,20 @@ export type DecodeError = {
84
75
  */
85
76
  export const result: (raw: Raw) => Result.Result<Size2, Error> = internal.result
86
77
  /**
87
- * Validates untrusted input of any shape, for example a value read from a
88
- * save file. Descriptors built from this module use it when restoring
89
- * snapshots.
78
+ * The codec of `Size2` (a Standard Schema, see `Descriptor.Codec`): give it to
79
+ * `Descriptor.ConstructedComponent(...)` so raw input and saves are validated.
80
+ * Its saved form is the raw shape, so it needs no `encode`. To put a `Size2`
81
+ * inside your own object schema, use {@link is} with your library's guard
82
+ * schema (`Schema.declare`, `z.custom`).
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * const Position = Descriptor.ConstructedComponent(Size2.codec)("Game/Position")
87
+ * ```
90
88
  *
91
89
  * @category Construction
92
90
  */
93
- export const decode: (raw: unknown) => Result.Result<Size2, Error | DecodeError> = internal.decode
91
+ export const codec: StandardSchema<Raw, Size2> = internal.codec
94
92
  /**
95
93
  * Validates one raw size and returns `null` on failure.
96
94
  *
@@ -98,11 +96,12 @@ export const decode: (raw: unknown) => Result.Result<Size2, Error | DecodeError>
98
96
  */
99
97
  export const option: (raw: Raw) => Size2 | null = internal.option
100
98
  /**
101
- * Checks whether one raw size already satisfies the size invariants.
99
+ * Whether any value is a valid `Size2`: a guard, also for building a
100
+ * `Size2` field in your own schema (`Schema.declare(Size2.is)`).
102
101
  *
103
102
  * @category Construction
104
103
  */
105
- export const is: (raw: Raw) => raw is Raw & Size2 = internal.is
104
+ export const is: (raw: unknown) => raw is Size2 = internal.is
106
105
  /**
107
106
  * Converts one branded size back to a fresh raw object.
108
107
  *
package/src/Vector2.ts CHANGED
@@ -36,6 +36,7 @@
36
36
  * Immutable vector math and normalization helpers on already-validated values.
37
37
  */
38
38
  import type * as Brand from "@typeonce/ritmo/Brand"
39
+ import type { StandardSchema } from "@typeonce/ritmo/Descriptor"
39
40
  import * as internal from "./internal/vector2.ts"
40
41
  import type * as Result from "@typeonce/ritmo/Result"
41
42
  import type * as Scalar from "./Scalar.ts"
@@ -72,16 +73,6 @@ export type Error = {
72
73
  readonly input: Raw
73
74
  }
74
75
 
75
- /**
76
- * Failure produced by `decode` when the input does not have the raw shape.
77
- *
78
- * @category Vector Types
79
- */
80
- export type DecodeError = {
81
- readonly tag: "Vector2/Malformed"
82
- readonly input: unknown
83
- }
84
-
85
76
  /**
86
77
  * Explicit failure produced when a zero-length vector cannot be normalized.
87
78
  *
@@ -103,13 +94,20 @@ export type NormalizeError = {
103
94
  */
104
95
  export const result: (raw: Raw) => Result.Result<Vector2, Error> = internal.result
105
96
  /**
106
- * Validates untrusted input of any shape, for example a value read from a
107
- * save file. Descriptors built from this module use it when restoring
108
- * snapshots.
97
+ * The codec of `Vector2` (a Standard Schema, see `Descriptor.Codec`): give it to
98
+ * `Descriptor.ConstructedComponent(...)` so raw input and saves are validated.
99
+ * Its saved form is the raw shape, so it needs no `encode`. To put a `Vector2`
100
+ * inside your own object schema, use {@link is} with your library's guard
101
+ * schema (`Schema.declare`, `z.custom`).
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * const Position = Descriptor.ConstructedComponent(Vector2.codec)("Game/Position")
106
+ * ```
109
107
  *
110
108
  * @category Construction
111
109
  */
112
- export const decode: (raw: unknown) => Result.Result<Vector2, Error | DecodeError> = internal.decode
110
+ export const codec: StandardSchema<Raw, Vector2> = internal.codec
113
111
  /**
114
112
  * Validates one raw vector and returns `null` on failure.
115
113
  *
@@ -117,11 +115,12 @@ export const decode: (raw: unknown) => Result.Result<Vector2, Error | DecodeErro
117
115
  */
118
116
  export const option: (raw: Raw) => Vector2 | null = internal.option
119
117
  /**
120
- * Checks whether one raw vector already satisfies the vector invariants.
118
+ * Whether any value is a valid `Vector2`: a guard, also for building a
119
+ * `Vector2` field in your own schema (`Schema.declare(Vector2.is)`).
121
120
  *
122
121
  * @category Construction
123
122
  */
124
- export const is: (raw: Raw) => raw is Raw & Vector2 = internal.is
123
+ export const is: (raw: unknown) => raw is Vector2 = internal.is
125
124
  /**
126
125
  * Converts one branded vector back to a fresh raw object.
127
126
  *
package/src/index.ts CHANGED
@@ -2,9 +2,9 @@
2
2
  * Math for game code.
3
3
  *
4
4
  * Validated values (`Scalar`, `Vector2`, `Size2`, `Aabb`) expose
5
- * Result-returning constructors, so they plug into
6
- * `Descriptor.ConstructedComponent(...)` / `ConstructedResource(...)` and are
7
- * validated once at the boundary. `InputAxis` normalizes directional input.
5
+ * Result-returning constructors, a guard (`is`), and one `codec` each, so
6
+ * they plug into `Descriptor.ConstructedComponent(Vector2.codec)` /
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
10
  * `Box2` edges, `Collide2` swept circle collision, a `SpatialGrid` broad
@@ -1,3 +1,4 @@
1
+ import { codecOf } from "./codec.ts"
1
2
  import * as Result from "@typeonce/ritmo/Result"
2
3
  import * as Size2 from "../Size2.ts"
3
4
  import * as Vector2 from "../Vector2.ts"
@@ -36,7 +37,7 @@ export const option = (raw: Aabb.Raw): Aabb.Aabb | null => {
36
37
  return next.ok ? next.value : null
37
38
  }
38
39
 
39
- export const is = (raw: Aabb.Raw): raw is Aabb.Raw & Aabb.Aabb => result(raw).ok
40
+ export const is = (raw: unknown): raw is Aabb.Aabb => decode(raw).ok
40
41
 
41
42
  export const position = (aabb: Aabb.Aabb): Vector2.Vector2 => aabb.position
42
43
  export const size = (aabb: Aabb.Aabb): Size2.Size2 => aabb.size
@@ -66,10 +67,15 @@ export const overlapsVertically = (first: Aabb.Aabb, second: Aabb.Aabb): boolean
66
67
  export const translate = (aabb: Aabb.Aabb, delta: Vector2.Vector2): Aabb.Aabb =>
67
68
  make(Vector2.add(aabb.position, delta), aabb.size)
68
69
 
69
- export const decode = (raw: unknown): Result.Result<Aabb.Aabb, Aabb.Error | Aabb.DecodeError> => {
70
+ /** Untrusted input without the raw shape. */
71
+ type Malformed = { readonly tag: "Aabb/Malformed"; readonly input: unknown }
72
+
73
+ export const decode = (raw: unknown): Result.Result<Aabb.Aabb, Aabb.Error | Malformed> => {
70
74
  if (typeof raw !== "object" || raw === null) return Result.failure({ tag: "Aabb/Malformed", input: raw })
71
75
  const { position, size } = raw as Record<string, unknown>
72
76
  return isVector2Shape(position) && isSize2Shape(size)
73
77
  ? result({ position, size })
74
78
  : Result.failure({ tag: "Aabb/Malformed", input: raw })
75
79
  }
80
+
81
+ export const codec = codecOf<Aabb.Raw, Aabb.Aabb>(decode, "a box { x, y, width, height } of finite numbers with a non-negative size")
@@ -0,0 +1,21 @@
1
+ import type { StandardSchema } from "@typeonce/ritmo/Descriptor"
2
+ import type * as Result from "@typeonce/ritmo/Result"
3
+
4
+ /**
5
+ * The codec of one math type, from its decode: a Standard Schema whose saved
6
+ * form is the raw shape. The value is already plain data, so it needs no
7
+ * `encode`.
8
+ */
9
+ export const codecOf = <Raw, Value>(
10
+ decode: (raw: unknown) => Result.Result<Value, unknown>,
11
+ expected: string
12
+ ): StandardSchema<Raw, Value> => ({
13
+ "~standard": {
14
+ version: 1,
15
+ vendor: "ritmo-math",
16
+ validate: (raw) => {
17
+ const value = decode(raw)
18
+ return value.ok ? { value: value.value } : { issues: [{ message: `Expected ${expected}` }] }
19
+ }
20
+ }
21
+ })
@@ -1,18 +1,33 @@
1
- import * as Brand from "@typeonce/ritmo/Brand"
2
1
  import * as Result from "@typeonce/ritmo/Result"
3
2
  import type * as Scalar from "../Scalar.ts"
3
+ import { codecOf } from "./codec.ts"
4
4
 
5
- const finiteBrand = Brand.refine<Scalar.Finite, number, Scalar.Error>((value) =>
5
+ // A branded number's constructors, guard, and codec, from its check.
6
+ const refine = <Value extends number>(
7
+ validate: (raw: number) => Result.Result<Value, Scalar.Error>,
8
+ expected: string
9
+ ): Scalar.Constructor<Value> => ({
10
+ result: validate,
11
+ option: (raw) => {
12
+ const result = validate(raw)
13
+ return result.ok ? result.value : null
14
+ },
15
+ is: (raw): raw is Value => typeof raw === "number" && validate(raw).ok,
16
+ codec: codecOf<number, Value>((raw) => typeof raw === "number" ? validate(raw) : Result.failure(raw), expected)
17
+ })
18
+
19
+ export const Finite = refine<Scalar.Finite>((value) =>
6
20
  Number.isFinite(value)
7
21
  ? Result.success(value as Scalar.Finite)
8
22
  : Result.failure({
9
23
  tag: "Scalar/NotFinite",
10
24
  value
11
- })
25
+ }),
26
+ "a finite number"
12
27
  )
13
28
 
14
- const nonNegativeBrand = Brand.refine<Scalar.NonNegative, number, Scalar.Error>((value) => {
15
- const finite = finiteBrand.result(value)
29
+ export const NonNegative = refine<Scalar.NonNegative>((value) => {
30
+ const finite = Finite.result(value)
16
31
  if (!finite.ok) {
17
32
  return finite
18
33
  }
@@ -23,10 +38,10 @@ const nonNegativeBrand = Brand.refine<Scalar.NonNegative, number, Scalar.Error>(
23
38
  tag: "Scalar/Negative",
24
39
  value
25
40
  })
26
- })
41
+ }, "a finite number, 0 or more")
27
42
 
28
- const positiveBrand = Brand.refine<Scalar.Positive, number, Scalar.Error>((value) => {
29
- const finite = finiteBrand.result(value)
43
+ export const Positive = refine<Scalar.Positive>((value) => {
44
+ const finite = Finite.result(value)
30
45
  if (!finite.ok) {
31
46
  return finite
32
47
  }
@@ -37,11 +52,7 @@ const positiveBrand = Brand.refine<Scalar.Positive, number, Scalar.Error>((value
37
52
  tag: "Scalar/NonPositive",
38
53
  value
39
54
  })
40
- })
41
-
42
- export const Finite = finiteBrand
43
- export const NonNegative = nonNegativeBrand
44
- export const Positive = positiveBrand
55
+ }, "a finite number above 0")
45
56
 
46
57
  export const clamp = (value: number, min: number, max: number): Scalar.Finite =>
47
58
  Math.min(Math.max(value, min), max) as Scalar.Finite
@@ -1,3 +1,4 @@
1
+ import { codecOf } from "./codec.ts"
1
2
  import * as Result from "@typeonce/ritmo/Result"
2
3
  import * as Scalar from "../Scalar.ts"
3
4
  import type * as Size2 from "../Size2.ts"
@@ -28,7 +29,7 @@ export const option = (raw: Size2.Raw): Size2.Size2 | null => {
28
29
  return next.ok ? next.value : null
29
30
  }
30
31
 
31
- export const is = (raw: Size2.Raw): raw is Size2.Raw & Size2.Size2 => result(raw).ok
32
+ export const is = (raw: unknown): raw is Size2.Size2 => decode(raw).ok
32
33
 
33
34
  export const toRaw = (size: Size2.Size2): Size2.Raw => ({
34
35
  width: size.width,
@@ -44,5 +45,10 @@ export const height = (size: Size2.Size2): Scalar.NonNegative => size.height
44
45
  export const isRawShape = (raw: unknown): raw is Size2.Raw =>
45
46
  typeof raw === "object" && raw !== null && typeof (raw as Record<string, unknown>)["width"] === "number" && typeof (raw as Record<string, unknown>)["height"] === "number"
46
47
 
47
- export const decode = (raw: unknown): Result.Result<Size2.Size2, Size2.Error | Size2.DecodeError> =>
48
+ /** Untrusted input without the raw shape. */
49
+ type Malformed = { readonly tag: "Size2/Malformed"; readonly input: unknown }
50
+
51
+ export const decode = (raw: unknown): Result.Result<Size2.Size2, Size2.Error | Malformed> =>
48
52
  isRawShape(raw) ? result(raw) : Result.failure({ tag: "Size2/Malformed", input: raw })
53
+
54
+ export const codec = codecOf<Size2.Raw, Size2.Size2>(decode, "a size { width, height } of non-negative finite numbers")
@@ -1,3 +1,4 @@
1
+ import { codecOf } from "./codec.ts"
1
2
  import * as Result from "@typeonce/ritmo/Result"
2
3
  import * as Scalar from "../Scalar.ts"
3
4
  import type * as Vector2 from "../Vector2.ts"
@@ -28,7 +29,7 @@ export const option = (raw: Vector2.Raw): Vector2.Vector2 | null => {
28
29
  return next.ok ? next.value : null
29
30
  }
30
31
 
31
- export const is = (raw: Vector2.Raw): raw is Vector2.Raw & Vector2.Vector2 => result(raw).ok
32
+ export const is = (raw: unknown): raw is Vector2.Vector2 => decode(raw).ok
32
33
 
33
34
  export const toRaw = (vector: Vector2.Vector2): Vector2.Raw => ({
34
35
  x: vector.x,
@@ -111,5 +112,10 @@ export const normalizeXYOrZero = (x: number, y: number): {
111
112
  export const isRawShape = (raw: unknown): raw is Vector2.Raw =>
112
113
  typeof raw === "object" && raw !== null && typeof (raw as Record<string, unknown>)["x"] === "number" && typeof (raw as Record<string, unknown>)["y"] === "number"
113
114
 
114
- export const decode = (raw: unknown): Result.Result<Vector2.Vector2, Vector2.Error | Vector2.DecodeError> =>
115
+ /** Untrusted input without the raw shape. */
116
+ type Malformed = { readonly tag: "Vector2/Malformed"; readonly input: unknown }
117
+
118
+ export const decode = (raw: unknown): Result.Result<Vector2.Vector2, Vector2.Error | Malformed> =>
115
119
  isRawShape(raw) ? result(raw) : Result.failure({ tag: "Vector2/Malformed", input: raw })
120
+
121
+ export const codec = codecOf<Vector2.Raw, Vector2.Vector2>(decode, "a vector { x, y } of finite numbers")