bireactive 0.2.4 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/dist/animation/anim.js +4 -0
  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/coll.d.ts +7 -7
  9. package/dist/core/_counts.d.ts +48 -0
  10. package/dist/core/_counts.js +58 -0
  11. package/dist/core/cell.d.ts +182 -123
  12. package/dist/core/cell.js +1140 -721
  13. package/dist/core/debug.d.ts +25 -0
  14. package/dist/core/debug.js +121 -0
  15. package/dist/core/index.d.ts +9 -14
  16. package/dist/core/index.js +9 -14
  17. package/dist/core/lenses/aggregates.d.ts +1 -1
  18. package/dist/core/lenses/aggregates.js +4 -3
  19. package/dist/core/lenses/closed-form-policies.js +14 -9
  20. package/dist/core/lenses/decompositions.js +3 -3
  21. package/dist/core/lenses/domain-aggregates.js +5 -5
  22. package/dist/core/lenses/geometry.d.ts +1 -1
  23. package/dist/core/lenses/geometry.js +6 -7
  24. package/dist/core/lenses/index.d.ts +1 -0
  25. package/dist/core/lenses/index.js +1 -0
  26. package/dist/core/lenses/memory.d.ts +2 -2
  27. package/dist/core/lenses/memory.js +3 -3
  28. package/dist/core/lenses/snap.d.ts +18 -0
  29. package/dist/core/lenses/snap.js +145 -0
  30. package/dist/core/lenses/typed-factor.js +4 -3
  31. package/dist/core/optic.d.ts +13 -0
  32. package/dist/core/optic.js +44 -0
  33. package/dist/core/optics.d.ts +10 -0
  34. package/dist/core/optics.js +30 -0
  35. package/dist/core/store.d.ts +10 -0
  36. package/dist/core/store.js +85 -0
  37. package/dist/core/traits.d.ts +1 -0
  38. package/dist/core/values/audio.js +4 -5
  39. package/dist/core/values/box.js +7 -7
  40. package/dist/core/values/canvas.js +15 -18
  41. package/dist/core/values/color.js +5 -5
  42. package/dist/core/values/field.d.ts +70 -0
  43. package/dist/core/values/field.js +230 -0
  44. package/dist/core/values/gpu.d.ts +4 -2
  45. package/dist/core/values/gpu.js +11 -4
  46. package/dist/core/values/matrix.js +7 -7
  47. package/dist/core/values/num.d.ts +1 -1
  48. package/dist/core/values/num.js +1 -1
  49. package/dist/core/values/pose.js +4 -4
  50. package/dist/core/values/range.js +6 -6
  51. package/dist/core/values/str.js +8 -8
  52. package/dist/core/values/template.d.ts +1 -1
  53. package/dist/core/values/template.js +2 -1
  54. package/dist/core/values/transform.js +7 -7
  55. package/dist/core/values/tri.js +3 -3
  56. package/dist/core/values/vec.js +8 -12
  57. package/dist/ext/timeline.js +2 -2
  58. package/dist/formats/cst.d.ts +127 -0
  59. package/dist/formats/cst.js +280 -0
  60. package/dist/formats/edn.d.ts +2 -0
  61. package/dist/formats/edn.js +301 -0
  62. package/dist/formats/index.d.ts +6 -0
  63. package/dist/formats/index.js +8 -0
  64. package/dist/formats/json.d.ts +2 -0
  65. package/dist/formats/json.js +332 -0
  66. package/dist/formats/lens.d.ts +8 -0
  67. package/dist/formats/lens.js +51 -0
  68. package/dist/formats/toml.d.ts +2 -0
  69. package/dist/formats/toml.js +526 -0
  70. package/dist/formats/yaml.d.ts +2 -0
  71. package/dist/formats/yaml.js +661 -0
  72. package/dist/index.d.ts +10 -0
  73. package/dist/index.js +10 -0
  74. package/dist/jsx-dev-runtime.d.ts +2 -0
  75. package/dist/jsx-dev-runtime.js +5 -0
  76. package/dist/jsx-runtime.d.ts +54 -0
  77. package/dist/jsx-runtime.js +219 -0
  78. package/dist/learn/data.d.ts +49 -0
  79. package/dist/learn/data.js +181 -0
  80. package/dist/learn/index.d.ts +3 -0
  81. package/dist/learn/index.js +6 -0
  82. package/dist/learn/lens-net.d.ts +63 -0
  83. package/dist/learn/lens-net.js +219 -0
  84. package/dist/learn/mlp.d.ts +77 -0
  85. package/dist/learn/mlp.js +292 -0
  86. package/dist/propagators/csp.d.ts +13 -0
  87. package/dist/propagators/csp.js +52 -0
  88. package/dist/propagators/flex.d.ts +31 -0
  89. package/dist/propagators/flex.js +189 -0
  90. package/dist/propagators/graph.d.ts +73 -0
  91. package/dist/propagators/graph.js +543 -0
  92. package/dist/propagators/index.d.ts +8 -6
  93. package/dist/propagators/index.js +15 -6
  94. package/dist/propagators/lattice.d.ts +45 -0
  95. package/dist/propagators/lattice.js +113 -0
  96. package/dist/propagators/layout.d.ts +1 -27
  97. package/dist/propagators/layout.js +6 -175
  98. package/dist/propagators/numeric.d.ts +17 -0
  99. package/dist/propagators/numeric.js +93 -0
  100. package/dist/propagators/solver.d.ts +51 -0
  101. package/dist/propagators/solver.js +175 -0
  102. package/dist/schema/index.d.ts +1 -0
  103. package/dist/schema/index.js +3 -0
  104. package/dist/schema/lens.d.ts +121 -0
  105. package/dist/schema/lens.js +429 -0
  106. package/dist/shapes/annular-sector.js +4 -4
  107. package/dist/shapes/button.js +1 -1
  108. package/dist/shapes/circle.js +1 -1
  109. package/dist/shapes/drag-behaviors.d.ts +56 -0
  110. package/dist/shapes/drag-behaviors.js +102 -0
  111. package/dist/shapes/drag-spec.d.ts +52 -0
  112. package/dist/shapes/drag-spec.js +112 -0
  113. package/dist/shapes/handle.js +2 -2
  114. package/dist/shapes/index.d.ts +3 -1
  115. package/dist/shapes/index.js +3 -1
  116. package/dist/shapes/interaction.d.ts +2 -3
  117. package/dist/shapes/interaction.js +77 -56
  118. package/dist/shapes/label.js +7 -1
  119. package/dist/shapes/layout.d.ts +47 -1
  120. package/dist/shapes/layout.js +60 -2
  121. package/dist/shapes/rect.js +7 -7
  122. package/dist/shapes/shape.js +8 -8
  123. package/dist/web/diagram.js +2 -2
  124. package/package.json +24 -2
  125. package/dist/propagators/network.d.ts +0 -52
  126. package/dist/propagators/network.js +0 -185
  127. package/dist/propagators/propagator.d.ts +0 -12
  128. package/dist/propagators/propagator.js +0 -16
  129. package/dist/propagators/range.d.ts +0 -45
  130. package/dist/propagators/range.js +0 -147
  131. package/dist/propagators/relations.d.ts +0 -60
  132. package/dist/propagators/relations.js +0 -343
