bireactive 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +4 -4
  2. package/dist/{core/anim.d.ts → animation/animators.d.ts} +5 -3
  3. package/dist/{core/anim.js → animation/animators.js} +8 -5
  4. package/dist/animation/index.d.ts +1 -0
  5. package/dist/animation/index.js +1 -0
  6. package/dist/constraints/cluster.js +1 -1
  7. package/dist/constraints/expose.d.ts +17 -0
  8. package/dist/constraints/expose.js +61 -0
  9. package/dist/constraints/index.d.ts +1 -0
  10. package/dist/constraints/index.js +1 -0
  11. package/dist/constraints/linalg.d.ts +1 -8
  12. package/dist/constraints/linalg.js +5 -108
  13. package/dist/core/{signal.d.ts → cell.d.ts} +29 -6
  14. package/dist/core/{signal.js → cell.js} +134 -53
  15. package/dist/core/derived-geometry.d.ts +11 -0
  16. package/dist/core/derived-geometry.js +32 -0
  17. package/dist/core/index.d.ts +5 -10
  18. package/dist/core/index.js +5 -10
  19. package/dist/core/{aggregates.d.ts → lenses/aggregates.d.ts} +3 -14
  20. package/dist/core/{aggregates.js → lenses/aggregates.js} +5 -78
  21. package/dist/core/lenses/closed-form-policies.d.ts +6 -13
  22. package/dist/core/lenses/closed-form-policies.js +14 -24
  23. package/dist/core/lenses/decompositions.d.ts +14 -0
  24. package/dist/core/lenses/decompositions.js +224 -0
  25. package/dist/core/lenses/domain-aggregates.d.ts +10 -22
  26. package/dist/core/lenses/domain-aggregates.js +25 -39
  27. package/dist/core/{new-primitives.d.ts → lenses/geometry.d.ts} +11 -14
  28. package/dist/core/{new-primitives.js → lenses/geometry.js} +23 -37
  29. package/dist/core/lenses/index.d.ts +6 -4
  30. package/dist/core/lenses/index.js +6 -15
  31. package/dist/core/lenses/typed-factor.d.ts +1 -6
  32. package/dist/core/lenses/typed-factor.js +12 -114
  33. package/dist/core/{network-utils.d.ts → lifecycle.d.ts} +1 -1
  34. package/dist/core/{network-utils.js → lifecycle.js} +2 -2
  35. package/dist/core/linalg.d.ts +10 -0
  36. package/dist/core/linalg.js +109 -0
  37. package/dist/core/values/anchor.d.ts +1 -1
  38. package/dist/core/values/audio.d.ts +1 -1
  39. package/dist/core/values/audio.js +1 -1
  40. package/dist/core/values/bool.d.ts +1 -1
  41. package/dist/core/values/bool.js +1 -1
  42. package/dist/core/values/box.d.ts +2 -3
  43. package/dist/core/values/box.js +5 -6
  44. package/dist/core/values/canvas.d.ts +1 -1
  45. package/dist/core/values/canvas.js +2 -3
  46. package/dist/core/values/color.d.ts +2 -3
  47. package/dist/core/values/color.js +3 -4
  48. package/dist/core/values/flags.d.ts +1 -1
  49. package/dist/core/values/flags.js +1 -1
  50. package/dist/core/values/matrix.d.ts +1 -1
  51. package/dist/core/values/matrix.js +2 -3
  52. package/dist/core/values/num.d.ts +2 -3
  53. package/dist/core/values/num.js +2 -2
  54. package/dist/core/values/pose.d.ts +5 -1
  55. package/dist/core/values/pose.js +11 -1
  56. package/dist/core/values/range.d.ts +6 -4
  57. package/dist/core/values/range.js +16 -11
  58. package/dist/core/values/str.d.ts +1 -1
  59. package/dist/core/values/str.js +1 -1
  60. package/dist/core/values/template.d.ts +1 -1
  61. package/dist/core/values/transform.d.ts +2 -3
  62. package/dist/core/values/transform.js +2 -3
  63. package/dist/core/values/tri.d.ts +1 -1
  64. package/dist/core/values/tri.js +1 -1
  65. package/dist/core/values/vec.d.ts +2 -3
  66. package/dist/core/values/vec.js +4 -5
  67. package/dist/ext/timeline.js +3 -3
  68. package/dist/index.d.ts +1 -0
  69. package/dist/index.js +1 -0
  70. package/dist/propagators/layout.js +1 -1
  71. package/dist/shapes/handle.js +4 -4
  72. package/dist/shapes/shape.js +7 -7
  73. package/dist/shapes/transitions.js +2 -2
  74. package/package.json +7 -2
  75. package/dist/core/introspect.d.ts +0 -5
  76. package/dist/core/introspect.js +0 -31
  77. package/dist/core/lenses/factor-lens.d.ts +0 -42
  78. package/dist/core/lenses/factor-lens.js +0 -419
  79. package/dist/core/writable.d.ts +0 -15
  80. package/dist/core/writable.js +0 -29
  81. /package/dist/{core/tree.d.ts → tree.d.ts} +0 -0
  82. /package/dist/{core/tree.js → tree.js} +0 -0
