bireactive 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/dist/animation/anim.js +4 -0
  2. package/dist/coll.d.ts +7 -7
  3. package/dist/coll.js +3 -1
  4. package/dist/core/cell.d.ts +89 -66
  5. package/dist/core/cell.js +642 -401
  6. package/dist/core/index.d.ts +4 -14
  7. package/dist/core/index.js +4 -14
  8. package/dist/core/lenses/aggregates.d.ts +1 -1
  9. package/dist/core/lenses/aggregates.js +4 -3
  10. package/dist/core/lenses/closed-form-policies.js +6 -6
  11. package/dist/core/lenses/decompositions.js +3 -3
  12. package/dist/core/lenses/domain-aggregates.js +5 -5
  13. package/dist/core/lenses/geometry.d.ts +1 -1
  14. package/dist/core/lenses/geometry.js +6 -7
  15. package/dist/core/lenses/memory.d.ts +2 -2
  16. package/dist/core/lenses/memory.js +3 -3
  17. package/dist/core/lenses/typed-factor.js +4 -3
  18. package/dist/core/traits.d.ts +1 -0
  19. package/dist/core/values/box.js +7 -7
  20. package/dist/core/values/color.js +5 -5
  21. package/dist/core/values/field.d.ts +70 -0
  22. package/dist/core/values/field.js +230 -0
  23. package/dist/core/values/gpu.d.ts +4 -2
  24. package/dist/core/values/gpu.js +11 -4
  25. package/dist/core/values/matrix.js +7 -7
  26. package/dist/core/values/num.d.ts +1 -1
  27. package/dist/core/values/num.js +1 -1
  28. package/dist/core/values/pose.js +4 -4
  29. package/dist/core/values/range.js +6 -6
  30. package/dist/core/values/template.d.ts +1 -1
  31. package/dist/core/values/template.js +2 -1
  32. package/dist/core/values/transform.js +7 -7
  33. package/dist/core/values/tri.js +3 -3
  34. package/dist/core/values/vec.js +8 -12
  35. package/dist/ext/timeline.js +2 -2
  36. package/dist/formats/cst.d.ts +127 -0
  37. package/dist/formats/cst.js +280 -0
  38. package/dist/formats/edn.d.ts +2 -0
  39. package/dist/formats/edn.js +301 -0
  40. package/dist/formats/index.d.ts +6 -0
  41. package/dist/formats/index.js +8 -0
  42. package/dist/formats/json.d.ts +2 -0
  43. package/dist/formats/json.js +332 -0
  44. package/dist/formats/lens.d.ts +8 -0
  45. package/dist/formats/lens.js +54 -0
  46. package/dist/formats/toml.d.ts +2 -0
  47. package/dist/formats/toml.js +526 -0
  48. package/dist/formats/yaml.d.ts +2 -0
  49. package/dist/formats/yaml.js +661 -0
  50. package/dist/index.d.ts +10 -0
  51. package/dist/index.js +10 -0
  52. package/dist/learn/data.d.ts +49 -0
  53. package/dist/learn/data.js +181 -0
  54. package/dist/learn/index.d.ts +3 -0
  55. package/dist/learn/index.js +6 -0
  56. package/dist/learn/lens-net.d.ts +63 -0
  57. package/dist/learn/lens-net.js +219 -0
  58. package/dist/learn/mlp.d.ts +77 -0
  59. package/dist/learn/mlp.js +292 -0
  60. package/dist/propagators/csp.d.ts +13 -0
  61. package/dist/propagators/csp.js +52 -0
  62. package/dist/propagators/flex.d.ts +31 -0
  63. package/dist/propagators/flex.js +189 -0
  64. package/dist/propagators/graph.d.ts +73 -0
  65. package/dist/propagators/graph.js +543 -0
  66. package/dist/propagators/index.d.ts +8 -6
  67. package/dist/propagators/index.js +15 -6
  68. package/dist/propagators/lattice.d.ts +45 -0
  69. package/dist/propagators/lattice.js +113 -0
  70. package/dist/propagators/layout.d.ts +1 -27
  71. package/dist/propagators/layout.js +6 -175
  72. package/dist/propagators/numeric.d.ts +17 -0
  73. package/dist/propagators/numeric.js +93 -0
  74. package/dist/propagators/solver.d.ts +51 -0
  75. package/dist/propagators/solver.js +175 -0
  76. package/dist/schema/index.d.ts +1 -0
  77. package/dist/schema/index.js +3 -0
  78. package/dist/schema/lens.d.ts +121 -0
  79. package/dist/schema/lens.js +429 -0
  80. package/dist/shapes/annular-sector.js +4 -4
  81. package/dist/shapes/button.js +1 -1
  82. package/dist/shapes/circle.js +1 -1
  83. package/dist/shapes/handle.js +2 -2
  84. package/dist/shapes/label.js +1 -1
  85. package/dist/shapes/layout.js +2 -2
  86. package/dist/shapes/rect.js +7 -7
  87. package/dist/shapes/shape.js +8 -8
  88. package/dist/tex/tex.js +9 -2
  89. package/dist/web/diagram.js +2 -2
  90. package/package.json +9 -19
  91. package/dist/propagators/network.d.ts +0 -52
  92. package/dist/propagators/network.js +0 -185
  93. package/dist/propagators/propagator.d.ts +0 -12
  94. package/dist/propagators/propagator.js +0 -16
  95. package/dist/propagators/range.d.ts +0 -45
  96. package/dist/propagators/range.js +0 -147
  97. package/dist/propagators/relations.d.ts +0 -60
  98. package/dist/propagators/relations.js +0 -343
