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,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 {};
|