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.
- package/LICENSE +21 -0
- package/README.md +81 -0
- package/dist/animation/anim.d.ts +57 -0
- package/dist/animation/anim.js +318 -0
- package/dist/animation/combinators.d.ts +39 -0
- package/dist/animation/combinators.js +113 -0
- package/dist/animation/easings.d.ts +5 -0
- package/dist/animation/easings.js +5 -0
- package/dist/animation/index.d.ts +3 -0
- package/dist/animation/index.js +3 -0
- package/dist/assert/algebra.d.ts +20 -0
- package/dist/assert/algebra.js +79 -0
- package/dist/assert/claim.d.ts +40 -0
- package/dist/assert/claim.js +129 -0
- package/dist/assert/index.d.ts +7 -0
- package/dist/assert/index.js +19 -0
- package/dist/assert/predicates.d.ts +18 -0
- package/dist/assert/predicates.js +43 -0
- package/dist/assert/record.d.ts +20 -0
- package/dist/assert/record.js +78 -0
- package/dist/assert/scope.d.ts +42 -0
- package/dist/assert/scope.js +233 -0
- package/dist/assert/span.d.ts +37 -0
- package/dist/assert/span.js +68 -0
- package/dist/assert/tree.d.ts +22 -0
- package/dist/assert/tree.js +65 -0
- package/dist/code/code.d.ts +70 -0
- package/dist/code/code.js +361 -0
- package/dist/code/index.d.ts +2 -0
- package/dist/code/index.js +9 -0
- package/dist/code/morph.d.ts +5 -0
- package/dist/code/morph.js +194 -0
- package/dist/code/tokenize.d.ts +8 -0
- package/dist/code/tokenize.js +51 -0
- package/dist/constraints/cluster.d.ts +83 -0
- package/dist/constraints/cluster.js +213 -0
- package/dist/constraints/drivers.d.ts +15 -0
- package/dist/constraints/drivers.js +40 -0
- package/dist/constraints/factories.d.ts +73 -0
- package/dist/constraints/factories.js +248 -0
- package/dist/constraints/index.d.ts +11 -0
- package/dist/constraints/index.js +39 -0
- package/dist/constraints/interaction.d.ts +21 -0
- package/dist/constraints/interaction.js +148 -0
- package/dist/constraints/linalg.d.ts +18 -0
- package/dist/constraints/linalg.js +141 -0
- package/dist/constraints/phases.d.ts +21 -0
- package/dist/constraints/phases.js +60 -0
- package/dist/constraints/physics.d.ts +34 -0
- package/dist/constraints/physics.js +128 -0
- package/dist/constraints/rigid.d.ts +210 -0
- package/dist/constraints/rigid.js +835 -0
- package/dist/constraints/solver.d.ts +107 -0
- package/dist/constraints/solver.js +510 -0
- package/dist/constraints/term.d.ts +50 -0
- package/dist/constraints/term.js +80 -0
- package/dist/constraints/terms.d.ts +80 -0
- package/dist/constraints/terms.js +302 -0
- package/dist/constraints/world.d.ts +31 -0
- package/dist/constraints/world.js +245 -0
- package/dist/core/aggregates.d.ts +64 -0
- package/dist/core/aggregates.js +198 -0
- package/dist/core/anim.d.ts +84 -0
- package/dist/core/anim.js +301 -0
- package/dist/core/index.d.ts +38 -0
- package/dist/core/index.js +38 -0
- package/dist/core/introspect.d.ts +5 -0
- package/dist/core/introspect.js +31 -0
- package/dist/core/lenses/closed-form-policies.d.ts +64 -0
- package/dist/core/lenses/closed-form-policies.js +452 -0
- package/dist/core/lenses/domain-aggregates.d.ts +54 -0
- package/dist/core/lenses/domain-aggregates.js +259 -0
- package/dist/core/lenses/factor-lens.d.ts +42 -0
- package/dist/core/lenses/factor-lens.js +419 -0
- package/dist/core/lenses/index.d.ts +5 -0
- package/dist/core/lenses/index.js +16 -0
- package/dist/core/lenses/memory.d.ts +47 -0
- package/dist/core/lenses/memory.js +102 -0
- package/dist/core/lenses/typed-factor.d.ts +45 -0
- package/dist/core/lenses/typed-factor.js +376 -0
- package/dist/core/network-utils.d.ts +14 -0
- package/dist/core/network-utils.js +62 -0
- package/dist/core/new-primitives.d.ts +33 -0
- package/dist/core/new-primitives.js +113 -0
- package/dist/core/signal.d.ts +254 -0
- package/dist/core/signal.js +1349 -0
- package/dist/core/traits.d.ts +61 -0
- package/dist/core/traits.js +56 -0
- package/dist/core/tree.d.ts +23 -0
- package/dist/core/tree.js +62 -0
- package/dist/core/values/anchor.d.ts +23 -0
- package/dist/core/values/anchor.js +23 -0
- package/dist/core/values/audio.d.ts +33 -0
- package/dist/core/values/audio.js +107 -0
- package/dist/core/values/bool.d.ts +37 -0
- package/dist/core/values/bool.js +75 -0
- package/dist/core/values/box.d.ts +77 -0
- package/dist/core/values/box.js +211 -0
- package/dist/core/values/canvas.d.ts +71 -0
- package/dist/core/values/canvas.js +495 -0
- package/dist/core/values/color.d.ts +49 -0
- package/dist/core/values/color.js +106 -0
- package/dist/core/values/flags.d.ts +18 -0
- package/dist/core/values/flags.js +50 -0
- package/dist/core/values/gpu.d.ts +74 -0
- package/dist/core/values/gpu.js +426 -0
- package/dist/core/values/matrix.d.ts +53 -0
- package/dist/core/values/matrix.js +140 -0
- package/dist/core/values/num.d.ts +62 -0
- package/dist/core/values/num.js +166 -0
- package/dist/core/values/pose.d.ts +31 -0
- package/dist/core/values/pose.js +83 -0
- package/dist/core/values/range.d.ts +83 -0
- package/dist/core/values/range.js +167 -0
- package/dist/core/values/str.d.ts +76 -0
- package/dist/core/values/str.js +346 -0
- package/dist/core/values/template.d.ts +49 -0
- package/dist/core/values/template.js +148 -0
- package/dist/core/values/transform.d.ts +49 -0
- package/dist/core/values/transform.js +115 -0
- package/dist/core/values/tri.d.ts +31 -0
- package/dist/core/values/tri.js +95 -0
- package/dist/core/values/vec.d.ts +72 -0
- package/dist/core/values/vec.js +219 -0
- package/dist/core/writable.d.ts +15 -0
- package/dist/core/writable.js +29 -0
- package/dist/ext/events.d.ts +10 -0
- package/dist/ext/events.js +31 -0
- package/dist/ext/index.d.ts +4 -0
- package/dist/ext/index.js +4 -0
- package/dist/ext/snapshot.d.ts +8 -0
- package/dist/ext/snapshot.js +29 -0
- package/dist/ext/timeline.d.ts +56 -0
- package/dist/ext/timeline.js +94 -0
- package/dist/ext/waapi.d.ts +25 -0
- package/dist/ext/waapi.js +198 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +10 -0
- package/dist/propagators/index.d.ts +6 -0
- package/dist/propagators/index.js +6 -0
- package/dist/propagators/layout.d.ts +68 -0
- package/dist/propagators/layout.js +336 -0
- package/dist/propagators/network.d.ts +52 -0
- package/dist/propagators/network.js +185 -0
- package/dist/propagators/propagator.d.ts +12 -0
- package/dist/propagators/propagator.js +16 -0
- package/dist/propagators/range.d.ts +45 -0
- package/dist/propagators/range.js +147 -0
- package/dist/propagators/relations.d.ts +60 -0
- package/dist/propagators/relations.js +343 -0
- package/dist/shapes/annular-sector.d.ts +15 -0
- package/dist/shapes/annular-sector.js +64 -0
- package/dist/shapes/button.d.ts +14 -0
- package/dist/shapes/button.js +31 -0
- package/dist/shapes/choreographers.d.ts +22 -0
- package/dist/shapes/choreographers.js +69 -0
- package/dist/shapes/circle.d.ts +17 -0
- package/dist/shapes/circle.js +57 -0
- package/dist/shapes/clip.d.ts +5 -0
- package/dist/shapes/clip.js +31 -0
- package/dist/shapes/connect.d.ts +16 -0
- package/dist/shapes/connect.js +70 -0
- package/dist/shapes/curve.d.ts +60 -0
- package/dist/shapes/curve.js +285 -0
- package/dist/shapes/dashed.d.ts +16 -0
- package/dist/shapes/dashed.js +142 -0
- package/dist/shapes/debug.d.ts +43 -0
- package/dist/shapes/debug.js +97 -0
- package/dist/shapes/group.d.ts +5 -0
- package/dist/shapes/group.js +10 -0
- package/dist/shapes/handle.d.ts +32 -0
- package/dist/shapes/handle.js +88 -0
- package/dist/shapes/index.d.ts +23 -0
- package/dist/shapes/index.js +23 -0
- package/dist/shapes/interaction.d.ts +32 -0
- package/dist/shapes/interaction.js +187 -0
- package/dist/shapes/label.d.ts +20 -0
- package/dist/shapes/label.js +42 -0
- package/dist/shapes/layout.d.ts +29 -0
- package/dist/shapes/layout.js +74 -0
- package/dist/shapes/line.d.ts +21 -0
- package/dist/shapes/line.js +79 -0
- package/dist/shapes/list.d.ts +18 -0
- package/dist/shapes/list.js +51 -0
- package/dist/shapes/mount.d.ts +7 -0
- package/dist/shapes/mount.js +10 -0
- package/dist/shapes/path.d.ts +77 -0
- package/dist/shapes/path.js +227 -0
- package/dist/shapes/rect.d.ts +30 -0
- package/dist/shapes/rect.js +131 -0
- package/dist/shapes/shape.d.ts +132 -0
- package/dist/shapes/shape.js +306 -0
- package/dist/shapes/text.d.ts +24 -0
- package/dist/shapes/text.js +53 -0
- package/dist/shapes/tokens.d.ts +28 -0
- package/dist/shapes/tokens.js +27 -0
- package/dist/shapes/transitions.d.ts +23 -0
- package/dist/shapes/transitions.js +62 -0
- package/dist/tex/decorations.d.ts +26 -0
- package/dist/tex/decorations.js +116 -0
- package/dist/tex/index.d.ts +5 -0
- package/dist/tex/index.js +5 -0
- package/dist/tex/marker.d.ts +17 -0
- package/dist/tex/marker.js +63 -0
- package/dist/tex/motion.d.ts +43 -0
- package/dist/tex/motion.js +290 -0
- package/dist/tex/parts.d.ts +65 -0
- package/dist/tex/parts.js +149 -0
- package/dist/tex/tex.d.ts +45 -0
- package/dist/tex/tex.js +244 -0
- package/dist/web/attr.d.ts +16 -0
- package/dist/web/attr.js +98 -0
- package/dist/web/diagram.d.ts +49 -0
- package/dist/web/diagram.js +260 -0
- package/dist/web/index.d.ts +6 -0
- package/dist/web/index.js +6 -0
- package/dist/web/md-marker.d.ts +6 -0
- package/dist/web/md-marker.js +39 -0
- package/dist/web/md-tex.d.ts +6 -0
- package/dist/web/md-tex.js +61 -0
- package/dist/web/raf.d.ts +6 -0
- package/dist/web/raf.js +24 -0
- package/dist/web/viewport.d.ts +7 -0
- package/dist/web/viewport.js +13 -0
- 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
|
+
}
|