bireactive 0.3.0 → 0.3.2

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 (117) hide show
  1. package/README.md +14 -7
  2. package/dist/automerge/doc-cell.d.ts +20 -0
  3. package/dist/automerge/doc-cell.js +80 -0
  4. package/dist/automerge/index.d.ts +3 -0
  5. package/dist/automerge/index.js +12 -0
  6. package/dist/automerge/reconcile.d.ts +5 -0
  7. package/dist/automerge/reconcile.js +63 -0
  8. package/dist/core/_counts.d.ts +48 -0
  9. package/dist/core/_counts.js +51 -0
  10. package/dist/core/cell.d.ts +148 -112
  11. package/dist/core/cell.js +945 -768
  12. package/dist/core/debug.d.ts +25 -0
  13. package/dist/core/debug.js +121 -0
  14. package/dist/core/derived-geometry.js +4 -7
  15. package/dist/core/index.d.ts +9 -2
  16. package/dist/core/index.js +8 -1
  17. package/dist/core/lenses/aggregates.d.ts +42 -52
  18. package/dist/core/lenses/aggregates.js +225 -116
  19. package/dist/core/lenses/geometry.d.ts +22 -4
  20. package/dist/core/lenses/geometry.js +59 -27
  21. package/dist/core/lenses/index.d.ts +6 -6
  22. package/dist/core/lenses/index.js +6 -6
  23. package/dist/core/lenses/memory.js +4 -17
  24. package/dist/core/lenses/numerical.d.ts +100 -0
  25. package/dist/core/lenses/{typed-factor.js → numerical.js} +136 -34
  26. package/dist/core/lenses/point-cloud.d.ts +67 -0
  27. package/dist/core/lenses/{closed-form-policies.js → point-cloud.js} +226 -84
  28. package/dist/core/lenses/snap.d.ts +18 -0
  29. package/dist/core/lenses/snap.js +138 -0
  30. package/dist/core/lenses/text.d.ts +40 -0
  31. package/dist/core/lenses/text.js +202 -0
  32. package/dist/core/lifecycle.js +3 -6
  33. package/dist/core/linalg.js +5 -11
  34. package/dist/core/optic.d.ts +13 -0
  35. package/dist/core/optic.js +39 -0
  36. package/dist/core/optics.d.ts +10 -0
  37. package/dist/core/optics.js +26 -0
  38. package/dist/core/store.d.ts +9 -0
  39. package/dist/core/store.js +77 -0
  40. package/dist/core/traits.d.ts +4 -7
  41. package/dist/core/traits.js +8 -12
  42. package/dist/core/values/anchor.js +0 -4
  43. package/dist/core/values/arr.d.ts +110 -0
  44. package/dist/core/values/arr.js +336 -0
  45. package/dist/core/values/audio.d.ts +8 -9
  46. package/dist/core/values/audio.js +11 -28
  47. package/dist/core/values/bool.d.ts +11 -11
  48. package/dist/core/values/bool.js +12 -22
  49. package/dist/core/values/box.d.ts +15 -20
  50. package/dist/core/values/box.js +20 -33
  51. package/dist/core/values/canvas.d.ts +18 -25
  52. package/dist/core/values/canvas.js +32 -66
  53. package/dist/core/values/color.d.ts +5 -7
  54. package/dist/core/values/color.js +5 -11
  55. package/dist/core/values/field.d.ts +6 -7
  56. package/dist/core/values/field.js +10 -35
  57. package/dist/core/values/flags.d.ts +1 -2
  58. package/dist/core/values/flags.js +1 -17
  59. package/dist/core/values/gpu.d.ts +6 -10
  60. package/dist/core/values/gpu.js +8 -22
  61. package/dist/core/values/matrix.d.ts +2 -4
  62. package/dist/core/values/matrix.js +2 -12
  63. package/dist/core/values/num.d.ts +19 -28
  64. package/dist/core/values/num.js +23 -41
  65. package/dist/core/values/pose.d.ts +2 -4
  66. package/dist/core/values/pose.js +3 -12
  67. package/dist/core/values/range.d.ts +18 -26
  68. package/dist/core/values/range.js +22 -39
  69. package/dist/core/values/reg/ambiguity.d.ts +8 -0
  70. package/dist/core/values/reg/ambiguity.js +131 -0
  71. package/dist/core/values/reg/engine.d.ts +91 -0
  72. package/dist/core/values/reg/engine.js +373 -0
  73. package/dist/core/values/reg/nfa.d.ts +42 -0
  74. package/dist/core/values/reg/nfa.js +391 -0
  75. package/dist/core/values/reg/regex.d.ts +7 -0
  76. package/dist/core/values/reg/regex.js +318 -0
  77. package/dist/core/values/reg/types.d.ts +60 -0
  78. package/dist/core/values/reg/types.js +3 -0
  79. package/dist/core/values/reg.d.ts +250 -0
  80. package/dist/core/values/reg.js +649 -0
  81. package/dist/core/values/str.d.ts +16 -60
  82. package/dist/core/values/str.js +133 -315
  83. package/dist/core/values/template.js +1 -24
  84. package/dist/core/values/transform.d.ts +3 -5
  85. package/dist/core/values/transform.js +3 -12
  86. package/dist/core/values/tri.d.ts +9 -10
  87. package/dist/core/values/tri.js +9 -15
  88. package/dist/core/values/vec.d.ts +9 -24
  89. package/dist/core/values/vec.js +9 -64
  90. package/dist/formats/lens.js +6 -9
  91. package/dist/index.d.ts +0 -11
  92. package/dist/index.js +1 -11
  93. package/dist/jsx-dev-runtime.d.ts +2 -0
  94. package/dist/jsx-dev-runtime.js +5 -0
  95. package/dist/jsx-runtime.d.ts +54 -0
  96. package/dist/jsx-runtime.js +219 -0
  97. package/dist/schema/lens.js +5 -5
  98. package/dist/shapes/drag-behaviors.d.ts +56 -0
  99. package/dist/shapes/drag-behaviors.js +102 -0
  100. package/dist/shapes/drag-spec.d.ts +52 -0
  101. package/dist/shapes/drag-spec.js +112 -0
  102. package/dist/shapes/index.d.ts +3 -1
  103. package/dist/shapes/index.js +3 -1
  104. package/dist/shapes/interaction.d.ts +2 -3
  105. package/dist/shapes/interaction.js +77 -56
  106. package/dist/shapes/label.js +6 -0
  107. package/dist/shapes/layout.d.ts +47 -1
  108. package/dist/shapes/layout.js +59 -1
  109. package/package.json +22 -1
  110. package/dist/coll.d.ts +0 -74
  111. package/dist/coll.js +0 -210
  112. package/dist/core/lenses/closed-form-policies.d.ts +0 -57
  113. package/dist/core/lenses/decompositions.d.ts +0 -14
  114. package/dist/core/lenses/decompositions.js +0 -224
  115. package/dist/core/lenses/domain-aggregates.d.ts +0 -42
  116. package/dist/core/lenses/domain-aggregates.js +0 -245
  117. package/dist/core/lenses/typed-factor.d.ts +0 -40
