bireactive 0.2.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 (225) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -0
  3. package/dist/animation/anim.d.ts +57 -0
  4. package/dist/animation/anim.js +318 -0
  5. package/dist/animation/combinators.d.ts +39 -0
  6. package/dist/animation/combinators.js +113 -0
  7. package/dist/animation/easings.d.ts +5 -0
  8. package/dist/animation/easings.js +5 -0
  9. package/dist/animation/index.d.ts +3 -0
  10. package/dist/animation/index.js +3 -0
  11. package/dist/assert/algebra.d.ts +20 -0
  12. package/dist/assert/algebra.js +79 -0
  13. package/dist/assert/claim.d.ts +40 -0
  14. package/dist/assert/claim.js +129 -0
  15. package/dist/assert/index.d.ts +7 -0
  16. package/dist/assert/index.js +19 -0
  17. package/dist/assert/predicates.d.ts +18 -0
  18. package/dist/assert/predicates.js +43 -0
  19. package/dist/assert/record.d.ts +20 -0
  20. package/dist/assert/record.js +78 -0
  21. package/dist/assert/scope.d.ts +42 -0
  22. package/dist/assert/scope.js +233 -0
  23. package/dist/assert/span.d.ts +37 -0
  24. package/dist/assert/span.js +68 -0
  25. package/dist/assert/tree.d.ts +22 -0
  26. package/dist/assert/tree.js +65 -0
  27. package/dist/code/code.d.ts +70 -0
  28. package/dist/code/code.js +361 -0
  29. package/dist/code/index.d.ts +2 -0
  30. package/dist/code/index.js +9 -0
  31. package/dist/code/morph.d.ts +5 -0
  32. package/dist/code/morph.js +194 -0
  33. package/dist/code/tokenize.d.ts +8 -0
  34. package/dist/code/tokenize.js +51 -0
  35. package/dist/constraints/cluster.d.ts +83 -0
  36. package/dist/constraints/cluster.js +213 -0
  37. package/dist/constraints/drivers.d.ts +15 -0
  38. package/dist/constraints/drivers.js +40 -0
  39. package/dist/constraints/factories.d.ts +73 -0
  40. package/dist/constraints/factories.js +248 -0
  41. package/dist/constraints/index.d.ts +11 -0
  42. package/dist/constraints/index.js +39 -0
  43. package/dist/constraints/interaction.d.ts +21 -0
  44. package/dist/constraints/interaction.js +148 -0
  45. package/dist/constraints/linalg.d.ts +18 -0
  46. package/dist/constraints/linalg.js +141 -0
  47. package/dist/constraints/phases.d.ts +21 -0
  48. package/dist/constraints/phases.js +60 -0
  49. package/dist/constraints/physics.d.ts +34 -0
  50. package/dist/constraints/physics.js +128 -0
  51. package/dist/constraints/rigid.d.ts +210 -0
  52. package/dist/constraints/rigid.js +835 -0
  53. package/dist/constraints/solver.d.ts +107 -0
  54. package/dist/constraints/solver.js +510 -0
  55. package/dist/constraints/term.d.ts +50 -0
  56. package/dist/constraints/term.js +80 -0
  57. package/dist/constraints/terms.d.ts +80 -0
  58. package/dist/constraints/terms.js +302 -0
  59. package/dist/constraints/world.d.ts +31 -0
  60. package/dist/constraints/world.js +245 -0
  61. package/dist/core/aggregates.d.ts +64 -0
  62. package/dist/core/aggregates.js +198 -0
  63. package/dist/core/anim.d.ts +84 -0
  64. package/dist/core/anim.js +301 -0
  65. package/dist/core/index.d.ts +38 -0
  66. package/dist/core/index.js +38 -0
  67. package/dist/core/introspect.d.ts +5 -0
  68. package/dist/core/introspect.js +31 -0
  69. package/dist/core/lenses/closed-form-policies.d.ts +64 -0
  70. package/dist/core/lenses/closed-form-policies.js +452 -0
  71. package/dist/core/lenses/domain-aggregates.d.ts +54 -0
  72. package/dist/core/lenses/domain-aggregates.js +259 -0
  73. package/dist/core/lenses/factor-lens.d.ts +42 -0
  74. package/dist/core/lenses/factor-lens.js +419 -0
  75. package/dist/core/lenses/index.d.ts +5 -0
  76. package/dist/core/lenses/index.js +16 -0
  77. package/dist/core/lenses/memory.d.ts +47 -0
  78. package/dist/core/lenses/memory.js +102 -0
  79. package/dist/core/lenses/typed-factor.d.ts +45 -0
  80. package/dist/core/lenses/typed-factor.js +376 -0
  81. package/dist/core/network-utils.d.ts +14 -0
  82. package/dist/core/network-utils.js +62 -0
  83. package/dist/core/new-primitives.d.ts +33 -0
  84. package/dist/core/new-primitives.js +113 -0
  85. package/dist/core/signal.d.ts +254 -0
  86. package/dist/core/signal.js +1349 -0
  87. package/dist/core/traits.d.ts +61 -0
  88. package/dist/core/traits.js +56 -0
  89. package/dist/core/tree.d.ts +23 -0
  90. package/dist/core/tree.js +62 -0
  91. package/dist/core/values/anchor.d.ts +23 -0
  92. package/dist/core/values/anchor.js +23 -0
  93. package/dist/core/values/audio.d.ts +33 -0
  94. package/dist/core/values/audio.js +107 -0
  95. package/dist/core/values/bool.d.ts +37 -0
  96. package/dist/core/values/bool.js +75 -0
  97. package/dist/core/values/box.d.ts +77 -0
  98. package/dist/core/values/box.js +211 -0
  99. package/dist/core/values/canvas.d.ts +71 -0
  100. package/dist/core/values/canvas.js +495 -0
  101. package/dist/core/values/color.d.ts +49 -0
  102. package/dist/core/values/color.js +106 -0
  103. package/dist/core/values/flags.d.ts +18 -0
  104. package/dist/core/values/flags.js +50 -0
  105. package/dist/core/values/gpu.d.ts +74 -0
  106. package/dist/core/values/gpu.js +426 -0
  107. package/dist/core/values/matrix.d.ts +53 -0
  108. package/dist/core/values/matrix.js +140 -0
  109. package/dist/core/values/num.d.ts +62 -0
  110. package/dist/core/values/num.js +166 -0
  111. package/dist/core/values/pose.d.ts +31 -0
  112. package/dist/core/values/pose.js +83 -0
  113. package/dist/core/values/range.d.ts +83 -0
  114. package/dist/core/values/range.js +167 -0
  115. package/dist/core/values/str.d.ts +76 -0
  116. package/dist/core/values/str.js +346 -0
  117. package/dist/core/values/template.d.ts +49 -0
  118. package/dist/core/values/template.js +148 -0
  119. package/dist/core/values/transform.d.ts +49 -0
  120. package/dist/core/values/transform.js +115 -0
  121. package/dist/core/values/tri.d.ts +31 -0
  122. package/dist/core/values/tri.js +95 -0
  123. package/dist/core/values/vec.d.ts +72 -0
  124. package/dist/core/values/vec.js +219 -0
  125. package/dist/core/writable.d.ts +15 -0
  126. package/dist/core/writable.js +29 -0
  127. package/dist/ext/events.d.ts +10 -0
  128. package/dist/ext/events.js +31 -0
  129. package/dist/ext/index.d.ts +4 -0
  130. package/dist/ext/index.js +4 -0
  131. package/dist/ext/snapshot.d.ts +8 -0
  132. package/dist/ext/snapshot.js +29 -0
  133. package/dist/ext/timeline.d.ts +56 -0
  134. package/dist/ext/timeline.js +94 -0
  135. package/dist/ext/waapi.d.ts +25 -0
  136. package/dist/ext/waapi.js +198 -0
  137. package/dist/index.d.ts +8 -0
  138. package/dist/index.js +10 -0
  139. package/dist/propagators/index.d.ts +6 -0
  140. package/dist/propagators/index.js +6 -0
  141. package/dist/propagators/layout.d.ts +68 -0
  142. package/dist/propagators/layout.js +336 -0
  143. package/dist/propagators/network.d.ts +52 -0
  144. package/dist/propagators/network.js +185 -0
  145. package/dist/propagators/propagator.d.ts +12 -0
  146. package/dist/propagators/propagator.js +16 -0
  147. package/dist/propagators/range.d.ts +45 -0
  148. package/dist/propagators/range.js +147 -0
  149. package/dist/propagators/relations.d.ts +60 -0
  150. package/dist/propagators/relations.js +343 -0
  151. package/dist/shapes/annular-sector.d.ts +15 -0
  152. package/dist/shapes/annular-sector.js +64 -0
  153. package/dist/shapes/button.d.ts +14 -0
  154. package/dist/shapes/button.js +31 -0
  155. package/dist/shapes/choreographers.d.ts +22 -0
  156. package/dist/shapes/choreographers.js +69 -0
  157. package/dist/shapes/circle.d.ts +17 -0
  158. package/dist/shapes/circle.js +57 -0
  159. package/dist/shapes/clip.d.ts +5 -0
  160. package/dist/shapes/clip.js +31 -0
  161. package/dist/shapes/connect.d.ts +16 -0
  162. package/dist/shapes/connect.js +70 -0
  163. package/dist/shapes/curve.d.ts +60 -0
  164. package/dist/shapes/curve.js +285 -0
  165. package/dist/shapes/dashed.d.ts +16 -0
  166. package/dist/shapes/dashed.js +142 -0
  167. package/dist/shapes/debug.d.ts +43 -0
  168. package/dist/shapes/debug.js +97 -0
  169. package/dist/shapes/group.d.ts +5 -0
  170. package/dist/shapes/group.js +10 -0
  171. package/dist/shapes/handle.d.ts +32 -0
  172. package/dist/shapes/handle.js +88 -0
  173. package/dist/shapes/index.d.ts +23 -0
  174. package/dist/shapes/index.js +23 -0
  175. package/dist/shapes/interaction.d.ts +32 -0
  176. package/dist/shapes/interaction.js +187 -0
  177. package/dist/shapes/label.d.ts +20 -0
  178. package/dist/shapes/label.js +42 -0
  179. package/dist/shapes/layout.d.ts +29 -0
  180. package/dist/shapes/layout.js +74 -0
  181. package/dist/shapes/line.d.ts +21 -0
  182. package/dist/shapes/line.js +79 -0
  183. package/dist/shapes/list.d.ts +18 -0
  184. package/dist/shapes/list.js +51 -0
  185. package/dist/shapes/mount.d.ts +7 -0
  186. package/dist/shapes/mount.js +10 -0
  187. package/dist/shapes/path.d.ts +77 -0
  188. package/dist/shapes/path.js +227 -0
  189. package/dist/shapes/rect.d.ts +30 -0
  190. package/dist/shapes/rect.js +131 -0
  191. package/dist/shapes/shape.d.ts +132 -0
  192. package/dist/shapes/shape.js +306 -0
  193. package/dist/shapes/text.d.ts +24 -0
  194. package/dist/shapes/text.js +53 -0
  195. package/dist/shapes/tokens.d.ts +28 -0
  196. package/dist/shapes/tokens.js +27 -0
  197. package/dist/shapes/transitions.d.ts +23 -0
  198. package/dist/shapes/transitions.js +62 -0
  199. package/dist/tex/decorations.d.ts +26 -0
  200. package/dist/tex/decorations.js +116 -0
  201. package/dist/tex/index.d.ts +5 -0
  202. package/dist/tex/index.js +5 -0
  203. package/dist/tex/marker.d.ts +17 -0
  204. package/dist/tex/marker.js +63 -0
  205. package/dist/tex/motion.d.ts +43 -0
  206. package/dist/tex/motion.js +290 -0
  207. package/dist/tex/parts.d.ts +65 -0
  208. package/dist/tex/parts.js +149 -0
  209. package/dist/tex/tex.d.ts +45 -0
  210. package/dist/tex/tex.js +244 -0
  211. package/dist/web/attr.d.ts +16 -0
  212. package/dist/web/attr.js +98 -0
  213. package/dist/web/diagram.d.ts +49 -0
  214. package/dist/web/diagram.js +260 -0
  215. package/dist/web/index.d.ts +6 -0
  216. package/dist/web/index.js +6 -0
  217. package/dist/web/md-marker.d.ts +6 -0
  218. package/dist/web/md-marker.js +39 -0
  219. package/dist/web/md-tex.d.ts +6 -0
  220. package/dist/web/md-tex.js +61 -0
  221. package/dist/web/raf.d.ts +6 -0
  222. package/dist/web/raf.js +24 -0
  223. package/dist/web/viewport.d.ts +7 -0
  224. package/dist/web/viewport.js +13 -0
  225. package/package.json +87 -0
