bireactive 0.2.0 → 0.2.1

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 (82) hide show
  1. package/README.md +4 -4
  2. package/dist/{core/anim.d.ts → animation/animators.d.ts} +5 -3
  3. package/dist/{core/anim.js → animation/animators.js} +8 -5
  4. package/dist/animation/index.d.ts +1 -0
  5. package/dist/animation/index.js +1 -0
  6. package/dist/constraints/cluster.js +1 -1
  7. package/dist/constraints/expose.d.ts +17 -0
  8. package/dist/constraints/expose.js +61 -0
  9. package/dist/constraints/index.d.ts +1 -0
  10. package/dist/constraints/index.js +1 -0
  11. package/dist/constraints/linalg.d.ts +1 -8
  12. package/dist/constraints/linalg.js +5 -108
  13. package/dist/core/{signal.d.ts → cell.d.ts} +29 -6
  14. package/dist/core/{signal.js → cell.js} +134 -53
  15. package/dist/core/derived-geometry.d.ts +11 -0
  16. package/dist/core/derived-geometry.js +32 -0
  17. package/dist/core/index.d.ts +5 -10
  18. package/dist/core/index.js +5 -10
  19. package/dist/core/{aggregates.d.ts → lenses/aggregates.d.ts} +3 -14
  20. package/dist/core/{aggregates.js → lenses/aggregates.js} +5 -78
  21. package/dist/core/lenses/closed-form-policies.d.ts +6 -13
  22. package/dist/core/lenses/closed-form-policies.js +14 -24
  23. package/dist/core/lenses/decompositions.d.ts +14 -0
  24. package/dist/core/lenses/decompositions.js +224 -0
  25. package/dist/core/lenses/domain-aggregates.d.ts +10 -22
  26. package/dist/core/lenses/domain-aggregates.js +25 -39
  27. package/dist/core/{new-primitives.d.ts → lenses/geometry.d.ts} +11 -14
  28. package/dist/core/{new-primitives.js → lenses/geometry.js} +23 -37
  29. package/dist/core/lenses/index.d.ts +6 -4
  30. package/dist/core/lenses/index.js +6 -15
  31. package/dist/core/lenses/typed-factor.d.ts +1 -6
  32. package/dist/core/lenses/typed-factor.js +12 -114
  33. package/dist/core/{network-utils.d.ts → lifecycle.d.ts} +1 -1
  34. package/dist/core/{network-utils.js → lifecycle.js} +2 -2
  35. package/dist/core/linalg.d.ts +10 -0
  36. package/dist/core/linalg.js +109 -0
  37. package/dist/core/values/anchor.d.ts +1 -1
  38. package/dist/core/values/audio.d.ts +1 -1
  39. package/dist/core/values/audio.js +1 -1
  40. package/dist/core/values/bool.d.ts +1 -1
  41. package/dist/core/values/bool.js +1 -1
  42. package/dist/core/values/box.d.ts +2 -3
  43. package/dist/core/values/box.js +5 -6
  44. package/dist/core/values/canvas.d.ts +1 -1
  45. package/dist/core/values/canvas.js +2 -3
  46. package/dist/core/values/color.d.ts +2 -3
  47. package/dist/core/values/color.js +3 -4
  48. package/dist/core/values/flags.d.ts +1 -1
  49. package/dist/core/values/flags.js +1 -1
  50. package/dist/core/values/matrix.d.ts +1 -1
  51. package/dist/core/values/matrix.js +2 -3
  52. package/dist/core/values/num.d.ts +2 -3
  53. package/dist/core/values/num.js +2 -2
  54. package/dist/core/values/pose.d.ts +5 -1
  55. package/dist/core/values/pose.js +11 -1
  56. package/dist/core/values/range.d.ts +6 -4
  57. package/dist/core/values/range.js +16 -11
  58. package/dist/core/values/str.d.ts +1 -1
  59. package/dist/core/values/str.js +1 -1
  60. package/dist/core/values/template.d.ts +1 -1
  61. package/dist/core/values/transform.d.ts +2 -3
  62. package/dist/core/values/transform.js +2 -3
  63. package/dist/core/values/tri.d.ts +1 -1
  64. package/dist/core/values/tri.js +1 -1
  65. package/dist/core/values/vec.d.ts +2 -3
  66. package/dist/core/values/vec.js +4 -5
  67. package/dist/ext/timeline.js +3 -3
  68. package/dist/index.d.ts +1 -0
  69. package/dist/index.js +1 -0
  70. package/dist/propagators/layout.js +1 -1
  71. package/dist/shapes/handle.js +4 -4
  72. package/dist/shapes/shape.js +7 -7
  73. package/dist/shapes/transitions.js +2 -2
  74. package/package.json +7 -2
  75. package/dist/core/introspect.d.ts +0 -5
  76. package/dist/core/introspect.js +0 -31
  77. package/dist/core/lenses/factor-lens.d.ts +0 -42
  78. package/dist/core/lenses/factor-lens.js +0 -419
  79. package/dist/core/writable.d.ts +0 -15
  80. package/dist/core/writable.js +0 -29
  81. /package/dist/{core/tree.d.ts → tree.d.ts} +0 -0
  82. /package/dist/{core/tree.js → tree.js} +0 -0
@@ -6,11 +6,11 @@
6
6
  // line/circle, and PCA decompose into combinations of them.
7
7
  //