@@ -0,0 +1,102 @@
1
+ // drag-behaviors.ts — Dragology-style drag *modifiers* layered over the
2
+ // scene graph. The model-driven cores (`closest`, `between`, `whenFar`)
3
+ // live in `core/lenses/snap.ts`; these wire them to pointer input and the
4
+ // animation clock.
5
+ //
6
+ // The key idea, and the answer to "drag-and-drop complicates state": the
7
+ // floating offset and the spring-settle are TRANSIENT drag state, held in
8
+ // the animator, never written to the model. The model only ever sees the
9
+ // committed drop.
10
+ import { spring } from "../animation/index.js";
11
+ import { cell, derive, effect, Vec, vec, } from "../core/index.js";
12
+ import { drag } from "./interaction.js";
13
+ /** Dragology's `withFloating`: while held, `pos` follows the pointer
14
+ * directly (via the robust `drag` wiring — grab offset, touch, capture);
15
+ * on release it springs to `home` (the resolved target, e.g. a `closest`
16
+ * snap position or a layout slot). `pos` is the caller-owned display cell
17
+ * the shape renders from.
18
+ *
19
+ * const pos = vec(home.peek());
20
+ * const dot = s(circle(pos, 10));
21
+ * const { anim } = floating(dot, pos, home);
22
+ * this.anim.start(anim);
23
+ *
24
+ * While dragging, the settle spring is frozen (rate 0) so it never fights
25
+ * the pointer; on release it re-engages and eases `pos` home. */
26
+ export function floating(shape, pos, home, opts = {}) {
27
+ const dragging = cell(false);
28
+ const dispose = drag(shape, pos, dragging);
29
+ const anim = spring(pos, home, {
30
+ omega: opts.omega ?? 24,
31
+ zeta: opts.zeta ?? 0.9,
32
+ ...opts,
33
+ // Never completes (re-engages every release) and yields to the pointer
34
+ // while held.
35
+ precision: 0,
36
+ rate: () => (dragging.value ? 0 : (opts.rate?.() ?? 1)),
37
+ });
38
+ return { dragging, anim, dispose };
39
+ }
40
+ // ── drag lifecycle ──────────────────────────────────────────────────
41
+ // The `was`-flag edge and z-raise every demo hand-rolls, factored out, plus a
42
+ // model-driven driver that ties a `Drag<M>` spec (drag-spec.ts) to the
43
+ // grab→preview→commit lifecycle — the spec is built once per grab (like
44
+ // Dragology's `dragologyOnDrag`), so candidate states are enumerated then.
45
+ /** Run `grab`/`drop` on the rising/falling edge of `active`. */
46
+ export function onGesture(active, edges) {
47
+ let was = false;
48
+ return effect(() => {
49
+ const now = active.value;
50
+ if (now && !was)
51
+ edges.grab?.();
52
+ else if (!now && was)
53
+ edges.drop?.();
54
+ was = now;
55
+ });
56
+ }
57
+ /** Re-append shapes to raise them above siblings (z-order). */
58
+ export function raise(...shapes) {
59
+ for (const s of shapes)
60
+ s.el.parentElement?.appendChild(s.el);
61
+ }
62
+ /** Bind a committed `model` cell to a `Drag<M>` spec built at grab time. Owns
63
+ * the transient drag state (which element, the free pointer, the live preview)
64
+ * and commits the spec's drop on release — the demo only renders `preview`/`at`. */
65
+ export function dragModel(model, spec) {
66
+ const active = cell(null);
67
+ const pointer = vec(0, 0);
68
+ const live = cell(null);
69
+ const preview = derive(() => {
70
+ const s = live.value;
71
+ return s ? s.preview.value : model.value;
72
+ });
73
+ const at = Vec.derive(() => {
74
+ const s = live.value;
75
+ return s ? s.at.value : pointer.value;
76
+ });
77
+ const grip = (handle, id, seed, onGrab) => {
78
+ const dragging = cell(false);
79
+ const offDown = handle.on("pointerdown", () => {
80
+ pointer.value = seed();
81
+ active.value = id;
82
+ live.value = spec(id, pointer);
83
+ onGrab?.();
84
+ });
85
+ const offDrag = drag(handle, pointer, dragging);
86
+ const offEdge = onGesture(dragging, {
87
+ drop: () => {
88
+ const s = live.peek();
89
+ if (s)
90
+ model.value = s.drop.peek();
91
+ active.value = null;
92
+ live.value = null;
93
+ },
94
+ });
95
+ return () => {
96
+ offDown();
97
+ offDrag();
98
+ offEdge();
99
+ };
100
+ };
101
+ return { active, pointer, preview, at, grip };
102
+ }
@@ -0,0 +1,52 @@
1
+ import { type Read } from "../core/index.js";
2
+ type V = {
3
+ x: number;
4
+ y: number;
5
+ };
6
+ /** A drag behavior: Dragology's `DragBehavior`, reactive and parametric in the
7
+ * MODEL `M` (positions are just `M = Vec`). */
8
+ export interface Drag<M> {
9
+ /** Model rendered this frame (non-dragged elements reflow toward this). */
10
+ preview: Read<M>;
11
+ /** Model committed on release. */
12
+ drop: Read<M>;
13
+ /** Where the dragged handle sits this frame (the renderer floats it here). */
14
+ at: Read<V>;
15
+ /** Residual |pointer − achievable|; combinators arbitrate on this. */
16
+ gap: Read<number>;
17
+ }
18
+ /** `d.fixed`: a single reachable model; `locate` reads where the dragged handle
19
+ * lands in it (a layout cell, or a pure layout fn). */
20
+ declare function fixed<M>(pointer: Read<V>, state: M, locate: (m: M) => V): Drag<M>;
21
+ /** `d.vary`: a continuous family; `place` is the BACKWARD map pointer→model (a
22
+ * lens / `argminVec`, not numerical search), `gap` the residual off the family. */
23
+ declare function vary<M>(pointer: Read<V>, place: (p: V) => M, locate: (m: M) => V): Drag<M>;
24
+ /** `d.closest`: the behavior with the smallest `gap` (discrete snapping and
25
+ * continuous tracks both pick with it). */
26
+ declare function closest<M>(bs: readonly Drag<M>[]): Drag<M>;
27
+ /** `d.between`: free motion in the candidates' convex hull; preview is their
28
+ * barycentric blend (`mix` any `Lerp`/`Linear` model). Unlike `closest` it does
29
+ * NOT snap — it rests at the blend, so `drop` is the previewed mix. */
30
+ declare function between<M>(pointer: Read<V>, bs: readonly Drag<M>[], mix: (ms: readonly M[], ws: readonly number[]) => M): Drag<M>;
31
+ /** `d.whenFar`: use `near` unless its gap exceeds `radius`, then `far` (snap
32
+ * into a port, else float free). */
33
+ declare function whenFar<M>(near: Drag<M>, far: Drag<M>, radius: number): Drag<M>;
34
+ /** `d.withFloating`: the dragged handle follows the pointer while the rest
35
+ * reflow (they already do — `preview` is reactive); just an `at` override. */
36
+ declare function withFloating<M>(pointer: Read<V>, b: Drag<M>): Drag<M>;
37
+ /** `d.onDrop`: transform the committed model (create/destroy, snap-to-grid),
38
+ * the escape hatch beyond repositional drags. */
39
+ declare function onDrop<M>(b: Drag<M>, f: (m: M) => M): Drag<M>;
40
+ /** The drag-behavior DSL (Dragology's `d.`): primitives `fixed`/`vary`,
41
+ * combinators `closest`/`between`/`whenFar`, modifiers `withFloating`/`onDrop`.
42
+ * Build a `Drag<M>` once at grab; the renderer reads `preview`/`at`/`drop`. */
43
+ export declare const d: {
44
+ readonly fixed: typeof fixed;
45
+ readonly vary: typeof vary;
46
+ readonly closest: typeof closest;
47
+ readonly between: typeof between;
48
+ readonly whenFar: typeof whenFar;
49
+ readonly withFloating: typeof withFloating;
50
+ readonly onDrop: typeof onDrop;
51
+ };
52
+ export {};
@@ -0,0 +1,112 @@
1
+ // drag-spec.ts — the general drag algebra (Dragology's `d.` DSL), model-driven.
2
+ //
3
+ // Dragology's closed object isn't "snap"; it's a behavior
4
+ //
5
+ // DragBehavior = (pointer) → { preview, dropState, gap }
6
+ //
7
+ // — the whole previewed MODEL this frame, the model to commit on release, and
8
+ // the residual the combinators arbitrate on. Snapping is one primitive of it.
9
+ //
10
+ // Reactive form: the result is cells already tracking a shared pointer, so
11
+ // nothing recomputes per frame by hand. And the deep bit — "drawing knows
12
+ // state→drawing, computing knows drawing→state" (a lens) — is FREE here:
13
+ // Dragology synthesizes drawing→state by speculative rendering (render every
14
+ // candidate, extract positions; quadratic), whereas bireactive already has it
15
+ // as the backward lens, so `vary` is a lens write and `preview` is a reactive
16
+ // previewed model (springs interpolate it — no view-diffing, no re-render).
17
+ //
18
+ // Exposed as `d` to mirror the paper. It builds on core's pointer math
19
+ // (`hullWeights` for `between`, `nearestIndex`'s sticky selection mirrored in
20
+ // `closest`). Renderer contract: render non-dragged elements from `preview`,
21
+ // the dragged one at `at`, and commit `drop` on release.
22
+ import { derive, hullWeights } from "../core/index.js";
23
+ const dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
24
+ /** `d.fixed`: a single reachable model; `locate` reads where the dragged handle
25
+ * lands in it (a layout cell, or a pure layout fn). */
26
+ function fixed(pointer, state, locate) {
27
+ const s = derive(() => state);
28
+ const at = derive(() => locate(state));
29
+ return { preview: s, drop: s, at, gap: derive(() => dist(pointer.value, at.value)) };
30
+ }
31
+ /** `d.vary`: a continuous family; `place` is the BACKWARD map pointer→model (a
32
+ * lens / `argminVec`, not numerical search), `gap` the residual off the family. */
33
+ function vary(pointer, place, locate) {
34
+ const preview = derive(() => place(pointer.value));
35
+ const at = derive(() => locate(preview.value));
36
+ return { preview, drop: preview, at, gap: derive(() => dist(pointer.value, at.value)) };
37
+ }
38
+ /** `d.closest`: the behavior with the smallest `gap` (discrete snapping and
39
+ * continuous tracks both pick with it). */
40
+ function closest(bs) {
41
+ const idx = derive(() => {
42
+ let best = 0;
43
+ let bg = Number.POSITIVE_INFINITY;
44
+ for (let i = 0; i < bs.length; i++) {
45
+ const g = bs[i].gap.value;
46
+ if (g < bg) {
47
+ bg = g;
48
+ best = i;
49
+ }
50
+ }
51
+ return best;
52
+ });
53
+ return {
54
+ preview: derive(() => bs[idx.value].preview.value),
55
+ drop: derive(() => bs[idx.value].drop.value),
56
+ at: derive(() => bs[idx.value].at.value),
57
+ gap: derive(() => bs[idx.value].gap.value),
58
+ };
59
+ }
60
+ /** `d.between`: free motion in the candidates' convex hull; preview is their
61
+ * barycentric blend (`mix` any `Lerp`/`Linear` model). Unlike `closest` it does
62
+ * NOT snap — it rests at the blend, so `drop` is the previewed mix. */
63
+ function between(pointer, bs, mix) {
64
+ const ws = derive(() => hullWeights(pointer.value, bs.map(b => b.at.value)));
65
+ const at = derive(() => {
66
+ const w = ws.value;
67
+ let x = 0;
68
+ let y = 0;
69
+ bs.forEach((b, i) => {
70
+ const a = b.at.value;
71
+ x += w[i] * a.x;
72
+ y += w[i] * a.y;
73
+ });
74
+ return { x, y };
75
+ });
76
+ const blend = derive(() => mix(bs.map(b => b.preview.value), ws.value));
77
+ return { preview: blend, drop: blend, at, gap: derive(() => dist(pointer.value, at.value)) };
78
+ }
79
+ /** `d.whenFar`: use `near` unless its gap exceeds `radius`, then `far` (snap
80
+ * into a port, else float free). */
81
+ function whenFar(near, far, radius) {
82
+ const pickFar = derive(() => near.gap.value > radius);
83
+ const sel = (f) => derive(() => f(pickFar.value ? far : near).value);
84
+ return {
85
+ preview: sel(b => b.preview),
86
+ drop: sel(b => b.drop),
87
+ at: sel(b => b.at),
88
+ gap: sel(b => b.gap),
89
+ };
90
+ }
91
+ /** `d.withFloating`: the dragged handle follows the pointer while the rest
92
+ * reflow (they already do — `preview` is reactive); just an `at` override. */
93
+ function withFloating(pointer, b) {
94
+ return { preview: b.preview, drop: b.drop, gap: b.gap, at: pointer };
95
+ }
96
+ /** `d.onDrop`: transform the committed model (create/destroy, snap-to-grid),
97
+ * the escape hatch beyond repositional drags. */
98
+ function onDrop(b, f) {
99
+ return { preview: b.preview, at: b.at, gap: b.gap, drop: derive(() => f(b.drop.value)) };
100
+ }
101
+ /** The drag-behavior DSL (Dragology's `d.`): primitives `fixed`/`vary`,
102
+ * combinators `closest`/`between`/`whenFar`, modifiers `withFloating`/`onDrop`.
103
+ * Build a `Drag<M>` once at grab; the renderer reads `preview`/`at`/`drop`. */
104
+ export const d = {
105
+ fixed,
106
+ vary,
107
+ closest,
108
+ between,
109
+ whenFar,
110
+ withFloating,
111
+ onDrop,
112
+ };
@@ -1,5 +1,5 @@
1
1
  // handle.* — writable derived shapes (draggable circles wired to a Vec).
