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,419 @@
1
+ // factor-lens.ts — N→M lens prototypes (Vec-specific monoliths).
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
+ // Two regimes:
6
+ //
7
+ // 1. Numerical (Jacobian-LSQ): `factorLens` builds an M×N Jacobian by
8
+ // finite differences and solves `(J W Jᵀ + λI) k = δy` for `k`,
9
+ // then writes `δx = W Jᵀ k`. δy is sparse (only the written
10
+ // channel), so the solve leaves other channels near-stationary.
11
+ // Invariance is approximate, set by the local condition number.
12
+ //
13
+ // 2. Closed-form (geometric): `procrustesLens`, `bboxLens`,
14
+ // `meanDiffLens` — hand-rolled bwd via the right group action, so
15
+ // cross-channel invariance is EXACT. Cost: crafted per topology.
16
+ //
17
+ // `bundleLens` sketches the 1→M dual (single source → M coupled views).
18
+ import { Num, Vec } from "../index.js";
19
+ export function factorLens(inputs, forwards, opts = {}) {
20
+ const N = inputs.length;
21
+ const M = forwards.length;
22
+ if (M === 0)
23
+ return [];
24
+ const w = opts.inputWeights ?? new Array(N).fill(1);
25
+ const eps = opts.eps ?? 1e-5;
26
+ const lambda = opts.damping ?? 1e-6;
27
+ // Per-call (NOT per-cell) scratch; safe because writes execute
28
+ // synchronously inside `_setWithExclusion`.
29
+ const J = new Array(M * N);
30
+ const A = new Array(M * M);
31
+ const Ainv = new Array(M * M);
32
+ const ys = new Array(M);
33
+ const dy = new Array(M);
34
+ const kvec = new Array(M);
35
+ const outputs = [];
36
+ for (let outIdx = 0; outIdx < M; outIdx++) {
37
+ const idx = outIdx;
38
+ const out = new Array(N);
39
+ const cell = Num.lens(inputs, (vals) => forwards[idx](vals), (target, valsReadonly) => {
40
+ // Snapshot inputs into a mutable scratch so FD perturbations
41
+ // don't leak into upstream cell state.
42
+ const xs = valsReadonly;
43
+ const xsm = xs.slice();
44
+ for (let j = 0; j < M; j++)
45
+ ys[j] = forwards[j](xsm);
46
+ for (let j = 0; j < M; j++)
47
+ dy[j] = 0;
48
+ dy[idx] = target - ys[idx];
49
+ // Build Jacobian column-by-column.
50
+ for (let i = 0; i < N; i++) {
51
+ const saved = xsm[i];
52
+ xsm[i] = saved + eps;
53
+ for (let j = 0; j < M; j++) {
54
+ J[j * N + i] = (forwards[j](xsm) - ys[j]) / eps;
55
+ }
56
+ xsm[i] = saved;
57
+ }
58
+ // A = J W Jᵀ + λI
59
+ for (let r = 0; r < M; r++) {
60
+ for (let c = 0; c < M; c++) {
61
+ let s = 0;
62
+ for (let i = 0; i < N; i++)
63
+ s += J[r * N + i] * w[i] * J[c * N + i];
64
+ A[r * M + c] = s + (r === c ? lambda : 0);
65
+ }
66
+ }
67
+ if (!invertMatrix(A, M, Ainv)) {
68
+ // Singular — leave inputs unchanged.
69
+ for (let i = 0; i < N; i++)
70
+ out[i] = undefined;
71
+ return out;
72
+ }
73
+ for (let r = 0; r < M; r++) {
74
+ let s = 0;
75
+ for (let c = 0; c < M; c++)
76
+ s += Ainv[r * M + c] * dy[c];
77
+ kvec[r] = s;
78
+ }
79
+ for (let i = 0; i < N; i++) {
80
+ let dxi = 0;
81
+ for (let r = 0; r < M; r++)
82
+ dxi += J[r * N + i] * kvec[r];
83
+ out[i] = xsm[i] + w[i] * dxi;
84
+ }
85
+ return out;
86
+ });
87
+ outputs.push(cell);
88
+ }
89
+ return outputs;
90
+ }
91
+ /** Gauss-Jordan inverse of a row-major M×M matrix. Returns false if
92
+ * singular (pivot below 1e-14). Allocates one 2M-wide row buffer.
93
+ * For M ≤ ~10 this is competitive with LAPACK and avoids the dep. */
94
+ function invertMatrix(A, M, out) {
95
+ const W = 2 * M;
96
+ const aug = new Array(M * W);
97
+ for (let r = 0; r < M; r++) {
98
+ for (let c = 0; c < M; c++)
99
+ aug[r * W + c] = A[r * M + c];
100
+ for (let c = 0; c < M; c++)
101
+ aug[r * W + M + c] = r === c ? 1 : 0;
102
+ }
103
+ for (let i = 0; i < M; i++) {
104
+ let p = i;
105
+ let pv = Math.abs(aug[i * W + i]);
106
+ for (let r = i + 1; r < M; r++) {
107
+ const v = Math.abs(aug[r * W + i]);
108
+ if (v > pv) {
109
+ pv = v;
110
+ p = r;
111
+ }
112
+ }
113
+ if (pv < 1e-14)
114
+ return false;
115
+ if (p !== i) {
116
+ for (let c = 0; c < W; c++) {
117
+ const t = aug[i * W + c];
118
+ aug[i * W + c] = aug[p * W + c];
119
+ aug[p * W + c] = t;
120
+ }
121
+ }
122
+ const inv = 1 / aug[i * W + i];
123
+ for (let c = 0; c < W; c++)
124
+ aug[i * W + c] = aug[i * W + c] * inv;
125
+ for (let r = 0; r < M; r++) {
126
+ if (r === i)
127
+ continue;
128
+ const f = aug[r * W + i];
129
+ if (f === 0)
130
+ continue;
131
+ for (let c = 0; c < W; c++)
132
+ aug[r * W + c] = aug[r * W + c] - f * aug[i * W + c];
133
+ }
134
+ }
135
+ for (let r = 0; r < M; r++) {
136
+ for (let c = 0; c < M; c++)
137
+ out[r * M + c] = aug[r * W + M + c];
138
+ }
139
+ return true;
140
+ }
141
+ // meanDiffLens — M=2 isomorphism baseline.
142
+ //
143
+ // (a, b) → ((a+b)/2, a−b). Square full-rank linear lens; bwd is the
144
+ // inverse change of basis — exact, cross-channel invariant. Sanity
145
+ // baseline for the property tests.
146
+ export function meanDiffLens(a, b) {
147
+ const mean = Num.lens([a, b], vals => (vals[0] + vals[1]) / 2, (target, vals) => {
148
+ const d = vals[0] - vals[1];
149
+ return [target + d / 2, target - d / 2];
150
+ });
151
+ const diff = Num.lens([a, b], vals => vals[0] - vals[1], (target, vals) => {
152
+ const m = (vals[0] + vals[1]) / 2;
153
+ return [m + target / 2, m - target / 2];
154
+ });
155
+ return { mean, diff };
156
+ }
157
+ // procrustesLens — closed-form similarity (the showcase).
158
+ //
159
+ // K writable Vecs → {centroid, rotation (angle of point[0] about
160
+ // centroid), scale (its distance from centroid)}. Each bwd is a
161
+ // closed-form transform about the centroid:
162
+ // write centroid → translate every point by (c − old c)
163
+ // write rotation → rotate every point about centroid by (θ − old θ)
164
+ // write scale → scale every point about centroid by (s / old s)
165
+ // These commute on the cluster's similarity orbit, so the three outputs
166
+ // have EXACT cross-channel invariance (cf. `procrustesJacobianLens`'s
167
+ // approximate version). Degenerate: K < 2 leaves rotation/scale
168
+ // undefined; a collapsed cluster (scale → 0) makes rotation singular and
169
+ // scale writes no-ops; target scale = 0 collapses to the centroid.
170
+ export function procrustesLens(points) {
171
+ const K = points.length;
172
+ if (K < 2)
173
+ throw new Error("procrustesLens: need ≥ 2 points");
174
+ const centroid = Vec.lens(points, (vals) => {
175
+ let sx = 0;
176
+ let sy = 0;
177
+ for (let i = 0; i < K; i++) {
178
+ sx += vals[i].x;
179
+ sy += vals[i].y;
180
+ }
181
+ return { x: sx / K, y: sy / K };
182
+ }, (target, vals) => {
183
+ let sx = 0;
184
+ let sy = 0;
185
+ for (let i = 0; i < K; i++) {
186
+ sx += vals[i].x;
187
+ sy += vals[i].y;
188
+ }
189
+ const dx = target.x - sx / K;
190
+ const dy = target.y - sy / K;
191
+ const out = new Array(K);
192
+ for (let i = 0; i < K; i++)
193
+ out[i] = { x: vals[i].x + dx, y: vals[i].y + dy };
194
+ return out;
195
+ });
196
+ const rotation = Num.lens(points, (vals) => {
197
+ let sx = 0;
198
+ let sy = 0;
199
+ for (let i = 0; i < K; i++) {
200
+ sx += vals[i].x;
201
+ sy += vals[i].y;
202
+ }
203
+ const cx = sx / K;
204
+ const cy = sy / K;
205
+ return Math.atan2(vals[0].y - cy, vals[0].x - cx);
206
+ }, (target, vals) => {
207
+ let sx = 0;
208
+ let sy = 0;
209
+ for (let i = 0; i < K; i++) {
210
+ sx += vals[i].x;
211
+ sy += vals[i].y;
212
+ }
213
+ const cx = sx / K;
214
+ const cy = sy / K;
215
+ const rx0 = vals[0].x - cx;
216
+ const ry0 = vals[0].y - cy;
217
+ if (rx0 * rx0 + ry0 * ry0 < 1e-24) {
218
+ // Collapsed cluster; no angle to rotate from.
219
+ return vals.map(() => undefined);
220
+ }
221
+ const oldθ = Math.atan2(ry0, rx0);
222
+ const dθ = target - oldθ;
223
+ const cos = Math.cos(dθ);
224
+ const sin = Math.sin(dθ);
225
+ const out = new Array(K);
226
+ for (let i = 0; i < K; i++) {
227
+ const rx = vals[i].x - cx;
228
+ const ry = vals[i].y - cy;
229
+ out[i] = { x: cx + cos * rx - sin * ry, y: cy + sin * rx + cos * ry };
230
+ }
231
+ return out;
232
+ });
233
+ const centroidOf = (vals) => {
234
+ let sx = 0;
235
+ let sy = 0;
236
+ for (let i = 0; i < K; i++) {
237
+ sx += vals[i].x;
238
+ sy += vals[i].y;
239
+ }
240
+ return { x: sx / K, y: sy / K };
241
+ };
242
+ const refreshDevs = (devs, vals) => {
243
+ const c = centroidOf(vals);
244
+ return devs.map((d, i) => {
245
+ const dx = vals[i].x - c.x;
246
+ const dy = vals[i].y - c.y;
247
+ return dx * dx + dy * dy > 1e-18 ? { x: dx, y: dy } : d;
248
+ });
249
+ };
250
+ const scale = Num.lens(points, {
251
+ init: (vals) => {
252
+ const c = centroidOf(vals);
253
+ return { devs: vals.map(v => ({ x: v.x - c.x, y: v.y - c.y })) };
254
+ },
255
+ step: (vals, c) => ({ devs: refreshDevs(c.devs, vals) }),
256
+ fwd: (vals) => {
257
+ const c = centroidOf(vals);
258
+ return Math.hypot(vals[0].x - c.x, vals[0].y - c.y);
259
+ },
260
+ bwd: (target, vals, c) => {
261
+ const cen = centroidOf(vals);
262
+ const d0 = c.devs[0];
263
+ const r0 = Math.hypot(d0.x, d0.y);
264
+ if (r0 < 1e-12)
265
+ return { updates: vals.map(() => undefined), complement: c };
266
+ const k = target / r0;
267
+ const out = c.devs.map(d => ({ x: cen.x + k * d.x, y: cen.y + k * d.y }));
268
+ return { updates: out, complement: c };
269
+ },
270
+ });
271
+ return { centroid, rotation, scale };
272
+ }
273
+ // bboxLens — closed-form axis-aligned bounding box.
274
+ //
275
+ // K Vecs → {center, size}. Forward is min/max (piecewise-constant
276
+ // Jacobian — fatal for FD), but the closed-form bwd is exact:
277
+ // write center → translate all points by (c − old c)
278
+ // write size → scale all about center by component-wise ratio
279
+ // Center↔size invariance is exact. Degenerate axes (size = 0) write
280
+ // as no-ops; negative size reflects (kept permissive).
281
+ export function bboxLens(points) {
282
+ const K = points.length;
283
+ if (K < 1)
284
+ throw new Error("bboxLens: need ≥ 1 point");
285
+ const computeBox = (vals) => {
286
+ let minX = Number.POSITIVE_INFINITY;
287
+ let minY = Number.POSITIVE_INFINITY;
288
+ let maxX = Number.NEGATIVE_INFINITY;
289
+ let maxY = Number.NEGATIVE_INFINITY;
290
+ for (let i = 0; i < K; i++) {
291
+ const x = vals[i].x;
292
+ const y = vals[i].y;
293
+ if (x < minX)
294
+ minX = x;
295
+ if (x > maxX)
296
+ maxX = x;
297
+ if (y < minY)
298
+ minY = y;
299
+ if (y > maxY)
300
+ maxY = y;
301
+ }
302
+ return {
303
+ cx: (minX + maxX) / 2,
304
+ cy: (minY + maxY) / 2,
305
+ sx: maxX - minX,
306
+ sy: maxY - minY,
307
+ };
308
+ };
309
+ const center = Vec.lens(points, (vals) => {
310
+ const b = computeBox(vals);
311
+ return { x: b.cx, y: b.cy };
312
+ }, (target, vals) => {
313
+ const b = computeBox(vals);
314
+ const dx = target.x - b.cx;
315
+ const dy = target.y - b.cy;
316
+ const out = new Array(K);
317
+ for (let i = 0; i < K; i++)
318
+ out[i] = { x: vals[i].x + dx, y: vals[i].y + dy };
319
+ return out;
320
+ });
321
+ const refreshFracs = (fracs, vals) => {
322
+ const b = computeBox(vals);
323
+ const hx = b.sx > 1e-12 ? b.sx / 2 : 0;
324
+ const hy = b.sy > 1e-12 ? b.sy / 2 : 0;
325
+ return fracs.map((f, i) => ({
326
+ x: hx > 0 ? (vals[i].x - b.cx) / hx : f.x,
327
+ y: hy > 0 ? (vals[i].y - b.cy) / hy : f.y,
328
+ }));
329
+ };
330
+ const size = Vec.lens(points, {
331
+ init: (vals) => {
332
+ const b = computeBox(vals);
333
+ const halfX0 = b.sx > 1e-12 ? b.sx / 2 : 1;
334
+ const halfY0 = b.sy > 1e-12 ? b.sy / 2 : 1;
335
+ return {
336
+ fracs: vals.map(v => ({
337
+ x: b.sx > 1e-12 ? (v.x - b.cx) / halfX0 : 0,
338
+ y: b.sy > 1e-12 ? (v.y - b.cy) / halfY0 : 0,
339
+ })),
340
+ };
341
+ },
342
+ step: (vals, c) => ({ fracs: refreshFracs(c.fracs, vals) }),
343
+ fwd: (vals) => {
344
+ const b = computeBox(vals);
345
+ return { x: b.sx, y: b.sy };
346
+ },
347
+ bwd: (target, vals, c) => {
348
+ const b = computeBox(vals);
349
+ const halfTx = target.x / 2;
350
+ const halfTy = target.y / 2;
351
+ const out = c.fracs.map(f => ({ x: b.cx + f.x * halfTx, y: b.cy + f.y * halfTy }));
352
+ return { updates: out, complement: c };
353
+ },
354
+ });
355
+ return { center, size };
356
+ }
357
+ // procrustesJacobianLens — comparison point.
358
+ //
359
+ // Same forward map as `procrustesLens` but with the generic Jacobian-LSQ
360
+ // bwd, to quantify the numerical path vs. the closed-form one.
361
+ export function procrustesJacobianLens(points) {
362
+ const K = points.length;
363
+ if (K < 2)
364
+ throw new Error("procrustesJacobianLens: need ≥ 2 points");
365
+ // Flatten K Vecs into 2K scalar field lenses.
366
+ const xs = [];
367
+ const ys = [];
368
+ for (const p of points) {
369
+ xs.push(Num.lens([p], v => v[0].x, (t, v) => [{ x: t, y: v[0].y }]));
370
+ ys.push(Num.lens([p], v => v[0].y, (t, v) => [{ x: v[0].x, y: t }]));
371
+ }
372
+ // factorLens wants a flat input array.
373
+ const flat = [];
374
+ for (let i = 0; i < K; i++) {
375
+ flat.push(xs[i], ys[i]);
376
+ }
377
+ // Indexing helpers
378
+ const xAt = (a, i) => a[2 * i];
379
+ const yAt = (a, i) => a[2 * i + 1];
380
+ const fwdCx = (a) => {
381
+ let s = 0;
382
+ for (let i = 0; i < K; i++)
383
+ s += xAt(a, i);
384
+ return s / K;
385
+ };
386
+ const fwdCy = (a) => {
387
+ let s = 0;
388
+ for (let i = 0; i < K; i++)
389
+ s += yAt(a, i);
390
+ return s / K;
391
+ };
392
+ const fwdRot = (a) => {
393
+ return Math.atan2(yAt(a, 0) - fwdCy(a), xAt(a, 0) - fwdCx(a));
394
+ };
395
+ const fwdScale = (a) => {
396
+ return Math.hypot(xAt(a, 0) - fwdCx(a), yAt(a, 0) - fwdCy(a));
397
+ };
398
+ const [centroidX, centroidY, rotation, scale] = factorLens(flat, [fwdCx, fwdCy, fwdRot, fwdScale], { damping: 1e-4 });
399
+ return { centroidX, centroidY, rotation, scale };
400
+ }
401
+ export function bundleLens(pose, rotateAbout) {
402
+ const position = Vec.lens([pose], (v) => ({ x: v[0].x, y: v[0].y }), (target, v) => [{ ...v[0], x: target.x, y: target.y }]);
403
+ const rotation = Num.lens([pose], (v) => v[0].theta, (target, v) => {
404
+ const cur = v[0];
405
+ const dθ = target - cur.theta;
406
+ const cos = Math.cos(dθ);
407
+ const sin = Math.sin(dθ);
408
+ const rx = cur.x - rotateAbout.x;
409
+ const ry = cur.y - rotateAbout.y;
410
+ return [
411
+ {
412
+ x: rotateAbout.x + cos * rx - sin * ry,
413
+ y: rotateAbout.y + sin * rx + cos * ry,
414
+ theta: target,
415
+ },
416
+ ];
417
+ });
418
+ return { position, rotation };
419
+ }
@@ -0,0 +1,5 @@
1
+ export { bestFitCircleLens, bestFitLineLens, pcaLens, procrustesViaBuildingBlocks, rigidTranslate, rotateAbout, scaleAbout, scaleAboutXY, totalLens, } from "./closed-form-policies.js";
2
+ export { bezierGestaltLens, crossfade, meanColor, meanOf, mix, paletteLens, rigidTranslateOf, select, spreadOf, timeSeriesLens, } from "./domain-aggregates.js";
3
+ export { bboxLens, bundleLens, type FactorLensOpts, factorLens, meanDiffLens, procrustesJacobianLens, procrustesLens, } from "./factor-lens.js";
4
+ export { type ContinuousOpts, continuous, type RememberOpts, remember, } from "./memory.js";
5
+ export { bundle, type FactorOpts, type FactorResult, factor, factorTuple, type OutputSpec, type PackedInput, procrustesTyped, } from "./typed-factor.js";
@@ -0,0 +1,16 @@
1
+ // core/lenses/index.ts — N→M and 1→M bidirectional lens primitives.
2
+ //
3
+ // Three layers:
4
+ // 1. NUMERICAL — `factor`, `factorTuple`, `bundle`: generic
5
+ // Jacobian-LSQ, the escape hatch when no closed-form policy fits.
6
+ // 2. CLOSED-FORM POLICIES — `rigidTranslate`, `rotateAbout`,
7
+ // `scaleAbout`, `scaleAboutXY`: exact group-action primitives.
8
+ // 3. DECOMPOSITIONS — `procrustesLens`, `bboxLens`, `bestFitLine`,
9
+ // etc.: composed M-output views over the policies and aggregates.
10
+ //
11
+ // See BIDIRECTIONAL-LENSES.md for the engine substrate.
12
+ export { bestFitCircleLens, bestFitLineLens, pcaLens, procrustesViaBuildingBlocks, rigidTranslate, rotateAbout, scaleAbout, scaleAboutXY, totalLens, } from "./closed-form-policies.js";
13
+ export { bezierGestaltLens, crossfade, meanColor, meanOf, mix, paletteLens, rigidTranslateOf, select, spreadOf, timeSeriesLens, } from "./domain-aggregates.js";
14
+ export { bboxLens, bundleLens, factorLens, meanDiffLens, procrustesJacobianLens, procrustesLens, } from "./factor-lens.js";
15
+ export { continuous, remember, } from "./memory.js";
16
+ export { bundle, factor, factorTuple, procrustesTyped, } from "./typed-factor.js";
@@ -0,0 +1,47 @@
1
+ import { type Cell, Num, type Traits, type Writable } from "../index.js";
2
+ /** Options for {@link remember}. `anchor` is the fixed point sources scale
3
+ * about (a pivot, or the live centroid); `feature` is the writable scalar
4
+ * (a radius, mean distance, or sum). */
5
+ export interface RememberOpts<T> {
6
+ /** Point sources scale about — a constant pivot or a derived centroid. */
7
+ anchor: (vals: readonly T[]) => T;
8
+ /** The scalar view (the forward): radius, mean radius, spread, sum, … */
9
+ feature: (vals: readonly T[], anchor: T) => number;
10
+ /** Feature is non-negative ⇒ a same-magnitude write (e.g. `-r`) is a
11
+ * no-op (PutGet on a magnitude). Default `true`; set `false` for a
12
+ * signed total. */
13
+ magnitude?: boolean;
14
+ /** Degeneracy threshold; below it the cluster is "collapsed" and the
15
+ * stored shape drives reinflation. Default `1e-9`. */
16
+ eps?: number;
17
+ /** Normalized shape to seed when the feature is degenerate at init and
18
+ * no prior good shape exists. Default: zero deviations (stays
19
+ * collapsed). `totalLens` passes uniform `1/K` for an even split. */
20
+ seed?: (vals: readonly T[]) => T[];
21
+ }
22
+ /** Scalar shape-memory lens. Reads `feature(sources)`; writing it scales
23
+ * the cluster about `anchor` so the new feature matches, reinflating the
24
+ * remembered shape when the cluster has collapsed onto the anchor. The
25
+ * complement is the per-source deviation normalized by the feature. */
26
+ export declare function remember<T, S extends Cell<T> & Traits<T, "linear">>(sources: readonly Writable<S>[], opts: RememberOpts<T>): Writable<Num>;
27
+ /** Options for {@link continuous}. `raw` reads the cyclic value (mod
28
+ * `period`) and whether it's defined; `apply` realizes an unwrapped
29
+ * target back onto the sources given the current unwrapped reading. */
30
+ export interface ContinuousOpts<T> {
31
+ /** Cycle length: `2π` for a full angle, `π` for an axis (sign-free). */
32
+ period: number;
33
+ /** Raw cyclic reading. `defined: false` (e.g. a collapsed cloud has no
34
+ * axis) holds the last emitted value and freezes the sources. */
35
+ raw: (vals: readonly T[]) => {
36
+ value: number;
37
+ defined: boolean;
38
+ };
39
+ /** Realize `target` (already unwrapped, absolute) onto the sources,
40
+ * given the `current` unwrapped reading (for a delta). */
41
+ apply: (target: number, vals: readonly T[], current: number) => readonly (T | undefined)[];
42
+ }
43
+ /** Continuous (winding-aware) lens over a cyclic reading. The complement
44
+ * tracks the last emitted value and unwraps each raw reading to the
45
+ * nearest representative modulo `period`, so the view follows the source
46
+ * across branch cuts instead of jumping a full period. */
47
+ export declare function continuous<T>(sources: readonly Writable<Cell<T>>[], opts: ContinuousOpts<T>): Writable<Num>;
@@ -0,0 +1,102 @@
1
+ // memory.ts — path-dependent (stateful-complement) lens combinators.
2
+ //
3
+ // Two backward directions are not functions of the current source alone;
4
+ // they depend on where the value has BEEN. Both were hand-rolled per site
5
+ // across the closed-form-policy and aggregate lenses, each with its own
6
+ // degeneracy threshold. They factor into two combinators:
7
+ //
8
+ // remember — a magnitude/total view of a cluster: sources live at
9
+ // `anchorᵢ + shapeᵢ · scale`, `scale` is the writable
10
+ // scalar. The complement holds the normalized shape so a
11
+ // collapse to the anchor (scale → 0) reinflates it. Covers
12
+ // scaleAbout, bestFitCircle.radius, spreadOf, totalLens.
13
+ // continuous — a cyclic (mod-`period`) view lifted to its universal
14
+ // cover: the complement tracks the last emitted value and
15
+ // unwraps the raw reading to the nearest sheet, so the
16
+ // view never tears at a branch cut. Covers the eigenvector
17
+ // angle of bestFitLine; the primitive behind winding.
18
+ import { Num } from "../index.js";
19
+ /** Scalar shape-memory lens. Reads `feature(sources)`; writing it scales
20
+ * the cluster about `anchor` so the new feature matches, reinflating the
21
+ * remembered shape when the cluster has collapsed onto the anchor. The
22
+ * complement is the per-source deviation normalized by the feature. */
23
+ export function remember(sources, opts) {
24
+ const K = sources.length;
25
+ if (K < 1)
26
+ throw new Error("remember: need ≥ 1 source");
27
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic class/trait lookup
28
+ const Cls = sources[0].constructor;
29
+ const lin = Cls.traits?.linear;
30
+ if (!lin)
31
+ throw new Error(`remember: ${Cls.name ?? "?"} has no traits.linear`);
32
+ const { anchor, feature, seed } = opts;
33
+ const magnitude = opts.magnitude ?? true;
34
+ const eps = opts.eps ?? 1e-9;
35
+ const zero = (vals, a) => vals.map(v => lin.scale(lin.sub(v, a), 0));
36
+ // Normalized deviations `(vᵢ − anchor) / feature`, refreshed whole while
37
+ // the feature is non-degenerate; below `eps` the prior shape (or the
38
+ // seed) is held so a collapse reinflates it.
39
+ const shapeOf = (vals, a, f, prev) => {
40
+ if (f <= eps)
41
+ return prev ?? seed?.(vals) ?? zero(vals, a);
42
+ const inv = 1 / f;
43
+ return vals.map(v => lin.scale(lin.sub(v, a), inv));
44
+ };
45
+ // biome-ignore lint/suspicious/noExplicitAny: spec is checked structurally
46
+ return Num.lens(sources, {
47
+ init: (vals) => {
48
+ const a = anchor(vals);
49
+ return { shape: shapeOf(vals, a, feature(vals, a), null) };
50
+ },
51
+ step: (vals, c) => {
52
+ const a = anchor(vals);
53
+ return { shape: shapeOf(vals, a, feature(vals, a), c.shape) };
54
+ },
55
+ fwd: (vals) => feature(vals, anchor(vals)),
56
+ bwd: (target, vals, c) => {
57
+ const a = anchor(vals);
58
+ const f = feature(vals, a);
59
+ // Magnitude is lossy (|−f| = f): a same-magnitude target re-projects
60
+ // to the current feature, so the cluster is left put.
61
+ if (magnitude && Math.abs(target) === f) {
62
+ return { updates: vals.map(() => undefined), complement: c };
63
+ }
64
+ if (f > eps) {
65
+ const k = target / f;
66
+ return { updates: vals.map(v => lin.add(a, lin.scale(lin.sub(v, a), k))), complement: c };
67
+ }
68
+ return { updates: c.shape.map(s => lin.add(a, lin.scale(s, target))), complement: c };
69
+ },
70
+ });
71
+ }
72
+ /** Continuous (winding-aware) lens over a cyclic reading. The complement
73
+ * tracks the last emitted value and unwraps each raw reading to the
74
+ * nearest representative modulo `period`, so the view follows the source
75
+ * across branch cuts instead of jumping a full period. */
76
+ export function continuous(sources, opts) {
77
+ const { period, raw, apply } = opts;
78
+ const wrap = (x) => x - period * Math.round(x / period);
79
+ const unwrap = (rawv, prev) => prev + wrap(rawv - prev);
80
+ // biome-ignore lint/suspicious/noExplicitAny: spec is checked structurally
81
+ return Num.lens(sources, {
82
+ init: (vals) => {
83
+ const r = raw(vals);
84
+ return { prev: r.defined ? r.value : 0 };
85
+ },
86
+ step: (vals, c) => {
87
+ const r = raw(vals);
88
+ return r.defined ? { prev: unwrap(r.value, c.prev) } : c;
89
+ },
90
+ fwd: (vals, c) => {
91
+ const r = raw(vals);
92
+ return r.defined ? unwrap(r.value, c.prev) : c.prev;
93
+ },
94
+ bwd: (target, vals, c) => {
95
+ const r = raw(vals);
96
+ if (!r.defined)
97
+ return { updates: vals.map(() => undefined), complement: { prev: target } };
98
+ const current = unwrap(r.value, c.prev);
99
+ return { updates: apply(target, vals, current), complement: { prev: target } };
100
+ },
101
+ });
102
+ }
@@ -0,0 +1,45 @@
1
+ import { type Cell, type Inner, Num, type Read, type Traits, Vec, type 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>;
41
+ export declare function procrustesTyped(points: readonly PackedInput<Inner<Vec>>[]): {
42
+ centroid: Writable<Vec>;
43
+ rotation: Writable<Num>;
44
+ scale: Writable<Num>;
45
+ };