@@ -1,419 +0,0 @@
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
- }
@@ -1,15 +0,0 @@
1
- import { Cell, type Inner, type Writable, type WritableBrand } from "./signal.js";
2
- /** Bidirectional field lens onto `parent.value[key]`; write spread-
3
- * replaces the composite. Cached per (instance, key). Return type is
4
- * conditional: `Writable<Cls>` on a writable parent, bare `Cls` on RO
5
- * (runtime dispatch in `Cell.fieldOf` mirrors this).
6
- *
7
- * get x() { return field(this, "x", Num); } */
8
- export declare function field<S extends Cell<any>, K extends keyof Inner<S>, C extends new (...args: never[]) => Cell<Inner<S>[K]>>(parent: S, key: K, Cls: C): S extends WritableBrand ? Writable<InstanceType<C>> : InstanceType<C>;
9
- /** Read-only derived view via `Cls.derive(parent, fn)`. Cached per
10
- * (instance, key); always bare `Cls` (RO).
11
- *
12
- * get magnitude() {
13
- * return derived(this, "magnitude", Num, v => Math.hypot(v.x, v.y));
14
- * } */
15
- export declare function derived<S extends Cell<any>, C extends new (...args: never[]) => Cell<any>>(parent: S, key: string | symbol, Cls: C, fn: (v: Inner<S>) => Inner<InstanceType<C>>): InstanceType<C>;
@@ -1,29 +0,0 @@
1
- // writable.ts — value-class authoring helpers.
2
- //
3
- // `field(this, "x", Num)` — bidirectional field lens; conditional
4
- // return (writable on writable parent, bare on RO). `derived(this,
5
- // "k", Cls, fn)` — read-only derived view via `Cls.derive`. The choice
6
- // between them IS the local declaration of writability at each getter,
7
- // mirroring `: this` invertible method returns. For arbitrary cached
8
- // views, use `lazy()` from "../signal" directly.
9
- import { Cell, lazy } from "./signal.js";
10
- /** Bidirectional field lens onto `parent.value[key]`; write spread-
11
- * replaces the composite. Cached per (instance, key). Return type is
12
- * conditional: `Writable<Cls>` on a writable parent, bare `Cls` on RO
13
- * (runtime dispatch in `Cell.fieldOf` mirrors this).
14
- *
15
- * get x() { return field(this, "x", Num); } */
16
- export function field(parent, key, Cls) {
17
- return lazy(parent, key, () => Cell.fieldOf(parent, key, Cls));
18
- }
19
- /** Read-only derived view via `Cls.derive(parent, fn)`. Cached per
20
- * (instance, key); always bare `Cls` (RO).
21
- *
22
- * get magnitude() {
23
- * return derived(this, "magnitude", Num, v => Math.hypot(v.x, v.y));
24
- * } */
25
- // biome-ignore lint/suspicious/noExplicitAny: variance escape, mirrors Cls.derive
26
- export function derived(parent, key, Cls, fn) {
27
- // biome-ignore lint/suspicious/noExplicitAny: variance escape on Cls.derive
28
- return lazy(parent, key, () => Cls.derive(parent, fn));
29
- }
File without changes
File without changes