@@ -0,0 +1,230 @@
1
+ // field.ts — dense, GPU-resident reactive field: a grid of `T` as ONE cell.
2
+ //
3
+ // A Field is a single reactive node, not one-per-texel: the value is a header
4
+ // {tex, w, h, epoch} (the Canvas handle trick, see gpu.ts), the graph compares
5
+ // `epoch`, and no pixel crosses the bus. So a 1000×1000 field costs the engine
6
+ // exactly what a 4×4 does — graph size is the lens-chain length, independent of
7
+ // resolution. Per-frame work is allocation-free: `evolve`/`splat` ping-pong
8
+ // through reused scratch textures and mint only the small header.
9
+ //
10
+ // Generic over the texel encoding via `Kind<T>` (channel `dim` + the `Pack`
11
+ // codec + the boundary cell class). The dense interior stays a flat RGBA32F
12
+ // texture forever; `T` (Num/Vec/Color) is instantiated ONLY at the boundary —
13
+ // `mean`, `regionMean` — never per-texel. `dim` (1/2/4) selects the channel
14
+ // mask: scalar (heatmap / SDF / density), vector (flow / gradient), colour
15
+ // (i.e. a Canvas).
16
+ //
17
+ // The point of interest is the GPU↔reactivity bridge. `evolve` runs a sim on
18
+ // its own clock (a raf/Anim loop), committing one new value per frame; the
19
+ // reductions (`mean`, `regionMean`) are ordinary read-only derived cells, so
20
+ // they recompute when the field's epoch changes and only *propagate* when their
21
+ // scalar actually moves. Reactive logic downstream (`density > 0.5` → effect)
22
+ // is thereby loosely coupled to time: it observes a continuously-running GPU
23
+ // simulation without knowing a frame exists.
24
+ import { Cell, reader } from "../cell.js";
25
+ import { Canvas, stamp as canvasStamp } from "./canvas.js";
26
+ import { Color } from "./color.js";
27
+ import { newTex, pass, reduceMean, scratch, scratch2 } from "./gpu.js";
28
+ import { Num } from "./num.js";
29
+ import { Vec } from "./vec.js";
30
+ let EPOCH = 0;
31
+ /** Stamp a texture with a fresh epoch — the only way to mint a value. */
32
+ const stamp = (tex, w, h) => ({
33
+ tex,
34
+ w,
35
+ h,
36
+ epoch: ++EPOCH,
37
+ });
38
+ const equals = (a, b) => a.epoch === b.epoch;
39
+ /** Scalar field (1 channel) — heatmaps, SDFs, density, height. */
40
+ export const Scalar = { dim: 1, pack: Num.traits.pack, cls: Num };
41
+ /** Vector field (2 channels) — flow, gradient, displacement, RD chemicals. */
42
+ export const Vector = { dim: 2, pack: Vec.traits.pack, cls: Vec };
43
+ /** Colour field (4 channels) — i.e. a Canvas. */
44
+ export const Colour = {
45
+ dim: 4,
46
+ pack: Color.traits.pack,
47
+ cls: Color,
48
+ };
49
+ const HEAD = `#version 300 es
50
+ precision highp float;
51
+ in vec2 v_uv;
52
+ out vec4 o;`;
53
+ const MASK = `${HEAD}
54
+ uniform sampler2D u_src; uniform vec2 u_res; uniform vec2 u_o; uniform vec2 u_wh;
55
+ void main() {
56
+ vec2 px = v_uv * u_res;
57
+ vec4 s = texture(u_src, v_uv);
58
+ bool inside = px.x >= u_o.x && px.y >= u_o.y && px.x < u_o.x + u_wh.x && px.y < u_o.y + u_wh.y;
59
+ o = inside ? vec4(s.rgb, 1.0) : vec4(0.0);
60
+ }`;
61
+ const SPLAT = `${HEAD}
62
+ uniform sampler2D u_src; uniform vec2 u_res; uniform vec2 u_c; uniform float u_r;
63
+ uniform vec3 u_v; uniform float u_mix;
64
+ void main() {
65
+ vec4 s = texture(u_src, v_uv);
66
+ float d = distance(v_uv * u_res, u_c);
67
+ float f = clamp(exp(-(d * d) / (u_r * u_r)) * u_mix, 0.0, 1.0);
68
+ o = vec4(mix(s.rgb, u_v, f), s.a);
69
+ }`;
70
+ /** GLSL float literal (always carries a decimal point). */
71
+ const glf = (n) => (Number.isInteger(n) ? `${n}.0` : String(n));
72
+ const glv3 = (c) => `${glf(c[0])}, ${glf(c[1])}, ${glf(c[2])}`;
73
+ /** Build a piecewise-linear ramp shader over `channel` (baked per stop set,
74
+ * cached by source string in gpu.ts's program cache). */
75
+ function rampFrag(channel, stops) {
76
+ const ch = "rgba"[channel] ?? "r";
77
+ let body = ` vec3 c = vec3(${glv3(stops[0][1])});\n`;
78
+ for (let i = 1; i < stops.length; i++) {
79
+ const p0 = stops[i - 1][0];
80
+ const [p1, col] = stops[i];
81
+ body += ` c = mix(c, vec3(${glv3(col)}), smoothstep(${glf(p0)}, ${glf(p1)}, x));\n`;
82
+ }
83
+ return `${HEAD}
84
+ uniform sampler2D u_src;
85
+ void main() {
86
+ float x = texture(u_src, v_uv).${ch};
87
+ ${body} o = vec4(c, 1.0);
88
+ }`;
89
+ }
90
+ /** Read-only typed view via the kind's boundary class (`Num`/`Vec`/`Color`).
91
+ * Cast through the concrete `derive` signature — the static's polymorphic
92
+ * `this` rejects the abstract `Ctor<T>`, but at runtime `k.cls` is concrete. */
93
+ function deriveT(k, parent, fn) {
94
+ const d = k.cls;
95
+ return d.derive(parent, fn);
96
+ }
97
+ const TMP = new Float64Array(4);
98
+ /** Pack a reduction's `[r,g,b,a]` mean into a boundary value of kind `k`. */
99
+ function packT(k, m) {
100
+ TMP[0] = m[0];
101
+ TMP[1] = m[1];
102
+ TMP[2] = m[2];
103
+ TMP[3] = m[3];
104
+ return k.pack.write(TMP, 0);
105
+ }
106
+ function bindUniform(s, name, val) {
107
+ if (typeof val === "number")
108
+ s.f(name, val);
109
+ else if (val.length === 2)
110
+ s.v2(name, val[0], val[1]);
111
+ else if (val.length === 3)
112
+ s.v3(name, val[0], val[1], val[2]);
113
+ else
114
+ throw new Error(`field.evolve: uniform ${name} must be number | vec2 | vec3`);
115
+ }
116
+ const EMPTY = { tex: null, w: 0, h: 0, epoch: 0 };
117
+ export class Field extends Cell {
118
+ kind;
119
+ static traits = { equals };
120
+ /** Feedback-safe ping-pong pair for `evolve`/`splat`, lazily allocated. */
121
+ io = null;
122
+ constructor(kind, v = EMPTY) {
123
+ super(v, { equals });
124
+ this.kind = kind;
125
+ }
126
+ pingTex() {
127
+ return (this.io ??= scratch2());
128
+ }
129
+ /** Step the field by `steps` GPU passes of `frag` (which samples the current
130
+ * field as `u_src`; `u_texel` = 1/size is provided). One value is committed
131
+ * after the substeps, so the reactive graph sees a single new epoch/frame. */
132
+ evolve(frag, uniforms, steps = 1) {
133
+ const ping = this.pingTex();
134
+ const v = this.peek();
135
+ const w = v.w;
136
+ const h = v.h;
137
+ let src = v.tex;
138
+ let last = null;
139
+ const entries = uniforms ? Object.entries(uniforms) : null;
140
+ for (let i = 0; i < steps; i++) {
141
+ const dst = ping(w, h, src);
142
+ pass(frag, dst, s => {
143
+ s.tex("u_src", 0, src);
144
+ s.v2("u_texel", 1 / w, 1 / h);
145
+ if (entries)
146
+ for (const [name, val] of entries)
147
+ bindUniform(s, name, val);
148
+ });
149
+ src = dst.tex;
150
+ last = dst;
151
+ }
152
+ if (last)
153
+ this.value = stamp(last.tex, w, h);
154
+ }
155
+ /** Stamp a Gaussian disc of `value` at data pixel `(x, y)`, radius `r`. The
156
+ * raster interaction primitive — seed a sim, inject, paint density. */
157
+ splat(x, y, r, value, strength = 1) {
158
+ const ping = this.pingTex();
159
+ const v = this.peek();
160
+ this.kind.pack.read(value, TMP, 0);
161
+ const dst = ping(v.w, v.h, v.tex);
162
+ pass(SPLAT, dst, s => {
163
+ s.tex("u_src", 0, v.tex);
164
+ s.v2("u_res", v.w, v.h);
165
+ s.v2("u_c", x, y);
166
+ s.f("u_r", r);
167
+ s.v3("u_v", TMP[0], TMP[1], TMP[2]);
168
+ s.f("u_mix", strength);
169
+ });
170
+ this.value = stamp(dst.tex, v.w, v.h);
171
+ }
172
+ /** Whole-field mean as a read-only `T` cell. Reactive: recomputes on every
173
+ * new epoch, propagates only when the value moves. One 1×1 GPU readback. */
174
+ mean() {
175
+ const k = this.kind;
176
+ return deriveT(k, this, v => packT(k, reduceMean({ tex: v.tex, w: v.w, h: v.h })));
177
+ }
178
+ /** Mean over a sub-rectangle (data pixels, reactive `box`) as a read-only
179
+ * `T` cell — the field's "density of this region" observation. */
180
+ regionMean(box) {
181
+ const k = this.kind;
182
+ const bf = reader(box);
183
+ const sc = scratch();
184
+ return deriveT(k, this, v => {
185
+ const b = bf();
186
+ const dst = sc(v.w, v.h);
187
+ pass(MASK, dst, s => {
188
+ s.tex("u_src", 0, v.tex);
189
+ s.v2("u_res", v.w, v.h);
190
+ s.v2("u_o", b.x, b.y);
191
+ s.v2("u_wh", b.w, b.h);
192
+ });
193
+ const m = reduceMean({ tex: dst.tex, w: v.w, h: v.h });
194
+ const cov = m[3];
195
+ if (cov < 1e-6)
196
+ return packT(k, [0, 0, 0, 0]);
197
+ return packT(k, [m[0] / cov, m[1] / cov, m[2] / cov, 1]);
198
+ });
199
+ }
200
+ /** Render `channel` through a colormap to a read-only `Canvas`. Reactive:
201
+ * re-renders on every new epoch, so rendering is itself a reactive edge. */
202
+ colormap(channel, stops) {
203
+ const frag = rampFrag(channel, stops);
204
+ const sc = scratch();
205
+ return Canvas.derive(this, (v) => {
206
+ const out = sc(v.w, v.h);
207
+ pass(frag, out, s => s.tex("u_src", 0, v.tex));
208
+ return canvasStamp(out.tex, v.w, v.h);
209
+ });
210
+ }
211
+ }
212
+ /** Writable `Field<T>` of size `w×h`. `painter` fills each texel (returns a
213
+ * `T`); omit for an all-zero field. Linear-filtered for continuous sampling. */
214
+ export function field(kind, w, h, painter) {
215
+ const buf = new Float32Array(w * h * 4);
216
+ if (painter !== undefined) {
217
+ const t = new Float64Array(4);
218
+ let o = 0;
219
+ for (let y = 0; y < h; y++) {
220
+ for (let x = 0; x < w; x++, o += 4) {
221
+ kind.pack.read(painter(x, y), t, 0);
222
+ for (let c = 0; c < kind.dim; c++)
223
+ buf[o + c] = t[c];
224
+ if (kind.dim < 4)
225
+ buf[o + 3] = 1;
226
+ }
227
+ }
228
+ }
229
+ return new Field(kind, stamp(newTex(w, h, buf, true), w, h));
230
+ }
@@ -6,8 +6,10 @@ export interface Tex {
6
6
  w: number;
7
7
  h: number;
8
8
  }
