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.
Files changed (117) hide show
  1. package/README.md +14 -7
  2. package/dist/automerge/doc-cell.d.ts +20 -0
  3. package/dist/automerge/doc-cell.js +80 -0
  4. package/dist/automerge/index.d.ts +3 -0
  5. package/dist/automerge/index.js +12 -0
  6. package/dist/automerge/reconcile.d.ts +5 -0
  7. package/dist/automerge/reconcile.js +63 -0
  8. package/dist/core/_counts.d.ts +48 -0
  9. package/dist/core/_counts.js +51 -0
  10. package/dist/core/cell.d.ts +148 -112
  11. package/dist/core/cell.js +945 -768
  12. package/dist/core/debug.d.ts +25 -0
  13. package/dist/core/debug.js +121 -0
  14. package/dist/core/derived-geometry.js +4 -7
  15. package/dist/core/index.d.ts +9 -2
  16. package/dist/core/index.js +8 -1
  17. package/dist/core/lenses/aggregates.d.ts +42 -52
  18. package/dist/core/lenses/aggregates.js +225 -116
  19. package/dist/core/lenses/geometry.d.ts +22 -4
  20. package/dist/core/lenses/geometry.js +59 -27
  21. package/dist/core/lenses/index.d.ts +6 -6
  22. package/dist/core/lenses/index.js +6 -6
  23. package/dist/core/lenses/memory.js +4 -17
  24. package/dist/core/lenses/numerical.d.ts +100 -0
  25. package/dist/core/lenses/{typed-factor.js → numerical.js} +136 -34
  26. package/dist/core/lenses/point-cloud.d.ts +67 -0
  27. package/dist/core/lenses/{closed-form-policies.js → point-cloud.js} +226 -84
  28. package/dist/core/lenses/snap.d.ts +18 -0
  29. package/dist/core/lenses/snap.js +138 -0
  30. package/dist/core/lenses/text.d.ts +40 -0
  31. package/dist/core/lenses/text.js +202 -0
  32. package/dist/core/lifecycle.js +3 -6
  33. package/dist/core/linalg.js +5 -11
  34. package/dist/core/optic.d.ts +13 -0
  35. package/dist/core/optic.js +39 -0
  36. package/dist/core/optics.d.ts +10 -0
  37. package/dist/core/optics.js +26 -0
  38. package/dist/core/store.d.ts +9 -0
  39. package/dist/core/store.js +77 -0
  40. package/dist/core/traits.d.ts +4 -7
  41. package/dist/core/traits.js +8 -12
  42. package/dist/core/values/anchor.js +0 -4
  43. package/dist/core/values/arr.d.ts +110 -0
  44. package/dist/core/values/arr.js +336 -0
  45. package/dist/core/values/audio.d.ts +8 -9
  46. package/dist/core/values/audio.js +11 -28
  47. package/dist/core/values/bool.d.ts +11 -11
  48. package/dist/core/values/bool.js +12 -22
  49. package/dist/core/values/box.d.ts +15 -20
  50. package/dist/core/values/box.js +20 -33
  51. package/dist/core/values/canvas.d.ts +18 -25
  52. package/dist/core/values/canvas.js +32 -66
  53. package/dist/core/values/color.d.ts +5 -7
  54. package/dist/core/values/color.js +5 -11
  55. package/dist/core/values/field.d.ts +6 -7
  56. package/dist/core/values/field.js +10 -35
  57. package/dist/core/values/flags.d.ts +1 -2
  58. package/dist/core/values/flags.js +1 -17
  59. package/dist/core/values/gpu.d.ts +6 -10
  60. package/dist/core/values/gpu.js +8 -22
  61. package/dist/core/values/matrix.d.ts +2 -4
  62. package/dist/core/values/matrix.js +2 -12
  63. package/dist/core/values/num.d.ts +19 -28
  64. package/dist/core/values/num.js +23 -41
  65. package/dist/core/values/pose.d.ts +2 -4
  66. package/dist/core/values/pose.js +3 -12
  67. package/dist/core/values/range.d.ts +18 -26
  68. package/dist/core/values/range.js +22 -39
  69. package/dist/core/values/reg/ambiguity.d.ts +8 -0
  70. package/dist/core/values/reg/ambiguity.js +131 -0
  71. package/dist/core/values/reg/engine.d.ts +91 -0
  72. package/dist/core/values/reg/engine.js +373 -0
  73. package/dist/core/values/reg/nfa.d.ts +42 -0
  74. package/dist/core/values/reg/nfa.js +391 -0
  75. package/dist/core/values/reg/regex.d.ts +7 -0
  76. package/dist/core/values/reg/regex.js +318 -0
  77. package/dist/core/values/reg/types.d.ts +60 -0
  78. package/dist/core/values/reg/types.js +3 -0
  79. package/dist/core/values/reg.d.ts +250 -0
  80. package/dist/core/values/reg.js +649 -0
  81. package/dist/core/values/str.d.ts +16 -60
  82. package/dist/core/values/str.js +133 -315
  83. package/dist/core/values/template.js +1 -24
  84. package/dist/core/values/transform.d.ts +3 -5
  85. package/dist/core/values/transform.js +3 -12
  86. package/dist/core/values/tri.d.ts +9 -10
  87. package/dist/core/values/tri.js +9 -15
  88. package/dist/core/values/vec.d.ts +9 -24
  89. package/dist/core/values/vec.js +9 -64
  90. package/dist/formats/lens.js +6 -9
  91. package/dist/index.d.ts +0 -11
  92. package/dist/index.js +1 -11
  93. package/dist/jsx-dev-runtime.d.ts +2 -0
  94. package/dist/jsx-dev-runtime.js +5 -0
  95. package/dist/jsx-runtime.d.ts +54 -0
  96. package/dist/jsx-runtime.js +219 -0
  97. package/dist/schema/lens.js +5 -5
  98. package/dist/shapes/drag-behaviors.d.ts +56 -0
  99. package/dist/shapes/drag-behaviors.js +102 -0
  100. package/dist/shapes/drag-spec.d.ts +52 -0
  101. package/dist/shapes/drag-spec.js +112 -0
  102. package/dist/shapes/index.d.ts +3 -1
  103. package/dist/shapes/index.js +3 -1
  104. package/dist/shapes/interaction.d.ts +2 -3
  105. package/dist/shapes/interaction.js +77 -56
  106. package/dist/shapes/label.js +6 -0
  107. package/dist/shapes/layout.d.ts +47 -1
  108. package/dist/shapes/layout.js +59 -1
  109. package/package.json +22 -1
  110. package/dist/coll.d.ts +0 -74
  111. package/dist/coll.js +0 -210
  112. package/dist/core/lenses/closed-form-policies.d.ts +0 -57
  113. package/dist/core/lenses/decompositions.d.ts +0 -14
  114. package/dist/core/lenses/decompositions.js +0 -224
  115. package/dist/core/lenses/domain-aggregates.d.ts +0 -42
  116. package/dist/core/lenses/domain-aggregates.js +0 -245
  117. package/dist/core/lenses/typed-factor.d.ts +0 -40
@@ -1,16 +1,9 @@
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), 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(`closed-form-policies: ${name} has no traits.pivotal`);
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
- * 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 `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). 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). */
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. 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`). */
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 = rigidTranslate(points);
186
- // The principal axis is an eigenvector defined only up to sign, so the
187
- // raw atan2 jumps by π as the cloud rotates. `continuous` lifts it to its
188
- // universal cover (period π, since axis axis + π), tracking the last
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
- // Best-fit circle.
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 = rigidTranslate(points);
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
- // PCA / affine similarity decomposition.
265
- //
266
- // K points {mean: centroid, rotation: dominant-eigenvector angle,
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 mean = rigidTranslate(points);
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 { updates: scaleAlongAxis(vals, d.cx, d.cy, c.uX, c.uY, k), complement: c };
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 `remember` anchored at zero with a signed
421
- * sum feature: a collapse to zero reinflates the stored ratios, seeded
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
- // Every lens here is a group action about a pivot (translate, rotateAbout,
440
- // scaleAbout, scaleAboutXY, scaleAlongAxis) or a `remember`/`continuous`
441
- // shape-memory; the decompositions combine them, each measured against a
442
- // derived feature (centroid, principal axis, mean radius).
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 {};