@@ -1,224 +0,0 @@
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, SKIP, 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(() => SKIP);
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(() => SKIP), 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,42 +0,0 @@
1
- import { type Cell, Num, type Read, type Traits, type Val, Vec, type Writable } from "../index.js";
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>;
5
- /** Weighted blend of K branches over any `Linear` type. See module note. */
6
- export declare function mix<S extends Traits<any, "linear">>(weights: readonly Val<number>[], branches: readonly Writable<S>[]): Writable<S>;
7
- /** Two-branch router (mix simplex *vertex*): reads the live branch, writes
8
- * flow entirely to it, the other is left put. Flipping `cond` snaps the
9
- * output to the other branch's stored value. */
10
- export declare function select<S extends Traits<any, "linear">>(cond: Read<boolean>, whenFalse: Writable<S>, whenTrue: Writable<S>): Writable<S>;
11
- /** Two-branch crossfade (mix simplex *edge*): `lerp(a, b, t)`. Writing
12
- * keeps `t` fixed and splits the delta by influence. */
13
- export declare function crossfade<S extends Traits<any, "linear">>(t: Read<number>, a: Writable<S>, b: Writable<S>): Writable<S>;
14
- /** Mean radial distance from the centroid; write scales the cluster's
15
- * deviations so the new mean matches the target. Trait-driven via
16
- * `Linear` + `Metric`, so it works for any class declaring both (Vec,
17
- * Color, Pose, Box, Range, custom).
18
- *
19
- * Complement carries per-input deviations normalized by the current mean
20
- * radius, so `spread = T` places each input at `centroid + normDev_i * T`
21
- * and a collapse (spread → 0) reinflates the original SHAPE. Centroid is
22
- * recomputed every read/write, so an intervening mean translate is not
23
- * stale. */
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>[]): {
29
- mean: Writable<S>;
30
- spread: Writable<Num>;
31
- };
32
- export declare function bezierGestalt(p0: Writable<Vec>, p1: Writable<Vec>, p2: Writable<Vec>, p3: Writable<Vec>): {
33
- start: Writable<Vec>;
34
- end: Writable<Vec>;
35
- startTangent: Writable<Vec>;
36
- endTangent: Writable<Vec>;
37
- };
38
- /** Time-series scalar aggregate over Num values as (i, value_i) samples. */
39
- export declare function timeSeries(values: readonly Writable<Num>[]): {
40
- mean: Writable<Num>;
41
- slope: Writable<Num>;
42
- };
@@ -1,245 +0,0 @@
1
- // domain-aggregates.ts — closed-form lenses beyond point clouds.
2
- //
3
- // The group-action patterns from `closed-form-policies.ts`, applied to:
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.
8
- // All exact, idempotent, cross-channel invariant by construction.
9
- import { Num, reader, SKIP, Vec, } from "../index.js";
10
- import { remember } from "./memory.js";
11
- // Generic Linear-trait aggregates.
12
- //
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
- // biome-ignore lint/suspicious/noExplicitAny: variance escape
19
- export function mean(inputs) {
20
- if (inputs.length === 0)
21
- throw new Error("mean: need ≥ 1 input");
22
- // biome-ignore lint/suspicious/noExplicitAny: dynamic class lookup
23
- const Cls = inputs[0].constructor;
24
- // biome-ignore lint/suspicious/noExplicitAny: dynamic trait lookup
25
- const lin = Cls.traits?.linear;
26
- if (!lin)
27
- throw new Error(`mean: ${Cls.name ?? "?"} has no traits.linear`);
28
- const n = inputs.length;
29
- const inv = 1 / n;
30
- // biome-ignore lint/suspicious/noExplicitAny: variance escape on Cls.lens
31
- return Cls.lens(inputs,
32
- // biome-ignore lint/suspicious/noExplicitAny: variance escape
33
- (vals) => {
34
- let acc = vals[0];
35
- for (let i = 1; i < n; i++)
36
- acc = lin.add(acc, vals[i]);
37
- return lin.scale(acc, inv);
38
- },
39
- // biome-ignore lint/suspicious/noExplicitAny: variance escape
40
- (target, vals) => {
41
- let cur = vals[0];
42
- for (let i = 1; i < n; i++)
43
- cur = lin.add(cur, vals[i]);
44
- cur = lin.scale(cur, inv);
45
- const delta = lin.sub(target, cur);
46
- const out = new Array(n);
47
- for (let i = 0; i < n; i++)
48
- out[i] = lin.add(vals[i], delta);
49
- return out;
50
- });
51
- }
52
- // Weighted blend (the mix simplex).
53
- //
54
- // `mix` is `mean` with the uniform-weight assumption lifted: the read
55
- // is the normalized weighted sum `Σ wᵢ·aᵢ`, the write is the minimum-norm
56
- // delta `daᵢ = wᵢ·δ / Σwⱼ²` (the pseudoinverse of `wᵀ·da = δ`), so a
57
- // zero-weight branch is left untouched. Weights are read-only controls —
58
- // the bwd never writes them, keeping the blend fixed while the delta flows
59
- // into the branches.
60
- //
61
- // The control lives on the K-simplex: a one-hot vertex is `select`
62
- // (the live branch absorbs everything), a `(1−t, t)` edge is `crossfade`,
63
- // uniform weights recover `mean`. Reactive weights are dynamically
64
- // tracked (read via `.value` inside fwd), so flipping a Bool or sliding a
65
- // Num re-reads with no extra wiring.
66
- /** Weighted blend of K branches over any `Linear` type. See module note. */
67
- // biome-ignore lint/suspicious/noExplicitAny: variance escape
68
- export function mix(weights, branches) {
69
- const K = branches.length;
70
- if (K < 1)
71
- throw new Error("mix: need ≥ 1 branch");
72
- if (weights.length !== K)
73
- throw new Error("mix: weights/branches length mismatch");
74
- // biome-ignore lint/suspicious/noExplicitAny: dynamic class lookup
75
- const Cls = branches[0].constructor;
76
- // biome-ignore lint/suspicious/noExplicitAny: dynamic trait lookup
77
- const lin = Cls.traits?.linear;
78
- if (!lin)
79
- throw new Error(`mix: ${Cls.name ?? "?"} has no traits.linear`);
80
- const wf = weights.map(w => reader(w));
81
- // Normalized weights + Σw². Degenerate (all-zero) weights fall back to
82
- // uniform so the read stays defined.
83
- const readW = () => {
84
- const raw = wf.map(f => f());
85
- let sum = 0;
86
- for (const x of raw)
87
- sum += x;
88
- const w = Math.abs(sum) > 1e-12 ? raw.map(x => x / sum) : raw.map(() => 1 / K);
89
- let sumSq = 0;
90
- for (const x of w)
91
- sumSq += x * x;
92
- return { w, sumSq };
93
- };
94
- // biome-ignore lint/suspicious/noExplicitAny: variance escape on Cls.lens
95
- const combine = (vals, w) => {
96
- let acc = lin.scale(vals[0], w[0]);
97
- for (let i = 1; i < K; i++)
98
- acc = lin.add(acc, lin.scale(vals[i], w[i]));
99
- return acc;
100
- };
101
- // biome-ignore lint/suspicious/noExplicitAny: variance escape on Cls.lens
102
- return Cls.lens(branches,
103
- // biome-ignore lint/suspicious/noExplicitAny: variance escape
104
- (vals) => combine(vals, readW().w),
105
- // biome-ignore lint/suspicious/noExplicitAny: variance escape
106
- (target, vals) => {
107
- const { w, sumSq } = readW();
108
- const delta = lin.sub(target, combine(vals, w));
109
- if (sumSq < 1e-12)
110
- return vals.map(() => SKIP);
111
- const inv = 1 / sumSq;
112
- return vals.map((v, i) => w[i] === 0 ? SKIP : lin.add(v, lin.scale(delta, w[i] * inv)));
113
- });
114
- }
115
- /** Two-branch router (mix simplex *vertex*): reads the live branch, writes
116
- * flow entirely to it, the other is left put. Flipping `cond` snaps the
117
- * output to the other branch's stored value. */
118
- // biome-ignore lint/suspicious/noExplicitAny: variance escape
119
- export function select(cond, whenFalse, whenTrue) {
120
- return mix([Num.derive(() => (cond.value ? 0 : 1)), Num.derive(() => (cond.value ? 1 : 0))], [whenFalse, whenTrue]);
121
- }
122
- /** Two-branch crossfade (mix simplex *edge*): `lerp(a, b, t)`. Writing
123
- * keeps `t` fixed and splits the delta by influence. */
124
- // biome-ignore lint/suspicious/noExplicitAny: variance escape
125
- export function crossfade(t, a, b) {
126
- return mix([Num.derive(() => 1 - t.value), Num.derive(() => t.value)], [a, b]);
127
- }
128
- /** Mean radial distance from the centroid; write scales the cluster's
129
- * deviations so the new mean matches the target. Trait-driven via
130
- * `Linear` + `Metric`, so it works for any class declaring both (Vec,
131
- * Color, Pose, Box, Range, custom).
132
- *
133
- * Complement carries per-input deviations normalized by the current mean
134
- * radius, so `spread = T` places each input at `centroid + normDev_i * T`
135
- * and a collapse (spread → 0) reinflates the original SHAPE. Centroid is
136
- * recomputed every read/write, so an intervening mean translate is not
137
- * stale. */
138
- export function spread(inputs) {
139
- const K = inputs.length;
140
- if (K < 1)
141
- throw new Error("spread: need ≥ 1 input");
142
- // biome-ignore lint/suspicious/noExplicitAny: dynamic class lookup
143
- const Cls = inputs[0].constructor;
144
- const lin = Cls.traits?.linear;
145
- const met = Cls.traits?.metric;
146
- if (!lin || !met) {
147
- throw new Error(`spread: ${Cls.name ?? "?"} needs Linear + Metric`);
148
- }
149
- const inv = 1 / K;
150
- const centroid = (vals) => {
151
- let acc = vals[0];
152
- for (let i = 1; i < K; i++)
153
- acc = lin.add(acc, vals[i]);
154
- return lin.scale(acc, inv);
155
- };
156
- const meanSpread = (vals, ctr) => {
157
- let total = 0;
158
- for (let i = 0; i < K; i++)
159
- total += met(vals[i], ctr);
160
- return total * inv;
161
- };
162
- // Mean metric-distance from the centroid is a magnitude `remember`:
163
- // writing it scales the cluster's deviations about the centroid, and a
164
- // collapse (spread → 0) reinflates the remembered shape.
165
- return remember(inputs, {
166
- anchor: (vals) => centroid(vals),
167
- feature: (vals, ctr) => meanSpread(vals, ctr),
168
- });
169
- }
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) {
174
- return {
175
- mean: mean(colors),
176
- spread: spread(colors),
177
- };
178
- }
179
- export function bezierGestalt(p0, p1, p2, p3) {
180
- const start = Vec.lens([p0, p1], (vals) => vals[0], (target, vals) => {
181
- const dx = target.x - vals[0].x;
182
- const dy = target.y - vals[0].y;
183
- return [target, { x: vals[1].x + dx, y: vals[1].y + dy }];
184
- });
185
- const end = Vec.lens([p2, p3], (vals) => vals[1], (target, vals) => {
186
- const dx = target.x - vals[1].x;
187
- const dy = target.y - vals[1].y;
188
- return [{ x: vals[0].x + dx, y: vals[0].y + dy }, target];
189
- });
190
- const startTangent = Vec.lens([p0, p1], (vals) => ({ x: vals[1].x - vals[0].x, y: vals[1].y - vals[0].y }), (target, vals) => [SKIP, { x: vals[0].x + target.x, y: vals[0].y + target.y }]);
191
- const endTangent = Vec.lens([p2, p3], (vals) => ({ x: vals[1].x - vals[0].x, y: vals[1].y - vals[0].y }), (target, vals) => [{ x: vals[1].x - target.x, y: vals[1].y - target.y }, SKIP]);
192
- return { start, end, startTangent, endTangent };
193
- }
194
- // Time-series aggregates.
195
- //
196
- // Scalar values indexed by position → {mean, slope}:
197
- // mean := average; writes shift all values by the delta.
198
- // slope := least-squares slope of (i, value_i); writes tilt about mean.
199
- // mean and slope are invariant under each other (a y-shift preserves
200
- // slope; tilting about the mean preserves the mean).
201
- /** Time-series scalar aggregate over Num values as (i, value_i) samples. */
202
- export function timeSeries(values) {
203
- const N = values.length;
204
- if (N < 2)
205
- throw new Error("timeSeries: need ≥ 2 values");
206
- const mean = Num.lens(values, (vals) => {
207
- let s = 0;
208
- for (let i = 0; i < N; i++)
209
- s += vals[i];
210
- return s / N;
211
- }, (target, vals) => {
212
- let s = 0;
213
- for (let i = 0; i < N; i++)
214
- s += vals[i];
215
- const cur = s / N;
216
- const delta = target - cur;
217
- return vals.map(v => v + delta);
218
- });
219
- // Least-squares slope = Σ (i − idxMean)(v − mean) / Σ (i − idxMean)²,
220
- // idxMean = (N−1)/2 constant. Write tilts about the mean:
221
- // value_i = mean + (i − idxMean)·s.
222
- const idxMean = (N - 1) / 2;
223
- let denomSlope = 0;
224
- for (let i = 0; i < N; i++) {
225
- const di = i - idxMean;
226
- denomSlope += di * di;
227
- }
228
- const slope = Num.lens(values, (vals) => {
229
- let valMean = 0;
230
- for (let i = 0; i < N; i++)
231
- valMean += vals[i];
232
- valMean /= N;
233
- let num = 0;
234
- for (let i = 0; i < N; i++)
235
- num += (i - idxMean) * (vals[i] - valMean);
236
- return num / denomSlope;
237
- }, (target, vals) => {
238
- let valMean = 0;
239
- for (let i = 0; i < N; i++)
240
- valMean += vals[i];
241
- valMean /= N;
242
- return vals.map((_, i) => valMean + (i - idxMean) * target);
243
- });
244
- return { mean, slope };
245
- }
@@ -1,40 +0,0 @@
1
- import type { Cell, Inner, Read, Traits, Writable } from "../index.js";
2
- /** Input cell: writable cell whose value class declares the `pack`
3
- * trait. Vec, Num, Pose, Box, Color, Range all satisfy this. */
4
- export type PackedInput<T = any> = Writable<Read<T> & Traits<T, "pack">>;
5
- /** Output specification: a target class + a fwd from typed inputs to
6
- * the value the class wraps. Optional analytical Jacobian skips FD. */
7
- export interface OutputSpec<C extends new (...args: never[]) => Cell<any>> {
8
- Cls: C;
9
- fwd: (inputs: ReadonlyArray<any>) => Inner<InstanceType<C>>;
10
- /** Optional analytical Jacobian. Returns dim(Cls) rows, each of
11
- * length `sum(input pack dims)`. If supplied for ALL outputs, FD
12
- * is skipped entirely → faster AND exact (no eps drift). */
13
- jacobian?: (inputs: ReadonlyArray<any>) => readonly (readonly number[])[];
14
- }
15
- /** Result type: writable cell per output key, typed by the spec's Cls. */
16
- export type FactorResult<O extends Record<string, OutputSpec<any>>> = {
17
- [K in keyof O]: Writable<InstanceType<O[K]["Cls"]>>;
18
- };
19
- export interface FactorOpts {
20
- /** Per-input mobility weights. 0 = pinned. Defaults to 1 for all. */
21
- inputWeights?: readonly number[];
22
- /** Levenberg-Marquardt damping. Default 1e-6. */
23
- damping?: number;
24
- /** Finite-difference epsilon. Default 1e-5. */
25
- eps?: number;
26
- /** Auto-iterate the bwd until the written channel's reading is
27
- * within `tol` of target (or `maxIters` exhausted). Cheap when
28
- * forwards are linear (1 iter); needed for non-linear forwards
29
- * to land exactly without user-side loops. Default `false`. */
30
- converge?: boolean;
31
- /** Max iters when `converge: true`. Default 10. */
32
- maxIters?: number;
33
- /** Convergence tolerance (per-channel L2). Default 1e-4. */
34
- tol?: number;
35
- }
36
- export declare function factor<O extends Record<string, OutputSpec<any>>>(inputs: readonly PackedInput[], outputs: O, opts?: FactorOpts): FactorResult<O>;
37
- export declare function factorTuple<T extends readonly OutputSpec<any>[]>(inputs: readonly PackedInput[], outputs: readonly [...T], opts?: FactorOpts): {
38
- [K in keyof T]: Writable<InstanceType<T[K]["Cls"]>>;
39
- };
40
- export declare function bundle<T, O extends Record<string, OutputSpec<any>>>(source: Writable<Read<T> & Traits<T, "pack">>, views: O, opts?: FactorOpts): FactorResult<O>;