9
- /** Allocate an RGBA32F texture (`data` in 0–1 floats, or null). */
10
- export declare function newTex(w: number, h: number, data?: Float32Array | null): WebGLTexture;
9
+ /** Allocate an RGBA32F texture (`data` in 0–1 floats, or null). `linear`
10
+ * requests bilinear filtering (continuous Field sampling), silently
11
+ * downgraded to NEAREST where `OES_texture_float_linear` is unavailable. */
12
+ export declare function newTex(w: number, h: number, data?: Float32Array | null, linear?: boolean): WebGLTexture;
11
13
  export declare function disposeTex(t: WebGLTexture): void;
12
14
  /** A reusable owned texture, reallocated on size change — the GPU analog of
13
15
  * the CPU scratch buffer. */
@@ -21,9 +21,13 @@ export function gl() {
21
21
  throw new Error("WebGL2 unavailable");
22
22
  if (!ctx.getExtension("EXT_color_buffer_float"))
23
23
  throw new Error("EXT_color_buffer_float unavailable");
24
+ // Optional: linear filtering of RGBA32F (continuous Field sampling/render).
25
+ // Absent on some mobile GPUs — `newTex(..., linear)` falls back to NEAREST.
26
+ _floatLinear = ctx.getExtension("OES_texture_float_linear") !== null;
24
27
  _gl = ctx;
25
28
  return ctx;
26
29
  }