8
8
  // Layout: building-block actions (rigidTranslate, rotateAbout,
9
- // scaleAbout, scaleAboutXY), Procrustes re-expressed via them, then
10
- // closed-form decompositions (bestFitLine, bestFitCircle, pcaLens,
11
- // totalLens). All exact, idempotent, cross-channel invariant by
12
- // construction, on the same `Cls.lens` machinery — no engine changes.
13
- import { centroidLens, Num, Vec, } from "../index.js";
9
+ // scaleAbout, scaleAboutXY), then closed-form decompositions
10
+ // (bestFitLine, bestFitCircle, pca, total). All exact, idempotent,
11
+ // cross-channel invariant by construction, on the same `Cls.lens`
12
+ // machinery — no engine changes.
13
+ import { mean, Num, Vec, } from "../index.js";
14
14
  import { continuous, remember } from "./memory.js";
15
15
  // Pivotal trait lookup via the value class's `static traits.pivotal` slot.
16
16
  // biome-ignore lint/suspicious/noExplicitAny: dynamic trait lookup
@@ -25,9 +25,9 @@ function pivotalOf(input) {
25
25
  return p;
26
26
  }
27
27
  /** Writable centroid; on write, translates every point by the delta.
28
- * Alias of `centroidLens` under the "policy" naming. */
28
+ * The Vec-specific group-action reading of `mean`. */
29
29
  export function rigidTranslate(points) {
30
- return centroidLens(points);
30
+ return mean(points);
31
31
  }
32
32
  /** Writable angle from `pivot` to `points[0]`; write rotates every input
33
33
  * about `pivot` by (target − current) via its `Pivotal` trait.
@@ -35,7 +35,7 @@ export function rigidTranslate(points) {
35
35
  * Trait-generic: Vec rotates position; Pose rotates position AND
36
36
  * orientation. Rotation-about-pivot fixes the pivot and preserves radial
37
37
  * distances, so scale-about-pivot reads unchanged. `pivot` is reactive
38
- * (re-read per write); pass `centroidLens(points)` for rotation about
38
+ * (re-read per write); pass `rigidTranslate(points)` for rotation about
39
39
  * the cluster's own centroid. */
40
40
  export function rotateAbout(points, pivot) {
41
41
  const K = points.length;
@@ -153,16 +153,6 @@ export function scaleAboutXY(points, pivot) {
153
153
  },
154
154
  });
155
155
  }
156
- /** Same semantics as `factor-lens.ts`'s `procrustesLens`, decomposed
157
- * into three building-block lenses sharing a centroid. */
158
- export function procrustesViaBuildingBlocks(points) {
159
- if (points.length < 2)
160
- throw new Error("procrustes: need ≥ 2 points");
161
- const centroid = rigidTranslate(points);
162
- const rotation = rotateAbout(points, centroid);
163
- const scale = scaleAbout(points, centroid);
164
- return { centroid, rotation, scale };
165
- }
166
156
  // Best-fit line.
167
157
  //
168
158
  // K points → {point: centroid, direction: principal-axis angle}.
@@ -188,7 +178,7 @@ function covariance(points, cx, cy) {
188
178
  }
189
179
  return { cxx: cxx / K, cxy: cxy / K, cyy: cyy / K };
190
180
  }