2
- import { cell, mean, polar as polarLens, Vec, } from "../core/index.js";
2
+ import { cell, mean, polar as polarLens, SKIP, Vec, } from "../core/index.js";
3
3
  import { Circle } from "./circle.js";
4
4
  import { drag } from "./interaction.js";
5
5
  const COLOR = "var(--bireactive-handle, #2563eb)";
@@ -51,7 +51,7 @@ const scaleHandle = (shape, radius = 40, opts) => {
51
51
  // Reads center and scale; writes only scale.
52
52
  const pos = Vec.lens([shape.center, shape.scale], vals => ({ x: vals[0].x + radius * vals[1].x, y: vals[0].y }), (target, vals) => {
53
53
  const k = Math.max(0.05, Math.abs(target.x - vals[0].x) / radius);
54
- return [undefined, { x: k, y: k }];
54
+ return [SKIP, { x: k, y: k }];
55
55
  });
56
56
  return handleFn(pos, { cursor: "ew-resize", ...opts });
57
57
  };
@@ -7,11 +7,13 @@ export { type ArrowOpts, arrow, connect, ensureArrowMarker } from "./connect.js"
7
7
  export { Curve, type CurveOpts, type CurveSegment, curve, ellipse } from "./curve.js";
8
8
  export { dashedPath } from "./dashed.js";