30
+ let _floatLinear = false;
27
31
  const QUAD = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);
28
32
  const VERT = `#version 300 es
29
33
  in vec2 a_pos;
@@ -81,14 +85,17 @@ function fbo() {
81
85
  _fbo = gl().createFramebuffer();
82
86
  return _fbo;
83
87
  }
84
- /** Allocate an RGBA32F texture (`data` in 0–1 floats, or null). */
85
- export function newTex(w, h, data = null) {
88
+ /** Allocate an RGBA32F texture (`data` in 0–1 floats, or null). `linear`
89
+ * requests bilinear filtering (continuous Field sampling), silently
90
+ * downgraded to NEAREST where `OES_texture_float_linear` is unavailable. */
91
+ export function newTex(w, h, data = null, linear = false) {
86
92
  const g = gl();
87
93
  const t = g.createTexture();
88
94
  g.bindTexture(g.TEXTURE_2D, t);
89
95
  g.texImage2D(g.TEXTURE_2D, 0, g.RGBA32F, w, h, 0, g.RGBA, g.FLOAT, data);
90
- g.texParameteri(g.TEXTURE_2D, g.TEXTURE_MIN_FILTER, g.NEAREST);
91
- g.texParameteri(g.TEXTURE_2D, g.TEXTURE_MAG_FILTER, g.NEAREST);
96
+ const filter = linear && _floatLinear ? g.LINEAR : g.NEAREST;
97
+ g.texParameteri(g.TEXTURE_2D, g.TEXTURE_MIN_FILTER, filter);
98
+ g.texParameteri(g.TEXTURE_2D, g.TEXTURE_MAG_FILTER, filter);
92
99
  g.texParameteri(g.TEXTURE_2D, g.TEXTURE_WRAP_S, g.CLAMP_TO_EDGE);
93
100
  g.texParameteri(g.TEXTURE_2D, g.TEXTURE_WRAP_T, g.CLAMP_TO_EDGE);
94
101
  return t;
@@ -5,7 +5,7 @@
5
5
  // compile time. Two invertibles, both `: this` via `Cell#lens`:
6
6
  // - `multiply(b)` — inverse multiplies by `invert(b)`
7
7
  // - `invert()` — its own inverse
8
- import { Cell, cachedDerive, field, reader, } from "../cell.js";
8
+ import { Cell, cachedDerive, fieldLens, reader, } from "../cell.js";
9
9
  import { Num, num } from "./num.js";
10
10
  export const identity = () => ({ a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 });
11
11
  export const fromTranslate = (x, y) => ({ a: 1, b: 0, c: 0, d: 1, e: x, f: y });
@@ -93,22 +93,22 @@ export class Matrix extends Cell {
93
93
  return this.lens(invert, invert);
94
94
  }
95
95
  get a() {
96
- return field(this, "a", Num);
96
+ return fieldLens(this, "a", Num);
97
97
  }
98
98
  get b() {
99
- return field(this, "b", Num);
99
+ return fieldLens(this, "b", Num);
100
100
  }
101
101
  get c() {
102
- return field(this, "c", Num);
102
+ return fieldLens(this, "c", Num);
103
103
  }
104
104
  get d() {
105
- return field(this, "d", Num);
105
+ return fieldLens(this, "d", Num);
106
106
  }
107
107
  get e() {
108
- return field(this, "e", Num);
108
+ return fieldLens(this, "e", Num);
109
109
  }