@@ -0,0 +1,376 @@
1
+ // typed-factor.ts — heterogeneous-output factor lens.
2
+ //
3
+ // Generalises `factorLens` (scalar-only) to typed inputs/outputs via the
4
+ // `Pack` trait. Inputs and outputs are flat-packed; the Jacobian is the
5
+ // full M×N matrix; writing one channel sends a sparse δy through the LSQ
6
+ // pseudoinverse. Invariance is approximate (the Jacobian path) — use
7
+ // closed-form lenses like `procrustesLens` for exactness.
8
+ //
9
+ // const { centroid, rotation, scale } = factor([v1, v2, v3] as const, {
10
+ // centroid: { Cls: Vec, fwd: pts => … },
11
+ // rotation: { Cls: Num, fwd: pts => atan2(…) },
12
+ // scale: { Cls: Num, fwd: pts => hypot(…) },
13
+ // });
14
+ // centroid.value = { x: 100, y: 50 }; // typed
15
+ //
16
+ // `bundle()` is the 1→M case: `factor()` over a single typed source.
17
+ import { Num, Vec, } from "../index.js";
18
+ function getPack(cell) {
19
+ const ctor = cell.constructor;
20
+ const p = ctor.traits?.pack;
21
+ if (!p) {
22
+ const name = ctor.name ?? "?";
23
+ throw new Error(`typed-factor: ${name} has no traits.pack`);
24
+ }
25
+ return p;
26
+ }
27
+ function getPackFromCls(Cls) {
28
+ const p = Cls.traits?.pack;
29
+ if (!p) {
30
+ const name = Cls.name ?? "?";
31
+ throw new Error(`typed-factor: ${name} has no traits.pack`);
32
+ }
33
+ return p;
34
+ }
35
+ function cumOffsets(dims) {
36
+ const out = [];
37
+ let acc = 0;
38
+ for (const d of dims) {
39
+ out.push(acc);
40
+ acc += d;
41
+ }
42
+ return out;
43
+ }
44
+ export function factor(inputs, outputs, opts = {}) {
45
+ const inputCount = inputs.length;
46
+ if (inputCount === 0) {
47
+ throw new Error("typed-factor: need ≥ 1 input");
48
+ }
49
+ const inputPacks = inputs.map(s => getPack(s));
50
+ const inputDims = inputPacks.map(p => p.dim);
51
+ const inputOffsets = cumOffsets(inputDims);
52
+ const N = inputDims.reduce((s, d) => s + d, 0);
53
+ // Map each flat input index back to its source input index — used
54
+ // to know which typed input to re-unpack after FD perturbation.
55
+ const whichInput = new Array(N);
56
+ for (let k = 0; k < inputCount; k++) {
57
+ for (let d = 0; d < inputDims[k]; d++) {
58
+ whichInput[inputOffsets[k] + d] = k;
59
+ }
60
+ }
61
+ const outputKeys = Object.keys(outputs);
62
+ const outputCount = outputKeys.length;
63
+ if (outputCount === 0) {
64
+ throw new Error("typed-factor: need ≥ 1 output");
65
+ }
66
+ const outputSpecs = outputKeys.map(k => outputs[k]);
67
+ const outputPacks = outputSpecs.map(s => getPackFromCls(s.Cls));
68
+ const outputDims = outputPacks.map(p => p.dim);
69
+ const outputOffsets = cumOffsets(outputDims);
70
+ const M = outputDims.reduce((s, d) => s + d, 0);
71
+ const weights = opts.inputWeights ?? Array.from({ length: N }, () => 1);
72
+ if (weights.length !== N) {
73
+ throw new Error(`typed-factor: inputWeights length ${weights.length} ≠ flat input dim ${N}`);
74
+ }
75
+ const eps = opts.eps ?? 1e-5;
76
+ const lambda = opts.damping ?? 1e-6;
77
+ const converge = opts.converge ?? false;
78
+ const maxIters = opts.maxIters ?? 10;
79
+ const tol = opts.tol ?? 1e-4;
80
+ // All-or-nothing analytical Jacobian: skip FD only when every output
81
+ // supplies one (no mixed mode).
82
+ const useAnalyticalJ = outputSpecs.every(s => s.jacobian !== undefined);
83
+ // Shared scratch buffers — safe across the M cells because writes
84
+ // execute synchronously inside one `_setWithExclusion` call.
85
+ const flatIn = new Float64Array(N);
86
+ const flatOutBase = new Float64Array(M);
87
+ const flatOutPerturbed = new Float64Array(M);
88
+ const J = new Float64Array(M * N);
89
+ const A = new Float64Array(M * M);
90
+ const Ainv = new Float64Array(M * M);
91
+ const dy = new Float64Array(M);
92
+ const kvec = new Float64Array(M);
93
+ // Per-write driver, shared by all M cells.
94
+ const computeBwd = (channelIdx, // which named output is being written
95
+ target, vals) => {
96
+ // 1. Pack current inputs → flatIn
97
+ for (let k = 0; k < inputCount; k++) {
98
+ inputPacks[k].read(vals[k], flatIn, inputOffsets[k]);
99
+ }
100
+ // FD working copies: typedScratch[k] is re-unpacked when its slice of
101
+ // flatIn is perturbed.
102
+ const typedScratch = vals.slice();
103
+ // 2. Base outputs
104
+ for (let j = 0; j < outputCount; j++) {
105
+ const out = outputSpecs[j].fwd(typedScratch);
106
+ outputPacks[j].read(out, flatOutBase, outputOffsets[j]);
107
+ }
108
+ // 3. δy: sparse, only channelIdx's slice is non-zero.
109
+ dy.fill(0);
110
+ {
111
+ const dim = outputDims[channelIdx];
112
+ const baseOff = outputOffsets[channelIdx];
113
+ // Pack target into flatOutPerturbed (reused buffer, no allocation).
114
+ outputPacks[channelIdx].read(target, flatOutPerturbed, baseOff);
115
+ for (let i = 0; i < dim; i++) {
116
+ dy[baseOff + i] = flatOutPerturbed[baseOff + i] - flatOutBase[baseOff + i];
117
+ }
118
+ }
119
+ // 4. Build Jacobian. Either analytical (fast + exact) or FD.
120
+ if (useAnalyticalJ) {
121
+ for (let j = 0; j < outputCount; j++) {
122
+ const rows = outputSpecs[j].jacobian(typedScratch);
123
+ const dim = outputDims[j];
124
+ const baseOff = outputOffsets[j];
125
+ for (let d = 0; d < dim; d++) {
126
+ const row = rows[d];
127
+ for (let i = 0; i < N; i++) {
128
+ J[(baseOff + d) * N + i] = row[i];
129
+ }
130
+ }
131
+ }
132
+ }
133
+ else {
134
+ for (let i = 0; i < N; i++) {
135
+ const saved = flatIn[i];
136
+ flatIn[i] = saved + eps;
137
+ const k = whichInput[i];
138
+ typedScratch[k] = inputPacks[k].write(flatIn, inputOffsets[k]);
139
+ for (let j = 0; j < outputCount; j++) {
140
+ const o = outputSpecs[j].fwd(typedScratch);
141
+ outputPacks[j].read(o, flatOutPerturbed, outputOffsets[j]);
142
+ const dim = outputDims[j];
143
+ const baseOff = outputOffsets[j];
144
+ for (let d = 0; d < dim; d++) {
145
+ J[(baseOff + d) * N + i] =
146
+ (flatOutPerturbed[baseOff + d] - flatOutBase[baseOff + d]) / eps;
147
+ }
148
+ }
149
+ flatIn[i] = saved;
150
+ // Restore the affected typed input to its base value.
151
+ typedScratch[k] = inputPacks[k].write(flatIn, inputOffsets[k]);
152
+ }
153
+ }
154
+ // 5. A = J W J^T + λI
155
+ for (let r = 0; r < M; r++) {
156
+ for (let c = 0; c < M; c++) {
157
+ let s = 0;
158
+ for (let i = 0; i < N; i++) {
159
+ s += J[r * N + i] * weights[i] * J[c * N + i];
160
+ }
161
+ A[r * M + c] = s + (r === c ? lambda : 0);
162
+ }
163
+ }
164
+ // 6. Solve A · k = δy
165
+ if (!invertMatrix(A, M, Ainv)) {
166
+ return vals.map(() => undefined);
167
+ }
168
+ for (let r = 0; r < M; r++) {
169
+ let s = 0;
170
+ for (let c = 0; c < M; c++)
171
+ s += Ainv[r * M + c] * dy[c];
172
+ kvec[r] = s;
173
+ }
174
+ // 7. δx = W J^T k, applied to flatIn → produces new flat input vector.
175
+ // Then unpack per-input to typed updates.
176
+ const updates = new Array(inputCount);
177
+ for (let k = 0; k < inputCount; k++) {
178
+ const baseOff = inputOffsets[k];
179
+ const dim = inputDims[k];
180
+ let anyChange = false;
181
+ for (let d = 0; d < dim; d++) {
182
+ const flatIdx = baseOff + d;
183
+ const w = weights[flatIdx];
184
+ if (w === 0)
185
+ continue;
186
+ let dxi = 0;
187
+ for (let r = 0; r < M; r++)
188
+ dxi += J[r * N + flatIdx] * kvec[r];
189
+ const newVal = flatIn[flatIdx] + w * dxi;
190
+ if (newVal !== flatIn[flatIdx])
191
+ anyChange = true;
192
+ flatIn[flatIdx] = newVal;
193
+ }
194
+ updates[k] = anyChange
195
+ ? inputPacks[k].write(flatIn, baseOff)
196
+ : undefined;
197
+ }
198
+ return updates;
199
+ };
200
+ const result = {};
201
+ for (let k = 0; k < outputCount; k++) {
202
+ const idx = k;
203
+ const key = outputKeys[idx];
204
+ const spec = outputSpecs[idx];
205
+ // biome-ignore lint/suspicious/noExplicitAny: typed at facade
206
+ const Cls = spec.Cls;
207
+ // Auto-converging backward: iterate the single-Newton step until the
208
+ // channel's reading is within tol (1 iter when linear). Runs on a local
209
+ // copy inside `put`, so the engine applies converged inputs in one shot.
210
+ const outPack = outputPacks[idx];
211
+ const outDim = outputDims[idx];
212
+ const convergeBwd = (target, vals) => {
213
+ const targetBuf = new Float64Array(outDim);
214
+ const currentBuf = new Float64Array(outDim);
215
+ outPack.read(target, targetBuf, 0);
216
+ const cur = vals.slice();
217
+ for (let it = 0; it < maxIters; it++) {
218
+ const updates = computeBwd(idx, target, cur);
219
+ for (let i = 0; i < cur.length; i++) {
220
+ if (updates[i] !== undefined)
221
+ cur[i] = updates[i];
222
+ }
223
+ outPack.read(spec.fwd(cur), currentBuf, 0);
224
+ let sumSq = 0;
225
+ for (let d = 0; d < outDim; d++) {
226
+ const diff = targetBuf[d] - currentBuf[d];
227
+ sumSq += diff * diff;
228
+ }
229
+ if (Math.sqrt(sumSq) < tol)
230
+ break;
231
+ }
232
+ return cur;
233
+ };
234
+ const cell = Cls.lens(inputs, (vals) => spec.fwd(vals), converge
235
+ ? (target, vals) => convergeBwd(target, vals)
236
+ : (target, vals) => computeBwd(idx, target, vals));
237
+ result[key] = cell;
238
+ }
239
+ return result;
240
+ }
241
+ // factorTuple — positional API.
242
+ //
243
+ // Same engine, no names. Outputs are a tuple of specs; the result is a
244
+ // tuple of writables. Terser to destructure, but order-sensitive and
245
+ // loses self-documenting names.
246
+ export function factorTuple(inputs, outputs, opts = {}) {
247
+ // Wrap to named, call factor, unwrap (one-time setup; hot path identical).
248
+ // biome-ignore lint/suspicious/noExplicitAny: variance escape
249
+ const named = {};
250
+ for (let i = 0; i < outputs.length; i++)
251
+ named[String(i)] = outputs[i];
252
+ const result = factor(inputs, named, opts);
253
+ return outputs.map((_, i) => result[String(i)]);
254
+ }
255
+ // bundle — 1→M dual, sugar over factor() with one input.
256
+ //
257
+ // A single typed source factored into M coupled views — `factor()` with
258
+ // a length-1 input array. Writing a view sends a sparse δy through the
259
+ // Jacobian solve (small, since N = the source's pack dim).
260
+ export function bundle(source, views, opts = {}) {
261
+ // factor() takes an input array, so pass [source] and wrap each view's
262
+ // fwd to receive the single-element array form.
263
+ // biome-ignore lint/suspicious/noExplicitAny: variance escape
264
+ const wrapped = {};
265
+ for (const key of Object.keys(views)) {
266
+ const v = views[key];
267
+ wrapped[key] = {
268
+ Cls: v.Cls,
269
+ // biome-ignore lint/suspicious/noExplicitAny: variance escape
270
+ fwd: (inputs) => v.fwd([inputs[0]]),
271
+ jacobian: v.jacobian
272
+ ? // biome-ignore lint/suspicious/noExplicitAny: variance escape
273
+ (inputs) => v.jacobian([inputs[0]])
274
+ : undefined,
275
+ };
276
+ }
277
+ return factor([source], wrapped, opts);
278
+ }
279
+ // For field-style bundles, `field()` already covers the independent case;
280
+ // use `bundle()` when you want coupled writes through the Jacobian solve.
281
+ // Matrix inverse (Gauss-Jordan with partial pivoting).
282
+ function invertMatrix(A, M, out) {
283
+ const W = 2 * M;
284
+ const aug = new Float64Array(M * W);
285
+ for (let r = 0; r < M; r++) {
286
+ for (let c = 0; c < M; c++)
287
+ aug[r * W + c] = A[r * M + c];
288
+ for (let c = 0; c < M; c++)
289
+ aug[r * W + M + c] = r === c ? 1 : 0;
290
+ }
291
+ for (let i = 0; i < M; i++) {
292
+ let p = i;
293
+ let pv = Math.abs(aug[i * W + i]);
294
+ for (let r = i + 1; r < M; r++) {
295
+ const v = Math.abs(aug[r * W + i]);
296
+ if (v > pv) {
297
+ pv = v;
298
+ p = r;
299
+ }
300
+ }
301
+ if (pv < 1e-14)
302
+ return false;
303
+ if (p !== i) {
304
+ for (let c = 0; c < W; c++) {
305
+ const t = aug[i * W + c];
306
+ aug[i * W + c] = aug[p * W + c];
307
+ aug[p * W + c] = t;
308
+ }
309
+ }
310
+ const inv = 1 / aug[i * W + i];
311
+ for (let c = 0; c < W; c++)
312
+ aug[i * W + c] *= inv;
313
+ for (let r = 0; r < M; r++) {
314
+ if (r === i)
315
+ continue;
316
+ const f = aug[r * W + i];
317
+ if (f === 0)
318
+ continue;
319
+ for (let c = 0; c < W; c++)
320
+ aug[r * W + c] -= f * aug[i * W + c];
321
+ }
322
+ }
323
+ for (let r = 0; r < M; r++) {
324
+ for (let c = 0; c < M; c++)
325
+ out[r * M + c] = aug[r * W + M + c];
326
+ }
327
+ return true;
328
+ }
329
+ // Sugar: closed-form Procrustes via factor() typed API.
330
+ //
331
+ // Procrustes with typed outputs (centroid is a real Vec) but a
332
+ // Jacobian-LSQ bwd — a showcase for the typed ergonomics. The
333
+ // closed-form `procrustesLens` is faster and exact.
334
+ export function procrustesTyped(points) {
335
+ const K = points.length;
336
+ return factor(points, {
337
+ centroid: {
338
+ Cls: Vec,
339
+ fwd: (pts) => {
340
+ let sx = 0;
341
+ let sy = 0;
342
+ for (let i = 0; i < K; i++) {
343
+ sx += pts[i].x;
344
+ sy += pts[i].y;
345
+ }
346
+ return { x: sx / K, y: sy / K };
347
+ },
348
+ },
349
+ rotation: {
350
+ Cls: Num,
351
+ fwd: (pts) => {
352
+ let sx = 0;
353
+ let sy = 0;
354
+ for (let i = 0; i < K; i++) {
355
+ sx += pts[i].x;
356
+ sy += pts[i].y;
357
+ }
358
+ return Math.atan2(pts[0].y - sy / K, pts[0].x - sx / K);
359
+ },
360
+ },
361
+ scale: {
362
+ Cls: Num,
363
+ fwd: (pts) => {
364
+ let sx = 0;
365
+ let sy = 0;
366
+ for (let i = 0; i < K; i++) {
367
+ sx += pts[i].x;
368
+ sy += pts[i].y;
369
+ }
370
+ return Math.hypot(pts[0].x - sx / K, pts[0].y - sy / K);
371
+ },
372
+ },
373
+ // Damping bumped up — atan2/hypot are non-linear; without damping the
374
+ // first-Newton-step error compounds at the boundary of well-conditioned.
375
+ }, { damping: 1e-3 });
376
+ }
@@ -0,0 +1,14 @@
1
+ import { type Read } from "./signal.js";
2
+ /** Disposable handle. */
3
+ export interface Lifecycle {
4
+ dispose(): void;
5
+ }
6
+ /** Run `body(item)` per element on first sight (storing its cleanup) and
7
+ * run that cleanup when the element leaves. Identity is element
8
+ * reference — keep stable refs across mutations, don't re-create
9
+ * objects every frame. */
10
+ export declare function each<T>(source: Read<readonly T[]>, body: (item: T) => () => void): Lifecycle;
11
+ /** Run `body(v)` while `source.value` is truthy; run the returned
12
+ * cleanup on falsy and re-arm. (Distinct from `anim.ts`'s one-shot
13
+ * `when(sig)` Animator.) */
14
+ export declare function when<T>(source: Read<T>, body: (v: T) => () => void): Lifecycle;
@@ -0,0 +1,62 @@
1
+ // network-utils.ts — reactive-collection lifecycle helpers over the
2
+ // `network` model. Implemented purely via `effect`:
3
+ //
4
+ // `each(source, body)` — body runs per element (keyed by reference
5
+ // identity); cleanup on removal.
6
+ // `when(source, body)` — body runs while truthy; cleanup on falsy.
7
+ import { effect } from "./signal.js";
8
+ /** Run `body(item)` per element on first sight (storing its cleanup) and
9
+ * run that cleanup when the element leaves. Identity is element
10
+ * reference — keep stable refs across mutations, don't re-create
11
+ * objects every frame. */
12
+ export function each(source, body) {
13
+ const handles = new Map();
14
+ const eff = effect(() => {
15
+ const items = source.value;
16
+ const seen = new Set(items);
17
+ for (const item of items) {
18
+ if (!handles.has(item))
19
+ handles.set(item, body(item));
20
+ }
21
+ for (const [item, cleanup] of handles) {
22
+ if (!seen.has(item)) {
23
+ cleanup();
24
+ handles.delete(item);
25
+ }
26
+ }
27
+ });
28
+ return {
29
+ dispose() {
30
+ eff();
31
+ for (const cleanup of handles.values())
32
+ cleanup();
33
+ handles.clear();
34
+ },
35
+ };
36
+ }
37
+ /** Run `body(v)` while `source.value` is truthy; run the returned
38
+ * cleanup on falsy and re-arm. (Distinct from `anim.ts`'s one-shot
39
+ * `when(sig)` Animator.) */
40
+ export function when(source, body) {
41
+ let cleanup;
42
+ const eff = effect(() => {
43
+ const v = source.value;
44
+ if (v) {
45
+ if (cleanup === undefined)
46
+ cleanup = body(v);
47
+ }
48
+ else if (cleanup !== undefined) {
49
+ cleanup();
50
+ cleanup = undefined;
51
+ }
52
+ });
53
+ return {
54
+ dispose() {
55
+ eff();
56
+ if (cleanup !== undefined) {
57
+ cleanup();
58
+ cleanup = undefined;
59
+ }
60
+ },
61
+ };
62
+ }
@@ -0,0 +1,33 @@
1
+ import type { Cell, Writable } from "./signal.js";
2
+ import { Num } from "./values/num.js";
3
+ import { Vec } from "./values/vec.js";
4
+ type V = {
5
+ x: number;
6
+ y: number;
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;
13
+ /** Reflect `point` across the line through `axisStart`/`axisEnd`. Writes
14
+ * the reflected position back to `point` (axis unchanged); reflection is
15
+ * 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
+ /** Lerp between two Vecs at parameter `t`. Writing the interpolated point
18
+ * shifts both endpoints rigidly (preserving t). */
19
+ export declare function vecLerp(a: Cell<V>, b: Cell<V>, t: Cell<number>): Writable<Vec>;
20
+ /** Sum of two nums; writing the sum splits the delta equally between
21
+ * a and b (the "pulley" conservation pattern). */
22
+ export declare function pulleySum(a: Num, b: Num): Writable<Num>;
23
+ /** Difference of two nums: `a - b`. Writing the difference shifts
24
+ * both inputs symmetrically by ±half-delta. */
25
+ export declare function diffLens(a: Num, b: Num): Writable<Num>;
26
+ /** Mean of N nums, clamped to `[lo, hi]` on read and write (writes are
27
+ * clamped before the delta is distributed). */
28
+ 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
+ export {};
@@ -0,0 +1,113 @@
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));
9
+ }
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));
13
+ }
14
+ /** Reflect `point` across the line through `axisStart`/`axisEnd`. Writes
15
+ * the reflected position back to `point` (axis unchanged); reflection is
16
+ * involutive, so the same formula reads and writes. */
17
+ export function reflectionLens(point, axisStart, axisEnd) {
18
+ const reflect = (p, a, b) => {
19
+ const dx = b.x - a.x;
20
+ const dy = b.y - a.y;
21
+ const len2 = dx * dx + dy * dy;
22
+ if (len2 === 0)
23
+ return p;
24
+ const t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2;
25
+ const projX = a.x + t * dx;
26
+ const projY = a.y + t * dy;
27
+ return { x: 2 * projX - p.x, y: 2 * projY - p.y };
28
+ };
29
+ return Vec.lens([point, axisStart, axisEnd], vals => reflect(vals[0], vals[1], vals[2]), (target, vals) => [reflect(target, vals[1], vals[2]), undefined, undefined]);
30
+ }
31
+ /** Lerp between two Vecs at parameter `t`. Writing the interpolated point
32
+ * shifts both endpoints rigidly (preserving t). */
33
+ export function vecLerp(a, b, t) {
34
+ return Vec.lens([a, b, t], vals => {
35
+ const [av, bv, tv] = vals;
36
+ return { x: av.x + (bv.x - av.x) * tv, y: av.y + (bv.y - av.y) * tv };
37
+ }, (target, vals) => {
38
+ const [av, bv, tv] = vals;
39
+ const dx = target.x - (av.x + (bv.x - av.x) * tv);
40
+ const dy = target.y - (av.y + (bv.y - av.y) * tv);
41
+ return [{ x: av.x + dx, y: av.y + dy }, { x: bv.x + dx, y: bv.y + dy }, undefined];
42
+ });
43
+ }
44
+ /** Sum of two nums; writing the sum splits the delta equally between
45
+ * a and b (the "pulley" conservation pattern). */
46
+ export function pulleySum(a, b) {
47
+ return Num.lens([a, b], vals => vals[0] + vals[1], (target, vals) => {
48
+ const [av, bv] = vals;
49
+ const cur = av + bv;
50
+ const delta = target - cur;
51
+ return [av + delta / 2, bv + delta / 2];
52
+ });
53
+ }
54
+ /** Difference of two nums: `a - b`. Writing the difference shifts
55
+ * both inputs symmetrically by ±half-delta. */
56
+ export function diffLens(a, b) {
57
+ return Num.lens([a, b], vals => vals[0] - vals[1], (target, vals) => {
58
+ const [av, bv] = vals;
59
+ const cur = av - bv;
60
+ const delta = target - cur;
61
+ return [av + delta / 2, bv - delta / 2];
62
+ });
63
+ }
64
+ /** Mean of N nums, clamped to `[lo, hi]` on read and write (writes are
65
+ * clamped before the delta is distributed). */
66
+ export function clampedMean(parents, lo, hi) {
67
+ const n = parents.length;
68
+ const inv = 1 / n;
69
+ return Num.lens(parents, vals => {
70
+ const arr = vals;
71
+ let s = 0;
72
+ for (let i = 0; i < n; i++)
73
+ s += arr[i];
74
+ const m = s * inv;
75
+ return m < lo ? lo : m > hi ? hi : m;
76
+ }, (target, vals) => {
77
+ const arr = vals;
78
+ const clamped = target < lo ? lo : target > hi ? hi : target;
79
+ let s = 0;
80
+ for (let i = 0; i < n; i++)
81
+ s += arr[i];
82
+ const cur = s * inv;
83
+ const delta = clamped - cur;
84
+ const out = new Array(n);
85
+ for (let i = 0; i < n; i++)
86
+ out[i] = arr[i] + delta;
87
+ return out;
88
+ });
89
+ }
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
+ }