9
9
  export { debug } from "./debug.js";
10
+ export { type DragModel, dragModel, type FloatingOpts, type FloatingResult, floating, onGesture, raise, } from "./drag-behaviors.js";
11
+ export { type Drag, d } from "./drag-spec.js";
10
12
  export { group } from "./group.js";
11
13
  export { type HandleOpts, handle } from "./handle.js";
12
14
  export { cursor, drag, draggable, dragRotate, dragWithState, hoverSignal } from "./interaction.js";
13
15
  export { Label, type LabelOpts, label } from "./label.js";
14
- export { type ArrangeOpts, arrange, expand, grid, split } from "./layout.js";
16
+ export { type ArrangeOpts, arrange, expand, grid, split, type TreeStack, type TreeStackBox, type TreeStackOpts, treeStack, } from "./layout.js";
15
17
  export { Line, type LineOpts, line } from "./line.js";
16
18
  export { type ForEachOptions, forEach } from "./list.js";
17
19
  export { type Mount, mount } from "./mount.js";
@@ -7,11 +7,13 @@ export { arrow, connect, ensureArrowMarker } from "./connect.js";
7
7
  export { Curve, curve, ellipse } from "./curve.js";
8
8
  export { dashedPath } from "./dashed.js";
9
9
  export { debug } from "./debug.js";