110
110
  get f() {
111
- return field(this, "f", Num);
111
+ return fieldLens(this, "f", Num);
112
112
  }
113
113
  get determinant() {
114
114
  return cachedDerive(this, "determinant", Num, determinant);
@@ -61,7 +61,7 @@ export declare class Num extends Cell<V> {
61
61
  }
62
62
  /** Writable `Num`. Literal seeds a fresh cell; existing `Writable<Num>`
63
63
  * passes through by identity. RO sources are rejected at the type level —
64
- * use `Num.derive(...)` for reactive RO tracking, or `Num.from(...)` for
64
+ * use `Num.derive(...)` for reactive RO tracking, or `Num.coerce(...)` for
65
65
  * the permissive lift over any `Val<number>`. */
66
66
  export declare function num(v?: Init<Num>): Writable<Num>;
67
67
  export {};
@@ -177,7 +177,7 @@ export class Num extends Cell {
177
177
  }
178
178
  /** Writable `Num`. Literal seeds a fresh cell; existing `Writable<Num>`
179
179
  * passes through by identity. RO sources are rejected at the type level —
180
- * use `Num.derive(...)` for reactive RO tracking, or `Num.from(...)` for
180
+ * use `Num.derive(...)` for reactive RO tracking, or `Num.coerce(...)` for
181
181
  * the permissive lift over any `Val<number>`. */
182
182
  export function num(v = 0) {
183
183
  if (v instanceof Num)
@@ -4,7 +4,7 @@
4
4
  // a 3-DOF block and writes back through it, so renderers, drag handlers,
5
5
  // IK, and physics all observe the same value. Vec / Num lenses compose
6
6
  // for consumers that care only about translation or rotation.
7
- import { Cell, field } from "../cell.js";
7
+ import { Cell, fieldLens } from "../cell.js";
8
8
  import { Num } from "./num.js";
9
9
  export const add = (a, b) => ({
10
10
  x: a.x + b.x,
@@ -71,13 +71,13 @@ export class Pose extends Cell {
71
71
  super(v, { equals });
72
72
  }
73
73
  get x() {
74
- return field(this, "x", Num);
74
+ return fieldLens(this, "x", Num);
75
75
  }
76
76
  get y() {
77
- return field(this, "y", Num);
77
+ return fieldLens(this, "y", Num);
78
78
  }
79
79
  get theta() {
80
- return field(this, "theta", Num);
80
+ return fieldLens(this, "theta", Num);
81
81
  }
82
82
  }
83
83
  /** Writable `Pose`. Literal seeds a fresh cell; existing `Pose` passes
@@ -6,7 +6,7 @@
6
6
  // respectively); `.slider(t)` is the bidirectional `t ↔ lo + t·(hi - lo)`
7
7
  // iso. Traits: `linear`, `lerp`, `equals`, `pack` (scalar-only).
8
8
  import { tween } from "../../animation/index.js";
9
- import { Cell, cachedDerive, field, isReadonly, reader, readNow, } from "../cell.js";
9
+ import { Cell, cachedDerive, fieldLens, isReadonly, reader, readNow, SKIP, } from "../cell.js";
10
10
  import { Bool } from "./bool.js";
11
11
  import { Num, num } from "./num.js";
12
12
  export const add = (a, b) => ({ lo: a.lo + b.lo, hi: a.hi + b.hi });
@@ -62,11 +62,11 @@ export class Range extends Cell {
62
62
  }
63
63
  /** Start endpoint. Writes preserve `hi` (start-knob semantics). */
64
64
  get lo() {
65
- return field(this, "lo", Num);
65
+ return fieldLens(this, "lo", Num);
66
66
  }
67
67
  /** End endpoint. Writes preserve `lo` (end-knob semantics). */
68
68
  get hi() {
69
- return field(this, "hi", Num);
69
+ return fieldLens(this, "hi", Num);
70
70
  }
71
71
  get width() {
72
72
  return cachedDerive(this, "width", Num, width);
@@ -112,7 +112,7 @@ export class Range extends Cell {
112
112
  // defeats the mapped-tuple inference on `[this, t]`).
113
113
  return Num.lens([this, t], ([r, tv]) => sample(r, tv), (v, [r]) => {
114
114
  const w = r.hi - r.lo;
115
- return [undefined, w === 0 ? 0 : (v - r.lo) / w];
115
+ return [SKIP, w === 0 ? 0 : (v - r.lo) / w];
116
116
  });
117
117
  }
118
118
  /** Membership predicate. Conditional return type: a writable `Num`
@@ -127,8 +127,8 @@ export class Range extends Cell {
127
127
  return Bool.lens([this, v], (vals) => contains(vals[0], vals[1]), (target, vals) => {
128
128
  const [r, n] = vals;
129
129
  if (contains(r, n) === target)
130
- return [undefined, undefined];
131
- return [undefined, target ? clamp(r, n) : eject(r, n)];
130
+ return [SKIP, SKIP];
131
+ return [SKIP, target ? clamp(r, n) : eject(r, n)];
132
132
  });
133
133
  }
134
134
  }
@@ -1,4 +1,4 @@
1
- import type { Read, Writable } from "../cell.js";
1
+ import { type Read, type Writable } from "../cell.js";
2
2
  import { type Num } from "./num.js";
3
3
  import { Str } from "./str.js";
4
4
  /** Textual codec for a slot value. `parse` returns `undefined` to reject. */
@@ -20,6 +20,7 @@
20
20
  // stop prunes the no-op, and that slot stays put. A missing delimiter
21
21
  // rejects the whole edit. Adjacent slots with no literal between them are
22
22
  // ambiguous to split — avoid empty inter-slot literals.
23
+ import { SKIP } from "../cell.js";
23
24
  import { num } from "./num.js";
24
25
  import { Str, str } from "./str.js";
25
26
  /** Identity codec for string slots. */
@@ -66,7 +67,7 @@ const render = (literals, slots, vals) => {
66
67
  };
67
68
  const parse = (literals, slots, edited) => {
68
69
  const k = slots.length;
69
- const reject = () => new Array(k).fill(undefined);
70
+ const reject = () => new Array(k).fill(SKIP);
70
71
  const updates = reject();
71
72
  const lit0 = literals[0] ?? "";
72
73
  if (!edited.startsWith(lit0))
@@ -2,11 +2,11 @@
2
2
  //
3
3
  // Invertibles (`add`, `sub`) return `: this` and ride on
4
4
  // `Cell#lens(fwd, bwd)`; chained calls compose into a lens chain.
5
- // Field-lens getters use `field()`, so writability propagates through
5
+ // Field-lens getters use `fieldLens()`, so writability propagates through
6
6
  // nested chains
7
7
  // (`Transform.translate.x.value = 5` works on writable receivers).
8
8
  import { tween } from "../../animation/index.js";
9
- import { Cell, field, reader, readNow } from "../cell.js";
9
+ import { Cell, fieldLens, reader, readNow } from "../cell.js";
10
10
  import { Num } from "./num.js";
11
11
  import { Vec, add as vAdd, equals as vEquals, lerp as vLerp, metric as vMetric, scale as vScale, sub as vSub, } from "./vec.js";
12
12
  export const DEFAULT = {
@@ -75,19 +75,19 @@ export class Transform extends Cell {
75
75
  return Transform.derive(() => lerp(this.value, readNow(b), readNow(t)));
76
76
  }
77
77
  get translate() {
78
- return field(this, "translate", Vec);
78
+ return fieldLens(this, "translate", Vec);
79
79
  }
80
80
  get scale() {
81
- return field(this, "scale", Vec);
81
+ return fieldLens(this, "scale", Vec);
82
82
  }
83
83
  get origin() {
84
- return field(this, "origin", Vec);
84
+ return fieldLens(this, "origin", Vec);
85
85
  }
86
86
  get rotate() {
87
- return field(this, "rotate", Num);
87
+ return fieldLens(this, "rotate", Num);
88
88
  }
89
89
  get opacity() {
90
- return field(this, "opacity", Num);
90
+ return fieldLens(this, "opacity", Num);
91
91
  }
92
92
  /** Tween-builder, implied by the lerp trait. */
93
93
  to(target, dur, ease) {
@@ -3,7 +3,7 @@
3
3
  // `Tri.value ∈ { true, false, "mixed" }` — Bool plus an unknown state
4
4
  // fixed under negation. Strong-Kleene AND/OR follow the partial-info
5
5
  // reading (`mixed AND false` → `false`, `mixed AND true` → `mixed`).
6
- import { Cell } from "../cell.js";
6
+ import { Cell, SKIP } from "../cell.js";
7
7
  const equals = (a, b) => a === b;
8
8
  /** Kleene negation: `true` / `false` swap, `"mixed"` is fixed. */
9
9
  export const not = (a) => (a === "mixed" ? "mixed" : !a);
@@ -55,7 +55,7 @@ export class Tri extends Cell {
55
55
  return anyT;
56
56
  }, (target, _vs) => {
57
57
  if (target === "mixed")
58
- return parents.map(() => undefined);
58
+ return parents.map(() => SKIP);
59
59
  return parents.map(() => target);
60
60
  });
61
61
  }
@@ -81,7 +81,7 @@ export class Tri extends Cell {
81
81
  return "mixed";
82
82
  }, (target, _vs) => {
83
83
  if (target === "mixed")
84
- return parents.map(() => undefined);
84
+ return parents.map(() => SKIP);
85
85
  return parents.map(() => target);
86
86
  });
87
87
  }
@@ -2,9 +2,9 @@
2
2
  //
3
3
  // Invertibles return `: this` and ride on `Cell#lens(fwd, bwd)`;
4
4
  // chained calls compose into a lens chain. Field-lens getters use
5
- // `field()` (propagates writability); `cachedDerive()` wraps RO views.
5
+ // `fieldLens()` (propagates writability); `cachedDerive()` wraps RO views.
6
6
  import { tween } from "../../animation/index.js";
7
- import { Cell, cachedDerive, field, reader, readNow, } from "../cell.js";
7
+ import { Cell, cachedDerive, fieldLens, reader, readNow, SKIP, } from "../cell.js";
8
8
  import { Num, num } from "./num.js";
9
9
  export const add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
10
10
  export const sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
@@ -149,10 +149,10 @@ export class Vec extends Cell {
149
149
  return Num.derive(this, v => metric(v, readNow(other)));
150
150
  }
151
151
  get x() {
152
- return field(this, "x", Num);
152
+ return fieldLens(this, "x", Num);
153
153
  }
154
154
  get y() {
155
- return field(this, "y", Num);
155
+ return fieldLens(this, "y", Num);
156
156
  }
157
157
  get magnitude() {
158
158
  return cachedDerive(this, "magnitude", Num, v => Math.hypot(v.x, v.y));
@@ -200,28 +200,24 @@ export function polar(center, r, a, policy = "rotate") {
200
200
  bwd = (p, [cv, , av]) => {
201
201
  const dx = p.x - cv.x;
202
202
  const dy = p.y - cv.y;
203
- return [undefined, Math.hypot(dx, dy), nearestAngle(Math.atan2(dy, dx), av)];
203
+ return [SKIP, Math.hypot(dx, dy), nearestAngle(Math.atan2(dy, dx), av)];
204
204
  };
205
205
  break;
206
206
  case "translate":
207
207
  bwd = (p, [cv, rv, av]) => {
208
208
  const f = project(cv, rv, av);
209
- return [{ x: cv.x + (p.x - f.x), y: cv.y + (p.y - f.y) }, undefined, undefined];
209
+ return [{ x: cv.x + (p.x - f.x), y: cv.y + (p.y - f.y) }, SKIP, SKIP];
210
210
  };
211
211
  break;
212
212
  case "radial":
213
213
  bwd = (p, [cv, , av]) => {
214
214
  const dx = p.x - cv.x;
215
215
  const dy = p.y - cv.y;
216
- return [undefined, dx * Math.cos(av) + dy * Math.sin(av), undefined];
216
+ return [SKIP, dx * Math.cos(av) + dy * Math.sin(av), SKIP];
217
217
  };
218
218
  break;
219
219
  case "circular":
220
- bwd = (p, [cv, , av]) => [
221
- undefined,
222
- undefined,
223
- nearestAngle(Math.atan2(p.y - cv.y, p.x - cv.x), av),
224
- ];
220
+ bwd = (p, [cv, , av]) => [SKIP, SKIP, nearestAngle(Math.atan2(p.y - cv.y, p.x - cv.x), av)];
225
221
  break;
226
222
  }
227
223
  return Vec.lens([cSig, rSig, aSig], ([c, rv, av]) => project(c, rv, av), bwd);
@@ -32,8 +32,8 @@ class TimelineImpl {
32
32
  }
33
33
  }
34
34
  function makeClip(spec, clock) {
35
- const at = Num.from(spec.at);
36
- const dur = Num.from(spec.dur);
35
+ const at = Num.coerce(spec.at);
36
+ const dur = Num.coerce(spec.dur);
37
37
  const end = Num.derive(() => at.value + dur.value);
38
38
  // Bidirectional span when both inputs are writable; RO derive when
39
39
  // either is read-only. Mirrors `ResolvedSpan`.