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,452 @@
1
+ // closed-form-policies.ts — exact group-action lenses for point clouds.
2
+ //
3
+ // When an aggregate lens has a closed-form inverse, its bwd applies a
4
+ // GROUP ELEMENT to the source set. Translation, rotation-about-pivot,
5
+ // and scale-about-pivot are the building blocks; Procrustes, best-fit
6
+ // line/circle, and PCA decompose into combinations of them.
7
+ //
8
+ // Layout: building-block actions (rigidTranslate, rotateAbout,
9
+ // scaleAbout, scaleAboutXY), Procrustes re-expressed via them, then
10
+ // closed-form decompositions (bestFitLine, bestFitCircle, pcaLens,
11
+ // totalLens). All exact, idempotent, cross-channel invariant by
12
+ // construction, on the same `Cls.lens` machinery — no engine changes.
13
+ import { centroidLens, Num, Vec, } from "../index.js";
14
+ import { continuous, remember } from "./memory.js";
15
+ // Pivotal trait lookup via the value class's `static traits.pivotal` slot.
16
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic trait lookup
17
+ function pivotalOf(input) {
18
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic class lookup
19
+ const Cls = input.constructor;
20
+ const p = Cls.traits?.pivotal;
21
+ if (!p) {
22
+ const name = Cls.name ?? "?";
23
+ throw new Error(`closed-form-policies: ${name} has no traits.pivotal`);
24
+ }
25
+ return p;
26
+ }
27
+ /** Writable centroid; on write, translates every point by the delta.
28
+ * Alias of `centroidLens` under the "policy" naming. */
29
+ export function rigidTranslate(points) {
30
+ return centroidLens(points);
31
+ }
32
+ /** Writable angle from `pivot` to `points[0]`; write rotates every input
33
+ * about `pivot` by (target − current) via its `Pivotal` trait.
34
+ *
35
+ * Trait-generic: Vec rotates position; Pose rotates position AND
36
+ * orientation. Rotation-about-pivot fixes the pivot and preserves radial
37
+ * distances, so scale-about-pivot reads unchanged. `pivot` is reactive
38
+ * (re-read per write); pass `centroidLens(points)` for rotation about
39
+ * the cluster's own centroid. */
40
+ export function rotateAbout(points, pivot) {
41
+ const K = points.length;
42
+ if (K < 1)
43
+ throw new Error("rotateAbout: need ≥ 1 point");
44
+ const pv = pivotalOf(points[0]);
45
+ return Num.lens(points, (vals) => {
46
+ const p = pivot.peek();
47
+ return Math.atan2(vals[0].y - p.y, vals[0].x - p.x);
48
+ }, (target, vals) => {
49
+ const p = pivot.peek();
50
+ const rx0 = vals[0].x - p.x;
51
+ const ry0 = vals[0].y - p.y;
52
+ if (rx0 * rx0 + ry0 * ry0 < 1e-24) {
53
+ return vals.map(() => undefined);
54
+ }
55
+ const oldθ = Math.atan2(ry0, rx0);
56
+ const dθ = target - oldθ;
57
+ const out = new Array(K);
58
+ for (let i = 0; i < K; i++)
59
+ out[i] = pv.rotateAbout(vals[i], p, dθ);
60
+ return out;
61
+ });
62
+ }
63
+ /** Writable radial distance from pivot to `points[0]`; write scales every
64
+ * input radially about `pivot` (negative target reflects). Exact
65
+ * cross-channel invariance with `rotateAbout`.
66
+ *
67
+ * Complement carries per-point offsets from the pivot at the last
68
+ * non-degenerate state, so a collapse onto the pivot (radius ≈ 0)
69
+ * reinflates from the stored shape. Pose `theta` survives the round-trip
70
+ * (only spatial offset is stored). */
71
+ export function scaleAbout(points, pivot) {
72
+ const K = points.length;
73
+ if (K < 1)
74
+ throw new Error("scaleAbout: need ≥ 1 point");
75
+ // Eager lookup so an undeclared class fails at construction:
76
+ pivotalOf(points[0]);
77
+ const refresh = (devs, vals, p) => devs.map((d, i) => {
78
+ const dx = vals[i].x - p.x;
79
+ const dy = vals[i].y - p.y;
80
+ return dx * dx + dy * dy > 1e-18 ? { x: dx, y: dy } : d;
81
+ });
82
+ // biome-ignore lint/suspicious/noExplicitAny: variance escape — spec is checked structurally
83
+ return Num.lens(points, {
84
+ init: (vals) => {
85
+ const p = pivot.peek();
86
+ return { devs: vals.map(v => ({ x: v.x - p.x, y: v.y - p.y })) };
87
+ },
88
+ step: (vals, c) => ({ devs: refresh(c.devs, vals, pivot.peek()) }),
89
+ fwd: (vals) => {
90
+ const p = pivot.peek();
91
+ return Math.hypot(vals[0].x - p.x, vals[0].y - p.y);
92
+ },
93
+ bwd: (target, vals, c) => {
94
+ const p = pivot.peek();
95
+ // Lossy magnitude view: |−r| = r, so a same-magnitude target
96
+ // re-projects to the current radius and is absorbed (sources put).
97
+ const rNow = Math.hypot(vals[0].x - p.x, vals[0].y - p.y);
98
+ if (Math.abs(target) === rNow)
99
+ return { updates: vals.map(() => undefined), complement: c };
100
+ const d0 = c.devs[0];
101
+ const r0 = Math.hypot(d0.x, d0.y);
102
+ if (r0 < 1e-12)
103
+ return { updates: vals.map(() => undefined), complement: c };
104
+ const k = target / r0;
105
+ const out = vals.map((v, i) => ({
106
+ ...v,
107
+ x: p.x + k * c.devs[i].x,
108
+ y: p.y + k * c.devs[i].y,
109
+ }));
110
+ return { updates: out, complement: c };
111
+ },
112
+ });
113
+ }
114
+ /** Per-axis scale about a pivot. Vec-specific (Pivotal has no per-axis
115
+ * method yet). Complement carries per-point per-axis fractions of
116
+ * point 0's offset, so a per-axis collapse is recoverable (cf.
117
+ * `bboxLens.size`). */
118
+ export function scaleAboutXY(points, pivot) {
119
+ const K = points.length;
120
+ if (K < 1)
121
+ throw new Error("scaleAboutXY: need ≥ 1 point");
122
+ const refresh = (fracs, vals, p) => {
123
+ const ox = vals[0].x - p.x;
124
+ const oy = vals[0].y - p.y;
125
+ const okx = Math.abs(ox) > 1e-12;
126
+ const oky = Math.abs(oy) > 1e-12;
127
+ return fracs.map((f, i) => ({
128
+ x: okx ? (vals[i].x - p.x) / ox : f.x,
129
+ y: oky ? (vals[i].y - p.y) / oy : f.y,
130
+ }));
131
+ };
132
+ return Vec.lens(points, {
133
+ init: (vals) => {
134
+ const p = pivot.peek();
135
+ const ox = vals[0].x - p.x;
136
+ const oy = vals[0].y - p.y;
137
+ return {
138
+ fracs: vals.map(v => ({
139
+ x: Math.abs(ox) > 1e-12 ? (v.x - p.x) / ox : 0,
140
+ y: Math.abs(oy) > 1e-12 ? (v.y - p.y) / oy : 0,
141
+ })),
142
+ };
143
+ },
144
+ step: (vals, c) => ({ fracs: refresh(c.fracs, vals, pivot.peek()) }),
145
+ fwd: (vals) => {
146
+ const p = pivot.peek();
147
+ return { x: vals[0].x - p.x, y: vals[0].y - p.y };
148
+ },
149
+ bwd: (target, _vals, c) => {
150
+ const p = pivot.peek();
151
+ const out = c.fracs.map(f => ({ x: p.x + f.x * target.x, y: p.y + f.y * target.y }));
152
+ return { updates: out, complement: c };
153
+ },
154
+ });
155
+ }
156
+ /** Same semantics as `factor-lens.ts`'s `procrustesLens`, decomposed
157
+ * into three building-block lenses sharing a centroid. */
158
+ export function procrustesViaBuildingBlocks(points) {
159
+ if (points.length < 2)
160
+ throw new Error("procrustes: need ≥ 2 points");
161
+ const centroid = rigidTranslate(points);
162
+ const rotation = rotateAbout(points, centroid);
163
+ const scale = scaleAbout(points, centroid);
164
+ return { centroid, rotation, scale };
165
+ }
166
+ // Best-fit line.
167
+ //
168
+ // K points → {point: centroid, direction: principal-axis angle}.
169
+ // write point → rigidTranslate
170
+ // write direction → rotate all about centroid to set principal axis
171
+ // Invariance: principal axis is translation-invariant; centroid is
172
+ // invariant under rotation-about-itself.
173
+ /** Angle of the dominant eigenvector of symmetric 2×2 [[cxx,cxy],[cxy,cyy]]. */
174
+ function dominantAxisAngle(cxx, cxy, cyy) {
175
+ return 0.5 * Math.atan2(2 * cxy, cxx - cyy);
176
+ }
177
+ function covariance(points, cx, cy) {
178
+ const K = points.length;
179
+ let cxx = 0;
180
+ let cxy = 0;
181
+ let cyy = 0;
182
+ for (let i = 0; i < K; i++) {
183
+ const dx = points[i].x - cx;
184
+ const dy = points[i].y - cy;
185
+ cxx += dx * dx;
186
+ cxy += dx * dy;
187
+ cyy += dy * dy;
188
+ }
189
+ return { cxx: cxx / K, cxy: cxy / K, cyy: cyy / K };
190
+ }
191
+ export function bestFitLineLens(points) {
192
+ const K = points.length;
193
+ if (K < 2)
194
+ throw new Error("bestFitLine: need ≥ 2 points");
195
+ const point = rigidTranslate(points);
196
+ // The principal axis is an eigenvector — defined only up to sign, so the
197
+ // raw atan2 jumps by π as the cloud rotates. `continuous` lifts it to its
198
+ // universal cover (period π, since axis ≡ axis + π), tracking the last
199
+ // emitted angle so the direction stays continuous; a collapsed cloud has
200
+ // no axis (`defined: false`), so it freezes and stashes the target.
201
+ // Centroid + dominant-axis raw angle; `degenerate` when covariance vanishes.
202
+ const axisOf = (vals) => {
203
+ let sx = 0;
204
+ let sy = 0;
205
+ for (let i = 0; i < K; i++) {
206
+ sx += vals[i].x;
207
+ sy += vals[i].y;
208
+ }
209
+ const cx = sx / K;
210
+ const cy = sy / K;
211
+ const { cxx, cxy, cyy } = covariance(vals, cx, cy);
212
+ if (cxx + cyy < 1e-18)
213
+ return { cx, cy, rawθ: 0, degenerate: true };
214
+ return { cx, cy, rawθ: dominantAxisAngle(cxx, cxy, cyy), degenerate: false };
215
+ };
216
+ const direction = continuous(points, {
217
+ period: Math.PI,
218
+ raw: (vals) => {
219
+ const { rawθ, degenerate } = axisOf(vals);
220
+ return { value: rawθ, defined: !degenerate };
221
+ },
222
+ apply: (target, vals, current) => {
223
+ const { cx, cy } = axisOf(vals);
224
+ const dθ = target - current;
225
+ const cos = Math.cos(dθ);
226
+ const sin = Math.sin(dθ);
227
+ const out = new Array(K);
228
+ for (let i = 0; i < K; i++) {
229
+ const rx = vals[i].x - cx;
230
+ const ry = vals[i].y - cy;
231
+ out[i] = { x: cx + cos * rx - sin * ry, y: cy + sin * rx + cos * ry };
232
+ }
233
+ return out;
234
+ },
235
+ });
236
+ return { point, direction };
237
+ }
238
+ // Best-fit circle.
239
+ //
240
+ // K points → {center: centroid, radius: mean distance from center}.
241
+ // write center → rigidTranslate
242
+ // write radius → scale all about center by target/current
243
+ // Simplest closed-form fit (mean center). Invariance: translation
244
+ // preserves radii; uniform scale-about-center preserves the center.
245
+ export function bestFitCircleLens(points) {
246
+ const K = points.length;
247
+ if (K < 1)
248
+ throw new Error("bestFitCircle: need ≥ 1 point");
249
+ const center = rigidTranslate(points);
250
+ // Radius = mean distance from the centroid; writing it scales the cluster
251
+ // about the centroid, and a collapse (mean → 0) reinflates the remembered
252
+ // shape — exactly `remember`'s magnitude view, anchored at the centroid.
253
+ const centroidOf = (vals) => {
254
+ let sx = 0;
255
+ let sy = 0;
256
+ for (let i = 0; i < K; i++) {
257
+ sx += vals[i].x;
258
+ sy += vals[i].y;
259
+ }
260
+ return { x: sx / K, y: sy / K };
261
+ };
262
+ const meanRadius = (vals, c) => {
263
+ let sum = 0;
264
+ for (let i = 0; i < K; i++)
265
+ sum += Math.hypot(vals[i].x - c.x, vals[i].y - c.y);
266
+ return sum / K;
267
+ };
268
+ const radius = remember(points, {
269
+ anchor: (vals) => centroidOf(vals),
270
+ feature: (vals, c) => meanRadius(vals, c),
271
+ });
272
+ return { center, radius };
273
+ }
274
+ // PCA / affine similarity decomposition.
275
+ //
276
+ // K points → {mean: centroid, rotation: dominant-eigenvector angle,
277
+ // majorLength: √λ_major, minorLength: √λ_minor (per-axis std-devs)}.
278
+ // write mean → rigidTranslate
279
+ // write rotation → rotate all about mean to set principal axis
280
+ // write major/minor → scale along that axis by target/current
281
+ // Each write is a single group action; cross-channel invariance holds
282
+ // for all pairs.
283
+ export function pcaLens(points) {
284
+ const K = points.length;
285
+ if (K < 2)
286
+ throw new Error("pcaLens: need ≥ 2 points");
287
+ const mean = rigidTranslate(points);
288
+ // 2×2 symmetric eigendecomp → {θ, λ_major, λ_minor}; null when fully
289
+ // collapsed (λ_major ≈ 0).
290
+ const decompose = (vals) => {
291
+ let sx = 0;
292
+ let sy = 0;
293
+ for (let i = 0; i < K; i++) {
294
+ sx += vals[i].x;
295
+ sy += vals[i].y;
296
+ }
297
+ const cx = sx / K;
298
+ const cy = sy / K;
299
+ const { cxx, cxy, cyy } = covariance(vals, cx, cy);
300
+ const tr = cxx + cyy;
301
+ const disc = Math.sqrt((cxx - cyy) * (cxx - cyy) + 4 * cxy * cxy);
302
+ const lambdaMajor = (tr + disc) / 2;
303
+ const lambdaMinor = (tr - disc) / 2;
304
+ if (lambdaMajor < 1e-24)
305
+ return null;
306
+ const θ = 0.5 * Math.atan2(2 * cxy, cxx - cyy);
307
+ return { cx, cy, θ, lambdaMajor, lambdaMinor };
308
+ };
309
+ const rotation = Num.lens(points, (vals) => decompose(vals)?.θ ?? 0, (target, vals) => {
310
+ const d = decompose(vals);
311
+ if (!d)
312
+ return vals.map(() => undefined);
313
+ const dθ = target - d.θ;
314
+ const cos = Math.cos(dθ);
315
+ const sin = Math.sin(dθ);
316
+ const out = new Array(K);
317
+ for (let i = 0; i < K; i++) {
318
+ const rx = vals[i].x - d.cx;
319
+ const ry = vals[i].y - d.cy;
320
+ out[i] = { x: d.cx + cos * rx - sin * ry, y: d.cy + sin * rx + cos * ry };
321
+ }
322
+ return out;
323
+ });
324
+ // Scale by k along axis (ux, uy): project each point onto (u, u_perp),
325
+ // scale the u component, project back. Relative to mean.
326
+ const scaleAlongAxis = (vals, cx, cy, ux, uy, k) => {
327
+ const vx = -uy;
328
+ const vy = ux;
329
+ const out = new Array(K);
330
+ for (let i = 0; i < K; i++) {
331
+ const rx = vals[i].x - cx;
332
+ const ry = vals[i].y - cy;
333
+ const a = rx * ux + ry * uy;
334
+ const b = rx * vx + ry * vy;
335
+ const ap = a * k;
336
+ out[i] = { x: cx + ap * ux + b * vx, y: cy + ap * uy + b * vy };
337
+ }
338
+ return out;
339
+ };
340
+ // majorLength / minorLength: complement carries the axis basis and
341
+ // per-point projections (normalized by the std-devs) at the last
342
+ // non-degenerate state, so an axis collapse (λ → 0) reinflates from the
343
+ // stored geometry. Non-degenerate writes take the scaleAlongAxis fast path.
344
+ const buildAxisLens = (which) => {
345
+ // Decompose and rebuild the axis basis + normalized projections;
346
+ // returns the prior complement when fully collapsed.
347
+ const axisFrom = (d, c, vals) => {
348
+ const ux = which === "major" ? Math.cos(d.θ) : -Math.sin(d.θ);
349
+ const uy = which === "major" ? Math.sin(d.θ) : Math.cos(d.θ);
350
+ const vx = -uy;
351
+ const vy = ux;
352
+ const lenThis = Math.sqrt(Math.max(0, which === "major" ? d.lambdaMajor : d.lambdaMinor));
353
+ const lenOther = Math.sqrt(Math.max(0, which === "major" ? d.lambdaMinor : d.lambdaMajor));
354
+ // Only refresh projections on axes that aren't collapsed.
355
+ const invThis = lenThis > 1e-12 ? 1 / lenThis : null;
356
+ const invOther = lenOther > 1e-12 ? 1 / lenOther : null;
357
+ const projThis = c.projThis.slice();
358
+ const projOther = c.projOther.slice();
359
+ for (let i = 0; i < K; i++) {
360
+ const dx = vals[i].x - d.cx;
361
+ const dy = vals[i].y - d.cy;
362
+ if (invThis !== null)
363
+ projThis[i] = (dx * ux + dy * uy) * invThis;
364
+ if (invOther !== null)
365
+ projOther[i] = (dx * vx + dy * vy) * invOther;
366
+ }
367
+ return { uX: ux, uY: uy, vX: vx, vY: vy, lenThis, lenOther, projThis, projOther };
368
+ };
369
+ return Num.lens(points, {
370
+ init: (vals) => {
371
+ const seed = {
372
+ uX: 1,
373
+ uY: 0,
374
+ vX: 0,
375
+ vY: 1,
376
+ lenThis: 0,
377
+ lenOther: 0,
378
+ projThis: vals.map(() => 0),
379
+ projOther: vals.map(() => 0),
380
+ };
381
+ const d = decompose(vals);
382
+ return d ? axisFrom(d, seed, vals) : seed;
383
+ },
384
+ step: (vals, c) => {
385
+ const d = decompose(vals);
386
+ return d ? axisFrom(d, c, vals) : c;
387
+ },
388
+ fwd: (vals, c) => (decompose(vals) ? c.lenThis : 0),
389
+ bwd: (target, vals, c) => {
390
+ const d = decompose(vals);
391
+ if (d && c.lenThis > 1e-12) {
392
+ // Lossy magnitude view: a same-magnitude target re-projects to
393
+ // the current axis length and is absorbed (cluster left put).
394
+ if (Math.abs(target) === c.lenThis)
395
+ return { updates: vals.map(() => undefined), complement: c };
396
+ // Non-degenerate fast path: scale current cluster along axis.
397
+ const k = target / c.lenThis;
398
+ return { updates: scaleAlongAxis(vals, d.cx, d.cy, c.uX, c.uY, k), complement: c };
399
+ }
400
+ // Degenerate: reconstruct from complement. Centroid still
401
+ // derivable from current source (mean translates always work).
402
+ let sx = 0;
403
+ let sy = 0;
404
+ for (let i = 0; i < K; i++) {
405
+ sx += vals[i].x;
406
+ sy += vals[i].y;
407
+ }
408
+ const cx = sx / K;
409
+ const cy = sy / K;
410
+ const out = new Array(K);
411
+ for (let i = 0; i < K; i++) {
412
+ const a = c.projThis[i] * target;
413
+ const b = c.projOther[i] * c.lenOther;
414
+ out[i] = { x: cx + a * c.uX + b * c.vX, y: cy + a * c.uY + b * c.vY };
415
+ }
416
+ return { updates: out, complement: c };
417
+ },
418
+ });
419
+ };
420
+ const majorLength = buildAxisLens("major");
421
+ const minorLength = buildAxisLens("minor");
422
+ return { mean, rotation, majorLength, minorLength };
423
+ }
424
+ // Partition / simplex lens.
425
+ //
426
+ // K parts → {total}: writing total scales all parts proportionally.
427
+ // (A {total, ratios} form is possible but ratios on a K-simplex have
428
+ // K−1 DOF, so it's left out of this prototype.)
429
+ /** Writable total over K parts; write scales all parts proportionally,
430
+ * preserving their ratios. A `remember` anchored at zero with a signed
431
+ * sum feature: a collapse to zero reinflates the stored ratios, seeded
432
+ * uniform so an all-zero start splits evenly. */
433
+ export function totalLens(parts) {
434
+ const K = parts.length;
435
+ if (K < 1)
436
+ throw new Error("totalLens: need ≥ 1 part");
437
+ return remember(parts, {
438
+ anchor: () => 0,
439
+ feature: (vals) => {
440
+ let s = 0;
441
+ for (let i = 0; i < K; i++)
442
+ s += vals[i];
443
+ return s;
444
+ },
445
+ magnitude: false,
446
+ seed: () => parts.map(() => 1 / K),
447
+ });
448
+ }
449
+ // Every lens here is a group action about a pivot (translate, rotateAbout,
450
+ // scaleAbout, scaleAboutXY, scaleAlongAxis) or a `remember`/`continuous`
451
+ // shape-memory; the decompositions combine them, each measured against a
452
+ // derived feature (centroid, principal axis, mean radius).
@@ -0,0 +1,54 @@
1
+ import { type Cell, Num, type Read, type Traits, type Val, Vec, type Writable } from "../index.js";
2
+ /** Class-inferring mean (writable of `inputs[0]`'s class). Needs `linear`. */
3
+ export declare function meanOf<S extends Traits<any, "linear">>(inputs: readonly Writable<S>[]): Writable<S>;
4
+ /** Rigid-translate aggregate over any Linear type. Alias of `meanOf`,
5
+ * named for the geometric intent. */
6
+ export declare function rigidTranslateOf<S extends Traits<any, "linear">>(inputs: readonly Writable<S>[]): Writable<S>;
7
+ /** Weighted blend of K branches over any `Linear` type. See module note. */
8
+ export declare function mix<S extends Traits<any, "linear">>(weights: readonly Val<number>[], branches: readonly Writable<S>[]): Writable<S>;
9
+ /** Two-branch router (mix simplex *vertex*): reads the live branch, writes
10
+ * flow entirely to it, the other is left put. Flipping `cond` snaps the
11
+ * output to the other branch's stored value. */
12
+ export declare function select<S extends Traits<any, "linear">>(cond: Read<boolean>, whenFalse: Writable<S>, whenTrue: Writable<S>): Writable<S>;
13
+ /** Two-branch crossfade (mix simplex *edge*): `lerp(a, b, t)`. Writing
14
+ * keeps `t` fixed and splits the delta by influence. */
15
+ export declare function crossfade<S extends Traits<any, "linear">>(t: Read<number>, a: Writable<S>, b: Writable<S>): Writable<S>;
16
+ type ColorV = {
17
+ r: number;
18
+ g: number;
19
+ b: number;
20
+ a: number;
21
+ };
22
+ /** Mean color of a palette; write shifts every color by the delta
23
+ * (rigid translate in RGBA). Via `meanOf`. */
24
+ export declare function meanColor(colors: readonly Writable<Traits<ColorV, "linear">>[]): Writable<Traits<ColorV, "linear">>;
25
+ /** Mean radial distance from the centroid; write scales the cluster's
26
+ * deviations so the new mean matches the target. Trait-driven via
27
+ * `Linear` + `Metric`, so it works for any class declaring both (Vec,
28
+ * Color, Pose, Box, Range, custom).
29
+ *
30
+ * Complement carries per-input deviations normalized by the current mean
31
+ * radius, so `spread = T` places each input at `centroid + normDev_i * T`
32
+ * and a collapse (spread → 0) reinflates the original SHAPE. Centroid is
33
+ * recomputed every read/write, so an intervening mean translate is not
34
+ * stale. */
35
+ export declare function spreadOf<T extends NonNullable<unknown>, S extends Cell<T> & Traits<T, "linear" | "metric">>(inputs: readonly Writable<S>[]): Writable<Num>;
36
+ /** Palette decomposition: K values → {mean, spread}, i.e. centroid +
37
+ * uniform scale about it. `meanOf` ∘ `spreadOf`; works for any
38
+ * Linear + Metric class. */
39
+ export declare function paletteLens<T extends NonNullable<unknown>, S extends Cell<T> & Traits<T, "linear" | "metric">>(colors: readonly Writable<S>[]): {
40
+ mean: Writable<S>;
41
+ spread: Writable<Num>;
42
+ };
43
+ export declare function bezierGestaltLens(p0: Writable<Vec>, p1: Writable<Vec>, p2: Writable<Vec>, p3: Writable<Vec>): {
44
+ start: Writable<Vec>;
45
+ end: Writable<Vec>;
46
+ startTangent: Writable<Vec>;
47
+ endTangent: Writable<Vec>;
48
+ };
49
+ /** Time-series scalar aggregate over Num values as (i, value_i) samples. */
50
+ export declare function timeSeriesLens(values: readonly Writable<Num>[]): {
51
+ mean: Writable<Num>;
52
+ slope: Writable<Num>;
53
+ };
54
+ export {};