10
+ export { dragModel, floating, onGesture, raise, } from "./drag-behaviors.js";
11
+ export { d } from "./drag-spec.js";
10
12
  export { group } from "./group.js";
11
13
  export { handle } from "./handle.js";
12
14
  export { cursor, drag, draggable, dragRotate, dragWithState, hoverSignal } from "./interaction.js";
13
15
  export { Label, label } from "./label.js";
14
- export { arrange, expand, grid, split } from "./layout.js";
16
+ export { arrange, expand, grid, split, treeStack, } from "./layout.js";
15
17
  export { Line, line } from "./line.js";
16
18
  export { forEach } from "./list.js";
17
19
  export { mount } from "./mount.js";
@@ -19,9 +19,8 @@ export declare function draggable(handle: AnyShape, onDrag: (local: Inner<Vec>)
19
19
  * `shape.translate`. Grab offset is captured on pointerdown; optional
20
20
  * `dragging` reports active state. Defaults `cursor` to `"grab"`. */
21
21
  export declare function drag(shape: AnyShape, target: Writable<Vec>, dragging?: Writable<Cell<boolean>>): () => void;
22
- /** Wrap a `drag(shape, target)` call and return a local `dragging`
23
- * Cell<boolean>. Sugar for "give me a drag handle that exposes its
24
- * own state." */
22
+ /** Wrap a `drag(shape, target)` call and return a local `dragging` `Cell<boolean>`.
23
+ * Sugar for "give me a drag handle that exposes its own state." */
25
24
  export declare function dragWithState(shape: AnyShape, target: Writable<Vec>): {
26
25
  dragging: Cell<boolean>;
27
26
  dispose: () => void;
@@ -63,43 +63,51 @@ export function cursor(shape, init) {
63
63
  * `onState(active)` callback fires `true` on pointerdown and `false`
64
64
  * on pointerup/cancel — `Handle` uses it to drive `.dragging`. */
65
65
  export function draggable(handle, onDrag, onState) {
66
- let dragging = false;
67
66
  let pointerId = -1;
68
67
  let unblock = null;
69
68
  ownTouchGesture(handle);
70
- const offs = [];
71
- offs.push(handle.on("pointerdown", e => {
72
- const pe = e;
73
- dragging = true;
74
- pointerId = pe.pointerId;
75
- handle.el.setPointerCapture(pointerId);
76
- unblock = blockPageScroll();
77
- onState?.(true);
78
- onDrag(handle.toLocal(pe));
79
- }));
80
- offs.push(handle.on("pointermove", e => {
81
- if (!dragging)
69
+ const onMove = (e) => {
70
+ if (pointerId === -1 || e.pointerId !== pointerId)
82
71
  return;
83
72
  onDrag(handle.toLocal(e));
84
- }));
85
- const stop = () => {
86
- if (dragging && pointerId !== -1) {
87
- try {
88
- handle.el.releasePointerCapture(pointerId);
89
- }
90
- catch {
91
- /* ok */
92
- }
73
+ };
74
+ const stop = (e) => {
75
+ if (pointerId === -1 || (e && e.pointerId !== pointerId))
76
+ return;
77
+ try {
78
+ handle.el.releasePointerCapture(pointerId);
79
+ }
80
+ catch {
81
+ /* ok */
93
82
  }
94
- dragging = false;
95
83
  pointerId = -1;
84
+ window.removeEventListener("pointermove", onMove);
85
+ window.removeEventListener("pointerup", stop);
86
+ window.removeEventListener("pointercancel", stop);
96
87
  unblock?.();
97
88
  unblock = null;
98
89
  onState?.(false);
99
90
  };
100
- offs.push(handle.on("pointerup", stop));
101
- offs.push(handle.on("pointercancel", stop));
102
- return () => offs.forEach(d => d());
91
+ const offDown = handle.on("pointerdown", e => {
92
+ const pe = e;
93
+ pointerId = pe.pointerId;
94
+ try {
95
+ handle.el.setPointerCapture(pointerId);
96
+ }
97
+ catch {
98
+ /* ok */
99
+ }
100
+ unblock = blockPageScroll();
101
+ window.addEventListener("pointermove", onMove);
102
+ window.addEventListener("pointerup", stop);
103
+ window.addEventListener("pointercancel", stop);
104
+ onState?.(true);
105
+ onDrag(handle.toLocal(pe));
106
+ });
107
+ return () => {
108
+ offDown();
109
+ stop();
110
+ };
103
111
  }
104
112
  /** Bind pointer drag on `shape` directly to a writable `Vec` (no handle dot);
105
113
  * returns a disposer. `target` is in the SVG-root frame and coords are read
@@ -114,47 +122,60 @@ export function drag(shape, target, dragging) {
114
122
  let dy = 0;
115
123
  let pointerId = -1;
116
124
  let unblock = null;
117
- const offs = [];
118
- offs.push(shape.on("pointerdown", e => {
125
+ // Moves/ups are tracked on `window`, not the shape: pointer capture alone
126
+ // drops the gesture when the element is re-parented (z-raising) or the
127
+ // pointer outruns the shape, so a window listener is the reliable path.
128
+ const onMove = (e) => {
129
+ if (pointerId === -1 || e.pointerId !== pointerId)
130
+ return;
131
+ const world = shape.toWorld(e);
132
+ target.value = { x: world.x - dx, y: world.y - dy };
133
+ };
134
+ const stop = (e) => {
135
+ if (pointerId === -1 || (e && e.pointerId !== pointerId))
136
+ return;
137
+ try {
138
+ shape.el.releasePointerCapture(pointerId);
139
+ }
140
+ catch {
141
+ /* ok */
142
+ }
143
+ pointerId = -1;
144
+ window.removeEventListener("pointermove", onMove);
145
+ window.removeEventListener("pointerup", stop);
146
+ window.removeEventListener("pointercancel", stop);
147
+ unblock?.();
148
+ unblock = null;
149
+ if (dragging)
150
+ dragging.value = false;
151
+ };
152
+ const offDown = shape.on("pointerdown", e => {
119
153
  const pe = e;
120
154
  pointerId = pe.pointerId;
121
- shape.el.setPointerCapture(pointerId);
155
+ try {
156
+ shape.el.setPointerCapture(pointerId);
157
+ }
158
+ catch {
159
+ /* ok */
160
+ }
122
161
  unblock = blockPageScroll();
123
162
  const world = shape.toWorld(pe);
124
163
  const v = target.value;
125
164
  dx = world.x - v.x;
126
165
  dy = world.y - v.y;
166
+ window.addEventListener("pointermove", onMove);
167
+ window.addEventListener("pointerup", stop);
168
+ window.addEventListener("pointercancel", stop);
127
169
  if (dragging)
128
170
  dragging.value = true;
129
- }));
130
- offs.push(shape.on("pointermove", e => {
131
- if (pointerId === -1)
132
- return;
133
- const world = shape.toWorld(e);
134
- target.value = { x: world.x - dx, y: world.y - dy };
135
- }));
136
- const stop = () => {
137
- if (pointerId !== -1) {
138
- try {
139
- shape.el.releasePointerCapture(pointerId);
140
- }
141
- catch {
142
- /* ok */
143
- }
144
- pointerId = -1;
145
- }
146
- unblock?.();
147
- unblock = null;
148
- if (dragging)
149
- dragging.value = false;
171
+ });
172
+ return () => {
173
+ offDown();
174
+ stop();
150
175
  };
151
- offs.push(shape.on("pointerup", stop));
152
- offs.push(shape.on("pointercancel", stop));
153
- return () => offs.forEach(d => d());
154
176
  }