191
- export function bestFitLineLens(points) {
181
+ export function bestFitLine(points) {
192
182
  const K = points.length;
193
183
  if (K < 2)
194
184
  throw new Error("bestFitLine: need ≥ 2 points");
@@ -242,7 +232,7 @@ export function bestFitLineLens(points) {
242
232
  // write radius → scale all about center by target/current
243
233
  // Simplest closed-form fit (mean center). Invariance: translation
244
234
  // preserves radii; uniform scale-about-center preserves the center.
245
- export function bestFitCircleLens(points) {
235
+ export function bestFitCircle(points) {
246
236
  const K = points.length;
247
237
  if (K < 1)
248
238
  throw new Error("bestFitCircle: need ≥ 1 point");
@@ -280,10 +270,10 @@ export function bestFitCircleLens(points) {
280
270
  // write major/minor → scale along that axis by target/current
281
271
  // Each write is a single group action; cross-channel invariance holds
282
272
  // for all pairs.
283
- export function pcaLens(points) {
273
+ export function pca(points) {
284
274
  const K = points.length;
285
275
  if (K < 2)
286
- throw new Error("pcaLens: need ≥ 2 points");
276
+ throw new Error("pca: need ≥ 2 points");
287
277
  const mean = rigidTranslate(points);
288
278
  // 2×2 symmetric eigendecomp → {θ, λ_major, λ_minor}; null when fully
289
279
  // collapsed (λ_major ≈ 0).
@@ -430,10 +420,10 @@ export function pcaLens(points) {
430
420
  * preserving their ratios. A `remember` anchored at zero with a signed
431
421
  * sum feature: a collapse to zero reinflates the stored ratios, seeded
432
422
  * uniform so an all-zero start splits evenly. */
433
- export function totalLens(parts) {
423
+ export function total(parts) {
434
424
  const K = parts.length;
435
425
  if (K < 1)
436
- throw new Error("totalLens: need ≥ 1 part");
426
+ throw new Error("total: need ≥ 1 part");
437
427
  return remember(parts, {
438
428
  anchor: () => 0,
439
429
  feature: (vals) => {
@@ -0,0 +1,14 @@
1
+ import { Num, Vec, type Writable } from "../index.js";
2
+ export declare function meanDiff(a: Num, b: Num): {
3
+ mean: Writable<Num>;
4
+ diff: Writable<Num>;
5
+ };
6
+ export declare function procrustes(points: readonly Writable<Vec>[]): {
7
+ centroid: Writable<Vec>;
8
+ rotation: Writable<Num>;
9
+ scale: Writable<Num>;
10
+ };
11
+ export declare function bbox(points: readonly Writable<Vec>[]): {
12
+ center: Writable<Vec>;
13
+ size: Writable<Vec>;
14
+ };
@@ -0,0 +1,224 @@
1
+ // decompositions.ts — closed-form N→M lens decompositions (Vec/Num).
2
+ //
3
+ // N inputs → M coupled writable outputs, where writing one output
4
+ // preserves the readings of the other M−1 (cross-channel invariance).
5
+ // Each bwd is a hand-rolled group action (translate / rotate / scale
6
+ // about the centroid), so cross-channel invariance is EXACT and one
7
+ // write lands in O(K). For the generic numerical N→M escape hatch (when
8
+ // no closed form fits) see `factor` in `typed-factor.ts`.
9
+ import { Num, Vec } from "../index.js";
10
+ // meanDiff — M=2 isomorphism baseline.
11
+ //
12
+ // (a, b) → ((a+b)/2, a−b). Square full-rank linear lens; bwd is the
13
+ // inverse change of basis — exact, cross-channel invariant.
14
+ export function meanDiff(a, b) {
15
+ const mean = Num.lens([a, b], vals => (vals[0] + vals[1]) / 2, (target, vals) => {
16
+ const d = vals[0] - vals[1];
17
+ return [target + d / 2, target - d / 2];
18
+ });
19
+ const diff = Num.lens([a, b], vals => vals[0] - vals[1], (target, vals) => {
20
+ const m = (vals[0] + vals[1]) / 2;
21
+ return [m + target / 2, m - target / 2];
22
+ });
23
+ return { mean, diff };
24
+ }
25
+ // procrustes — closed-form similarity (the showcase).
26
+ //
27
+ // K writable Vecs → {centroid, rotation (angle of point[0] about
28
+ // centroid), scale (its distance from centroid)}. Each bwd is a
29
+ // closed-form transform about the centroid:
30
+ // write centroid → translate every point by (c − old c)
31
+ // write rotation → rotate every point about centroid by (θ − old θ)
32
+ // write scale → scale every point about centroid by (s / old s)
33
+ // These commute on the cluster's similarity orbit, so the three outputs
34
+ // have EXACT cross-channel invariance. Degenerate: K < 2 leaves
35
+ // rotation/scale undefined; a collapsed cluster (scale → 0) makes
36
+ // rotation singular and scale writes no-ops; target scale = 0 collapses
37
+ // to the centroid.
38
+ export function procrustes(points) {
39
+ const K = points.length;
40
+ if (K < 2)
41
+ throw new Error("procrustes: need ≥ 2 points");
42
+ const centroid = Vec.lens(points, (vals) => {
43
+ let sx = 0;
44
+ let sy = 0;
45
+ for (let i = 0; i < K; i++) {
46
+ sx += vals[i].x;
47
+ sy += vals[i].y;
48
+ }
49
+ return { x: sx / K, y: sy / K };
50
+ }, (target, vals) => {
51
+ let sx = 0;
52
+ let sy = 0;
53
+ for (let i = 0; i < K; i++) {
54
+ sx += vals[i].x;
55
+ sy += vals[i].y;
56
+ }
57
+ const dx = target.x - sx / K;
58
+ const dy = target.y - sy / K;
59
+ const out = new Array(K);
60
+ for (let i = 0; i < K; i++)
61
+ out[i] = { x: vals[i].x + dx, y: vals[i].y + dy };
62
+ return out;
63
+ });
64
+ const rotation = Num.lens(points, (vals) => {
65
+ let sx = 0;
66
+ let sy = 0;
67
+ for (let i = 0; i < K; i++) {
68
+ sx += vals[i].x;
69
+ sy += vals[i].y;
70
+ }
71
+ const cx = sx / K;
72
+ const cy = sy / K;
73
+ return Math.atan2(vals[0].y - cy, vals[0].x - cx);
74
+ }, (target, vals) => {
75
+ let sx = 0;
76
+ let sy = 0;
77
+ for (let i = 0; i < K; i++) {
78
+ sx += vals[i].x;
79
+ sy += vals[i].y;
80
+ }
81
+ const cx = sx / K;
82
+ const cy = sy / K;
83
+ const rx0 = vals[0].x - cx;
84
+ const ry0 = vals[0].y - cy;
85
+ if (rx0 * rx0 + ry0 * ry0 < 1e-24) {
86
+ // Collapsed cluster; no angle to rotate from.
87
+ return vals.map(() => undefined);
88
+ }
89
+ const oldθ = Math.atan2(ry0, rx0);
90
+ const dθ = target - oldθ;
91
+ const cos = Math.cos(dθ);
92
+ const sin = Math.sin(dθ);
93
+ const out = new Array(K);
94
+ for (let i = 0; i < K; i++) {
95
+ const rx = vals[i].x - cx;
96
+ const ry = vals[i].y - cy;
97
+ out[i] = { x: cx + cos * rx - sin * ry, y: cy + sin * rx + cos * ry };
98
+ }
99
+ return out;
100
+ });
101
+ const centroidOf = (vals) => {
102
+ let sx = 0;
103
+ let sy = 0;
104
+ for (let i = 0; i < K; i++) {
105
+ sx += vals[i].x;
106
+ sy += vals[i].y;
107
+ }
108
+ return { x: sx / K, y: sy / K };
109
+ };
110
+ const refreshDevs = (devs, vals) => {
111
+ const c = centroidOf(vals);
112
+ return devs.map((d, i) => {
113
+ const dx = vals[i].x - c.x;
114
+ const dy = vals[i].y - c.y;
115
+ return dx * dx + dy * dy > 1e-18 ? { x: dx, y: dy } : d;
116
+ });
117
+ };
118
+ const scale = Num.lens(points, {
119
+ init: (vals) => {
120
+ const c = centroidOf(vals);
121
+ return { devs: vals.map(v => ({ x: v.x - c.x, y: v.y - c.y })) };
122
+ },
123
+ step: (vals, c) => ({ devs: refreshDevs(c.devs, vals) }),
124
+ fwd: (vals) => {
125
+ const c = centroidOf(vals);
126
+ return Math.hypot(vals[0].x - c.x, vals[0].y - c.y);
127
+ },
128
+ bwd: (target, vals, c) => {
129
+ const cen = centroidOf(vals);
130
+ const d0 = c.devs[0];
131
+ const r0 = Math.hypot(d0.x, d0.y);
132
+ if (r0 < 1e-12)
133
+ return { updates: vals.map(() => undefined), complement: c };
134
+ const k = target / r0;
135
+ const out = c.devs.map(d => ({ x: cen.x + k * d.x, y: cen.y + k * d.y }));
136
+ return { updates: out, complement: c };
137
+ },
138
+ });
139
+ return { centroid, rotation, scale };
140
+ }
141
+ // bbox — closed-form axis-aligned bounding box.
142
+ //
143
+ // K Vecs → {center, size}. Forward is min/max (piecewise-constant
144
+ // Jacobian — fatal for FD), but the closed-form bwd is exact:
145
+ // write center → translate all points by (c − old c)
146
+ // write size → scale all about center by component-wise ratio
147
+ // Center↔size invariance is exact. Degenerate axes (size = 0) write
148
+ // as no-ops; negative size reflects (kept permissive).
149
+ export function bbox(points) {
150
+ const K = points.length;
151
+ if (K < 1)
152
+ throw new Error("bbox: need ≥ 1 point");
153
+ const computeBox = (vals) => {
154
+ let minX = Number.POSITIVE_INFINITY;
155
+ let minY = Number.POSITIVE_INFINITY;
156
+ let maxX = Number.NEGATIVE_INFINITY;
157
+ let maxY = Number.NEGATIVE_INFINITY;
158
+ for (let i = 0; i < K; i++) {
159
+ const x = vals[i].x;
160
+ const y = vals[i].y;
161
+ if (x < minX)
162
+ minX = x;
163
+ if (x > maxX)
164
+ maxX = x;
165
+ if (y < minY)
166
+ minY = y;
167
+ if (y > maxY)
168
+ maxY = y;
169
+ }
170
+ return {
171
+ cx: (minX + maxX) / 2,
172
+ cy: (minY + maxY) / 2,
173
+ sx: maxX - minX,
174
+ sy: maxY - minY,
175
+ };
176
+ };
177
+ const center = Vec.lens(points, (vals) => {
178
+ const b = computeBox(vals);
179
+ return { x: b.cx, y: b.cy };
180
+ }, (target, vals) => {
181
+ const b = computeBox(vals);
182
+ const dx = target.x - b.cx;
183
+ const dy = target.y - b.cy;
184
+ const out = new Array(K);
185
+ for (let i = 0; i < K; i++)
186
+ out[i] = { x: vals[i].x + dx, y: vals[i].y + dy };
187
+ return out;
188
+ });
189
+ const refreshFracs = (fracs, vals) => {
190
+ const b = computeBox(vals);
191
+ const hx = b.sx > 1e-12 ? b.sx / 2 : 0;
192
+ const hy = b.sy > 1e-12 ? b.sy / 2 : 0;
193
+ return fracs.map((f, i) => ({
194
+ x: hx > 0 ? (vals[i].x - b.cx) / hx : f.x,
195
+ y: hy > 0 ? (vals[i].y - b.cy) / hy : f.y,
196
+ }));
197
+ };
198
+ const size = Vec.lens(points, {
199
+ init: (vals) => {
200
+ const b = computeBox(vals);
201
+ const halfX0 = b.sx > 1e-12 ? b.sx / 2 : 1;
202
+ const halfY0 = b.sy > 1e-12 ? b.sy / 2 : 1;
203
+ return {
204
+ fracs: vals.map(v => ({
205
+ x: b.sx > 1e-12 ? (v.x - b.cx) / halfX0 : 0,
206
+ y: b.sy > 1e-12 ? (v.y - b.cy) / halfY0 : 0,
207
+ })),
208
+ };
209
+ },
210
+ step: (vals, c) => ({ fracs: refreshFracs(c.fracs, vals) }),
211
+ fwd: (vals) => {
212
+ const b = computeBox(vals);
213
+ return { x: b.sx, y: b.sy };
214
+ },
215
+ bwd: (target, vals, c) => {
216
+ const b = computeBox(vals);
217
+ const halfTx = target.x / 2;
218
+ const halfTy = target.y / 2;
219
+ const out = c.fracs.map(f => ({ x: b.cx + f.x * halfTx, y: b.cy + f.y * halfTy }));
220
+ return { updates: out, complement: c };
221
+ },
222
+ });
223
+ return { center, size };
224
+ }
@@ -1,9 +1,7 @@
1
1
  import { type Cell, Num, type Read, type Traits, type Val, Vec, type Writable } from "../index.js";
2
- /** Class-inferring mean (writable of `inputs[0]`'s class). Needs `linear`. */
3
- export declare function meanOf<S extends Traits<any, "linear">>(inputs: readonly Writable<S>[]): Writable<S>;
4
- /** Rigid-translate aggregate over any Linear type. Alias of `meanOf`,
5
- * named for the geometric intent. */
6
- export declare function rigidTranslateOf<S extends Traits<any, "linear">>(inputs: readonly Writable<S>[]): Writable<S>;
2
+ /** Equal-weight mean (writable of `inputs[0]`'s class); writes distribute
3
+ * the delta evenly. Class inferred from the first input; needs `linear`. */
4
+ export declare function mean<S extends Traits<any, "linear">>(inputs: readonly Writable<S>[]): Writable<S>;
7
5
  /** Weighted blend of K branches over any `Linear` type. See module note. */
8
6
  export declare function mix<S extends Traits<any, "linear">>(weights: readonly Val<number>[], branches: readonly Writable<S>[]): Writable<S>;
9
7
  /** Two-branch router (mix simplex *vertex*): reads the live branch, writes
@@ -13,15 +11,6 @@ export declare function select<S extends Traits<any, "linear">>(cond: Read<boole
13
11
  /** Two-branch crossfade (mix simplex *edge*): `lerp(a, b, t)`. Writing
14
12
  * keeps `t` fixed and splits the delta by influence. */
15
13
  export declare function crossfade<S extends Traits<any, "linear">>(t: Read<number>, a: Writable<S>, b: Writable<S>): Writable<S>;
16
- type ColorV = {
17
- r: number;
18
- g: number;
19
- b: number;
20
- a: number;
21
- };
22
- /** Mean color of a palette; write shifts every color by the delta
23
- * (rigid translate in RGBA). Via `meanOf`. */
24
- export declare function meanColor(colors: readonly Writable<Traits<ColorV, "linear">>[]): Writable<Traits<ColorV, "linear">>;
25
14
  /** Mean radial distance from the centroid; write scales the cluster's
26
15
  * deviations so the new mean matches the target. Trait-driven via
27
16
  * `Linear` + `Metric`, so it works for any class declaring both (Vec,
@@ -32,23 +21,22 @@ export declare function meanColor(colors: readonly Writable<Traits<ColorV, "line
32
21
  * and a collapse (spread → 0) reinflates the original SHAPE. Centroid is
33
22
  * recomputed every read/write, so an intervening mean translate is not
34
23
  * stale. */
35
- export declare function spreadOf<T extends NonNullable<unknown>, S extends Cell<T> & Traits<T, "linear" | "metric">>(inputs: readonly Writable<S>[]): Writable<Num>;
36
- /** Palette decomposition: K values → {mean, spread}, i.e. centroid +
37
- * uniform scale about it. `meanOf` ∘ `spreadOf`; works for any
38
- * Linear + Metric class. */
39
- export declare function paletteLens<T extends NonNullable<unknown>, S extends Cell<T> & Traits<T, "linear" | "metric">>(colors: readonly Writable<S>[]): {
24
+ export declare function spread<T extends NonNullable<unknown>, S extends Cell<T> & Traits<T, "linear" | "metric">>(inputs: readonly Writable<S>[]): Writable<Num>;
25
+ /** Mean/spread decomposition: K values → {mean, spread}, i.e. centroid +
26
+ * uniform scale about it. `mean` ∘ `spread`; works for any
27
+ * Linear + Metric class (palettes, point clouds, poses, …). */
28
+ export declare function meanSpread<T extends NonNullable<unknown>, S extends Cell<T> & Traits<T, "linear" | "metric">>(colors: readonly Writable<S>[]): {
40
29
  mean: Writable<S>;
41
30
  spread: Writable<Num>;
42
31
  };
43
- export declare function bezierGestaltLens(p0: Writable<Vec>, p1: Writable<Vec>, p2: Writable<Vec>, p3: Writable<Vec>): {
32
+ export declare function bezierGestalt(p0: Writable<Vec>, p1: Writable<Vec>, p2: Writable<Vec>, p3: Writable<Vec>): {
44
33
  start: Writable<Vec>;
45
34
  end: Writable<Vec>;
46
35
  startTangent: Writable<Vec>;
47
36
  endTangent: Writable<Vec>;
48
37
  };
49
38
  /** Time-series scalar aggregate over Num values as (i, value_i) samples. */
50
- export declare function timeSeriesLens(values: readonly Writable<Num>[]): {
39
+ export declare function timeSeries(values: readonly Writable<Num>[]): {
51
40
  mean: Writable<Num>;
52
41
  slope: Writable<Num>;
53
42
  };
54
- export {};
@@ -1,30 +1,30 @@
1
1
  // domain-aggregates.ts — closed-form lenses beyond point clouds.
2
2
  //
3
3
  // The group-action patterns from `closed-form-policies.ts`, applied to:
4
- // (1) Generic Linear/Metric-trait aggregates — `meanOf`, `spreadOf`,
5
- // `paletteLens` work for colors, poses, ranges, boxes for free.
6
- // (2) Color aggregates `meanColor`.
7
- // (3) Bezier gestalt handles ({start, end, startTangent, endTangent}).
8
- // (4) Time-series ({mean, slope}) over (i, value) samples.
4
+ // (1) Generic Linear/Metric-trait aggregates — `mean`, `spread`,
5
+ // `palette` work for colors, poses, ranges, boxes for free.
6
+ // (2) Bezier gestalt handles ({start, end, startTangent, endTangent}).
7
+ // (3) Time-series ({mean, slope}) over (i, value) samples.
9
8
  // All exact, idempotent, cross-channel invariant by construction.
10
9
  import { Num, reader, Vec, } from "../index.js";
11
10
  import { remember } from "./memory.js";
12
11
  // Generic Linear-trait aggregates.
13
12
  //
14
- // Ergonomic entry points over `meanLens` / `scaleAbout` that infer the
15
- // value class from the first input (`meanOf(colors)` vs
16
- // `meanLens(Color, colors)`). Same engine, no new infrastructure.
17
- /** Class-inferring mean (writable of `inputs[0]`'s class). Needs `linear`. */
13
+ // Ergonomic entry points that infer the value class from the first input
14
+ // (`mean(colors)` works for any `linear` class). Same engine, no new
15
+ // infrastructure.
16
+ /** Equal-weight mean (writable of `inputs[0]`'s class); writes distribute
17
+ * the delta evenly. Class inferred from the first input; needs `linear`. */
18
18
  // biome-ignore lint/suspicious/noExplicitAny: variance escape
19
- export function meanOf(inputs) {
19
+ export function mean(inputs) {
20
20
  if (inputs.length === 0)
21
- throw new Error("meanOf: need ≥ 1 input");
21
+ throw new Error("mean: need ≥ 1 input");
22
22
  // biome-ignore lint/suspicious/noExplicitAny: dynamic class lookup
23
23
  const Cls = inputs[0].constructor;
24
24
  // biome-ignore lint/suspicious/noExplicitAny: dynamic trait lookup
25
25
  const lin = Cls.traits?.linear;
26
26
  if (!lin)
27
- throw new Error(`meanOf: ${Cls.name ?? "?"} has no traits.linear`);
27
+ throw new Error(`mean: ${Cls.name ?? "?"} has no traits.linear`);
28
28
  const n = inputs.length;
29
29
  const inv = 1 / n;
30
30
  // biome-ignore lint/suspicious/noExplicitAny: variance escape on Cls.lens
@@ -49,15 +49,9 @@ export function meanOf(inputs) {
49
49
  return out;
50
50
  });
51
51
  }
52
- /** Rigid-translate aggregate over any Linear type. Alias of `meanOf`,
53
- * named for the geometric intent. */
54
- // biome-ignore lint/suspicious/noExplicitAny: variance escape
55
- export function rigidTranslateOf(inputs) {
56
- return meanOf(inputs);
57
- }
58
52
  // Weighted blend (the mix simplex).
59
53
  //
60
- // `mix` is `meanOf` with the uniform-weight assumption lifted: the read
54
+ // `mix` is `mean` with the uniform-weight assumption lifted: the read
61
55
  // is the normalized weighted sum `Σ wᵢ·aᵢ`, the write is the minimum-norm
62
56
  // delta `daᵢ = wᵢ·δ / Σwⱼ²` (the pseudoinverse of `wᵀ·da = δ`), so a
63
57
  // zero-weight branch is left untouched. Weights are read-only controls —
@@ -66,7 +60,7 @@ export function rigidTranslateOf(inputs) {
66
60
  //
67
61
  // The control lives on the K-simplex: a one-hot vertex is `select`
68
62
  // (the live branch absorbs everything), a `(1−t, t)` edge is `crossfade`,
69
- // uniform weights recover `meanOf`. Reactive weights are dynamically
63
+ // uniform weights recover `mean`. Reactive weights are dynamically
70
64
  // tracked (read via `.value` inside fwd), so flipping a Bool or sliding a
71
65
  // Num re-reads with no extra wiring.
72
66
  /** Weighted blend of K branches over any `Linear` type. See module note. */
@@ -131,11 +125,6 @@ export function select(cond, whenFalse, whenTrue) {
131
125
  export function crossfade(t, a, b) {
132
126
  return mix([Num.derive(() => 1 - t.value), Num.derive(() => t.value)], [a, b]);
133
127
  }
134
- /** Mean color of a palette; write shifts every color by the delta
135
- * (rigid translate in RGBA). Via `meanOf`. */
136
- export function meanColor(colors) {
137
- return meanOf(colors);
138
- }
139
128
  /** Mean radial distance from the centroid; write scales the cluster's
140
129
  * deviations so the new mean matches the target. Trait-driven via
141
130
  * `Linear` + `Metric`, so it works for any class declaring both (Vec,
@@ -146,16 +135,16 @@ export function meanColor(colors) {
146
135
  * and a collapse (spread → 0) reinflates the original SHAPE. Centroid is
147
136
  * recomputed every read/write, so an intervening mean translate is not
148
137
  * stale. */
149
- export function spreadOf(inputs) {
138
+ export function spread(inputs) {
150
139
  const K = inputs.length;
151
140
  if (K < 1)
152
- throw new Error("spreadOf: need ≥ 1 input");
141
+ throw new Error("spread: need ≥ 1 input");
153
142
  // biome-ignore lint/suspicious/noExplicitAny: dynamic class lookup
154
143
  const Cls = inputs[0].constructor;
155
144
  const lin = Cls.traits?.linear;
156
145
  const met = Cls.traits?.metric;
157
146
  if (!lin || !met) {
158
- throw new Error(`spreadOf: ${Cls.name ?? "?"} needs Linear + Metric`);
147
+ throw new Error(`spread: ${Cls.name ?? "?"} needs Linear + Metric`);
159
148
  }
160
149
  const inv = 1 / K;
161
150
  const centroid = (vals) => {
@@ -178,16 +167,16 @@ export function spreadOf(inputs) {
178
167
  feature: (vals, ctr) => meanSpread(vals, ctr),
179
168
  });
180
169
  }
181
- /** Palette decomposition: K values → {mean, spread}, i.e. centroid +
182
- * uniform scale about it. `meanOf` ∘ `spreadOf`; works for any
183
- * Linear + Metric class. */
184
- export function paletteLens(colors) {
170
+ /** Mean/spread decomposition: K values → {mean, spread}, i.e. centroid +
171
+ * uniform scale about it. `mean` ∘ `spread`; works for any
172
+ * Linear + Metric class (palettes, point clouds, poses, …). */
173
+ export function meanSpread(colors) {
185
174
  return {
186
- mean: meanOf(colors),
187
- spread: spreadOf(colors),
175
+ mean: mean(colors),
176
+ spread: spread(colors),
188
177
  };
189
178
  }
190
- export function bezierGestaltLens(p0, p1, p2, p3) {
179
+ export function bezierGestalt(p0, p1, p2, p3) {
191
180
  const start = Vec.lens([p0, p1], (vals) => vals[0], (target, vals) => {
192
181
  const dx = target.x - vals[0].x;
193
182
  const dy = target.y - vals[0].y;
@@ -210,7 +199,7 @@ export function bezierGestaltLens(p0, p1, p2, p3) {
210
199
  // mean and slope are invariant under each other (a y-shift preserves
211
200
  // slope; tilting about the mean preserves the mean).
212
201
  /** Time-series scalar aggregate over Num values as (i, value_i) samples. */
213
- export function timeSeriesLens(values) {
202
+ export function timeSeries(values) {
214
203
  const N = values.length;
215
204
  if (N < 2)
216
205
  throw new Error("timeSeries: need ≥ 2 values");
@@ -254,6 +243,3 @@ export function timeSeriesLens(values) {
254
243
  });
255
244
  return { mean, slope };
256
245
  }
257
- // `meanOf` / `rigidTranslateOf` / `spreadOf` are fully trait-driven
258
- // (Linear, Metric); `bezierGestalt` and `timeSeries` stay value-specific
259
- // (Vec / Num) since their operations don't benefit from the trait layer.
@@ -1,19 +1,20 @@
1
- import type { Cell, Writable } from "./signal.js";
2
- import { Num } from "./values/num.js";
3
- import { Vec } from "./values/vec.js";
1
+ import type { Cell, Read, Writable } from "../cell.js";
2
+ import { Num } from "../values/num.js";
3
+ import { Vec } from "../values/vec.js";
4
4
  type V = {
5
5
  x: number;
6
6
  y: number;
7
7
  };
8
- /** Distance between two Vecs. RO the inverse isn't unique. For a
9
- * writable variant see `radialLens`. */
10
- export declare function distanceLens(a: Cell<V>, b: Cell<V>): Num;
11
- /** Angle from `a` to `b`, in radians. RO. */
12
- export declare function angleLens(a: Cell<V>, b: Cell<V>): Num;
8
+ /** Distance between two Vecs; writing scales them symmetrically about
9
+ * their midpoint (collapse to 0 reinflates the last direction). */
10
+ export declare function distance(a: Writable<Vec>, b: Writable<Vec>): Writable<Num>;
11
+ /** Angle from `a` to `b`, in radians; writing rotates `b` about `a`
12
+ * (a fixed, separation preserved). */
13
+ export declare function angle(a: Read<V>, b: Writable<Vec>): Writable<Num>;
13
14
  /** Reflect `point` across the line through `axisStart`/`axisEnd`. Writes
14
15
  * the reflected position back to `point` (axis unchanged); reflection is
15
16
  * involutive, so the same formula reads and writes. */
16
- export declare function reflectionLens(point: Cell<V>, axisStart: Cell<V>, axisEnd: Cell<V>): Writable<Vec>;
17
+ export declare function reflection(point: Cell<V>, axisStart: Cell<V>, axisEnd: Cell<V>): Writable<Vec>;
17
18
  /** Lerp between two Vecs at parameter `t`. Writing the interpolated point
18
19
  * shifts both endpoints rigidly (preserving t). */
19
20
  export declare function vecLerp(a: Cell<V>, b: Cell<V>, t: Cell<number>): Writable<Vec>;
@@ -22,12 +23,8 @@ export declare function vecLerp(a: Cell<V>, b: Cell<V>, t: Cell<number>): Writab
22
23
  export declare function pulleySum(a: Num, b: Num): Writable<Num>;
23
24
  /** Difference of two nums: `a - b`. Writing the difference shifts
24
25
  * both inputs symmetrically by ±half-delta. */
25
- export declare function diffLens(a: Num, b: Num): Writable<Num>;
26
+ export declare function diff(a: Num, b: Num): Writable<Num>;
26
27
  /** Mean of N nums, clamped to `[lo, hi]` on read and write (writes are
27
28
  * clamped before the delta is distributed). */
28
29
  export declare function clampedMean(parents: readonly Num[], lo: number, hi: number): Writable<Num>;
29
- /** Quadratic Bézier point at parameter `t`. RO. */
30
- export declare function bezier2(p0: Cell<V>, p1: Cell<V>, p2: Cell<V>, t: Cell<number>): Vec;
31
- /** Cubic Bézier point at parameter `t`. RO. */
32
- export declare function bezier3(p0: Cell<V>, p1: Cell<V>, p2: Cell<V>, p3: Cell<V>, t: Cell<number>): Vec;
33
30
  export {};
@@ -1,20 +1,30 @@
1
- // new-primitives.ts — building blocks over the N-input `Cls.lens` /
2
- // `Cls.derive` forms. All are a few lines on top of the engine.
3
- import { Num } from "./values/num.js";
4
- import { Vec } from "./values/vec.js";
5
- /** Distance between two Vecs. RO — the inverse isn't unique. For a
6
- * writable variant see `radialLens`. */
7
- export function distanceLens(a, b) {
8
- return Num.derive([a, b], vals => Math.hypot(vals[0].x - vals[1].x, vals[0].y - vals[1].y));
1
+ // geometry.ts — geometric lens building blocks over the N-input
2
+ // `Cls.lens` / `Cls.derive` forms. All are a few lines on top of the engine.
3
+ import { Num } from "../values/num.js";
4
+ import { Vec } from "../values/vec.js";
5
+ import { rotateAbout } from "./closed-form-policies.js";
6
+ import { remember } from "./memory.js";
7
+ /** Distance between two Vecs; writing scales them symmetrically about
8
+ * their midpoint (collapse to 0 reinflates the last direction). */
9
+ export function distance(a, b) {
10
+ const points = [a, b];
11
+ return remember(points, {
12
+ anchor: (vals) => ({
13
+ x: (vals[0].x + vals[1].x) / 2,
14
+ y: (vals[0].y + vals[1].y) / 2,
15
+ }),
16
+ feature: (vals) => Math.hypot(vals[0].x - vals[1].x, vals[0].y - vals[1].y),
17
+ });
9
18
  }
10
- /** Angle from `a` to `b`, in radians. RO. */
11
- export function angleLens(a, b) {
12
- return Num.derive([a, b], vals => Math.atan2(vals[1].y - vals[0].y, vals[1].x - vals[0].x));
19
+ /** Angle from `a` to `b`, in radians; writing rotates `b` about `a`
20
+ * (a fixed, separation preserved). */
21
+ export function angle(a, b) {
22
+ return rotateAbout([b], a);
13
23
  }
14
24
  /** Reflect `point` across the line through `axisStart`/`axisEnd`. Writes
15
25
  * the reflected position back to `point` (axis unchanged); reflection is
16
26
  * involutive, so the same formula reads and writes. */
17
- export function reflectionLens(point, axisStart, axisEnd) {
27
+ export function reflection(point, axisStart, axisEnd) {
18
28
  const reflect = (p, a, b) => {
19
29
  const dx = b.x - a.x;
20
30
  const dy = b.y - a.y;
@@ -53,7 +63,7 @@ export function pulleySum(a, b) {
53
63
  }
54
64
  /** Difference of two nums: `a - b`. Writing the difference shifts
55
65
  * both inputs symmetrically by ±half-delta. */
56
- export function diffLens(a, b) {
66
+ export function diff(a, b) {
57
67
  return Num.lens([a, b], vals => vals[0] - vals[1], (target, vals) => {
58
68
  const [av, bv] = vals;
59
69
  const cur = av - bv;
@@ -87,27 +97,3 @@ export function clampedMean(parents, lo, hi) {
87
97
  return out;
88
98
  });
89
99
  }
90
- /** Quadratic Bézier point at parameter `t`. RO. */
91
- export function bezier2(p0, p1, p2, t) {
92
- return Vec.derive([p0, p1, p2, t], vals => {
93
- const [a, b, c, tv] = vals;
94
- const u = 1 - tv;
95
- return {
96
- x: u * u * a.x + 2 * u * tv * b.x + tv * tv * c.x,
97
- y: u * u * a.y + 2 * u * tv * b.y + tv * tv * c.y,
98
- };
99
- });
100
- }
101
- /** Cubic Bézier point at parameter `t`. RO. */
102
- export function bezier3(p0, p1, p2, p3, t) {
103
- return Vec.derive([p0, p1, p2, p3, t], vals => {
104
- const [a, b, c, d, tv] = vals;
105
- const u = 1 - tv;
106
- const u2 = u * u;
107
- const t2 = tv * tv;
108
- return {
109
- x: u2 * u * a.x + 3 * u2 * tv * b.x + 3 * u * t2 * c.x + t2 * tv * d.x,
110
- y: u2 * u * a.y + 3 * u2 * tv * b.y + 3 * u * t2 * c.y + t2 * tv * d.y,
111
- };
112
- });
113
- }