bireactive 0.3.0 → 0.3.2
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/README.md +14 -7
- package/dist/automerge/doc-cell.d.ts +20 -0
- package/dist/automerge/doc-cell.js +80 -0
- package/dist/automerge/index.d.ts +3 -0
- package/dist/automerge/index.js +12 -0
- package/dist/automerge/reconcile.d.ts +5 -0
- package/dist/automerge/reconcile.js +63 -0
- package/dist/core/_counts.d.ts +48 -0
- package/dist/core/_counts.js +51 -0
- package/dist/core/cell.d.ts +148 -112
- package/dist/core/cell.js +945 -768
- package/dist/core/debug.d.ts +25 -0
- package/dist/core/debug.js +121 -0
- package/dist/core/derived-geometry.js +4 -7
- package/dist/core/index.d.ts +9 -2
- package/dist/core/index.js +8 -1
- package/dist/core/lenses/aggregates.d.ts +42 -52
- package/dist/core/lenses/aggregates.js +225 -116
- package/dist/core/lenses/geometry.d.ts +22 -4
- package/dist/core/lenses/geometry.js +59 -27
- package/dist/core/lenses/index.d.ts +6 -6
- package/dist/core/lenses/index.js +6 -6
- package/dist/core/lenses/memory.js +4 -17
- package/dist/core/lenses/numerical.d.ts +100 -0
- package/dist/core/lenses/{typed-factor.js → numerical.js} +136 -34
- package/dist/core/lenses/point-cloud.d.ts +67 -0
- package/dist/core/lenses/{closed-form-policies.js → point-cloud.js} +226 -84
- package/dist/core/lenses/snap.d.ts +18 -0
- package/dist/core/lenses/snap.js +138 -0
- package/dist/core/lenses/text.d.ts +40 -0
- package/dist/core/lenses/text.js +202 -0
- package/dist/core/lifecycle.js +3 -6
- package/dist/core/linalg.js +5 -11
- package/dist/core/optic.d.ts +13 -0
- package/dist/core/optic.js +39 -0
- package/dist/core/optics.d.ts +10 -0
- package/dist/core/optics.js +26 -0
- package/dist/core/store.d.ts +9 -0
- package/dist/core/store.js +77 -0
- package/dist/core/traits.d.ts +4 -7
- package/dist/core/traits.js +8 -12
- package/dist/core/values/anchor.js +0 -4
- package/dist/core/values/arr.d.ts +110 -0
- package/dist/core/values/arr.js +336 -0
- package/dist/core/values/audio.d.ts +8 -9
- package/dist/core/values/audio.js +11 -28
- package/dist/core/values/bool.d.ts +11 -11
- package/dist/core/values/bool.js +12 -22
- package/dist/core/values/box.d.ts +15 -20
- package/dist/core/values/box.js +20 -33
- package/dist/core/values/canvas.d.ts +18 -25
- package/dist/core/values/canvas.js +32 -66
- package/dist/core/values/color.d.ts +5 -7
- package/dist/core/values/color.js +5 -11
- package/dist/core/values/field.d.ts +6 -7
- package/dist/core/values/field.js +10 -35
- package/dist/core/values/flags.d.ts +1 -2
- package/dist/core/values/flags.js +1 -17
- package/dist/core/values/gpu.d.ts +6 -10
- package/dist/core/values/gpu.js +8 -22
- package/dist/core/values/matrix.d.ts +2 -4
- package/dist/core/values/matrix.js +2 -12
- package/dist/core/values/num.d.ts +19 -28
- package/dist/core/values/num.js +23 -41
- package/dist/core/values/pose.d.ts +2 -4
- package/dist/core/values/pose.js +3 -12
- package/dist/core/values/range.d.ts +18 -26
- package/dist/core/values/range.js +22 -39
- package/dist/core/values/reg/ambiguity.d.ts +8 -0
- package/dist/core/values/reg/ambiguity.js +131 -0
- package/dist/core/values/reg/engine.d.ts +91 -0
- package/dist/core/values/reg/engine.js +373 -0
- package/dist/core/values/reg/nfa.d.ts +42 -0
- package/dist/core/values/reg/nfa.js +391 -0
- package/dist/core/values/reg/regex.d.ts +7 -0
- package/dist/core/values/reg/regex.js +318 -0
- package/dist/core/values/reg/types.d.ts +60 -0
- package/dist/core/values/reg/types.js +3 -0
- package/dist/core/values/reg.d.ts +250 -0
- package/dist/core/values/reg.js +649 -0
- package/dist/core/values/str.d.ts +16 -60
- package/dist/core/values/str.js +133 -315
- package/dist/core/values/template.js +1 -24
- package/dist/core/values/transform.d.ts +3 -5
- package/dist/core/values/transform.js +3 -12
- package/dist/core/values/tri.d.ts +9 -10
- package/dist/core/values/tri.js +9 -15
- package/dist/core/values/vec.d.ts +9 -24
- package/dist/core/values/vec.js +9 -64
- package/dist/formats/lens.js +6 -9
- package/dist/index.d.ts +0 -11
- package/dist/index.js +1 -11
- package/dist/jsx-dev-runtime.d.ts +2 -0
- package/dist/jsx-dev-runtime.js +5 -0
- package/dist/jsx-runtime.d.ts +54 -0
- package/dist/jsx-runtime.js +219 -0
- package/dist/schema/lens.js +5 -5
- package/dist/shapes/drag-behaviors.d.ts +56 -0
- package/dist/shapes/drag-behaviors.js +102 -0
- package/dist/shapes/drag-spec.d.ts +52 -0
- package/dist/shapes/drag-spec.js +112 -0
- package/dist/shapes/index.d.ts +3 -1
- package/dist/shapes/index.js +3 -1
- package/dist/shapes/interaction.d.ts +2 -3
- package/dist/shapes/interaction.js +77 -56
- package/dist/shapes/label.js +6 -0
- package/dist/shapes/layout.d.ts +47 -1
- package/dist/shapes/layout.js +59 -1
- package/package.json +22 -1
- package/dist/coll.d.ts +0 -74
- package/dist/coll.js +0 -210
- package/dist/core/lenses/closed-form-policies.d.ts +0 -57
- package/dist/core/lenses/decompositions.d.ts +0 -14
- package/dist/core/lenses/decompositions.js +0 -224
- package/dist/core/lenses/domain-aggregates.d.ts +0 -42
- package/dist/core/lenses/domain-aggregates.js +0 -245
- package/dist/core/lenses/typed-factor.d.ts +0 -40
|
@@ -1,16 +1,9 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
//
|
|
8
|
-
// Layout: building-block actions (rigidTranslate, rotateAbout,
|
|
9
|
-
// scaleAbout, scaleAboutXY), then closed-form decompositions
|
|
10
|
-
// (bestFitLine, bestFitCircle, pca, total). All exact, idempotent,
|
|
11
|
-
// cross-channel invariant by construction, on the same `Cls.lens`
|
|
12
|
-
// machinery — no engine changes.
|
|
13
|
-
import { mean, Num, SKIP, Vec, } from "../index.js";
|
|
1
|
+
// Exact group-action lenses for point clouds. The backward pass applies a
|
|
2
|
+
// group element (translate / rotate / scale about a pivot) to the whole
|
|
3
|
+
// source set, so the fits below (bestFitLine, bestFitCircle, pca, procrustes)
|
|
4
|
+
// are exact and cross-channel invariant.
|
|
5
|
+
import { Num, SKIP, Vec, } from "../index.js";
|
|
6
|
+
import { mean } from "./aggregates.js";
|
|
14
7
|
import { continuous, remember } from "./memory.js";
|
|
15
8
|
// Pivotal trait lookup via the value class's `static traits.pivotal` slot.
|
|
16
9
|
// biome-ignore lint/suspicious/noExplicitAny: dynamic trait lookup
|
|
@@ -20,23 +13,14 @@ function pivotalOf(input) {
|
|
|
20
13
|
const p = Cls.traits?.pivotal;
|
|
21
14
|
if (!p) {
|
|
22
15
|
const name = Cls.name ?? "?";
|
|
23
|
-
throw new Error(`
|
|
16
|
+
throw new Error(`point-cloud: ${name} has no traits.pivotal`);
|
|
24
17
|
}
|
|
25
18
|
return p;
|
|
26
19
|
}
|
|
27
|
-
/** Writable centroid; on write, translates every point by the delta.
|
|
28
|
-
* The Vec-specific group-action reading of `mean`. */
|
|
29
|
-
export function rigidTranslate(points) {
|
|
30
|
-
return mean(points);
|
|
31
|
-
}
|
|
32
20
|
/** Writable angle from `pivot` to `points[0]`; write rotates every input
|
|
33
|
-
* about `pivot` by (target − current) via its `Pivotal` trait
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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 `rigidTranslate(points)` for rotation about
|
|
39
|
-
* the cluster's own centroid. */
|
|
21
|
+
* about `pivot` by (target − current) via its `Pivotal` trait (Vec rotates
|
|
22
|
+
* position, Pose also rotates orientation). `pivot` is reactive; pass
|
|
23
|
+
* `mean(points)` to rotate about the cluster's own centroid. */
|
|
40
24
|
export function rotateAbout(points, pivot) {
|
|
41
25
|
const K = points.length;
|
|
42
26
|
if (K < 1)
|
|
@@ -61,13 +45,9 @@ export function rotateAbout(points, pivot) {
|
|
|
61
45
|
});
|
|
62
46
|
}
|
|
63
47
|
/** Writable radial distance from pivot to `points[0]`; write scales every
|
|
64
|
-
* input radially about `pivot` (negative target reflects).
|
|
65
|
-
*
|
|
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). */
|
|
48
|
+
* input radially about `pivot` (negative target reflects). The complement
|
|
49
|
+
* carries per-point offsets from the pivot, so a collapse onto it (radius
|
|
50
|
+
* ≈ 0) reinflates from the stored shape. Pose `theta` survives. */
|
|
71
51
|
export function scaleAbout(points, pivot) {
|
|
72
52
|
const K = points.length;
|
|
73
53
|
if (K < 1)
|
|
@@ -111,10 +91,9 @@ export function scaleAbout(points, pivot) {
|
|
|
111
91
|
},
|
|
112
92
|
});
|
|
113
93
|
}
|
|
114
|
-
/** Per-axis scale about a pivot
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* `bboxLens.size`). */
|
|
94
|
+
/** Per-axis scale about a pivot (Vec-specific). The complement carries
|
|
95
|
+
* per-point per-axis fractions of point 0's offset, so a per-axis collapse
|
|
96
|
+
* is recoverable. */
|
|
118
97
|
export function scaleAboutXY(points, pivot) {
|
|
119
98
|
const K = points.length;
|
|
120
99
|
if (K < 1)
|
|
@@ -153,13 +132,6 @@ export function scaleAboutXY(points, pivot) {
|
|
|
153
132
|
},
|
|
154
133
|
});
|
|
155
134
|
}
|
|
156
|
-
// Best-fit line.
|
|
157
|
-
//
|
|
158
|
-
// K points → {point: centroid, direction: principal-axis angle}.
|
|
159
|
-
// write point → rigidTranslate
|
|
160
|
-
// write direction → rotate all about centroid to set principal axis
|
|
161
|
-
// Invariance: principal axis is translation-invariant; centroid is
|
|
162
|
-
// invariant under rotation-about-itself.
|
|
163
135
|
/** Angle of the dominant eigenvector of symmetric 2×2 [[cxx,cxy],[cxy,cyy]]. */
|
|
164
136
|
function dominantAxisAngle(cxx, cxy, cyy) {
|
|
165
137
|
return 0.5 * Math.atan2(2 * cxy, cxx - cyy);
|
|
@@ -178,16 +150,16 @@ function covariance(points, cx, cy) {
|
|
|
178
150
|
}
|
|
179
151
|
return { cxx: cxx / K, cxy: cxy / K, cyy: cyy / K };
|
|
180
152
|
}
|
|
153
|
+
/** K points → {point: centroid, direction: principal-axis angle}. Writing
|
|
154
|
+
* `point` translates; writing `direction` rotates all about the centroid. */
|
|
181
155
|
export function bestFitLine(points) {
|
|
182
156
|
const K = points.length;
|
|
183
157
|
if (K < 2)
|
|
184
158
|
throw new Error("bestFitLine: need ≥ 2 points");
|
|
185
|
-
const point =
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
// emitted angle so the direction stays continuous; a collapsed cloud has
|
|
190
|
-
// no axis (`defined: false`), so it freezes and stashes the target.
|
|
159
|
+
const point = mean(points);
|
|
160
|
+
// Axis angle is an eigenvector direction (defined up to sign), so the raw
|
|
161
|
+
// atan2 jumps by π as the cloud rotates; `continuous` (period π) tracks the
|
|
162
|
+
// last emitted angle to stay continuous, freezing on a collapsed cloud.
|
|
191
163
|
// Centroid + dominant-axis raw angle; `degenerate` when covariance vanishes.
|
|
192
164
|
const axisOf = (vals) => {
|
|
193
165
|
let sx = 0;
|
|
@@ -225,18 +197,13 @@ export function bestFitLine(points) {
|
|
|
225
197
|
});
|
|
226
198
|
return { point, direction };
|
|
227
199
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
// K points → {center: centroid, radius: mean distance from center}.
|
|
231
|
-
// write center → rigidTranslate
|
|
232
|
-
// write radius → scale all about center by target/current
|
|
233
|
-
// Simplest closed-form fit (mean center). Invariance: translation
|
|
234
|
-
// preserves radii; uniform scale-about-center preserves the center.
|
|
200
|
+
/** K points → {center: centroid, radius: mean distance from center}. Writing
|
|
201
|
+
* `center` translates; writing `radius` scales all about the center. */
|
|
235
202
|
export function bestFitCircle(points) {
|
|
236
203
|
const K = points.length;
|
|
237
204
|
if (K < 1)
|
|
238
205
|
throw new Error("bestFitCircle: need ≥ 1 point");
|
|
239
|
-
const center =
|
|
206
|
+
const center = mean(points);
|
|
240
207
|
// Radius = mean distance from the centroid; writing it scales the cluster
|
|
241
208
|
// about the centroid, and a collapse (mean → 0) reinflates the remembered
|
|
242
209
|
// shape — exactly `remember`'s magnitude view, anchored at the centroid.
|
|
@@ -261,20 +228,14 @@ export function bestFitCircle(points) {
|
|
|
261
228
|
});
|
|
262
229
|
return { center, radius };
|
|
263
230
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
// majorLength: √λ_major, minorLength: √λ_minor (per-axis std-devs)}.
|
|
268
|
-
// write mean → rigidTranslate
|
|
269
|
-
// write rotation → rotate all about mean to set principal axis
|
|
270
|
-
// write major/minor → scale along that axis by target/current
|
|
271
|
-
// Each write is a single group action; cross-channel invariance holds
|
|
272
|
-
// for all pairs.
|
|
231
|
+
/** K points → {mean: centroid, rotation: dominant-eigenvector angle,
|
|
232
|
+
* majorLength/minorLength: per-axis std-devs (√λ)}. Each write is a single
|
|
233
|
+
* group action about the mean, so all pairs are cross-channel invariant. */
|
|
273
234
|
export function pca(points) {
|
|
274
235
|
const K = points.length;
|
|
275
236
|
if (K < 2)
|
|
276
237
|
throw new Error("pca: need ≥ 2 points");
|
|
277
|
-
const
|
|
238
|
+
const meanCell = mean(points);
|
|
278
239
|
// 2×2 symmetric eigendecomp → {θ, λ_major, λ_minor}; null when fully
|
|
279
240
|
// collapsed (λ_major ≈ 0).
|
|
280
241
|
const decompose = (vals) => {
|
|
@@ -383,9 +344,14 @@ export function pca(points) {
|
|
|
383
344
|
// the current axis length and is absorbed (cluster left put).
|
|
384
345
|
if (Math.abs(target) === c.lenThis)
|
|
385
346
|
return { updates: vals.map(() => SKIP), complement: c };
|
|
386
|
-
// Non-degenerate fast path: scale current cluster along axis.
|
|
347
|
+
// Non-degenerate fast path: scale current cluster along axis. The scale
|
|
348
|
+
// sets the axis length to |target|, so the complement is consistent
|
|
349
|
+
// without a post-write `step` (the engine no longer re-steps own writes).
|
|
387
350
|
const k = target / c.lenThis;
|
|
388
|
-
return {
|
|
351
|
+
return {
|
|
352
|
+
updates: scaleAlongAxis(vals, d.cx, d.cy, c.uX, c.uY, k),
|
|
353
|
+
complement: { ...c, lenThis: Math.abs(target) },
|
|
354
|
+
};
|
|
389
355
|
}
|
|
390
356
|
// Degenerate: reconstruct from complement. Centroid still
|
|
391
357
|
// derivable from current source (mean translates always work).
|
|
@@ -403,23 +369,17 @@ export function pca(points) {
|
|
|
403
369
|
const b = c.projOther[i] * c.lenOther;
|
|
404
370
|
out[i] = { x: cx + a * c.uX + b * c.vX, y: cy + a * c.uY + b * c.vY };
|
|
405
371
|
}
|
|
406
|
-
return { updates: out, complement: c };
|
|
372
|
+
return { updates: out, complement: { ...c, lenThis: Math.abs(target) } };
|
|
407
373
|
},
|
|
408
374
|
});
|
|
409
375
|
};
|
|
410
376
|
const majorLength = buildAxisLens("major");
|
|
411
377
|
const minorLength = buildAxisLens("minor");
|
|
412
|
-
return { mean, rotation, majorLength, minorLength };
|
|
378
|
+
return { mean: meanCell, rotation, majorLength, minorLength };
|
|
413
379
|
}
|
|
414
|
-
// Partition / simplex lens.
|
|
415
|
-
//
|
|
416
|
-
// K parts → {total}: writing total scales all parts proportionally.
|
|
417
|
-
// (A {total, ratios} form is possible but ratios on a K-simplex have
|
|
418
|
-
// K−1 DOF, so it's left out of this prototype.)
|
|
419
380
|
/** Writable total over K parts; write scales all parts proportionally,
|
|
420
|
-
* preserving their ratios. A
|
|
421
|
-
*
|
|
422
|
-
* uniform so an all-zero start splits evenly. */
|
|
381
|
+
* preserving their ratios. A collapse to zero reinflates the stored ratios,
|
|
382
|
+
* seeded uniform so an all-zero start splits evenly. */
|
|
423
383
|
export function total(parts) {
|
|
424
384
|
const K = parts.length;
|
|
425
385
|
if (K < 1)
|
|
@@ -436,7 +396,189 @@ export function total(parts) {
|
|
|
436
396
|
seed: () => parts.map(() => 1 / K),
|
|
437
397
|
});
|
|
438
398
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
399
|
+
/** K Vecs → {centroid, rotation (angle of point[0] about centroid), scale
|
|
400
|
+
* (its distance from centroid)}. Each write is a closed-form transform about
|
|
401
|
+
* the centroid (translate / rotate / scale), so the three are cross-channel
|
|
402
|
+
* invariant. A collapsed cluster makes rotation singular and scale a no-op. */
|
|
403
|
+
export function procrustes(points) {
|
|
404
|
+
const K = points.length;
|
|
405
|
+
if (K < 2)
|
|
406
|
+
throw new Error("procrustes: need ≥ 2 points");
|
|
407
|
+
const centroid = Vec.lens(points, (vals) => {
|
|
408
|
+
let sx = 0;
|
|
409
|
+
let sy = 0;
|
|
410
|
+
for (let i = 0; i < K; i++) {
|
|
411
|
+
sx += vals[i].x;
|
|
412
|
+
sy += vals[i].y;
|
|
413
|
+
}
|
|
414
|
+
return { x: sx / K, y: sy / K };
|
|
415
|
+
}, (target, vals) => {
|
|
416
|
+
let sx = 0;
|
|
417
|
+
let sy = 0;
|
|
418
|
+
for (let i = 0; i < K; i++) {
|
|
419
|
+
sx += vals[i].x;
|
|
420
|
+
sy += vals[i].y;
|
|
421
|
+
}
|
|
422
|
+
const dx = target.x - sx / K;
|
|
423
|
+
const dy = target.y - sy / K;
|
|
424
|
+
const out = new Array(K);
|
|
425
|
+
for (let i = 0; i < K; i++)
|
|
426
|
+
out[i] = { x: vals[i].x + dx, y: vals[i].y + dy };
|
|
427
|
+
return out;
|
|
428
|
+
});
|
|
429
|
+
const rotation = Num.lens(points, (vals) => {
|
|
430
|
+
let sx = 0;
|
|
431
|
+
let sy = 0;
|
|
432
|
+
for (let i = 0; i < K; i++) {
|
|
433
|
+
sx += vals[i].x;
|
|
434
|
+
sy += vals[i].y;
|
|
435
|
+
}
|
|
436
|
+
const cx = sx / K;
|
|
437
|
+
const cy = sy / K;
|
|
438
|
+
return Math.atan2(vals[0].y - cy, vals[0].x - cx);
|
|
439
|
+
}, (target, vals) => {
|
|
440
|
+
let sx = 0;
|
|
441
|
+
let sy = 0;
|
|
442
|
+
for (let i = 0; i < K; i++) {
|
|
443
|
+
sx += vals[i].x;
|
|
444
|
+
sy += vals[i].y;
|
|
445
|
+
}
|
|
446
|
+
const cx = sx / K;
|
|
447
|
+
const cy = sy / K;
|
|
448
|
+
const rx0 = vals[0].x - cx;
|
|
449
|
+
const ry0 = vals[0].y - cy;
|
|
450
|
+
if (rx0 * rx0 + ry0 * ry0 < 1e-24) {
|
|
451
|
+
// Collapsed cluster; no angle to rotate from.
|
|
452
|
+
return vals.map(() => SKIP);
|
|
453
|
+
}
|
|
454
|
+
const oldθ = Math.atan2(ry0, rx0);
|
|
455
|
+
const dθ = target - oldθ;
|
|
456
|
+
const cos = Math.cos(dθ);
|
|
457
|
+
const sin = Math.sin(dθ);
|
|
458
|
+
const out = new Array(K);
|
|
459
|
+
for (let i = 0; i < K; i++) {
|
|
460
|
+
const rx = vals[i].x - cx;
|
|
461
|
+
const ry = vals[i].y - cy;
|
|
462
|
+
out[i] = { x: cx + cos * rx - sin * ry, y: cy + sin * rx + cos * ry };
|
|
463
|
+
}
|
|
464
|
+
return out;
|
|
465
|
+
});
|
|
466
|
+
const centroidOf = (vals) => {
|
|
467
|
+
let sx = 0;
|
|
468
|
+
let sy = 0;
|
|
469
|
+
for (let i = 0; i < K; i++) {
|
|
470
|
+
sx += vals[i].x;
|
|
471
|
+
sy += vals[i].y;
|
|
472
|
+
}
|
|
473
|
+
return { x: sx / K, y: sy / K };
|
|
474
|
+
};
|
|
475
|
+
const refreshDevs = (devs, vals) => {
|
|
476
|
+
const c = centroidOf(vals);
|
|
477
|
+
return devs.map((d, i) => {
|
|
478
|
+
const dx = vals[i].x - c.x;
|
|
479
|
+
const dy = vals[i].y - c.y;
|
|
480
|
+
return dx * dx + dy * dy > 1e-18 ? { x: dx, y: dy } : d;
|
|
481
|
+
});
|
|
482
|
+
};
|
|
483
|
+
const scale = Num.lens(points, {
|
|
484
|
+
init: (vals) => {
|
|
485
|
+
const c = centroidOf(vals);
|
|
486
|
+
return { devs: vals.map(v => ({ x: v.x - c.x, y: v.y - c.y })) };
|
|
487
|
+
},
|
|
488
|
+
step: (vals, c) => ({ devs: refreshDevs(c.devs, vals) }),
|
|
489
|
+
fwd: (vals) => {
|
|
490
|
+
const c = centroidOf(vals);
|
|
491
|
+
return Math.hypot(vals[0].x - c.x, vals[0].y - c.y);
|
|
492
|
+
},
|
|
493
|
+
bwd: (target, vals, c) => {
|
|
494
|
+
const cen = centroidOf(vals);
|
|
495
|
+
const d0 = c.devs[0];
|
|
496
|
+
const r0 = Math.hypot(d0.x, d0.y);
|
|
497
|
+
if (r0 < 1e-12)
|
|
498
|
+
return { updates: vals.map(() => SKIP), complement: c };
|
|
499
|
+
const k = target / r0;
|
|
500
|
+
const out = c.devs.map(d => ({ x: cen.x + k * d.x, y: cen.y + k * d.y }));
|
|
501
|
+
return { updates: out, complement: c };
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
return { centroid, rotation, scale };
|
|
505
|
+
}
|
|
506
|
+
/** K Vecs → {center, size} of the axis-aligned bounding box. Writing `center`
|
|
507
|
+
* translates; writing `size` scales all about the center per-axis. Degenerate
|
|
508
|
+
* axes (size = 0) write as no-ops; negative size reflects. */
|
|
509
|
+
export function bbox(points) {
|
|
510
|
+
const K = points.length;
|
|
511
|
+
if (K < 1)
|
|
512
|
+
throw new Error("bbox: need ≥ 1 point");
|
|
513
|
+
const computeBox = (vals) => {
|
|
514
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
515
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
516
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
517
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
518
|
+
for (let i = 0; i < K; i++) {
|
|
519
|
+
const x = vals[i].x;
|
|
520
|
+
const y = vals[i].y;
|
|
521
|
+
if (x < minX)
|
|
522
|
+
minX = x;
|
|
523
|
+
if (x > maxX)
|
|
524
|
+
maxX = x;
|
|
525
|
+
if (y < minY)
|
|
526
|
+
minY = y;
|
|
527
|
+
if (y > maxY)
|
|
528
|
+
maxY = y;
|
|
529
|
+
}
|
|
530
|
+
return {
|
|
531
|
+
cx: (minX + maxX) / 2,
|
|
532
|
+
cy: (minY + maxY) / 2,
|
|
533
|
+
sx: maxX - minX,
|
|
534
|
+
sy: maxY - minY,
|
|
535
|
+
};
|
|
536
|
+
};
|
|
537
|
+
const center = Vec.lens(points, (vals) => {
|
|
538
|
+
const b = computeBox(vals);
|
|
539
|
+
return { x: b.cx, y: b.cy };
|
|
540
|
+
}, (target, vals) => {
|
|
541
|
+
const b = computeBox(vals);
|
|
542
|
+
const dx = target.x - b.cx;
|
|
543
|
+
const dy = target.y - b.cy;
|
|
544
|
+
const out = new Array(K);
|
|
545
|
+
for (let i = 0; i < K; i++)
|
|
546
|
+
out[i] = { x: vals[i].x + dx, y: vals[i].y + dy };
|
|
547
|
+
return out;
|
|
548
|
+
});
|
|
549
|
+
const refreshFracs = (fracs, vals) => {
|
|
550
|
+
const b = computeBox(vals);
|
|
551
|
+
const hx = b.sx > 1e-12 ? b.sx / 2 : 0;
|
|
552
|
+
const hy = b.sy > 1e-12 ? b.sy / 2 : 0;
|
|
553
|
+
return fracs.map((f, i) => ({
|
|
554
|
+
x: hx > 0 ? (vals[i].x - b.cx) / hx : f.x,
|
|
555
|
+
y: hy > 0 ? (vals[i].y - b.cy) / hy : f.y,
|
|
556
|
+
}));
|
|
557
|
+
};
|
|
558
|
+
const size = Vec.lens(points, {
|
|
559
|
+
init: (vals) => {
|
|
560
|
+
const b = computeBox(vals);
|
|
561
|
+
const halfX0 = b.sx > 1e-12 ? b.sx / 2 : 1;
|
|
562
|
+
const halfY0 = b.sy > 1e-12 ? b.sy / 2 : 1;
|
|
563
|
+
return {
|
|
564
|
+
fracs: vals.map(v => ({
|
|
565
|
+
x: b.sx > 1e-12 ? (v.x - b.cx) / halfX0 : 0,
|
|
566
|
+
y: b.sy > 1e-12 ? (v.y - b.cy) / halfY0 : 0,
|
|
567
|
+
})),
|
|
568
|
+
};
|
|
569
|
+
},
|
|
570
|
+
step: (vals, c) => ({ fracs: refreshFracs(c.fracs, vals) }),
|
|
571
|
+
fwd: (vals) => {
|
|
572
|
+
const b = computeBox(vals);
|
|
573
|
+
return { x: b.sx, y: b.sy };
|
|
574
|
+
},
|
|
575
|
+
bwd: (target, vals, c) => {
|
|
576
|
+
const b = computeBox(vals);
|
|
577
|
+
const halfTx = target.x / 2;
|
|
578
|
+
const halfTy = target.y / 2;
|
|
579
|
+
const out = c.fracs.map(f => ({ x: b.cx + f.x * halfTx, y: b.cy + f.y * halfTy }));
|
|
580
|
+
return { updates: out, complement: c };
|
|
581
|
+
},
|
|
582
|
+
});
|
|
583
|
+
return { center, size };
|
|
584
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Cell, type Read } from "../cell.js";
|
|
2
|
+
type V = {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
};
|
|
6
|
+
/** Convex-hull barycentric weights of `q` over `pts` (Σ = 1, all ≥ 0,
|
|
7
|
+
* clamped to the hull). Closed form for K ≤ 3; Frank–Wolfe for K > 3. */
|
|
8
|
+
export declare function hullWeights(q: V, pts: readonly V[]): number[];
|
|
9
|
+
export interface ClosestOpts {
|
|
10
|
+
/** Hysteresis margin (px): the current pick is kept until a rival is
|
|
11
|
+
* nearer by more than this. Default 0. */
|
|
12
|
+
sticky?: number;
|
|
13
|
+
}
|
|
14
|
+
/** Index of the candidate nearest `pointer`, with hysteresis. Read-only
|
|
15
|
+
* selection: the stickiness state lives in the lens complement (the
|
|
16
|
+
* sanctioned place for path-dependence), so reads stay pure. */
|
|
17
|
+
export declare function nearestIndex(pointer: Read<V>, candidates: readonly Read<V>[], opts?: ClosestOpts): Cell<number>;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Pointer math for the drag algebra: `hullWeights` (barycentric weights of a
|
|
2
|
+
// point in a convex hull) and `nearestIndex` (nearest candidate, with sticky
|
|
3
|
+
// hysteresis carried in the lens complement).
|
|
4
|
+
import { lens, SKIP } from "../cell.js";
|
|
5
|
+
const sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
|
|
6
|
+
const dot = (a, b) => a.x * b.x + a.y * b.y;
|
|
7
|
+
const dist2 = (a, b) => {
|
|
8
|
+
const dx = a.x - b.x;
|
|
9
|
+
const dy = a.y - b.y;
|
|
10
|
+
return dx * dx + dy * dy;
|
|
11
|
+
};
|
|
12
|
+
// ── convex-hull barycentric weights ─────────────────────────────────
|
|
13
|
+
/** Project `q` onto segment p0→p1; weights `[1−t, t]`, t clamped to [0,1]. */
|
|
14
|
+
function segmentWeights(q, p0, p1) {
|
|
15
|
+
const d = sub(p1, p0);
|
|
16
|
+
const len2 = dot(d, d);
|
|
17
|
+
if (len2 < 1e-18)
|
|
18
|
+
return [0.5, 0.5];
|
|
19
|
+
const t = Math.max(0, Math.min(1, dot(sub(q, p0), d) / len2));
|
|
20
|
+
return [1 - t, t];
|
|
21
|
+
}
|
|
22
|
+
/** Barycentric weights of `q` in triangle (p0,p1,p2), CLAMPED to the
|
|
23
|
+
* triangle: inside → the true coords; outside → the nearest point on the
|
|
24
|
+
* hull (an edge projection or a vertex), so the blend never extrapolates. */
|
|
25
|
+
function triangleWeights(q, p0, p1, p2) {
|
|
26
|
+
const v0 = sub(p1, p0);
|
|
27
|
+
const v1 = sub(p2, p0);
|
|
28
|
+
const v2 = sub(q, p0);
|
|
29
|
+
const d00 = dot(v0, v0);
|
|
30
|
+
const d01 = dot(v0, v1);
|
|
31
|
+
const d11 = dot(v1, v1);
|
|
32
|
+
const d20 = dot(v2, v0);
|
|
33
|
+
const d21 = dot(v2, v1);
|
|
34
|
+
const denom = d00 * d11 - d01 * d01;
|
|
35
|
+
if (Math.abs(denom) > 1e-18) {
|
|
36
|
+
const b1 = (d11 * d20 - d01 * d21) / denom;
|
|
37
|
+
const b2 = (d00 * d21 - d01 * d20) / denom;
|
|
38
|
+
const b0 = 1 - b1 - b2;
|
|
39
|
+
if (b0 >= -1e-9 && b1 >= -1e-9 && b2 >= -1e-9) {
|
|
40
|
+
const s = b0 + b1 + b2;
|
|
41
|
+
return [b0 / s, b1 / s, b2 / s];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Outside (or degenerate): nearest point on the three edges.
|
|
45
|
+
const e01 = segmentWeights(q, p0, p1);
|
|
46
|
+
const e12 = segmentWeights(q, p1, p2);
|
|
47
|
+
const e20 = segmentWeights(q, p2, p0);
|
|
48
|
+
const at01 = { x: p0.x * e01[0] + p1.x * e01[1], y: p0.y * e01[0] + p1.y * e01[1] };
|
|
49
|
+
const at12 = { x: p1.x * e12[0] + p2.x * e12[1], y: p1.y * e12[0] + p2.y * e12[1] };
|
|
50
|
+
const at20 = { x: p2.x * e20[0] + p0.x * e20[1], y: p2.y * e20[0] + p0.y * e20[1] };
|
|
51
|
+
const c = [
|
|
52
|
+
[dist2(q, at01), [e01[0], e01[1], 0]],
|
|
53
|
+
[dist2(q, at12), [0, e12[0], e12[1]]],
|
|
54
|
+
[dist2(q, at20), [e20[1], 0, e20[0]]],
|
|
55
|
+
];
|
|
56
|
+
c.sort((a, b) => a[0] - b[0]);
|
|
57
|
+
return c[0][1];
|
|
58
|
+
}
|
|
59
|
+
/** Frank–Wolfe projection of `q` onto the convex hull of `pts` in
|
|
60
|
+
* barycentric coordinates (used for K > 3). Minimises |Σ wᵢ·pᵢ − q|² over
|
|
61
|
+
* the simplex; O(K·iters), plenty fast for the handful of targets a drag
|
|
62
|
+
* ever offers. */
|
|
63
|
+
function hullProjectWeights(q, pts, iters = 60) {
|
|
64
|
+
const K = pts.length;
|
|
65
|
+
const w = new Array(K).fill(1 / K);
|
|
66
|
+
for (let t = 0; t < iters; t++) {
|
|
67
|
+
let cx = 0;
|
|
68
|
+
let cy = 0;
|
|
69
|
+
for (let i = 0; i < K; i++) {
|
|
70
|
+
cx += w[i] * pts[i].x;
|
|
71
|
+
cy += w[i] * pts[i].y;
|
|
72
|
+
}
|
|
73
|
+
const rx = cx - q.x;
|
|
74
|
+
const ry = cy - q.y;
|
|
75
|
+
let best = 0;
|
|
76
|
+
let bestG = Number.POSITIVE_INFINITY;
|
|
77
|
+
for (let i = 0; i < K; i++) {
|
|
78
|
+
const g = rx * pts[i].x + ry * pts[i].y;
|
|
79
|
+
if (g < bestG) {
|
|
80
|
+
bestG = g;
|
|
81
|
+
best = i;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const gamma = 2 / (t + 2);
|
|
85
|
+
for (let i = 0; i < K; i++)
|
|
86
|
+
w[i] = w[i] * (1 - gamma);
|
|
87
|
+
w[best] = w[best] + gamma;
|
|
88
|
+
}
|
|
89
|
+
return w;
|
|
90
|
+
}
|
|
91
|
+
/** Convex-hull barycentric weights of `q` over `pts` (Σ = 1, all ≥ 0,
|
|
92
|
+
* clamped to the hull). Closed form for K ≤ 3; Frank–Wolfe for K > 3. */
|
|
93
|
+
export function hullWeights(q, pts) {
|
|
94
|
+
const K = pts.length;
|
|
95
|
+
if (K === 0)
|
|
96
|
+
return [];
|
|
97
|
+
if (K === 1)
|
|
98
|
+
return [1];
|
|
99
|
+
if (K === 2)
|
|
100
|
+
return segmentWeights(q, pts[0], pts[1]);
|
|
101
|
+
if (K === 3)
|
|
102
|
+
return triangleWeights(q, pts[0], pts[1], pts[2]);
|
|
103
|
+
return hullProjectWeights(q, pts);
|
|
104
|
+
}
|
|
105
|
+
function pick(sources, prev, sticky) {
|
|
106
|
+
const p = sources[0];
|
|
107
|
+
let best = -1;
|
|
108
|
+
let bestD = Number.POSITIVE_INFINITY;
|
|
109
|
+
for (let i = 1; i < sources.length; i++) {
|
|
110
|
+
const d = dist2(sources[i], p);
|
|
111
|
+
if (d < bestD) {
|
|
112
|
+
bestD = d;
|
|
113
|
+
best = i - 1;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (sticky > 0 && prev >= 0 && prev + 1 < sources.length) {
|
|
117
|
+
const prevD = Math.sqrt(dist2(sources[prev + 1], p));
|
|
118
|
+
if (prevD - Math.sqrt(bestD) < sticky)
|
|
119
|
+
return prev;
|
|
120
|
+
}
|
|
121
|
+
return best;
|
|
122
|
+
}
|
|
123
|
+
/** Index of the candidate nearest `pointer`, with hysteresis. Read-only
|
|
124
|
+
* selection: the stickiness state lives in the lens complement (the
|
|
125
|
+
* sanctioned place for path-dependence), so reads stay pure. */
|
|
126
|
+
export function nearestIndex(pointer, candidates, opts = {}) {
|
|
127
|
+
const sticky = opts.sticky ?? 0;
|
|
128
|
+
const parents = [pointer, ...candidates];
|
|
129
|
+
return lens(parents, {
|
|
130
|
+
init: (sources) => ({ index: pick(sources, -1, 0) }),
|
|
131
|
+
step: (sources, c) => ({ index: pick(sources, c.index, sticky) }),
|
|
132
|
+
fwd: (_sources, c) => c.index,
|
|
133
|
+
bwd: (_t, sources, c) => ({
|
|
134
|
+
updates: sources.map(() => SKIP),
|
|
135
|
+
complement: c,
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Cell, Writable } from "../cell.js";
|
|
2
|
+
import { Str } from "../values/str.js";
|
|
3
|
+
type V = string;
|
|
4
|
+
/** Split `s` into words and separators. Returns:
|
|
5
|
+
*
|
|
6
|
+
* words[i] — the i-th run of word characters
|
|
7
|
+
* seps[0] — leading non-word characters (possibly empty)
|
|
8
|
+
* seps[i] — for 1 ≤ i ≤ words.length-1, the separator BETWEEN
|
|
9
|
+
* `words[i-1]` and `words[i]`
|
|
10
|
+
* seps[words.length] — trailing non-word characters
|
|
11
|
+
*
|
|
12
|
+
* Always satisfies `seps.length === words.length + 1`. */
|
|
13
|
+
export declare function parseWords(s: V): {
|
|
14
|
+
words: V[];
|
|
15
|
+
seps: V[];
|
|
16
|
+
};
|
|
17
|
+
/** Inverse of `parseWords`. Interleaves words with `seps`; added words
|
|
18
|
+
* get `" "` gaps, removed words keep the original trailing separator.
|
|
19
|
+
* A zero-word original (`seps.length === 1`) treats its one entry as
|
|
20
|
+
* lead only, so words append after it without double-counting as trail. */
|
|
21
|
+
export declare function rebuildWords(words: V[], seps: V[]): V;
|
|
22
|
+
/** Per-character case mask: `U` upper letter, `L` lower letter,
|
|
23
|
+
* `" "` non-letter. Length matches the source. */
|
|
24
|
+
export declare function caseMaskOf(s: V): string;
|
|
25
|
+
/** Apply a case mask to `target`, position by position. Mask positions
|
|
26
|
+
* beyond `target.length` are ignored; target positions beyond the
|
|
27
|
+
* mask keep their native case (e.g. user appended a longer word). */
|
|
28
|
+
export declare function applyCaseMask(target: V, mask: string): V;
|
|
29
|
+
/** Apply the case pattern of a source word to a target word. Detects
|
|
30
|
+
* all-upper / all-lower / title case, else falls back to position-wise
|
|
31
|
+
* `applyCaseMask`. Non-letter target chars always pass through unchanged
|
|
32
|
+
* (title-casing "-gng" → "-Gng"). */
|
|
33
|
+
export declare function applyCasePattern(target: V, mask: string): V;
|
|
34
|
+
/** Case-folded view of a string cell with word-aware case recovery on
|
|
35
|
+
* write. Read folds to lower (default) or upper; write recovers the
|
|
36
|
+
* source's per-word case — lookup priority: (1) content match (FIFO
|
|
37
|
+
* across duplicates); (2) per-position fallback for new content; (3)
|
|
38
|
+
* native for content beyond the source structure. */
|
|
39
|
+
export declare function caseFold(parent: Cell<V>, to?: "lower" | "upper"): Writable<Str>;
|
|
40
|
+
export {};
|