155
- /** Wrap a `drag(shape, target)` call and return a local `dragging`
156
- * Cell<boolean>. Sugar for "give me a drag handle that exposes its
157
- * own state." */
177
+ /** Wrap a `drag(shape, target)` call and return a local `dragging` `Cell<boolean>`.
178
+ * Sugar for "give me a drag handle that exposes its own state." */
158
179
  export function dragWithState(shape, target) {
159
180
  const dragging = cell(false);
160
181
  const dispose = drag(shape, target, dragging);
@@ -14,7 +14,7 @@ export class Label extends Shape {
14
14
  : typeof content === "function"
15
15
  ? derive(content)
16
16
  : cell(content);
17
- const sizeSig = Num.from(opts.size ?? tokens.fontSize);
17
+ const sizeSig = Num.coerce(opts.size ?? tokens.fontSize);
18
18
  const a = opts.align ?? { x: 0.5, y: 0.5 };
19
19
  super("text", () => {
20
20
  const text = flattenText(contentSig.value);
@@ -34,6 +34,12 @@ export class Label extends Shape {
34
34
  this.attr("dominant-baseline", yAttr(a.y));
35
35
  if (opts.bold)
36
36
  this.attr("font-weight", 700);
37
+ // Labels are decorative: never select on drag, never steal a pointer from
38
+ // the shape underneath (so text over a draggable doesn't break its grab).
39
+ const style = this.intrinsic.style;
40
+ style.userSelect = "none";
41
+ style.setProperty("-webkit-user-select", "none");
42
+ style.pointerEvents = "none";
37
43
  this.effect(() => {
38
44
  this.intrinsic.innerHTML = renderContent(contentSig.value);
39
45
  });
@@ -1,4 +1,4 @@
1
- import { Box, type Val } from "../core/index.js";
1
+ import { Box, type Read, type Val } from "../core/index.js";
2
2
  import type { Shape } from "./shape.js";
3
3
  export interface ArrangeOpts {
4
4
  /** Spacing between adjacent bounding boxes. Default 0. */
@@ -27,3 +27,49 @@ export declare function split(source: Box, axis: "x" | "y", parts: number | numb
27
27
  export declare function grid(source: Box, rows: number, cols: number, opts?: {
28
28
  gap?: Val<number>;
29
29
  }): Box[][];
30
+ export interface TreeStackBox {
31
+ x: number;
32
+ y: number;
33
+ w: number;
34
+ h: number;
35
+ }
36
+ export interface TreeStackOpts<Id> {
37
+ /** Top-level node ids, in order (read inside a derive; cell reads track). */
38
+ roots: () => readonly Id[];
39
+ /** A container's children, in order (read inside a derive). */
40
+ kids: (id: Id) => readonly Id[];
41
+ /** Containers stack their `kids`; non-containers are sized by `leaf`. */
42
+ container: (id: Id) => boolean;
43
+ /** Intrinsic size of a non-container node. */
44
+ leaf: (id: Id) => {
45
+ w: number;
46
+ h: number;
47
+ };
48
+ /** Top-left of a root node. */
49
+ origin: (id: Id) => {
50
+ x: number;
51
+ y: number;
52
+ };
53
+ /** Space reserved above a container's children (a title bar). Default 0. */
54
+ header?: number;
55
+ /** Inset around a container's children. Default 0. */
56
+ pad?: number;
57
+ /** Space between adjacent children. Default 0. */
58
+ gap?: number;
59
+ /** Minimum container width. Default 0. */
60
+ minWidth?: number;
61
+ /** Height of an empty container. Default `header + 2·pad`. */
62
+ emptyHeight?: number;
63
+ }
64
+ export interface TreeStack<Id> {
65
+ /** Reactive placement of every reachable node. */
66
+ readonly boxes: Read<Map<Id, TreeStackBox>>;
67
+ /** A node's box as a reactive `Box` (zero box when absent). */
68
+ box(id: Id): Box;
69
+ }
70
+ /** Intrinsic ("hug-contents") layout of a tree as nested vertical stacks:
71
+ * each container's size is the bottom-up sum of its children, each child is
72
+ * placed top-down from its container. Unlike `row`/`col` (which fit items
73
+ * into a fixed container), the containers grow to fit. Pure function of the
74
+ * inputs, so feeding it a *previewed* tree yields a previewed layout. */
75
+ export declare function treeStack<Id>(opts: TreeStackOpts<Id>): TreeStack<Id>;
@@ -1,5 +1,5 @@
1
1
  // Spatial composition primitives.
2
- import { Box, BoxMath, reader, transformBox } from "../core/index.js";
2
+ import { Box, boxExpand, derive, reader, transformBox, } from "../core/index.js";
3
3
  /** Lay out `shapes` in a row/column. First stays put; the rest bind
4
4
  * their `translate` reactively to sit `gap` past the previous.
5
5
  * Reflows on size or anchor change. */
@@ -40,7 +40,7 @@ export function arrange(shapes, axis, opts = {}) {
40
40
  /** Inflate a Box on each side by `by`. */
41
41
  export function expand(b, by) {
42
42
  const byFn = reader(by);
43
- return Box.derive(() => BoxMath.expand(b.value, byFn()));
43
+ return Box.derive(() => boxExpand(b.value, byFn()));
44
44
  }
45
45
  /** Split a Box along an axis into N reactive sub-Boxes.
46
46
  *
@@ -72,3 +72,61 @@ export function split(source, axis, parts, opts = {}) {
72
72
  export function grid(source, rows, cols, opts = {}) {
73
73
  return split(source, "y", rows, opts).map(row => split(row, "x", cols, opts));
74
74
  }
75
+ /** Intrinsic ("hug-contents") layout of a tree as nested vertical stacks:
76
+ * each container's size is the bottom-up sum of its children, each child is
77
+ * placed top-down from its container. Unlike `row`/`col` (which fit items
78
+ * into a fixed container), the containers grow to fit. Pure function of the
79
+ * inputs, so feeding it a *previewed* tree yields a previewed layout. */
80
+ export function treeStack(opts) {
81
+ const header = opts.header ?? 0;
82
+ const pad = opts.pad ?? 0;
83
+ const gap = opts.gap ?? 0;
84
+ const minW = opts.minWidth ?? 0;
85
+ const emptyH = opts.emptyHeight ?? header + 2 * pad;
86
+ const measure = (id) => {
87
+ if (!opts.container(id))
88
+ return opts.leaf(id);
89
+ const ks = opts.kids(id);
90
+ if (ks.length === 0)
91
+ return { w: minW, h: emptyH };
92
+ let maxw = 0;
93
+ let h = header + pad;
94
+ for (const c of ks) {
95
+ const m = measure(c);
96
+ maxw = Math.max(maxw, m.w);
97
+ h += m.h + gap;
98
+ }
99
+ return { w: Math.max(minW, maxw + 2 * pad), h: h + pad - gap };
100
+ };
101
+ const place = (id, x, y, out) => {
102
+ const m = measure(id);
103
+ out.set(id, { x, y, w: m.w, h: m.h });
104
+ if (opts.container(id)) {
105
+ let cy = y + header + pad;
106
+ for (const c of opts.kids(id)) {
107
+ place(c, x + pad, cy, out);
108
+ cy += measure(c).h + gap;
109
+ }
110
+ }
111
+ };
112
+ const boxes = derive(() => {
113
+ const out = new Map();
114
+ for (const r of opts.roots()) {
115
+ const o = opts.origin(r);
116
+ place(r, o.x, o.y, out);
117
+ }
118
+ return out;
119
+ });
120
+ const cache = new Map();
121
+ return {
122
+ boxes,
123
+ box(id) {
124
+ let b = cache.get(id);
125
+ if (!b) {
126
+ b = Box.derive(() => boxes.value.get(id) ?? { x: 0, y: 0, w: 0, h: 0 });
127
+ cache.set(id, b);
128
+ }
129
+ return b;
130
+ },
131
+ };
132
+ }