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.
- package/dist/animation/anim.js +4 -0
- package/dist/coll.d.ts +7 -7
- package/dist/coll.js +3 -1
- package/dist/core/cell.d.ts +89 -66
- package/dist/core/cell.js +642 -401
- package/dist/core/index.d.ts +4 -14
- package/dist/core/index.js +4 -14
- package/dist/core/lenses/aggregates.d.ts +1 -1
- package/dist/core/lenses/aggregates.js +4 -3
- package/dist/core/lenses/closed-form-policies.js +6 -6
- package/dist/core/lenses/decompositions.js +3 -3
- package/dist/core/lenses/domain-aggregates.js +5 -5
- package/dist/core/lenses/geometry.d.ts +1 -1
- package/dist/core/lenses/geometry.js +6 -7
- package/dist/core/lenses/memory.d.ts +2 -2
- package/dist/core/lenses/memory.js +3 -3
- package/dist/core/lenses/typed-factor.js +4 -3
- package/dist/core/traits.d.ts +1 -0
- package/dist/core/values/box.js +7 -7
- package/dist/core/values/color.js +5 -5
- package/dist/core/values/field.d.ts +70 -0
- package/dist/core/values/field.js +230 -0
- package/dist/core/values/gpu.d.ts +4 -2
- package/dist/core/values/gpu.js +11 -4
- package/dist/core/values/matrix.js +7 -7
- package/dist/core/values/num.d.ts +1 -1
- package/dist/core/values/num.js +1 -1
- package/dist/core/values/pose.js +4 -4
- package/dist/core/values/range.js +6 -6
- package/dist/core/values/template.d.ts +1 -1
- package/dist/core/values/template.js +2 -1
- package/dist/core/values/transform.js +7 -7
- package/dist/core/values/tri.js +3 -3
- package/dist/core/values/vec.js +8 -12
- package/dist/ext/timeline.js +2 -2
- package/dist/formats/cst.d.ts +127 -0
- package/dist/formats/cst.js +280 -0
- package/dist/formats/edn.d.ts +2 -0
- package/dist/formats/edn.js +301 -0
- package/dist/formats/index.d.ts +6 -0
- package/dist/formats/index.js +8 -0
- package/dist/formats/json.d.ts +2 -0
- package/dist/formats/json.js +332 -0
- package/dist/formats/lens.d.ts +8 -0
- package/dist/formats/lens.js +54 -0
- package/dist/formats/toml.d.ts +2 -0
- package/dist/formats/toml.js +526 -0
- package/dist/formats/yaml.d.ts +2 -0
- package/dist/formats/yaml.js +661 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/learn/data.d.ts +49 -0
- package/dist/learn/data.js +181 -0
- package/dist/learn/index.d.ts +3 -0
- package/dist/learn/index.js +6 -0
- package/dist/learn/lens-net.d.ts +63 -0
- package/dist/learn/lens-net.js +219 -0
- package/dist/learn/mlp.d.ts +77 -0
- package/dist/learn/mlp.js +292 -0
- package/dist/propagators/csp.d.ts +13 -0
- package/dist/propagators/csp.js +52 -0
- package/dist/propagators/flex.d.ts +31 -0
- package/dist/propagators/flex.js +189 -0
- package/dist/propagators/graph.d.ts +73 -0
- package/dist/propagators/graph.js +543 -0
- package/dist/propagators/index.d.ts +8 -6
- package/dist/propagators/index.js +15 -6
- package/dist/propagators/lattice.d.ts +45 -0
- package/dist/propagators/lattice.js +113 -0
- package/dist/propagators/layout.d.ts +1 -27
- package/dist/propagators/layout.js +6 -175
- package/dist/propagators/numeric.d.ts +17 -0
- package/dist/propagators/numeric.js +93 -0
- package/dist/propagators/solver.d.ts +51 -0
- package/dist/propagators/solver.js +175 -0
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +3 -0
- package/dist/schema/lens.d.ts +121 -0
- package/dist/schema/lens.js +429 -0
- package/dist/shapes/annular-sector.js +4 -4
- package/dist/shapes/button.js +1 -1
- package/dist/shapes/circle.js +1 -1
- package/dist/shapes/handle.js +2 -2
- package/dist/shapes/label.js +1 -1
- package/dist/shapes/layout.js +2 -2
- package/dist/shapes/rect.js +7 -7
- package/dist/shapes/shape.js +8 -8
- package/dist/tex/tex.js +9 -2
- package/dist/web/diagram.js +2 -2
- package/package.json +9 -19
- package/dist/propagators/network.d.ts +0 -52
- package/dist/propagators/network.js +0 -185
- package/dist/propagators/propagator.d.ts +0 -12
- package/dist/propagators/propagator.js +0 -16
- package/dist/propagators/range.d.ts +0 -45
- package/dist/propagators/range.js +0 -147
- package/dist/propagators/relations.d.ts +0 -60
- package/dist/propagators/relations.js +0 -343
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { type Cell, type Writable } from "../core/cell.js";
|
|
2
|
+
/** A plain JSON-ish record. */
|
|
3
|
+
export type Obj = Record<string, unknown>;
|
|
4
|
+
/** A value-level bidirectional transform carrying complement `C`. */
|
|
5
|
+
export interface VLens<S, V, C> {
|
|
6
|
+
init: (s: S) => C;
|
|
7
|
+
/** Refresh the complement from a (possibly externally-changed) source. */
|
|
8
|
+
step?: (s: S, c: C) => C;
|
|
9
|
+
fwd: (s: S, c: C) => V;
|
|
10
|
+
bwd: (v: V, s: S, c: C) => {
|
|
11
|
+
s: S;
|
|
12
|
+
c: C;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** An object→object lens (the common case). */
|
|
16
|
+
export type OLens = VLens<Obj, Obj, unknown>;
|
|
17
|
+
/** Rename a top-level field. Complement-honest about a colliding target:
|
|
18
|
+
* if the source already has `to`, that shadowed value is parked and
|
|
19
|
+
* restored on the way back (so GetPut holds even on malformed input). */
|
|
20
|
+
export declare function renameV(from: string, to: string): VLens<Obj, Obj, {
|
|
21
|
+
shadow: unknown;
|
|
22
|
+
}>;
|
|
23
|
+
/** A field a newer schema adds; the value lives in the complement. */
|
|
24
|
+
export declare function addV(key: string, initial: unknown): VLens<Obj, Obj, {
|
|
25
|
+
val: unknown;
|
|
26
|
+
}>;
|
|
27
|
+
/** A field a newer schema drops; the value (and position) live in the complement. */
|
|
28
|
+
export declare function removeV(key: string): VLens<Obj, Obj, {
|
|
29
|
+
val: unknown;
|
|
30
|
+
idx: number;
|
|
31
|
+
}>;
|
|
32
|
+
/** Move several top-level fields into a sub-object. Bijective. */
|
|
33
|
+
export declare function nestV(keys: readonly string[], under: string): VLens<Obj, Obj, null>;
|
|
34
|
+
/** How to split one string field into two and rejoin them. */
|
|
35
|
+
export interface SplitSpec {
|
|
36
|
+
split: (whole: string) => [string, string];
|
|
37
|
+
join: (a: string, b: string) => string;
|
|
38
|
+
}
|
|
39
|
+
/** Split one string field into two. Complement-honest: it stores the chosen
|
|
40
|
+
* halves AND the exact original string, so a clean read-write round-trip
|
|
41
|
+
* reproduces the source verbatim (trailing spaces, odd separators) instead
|
|
42
|
+
* of being re-guessed by `join(split(·))`. The lens Cambria couldn't make
|
|
43
|
+
* bidirectional ("firstName/lastName → fullName runs reliably one way"). */
|
|
44
|
+
export declare function splitV(key: string, into: readonly [string, string], spec: SplitSpec): VLens<Obj, Obj, {
|
|
45
|
+
a: string;
|
|
46
|
+
b: string;
|
|
47
|
+
whole: string;
|
|
48
|
+
}>;
|
|
49
|
+
/** A stateful 1→1 value transform on a single field's value. */
|
|
50
|
+
export interface FieldMap<C> {
|
|
51
|
+
init: (srcVal: unknown) => C;
|
|
52
|
+
step?: (srcVal: unknown, c: C) => C;
|
|
53
|
+
fwd: (srcVal: unknown, c: C) => unknown;
|
|
54
|
+
bwd: (viewVal: unknown, srcVal: unknown, c: C) => {
|
|
55
|
+
src: unknown;
|
|
56
|
+
complement: C;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** Apply a value-lens to a single field, optionally renaming the key. The
|
|
60
|
+
* workhorse behind `mapField`, `wrapField`, and `into`. */
|
|
61
|
+
export declare function onField<S, V, C>(key: string, viewKey: string, vl: VLens<S, V, C>): VLens<Obj, Obj, C>;
|
|
62
|
+
/** Scalar → array. Forward wraps the scalar as the array head; the *tail*
|
|
63
|
+
* (everything the older scalar schema can't see) lives in the complement.
|
|
64
|
+
* Writing the scalar replaces only the head and CONSERVES the tail — the
|
|
65
|
+
* consistency Cambria's `head`/`wrap` pair couldn't keep (its Appendix III
|
|
66
|
+
* "defective implementation" clobbered or dropped the unseen elements). */
|
|
67
|
+
export declare function wrapV(): VLens<unknown, unknown[], {
|
|
68
|
+
tail: unknown[];
|
|
69
|
+
}>;
|
|
70
|
+
/** Array → scalar head (the dual of `wrapV`). The tail stays in the source
|
|
71
|
+
* array, so no complement is needed: the head view writes back as
|
|
72
|
+
* `[head, ...tail]`, conserving the rest. Reorder or grow the array and the
|
|
73
|
+
* scalar tracks the new head; edit the scalar and only the head moves. */
|
|
74
|
+
export declare function headV(): VLens<unknown[], unknown, null>;
|
|
75
|
+
/** Left-to-right composition of value-lenses. The complement type is the
|
|
76
|
+
* (heterogeneous) product of the parts', so it's existential here. */
|
|
77
|
+
export declare function seq(...lenses: VLens<Obj, Obj, any>[]): VLens<Obj, Obj, unknown>;
|
|
78
|
+
/** Descend into a field and apply a value-lens to whatever lives there
|
|
79
|
+
* (a nested object, or an array — see `each`). Cambria's `in`. */
|
|
80
|
+
export declare function into<S, V, C>(key: string, vl: VLens<S, V, C>): VLens<Obj, Obj, C>;
|
|
81
|
+
/** Apply an element lens to every item of an array, keyed by `by` for stable
|
|
82
|
+
* identity. Inserts (new key), deletes (missing key), and reorders (view
|
|
83
|
+
* order) all round-trip, and each element keeps its OWN complement across
|
|
84
|
+
* the move — the array case Cambria flagged as unbuilt (`mapInto` plus
|
|
85
|
+
* "merging or splitting arrays"). */
|
|
86
|
+
export declare function eachBy(by: (e: Obj) => unknown, elem: VLens<Obj, Obj, any>): VLens<Obj[], Obj[], Map<unknown, unknown>>;
|
|
87
|
+
/** Positional `each` (element i ↔ element i). Use `eachBy` when elements can
|
|
88
|
+
* reorder; this is fine for stable structures (e.g. recursive trees). */
|
|
89
|
+
export declare function each(elem: VLens<Obj, Obj, any>): VLens<Obj[], Obj[], unknown[]>;
|
|
90
|
+
/** Build a self-referential lens. `recurse(self => …)` lets a lens use itself
|
|
91
|
+
* for nested occurrences — e.g. rename a field at every level of a subtask
|
|
92
|
+
* tree of arbitrary depth. Cambria's open "recursive schemas" case. */
|
|
93
|
+
export declare function recurse(build: (self: OLens) => OLens): OLens;
|
|
94
|
+
/** Lift a value-lens onto a reactive cell. */
|
|
95
|
+
export declare function toStep<C>(vl: VLens<Obj, Obj, C>): Step;
|
|
96
|
+
/** A migration step: a writable lens from one POJO schema to the next. */
|
|
97
|
+
export type Step = (src: Writable<Cell<Obj>>) => Writable<Cell<Obj>>;
|
|
98
|
+
/** Left-to-right composition of reactive steps (chains cells). */
|
|
99
|
+
export declare function pipe(...steps: Step[]): Step;
|
|
100
|
+
/** Rename a top-level field. */
|
|
101
|
+
export declare function renameField(from: string, to: string): Step;
|
|
102
|
+
/** Add a field the source can't represent (value parked in the complement). */
|
|
103
|
+
export declare function addField(key: string, initial: unknown): Step;
|
|
104
|
+
/** Drop a field (value + position parked in the complement). */
|
|
105
|
+
export declare function removeField(key: string): Step;
|
|
106
|
+
/** Move several top-level fields into a sub-object. */
|
|
107
|
+
export declare function nestFields(keys: readonly string[], under: string): Step;
|
|
108
|
+
/** Split one string field into two (complement-honest; see {@link splitV}). */
|
|
109
|
+
export declare function splitField(key: string, into: readonly [string, string], spec: SplitSpec): Step;
|
|
110
|
+
/** Scalar → array on a field, optionally renaming it (see {@link wrapV}). */
|
|
111
|
+
export declare function wrapField(key: string, rename?: string): Step;
|
|
112
|
+
/** Array → scalar head on a field, optionally renaming it (see {@link headV}). */
|
|
113
|
+
export declare function headField(key: string, rename?: string): Step;
|
|
114
|
+
/** A stateful 1→1 value transform on a single field, optionally renaming it. */
|
|
115
|
+
export declare function mapField<C>(key: string, m: FieldMap<C> & {
|
|
116
|
+
rename?: string;
|
|
117
|
+
}): Step;
|
|
118
|
+
/** Apply a whole sub-migration inside a nested object field. Cambria's `in`. */
|
|
119
|
+
export declare function inField(key: string, vl: VLens<Obj, Obj, unknown>): Step;
|
|
120
|
+
/** Apply an element migration to every item of an array field, keyed by `by`. */
|
|
121
|
+
export declare function mapElems(key: string, by: (e: Obj) => unknown, elem: VLens<Obj, Obj, any>): Step;
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
// schema/lens.ts — composable, complement-carrying lenses between plain POJOs.
|
|
2
|
+
//
|
|
3
|
+
// Two layers:
|
|
4
|
+
//
|
|
5
|
+
// • VLens<S,V,C> — a *value-level* bidirectional transform carrying a
|
|
6
|
+
// complement `C` (what the forward direction drops). VLenses compose
|
|
7
|
+
// (`seq`), descend into a field (`into`), map over arrays element-wise
|
|
8
|
+
// (`each`/`eachBy`), and recurse (`recurse`). The complement of a
|
|
9
|
+
// composite is the product of its parts' complements, so a lossy
|
|
10
|
+
// pipeline still round-trips — the property Cambria lacked.
|
|
11
|
+
//
|
|
12
|
+
// • Step — a VLens lifted onto a reactive cell (`toStep`). `pipe` chains
|
|
13
|
+
// Steps into the A→B→{C,D} reactive graph used by the demo.
|
|
14
|
+
//
|
|
15
|
+
// Orientation: the source `S` is the older / upstream schema; `fwd` builds
|
|
16
|
+
// the newer view, `bwd` writes an edit back. Information a newer schema adds,
|
|
17
|
+
// or distinctions an older one can't hold, lives in `C` — local view state,
|
|
18
|
+
// never in the shared source.
|
|
19
|
+
import { lens } from "../core/cell.js";
|
|
20
|
+
/** Sentinel for "this key was absent" (distinct from an explicit `undefined`). */
|
|
21
|
+
const ABSENT = Symbol("absent");
|
|
22
|
+
// ── object helpers (order-preserving, pure) ─────────────────────────
|
|
23
|
+
function omit(v, key) {
|
|
24
|
+
const out = {};
|
|
25
|
+
for (const k of Object.keys(v))
|
|
26
|
+
if (k !== key)
|
|
27
|
+
out[k] = v[k];
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
function replaceKey(v, oldKey, newKey, newVal) {
|
|
31
|
+
const out = {};
|
|
32
|
+
let placed = false;
|
|
33
|
+
for (const k of Object.keys(v)) {
|
|
34
|
+
if (k === oldKey) {
|
|
35
|
+
out[newKey] = newVal;
|
|
36
|
+
placed = true;
|
|
37
|
+
}
|
|
38
|
+
else if (k === newKey && oldKey !== newKey) {
|
|
39
|
+
// drop a colliding target slot; the rename owns this key now
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
out[k] = v[k];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (!placed)
|
|
46
|
+
out[newKey] = newVal;
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
function keyIndex(v, key) {
|
|
50
|
+
return Object.keys(v).indexOf(key);
|
|
51
|
+
}
|
|
52
|
+
/** Reinsert `key` (absent from `v`) at position `idx`. */
|
|
53
|
+
function insertAt(v, key, val, idx) {
|
|
54
|
+
const keys = Object.keys(v).filter(k => k !== key);
|
|
55
|
+
const at = idx < 0 ? keys.length : Math.min(idx, keys.length);
|
|
56
|
+
keys.splice(at, 0, key);
|
|
57
|
+
const out = {};
|
|
58
|
+
for (const k of keys)
|
|
59
|
+
out[k] = k === key ? val : v[k];
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
function insertPair(v, key, ka, va, kb, vb) {
|
|
63
|
+
const out = {};
|
|
64
|
+
for (const k of Object.keys(v)) {
|
|
65
|
+
if (k === key) {
|
|
66
|
+
out[ka] = va;
|
|
67
|
+
out[kb] = vb;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
out[k] = v[k];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
function collapsePair(v, ka, kb, key, whole) {
|
|
76
|
+
const out = {};
|
|
77
|
+
let placed = false;
|
|
78
|
+
for (const k of Object.keys(v)) {
|
|
79
|
+
if (k === ka || k === kb) {
|
|
80
|
+
if (!placed) {
|
|
81
|
+
out[key] = whole;
|
|
82
|
+
placed = true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
out[k] = v[k];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (!placed)
|
|
90
|
+
out[key] = whole;
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
function nestObj(v, keys, under) {
|
|
94
|
+
const set = new Set(keys);
|
|
95
|
+
const sub = {};
|
|
96
|
+
for (const k of keys)
|
|
97
|
+
if (k in v)
|
|
98
|
+
sub[k] = v[k];
|
|
99
|
+
const out = {};
|
|
100
|
+
let placed = false;
|
|
101
|
+
for (const k of Object.keys(v)) {
|
|
102
|
+
if (set.has(k)) {
|
|
103
|
+
if (!placed) {
|
|
104
|
+
out[under] = sub;
|
|
105
|
+
placed = true;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
out[k] = v[k];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (!placed)
|
|
113
|
+
out[under] = sub;
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
function unnestObj(v, keys, under) {
|
|
117
|
+
const sub = v[under] ?? {};
|
|
118
|
+
const out = {};
|
|
119
|
+
for (const k of Object.keys(v)) {
|
|
120
|
+
if (k === under) {
|
|
121
|
+
for (const kk of keys)
|
|
122
|
+
out[kk] = sub[kk];
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
out[k] = v[k];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
// ── value-level primitives ──────────────────────────────────────────
|
|
131
|
+
/** Rename a top-level field. Complement-honest about a colliding target:
|
|
132
|
+
* if the source already has `to`, that shadowed value is parked and
|
|
133
|
+
* restored on the way back (so GetPut holds even on malformed input). */
|
|
134
|
+
export function renameV(from, to) {
|
|
135
|
+
return {
|
|
136
|
+
init: v => ({ shadow: to in v ? v[to] : ABSENT }),
|
|
137
|
+
step: v => ({ shadow: to in v ? v[to] : ABSENT }),
|
|
138
|
+
fwd: v => replaceKey(v, from, to, v[from]),
|
|
139
|
+
bwd: (t, _v, c) => {
|
|
140
|
+
let s = replaceKey(t, to, from, t[to]);
|
|
141
|
+
if (c.shadow !== ABSENT)
|
|
142
|
+
s = { ...s, [to]: c.shadow };
|
|
143
|
+
return { s, c };
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/** A field a newer schema adds; the value lives in the complement. */
|
|
148
|
+
export function addV(key, initial) {
|
|
149
|
+
return {
|
|
150
|
+
init: () => ({ val: initial }),
|
|
151
|
+
fwd: (v, c) => ({ ...v, [key]: c.val }),
|
|
152
|
+
bwd: t => ({ s: omit(t, key), c: { val: t[key] } }),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/** A field a newer schema drops; the value (and position) live in the complement. */
|
|
156
|
+
export function removeV(key) {
|
|
157
|
+
const capture = (v) => ({ val: v[key], idx: keyIndex(v, key) });
|
|
158
|
+
return {
|
|
159
|
+
init: capture,
|
|
160
|
+
step: (v, c) => (key in v ? capture(v) : c),
|
|
161
|
+
fwd: v => omit(v, key),
|
|
162
|
+
bwd: (t, _v, c) => ({ s: insertAt(t, key, c.val, c.idx), c }),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/** Move several top-level fields into a sub-object. Bijective. */
|
|
166
|
+
export function nestV(keys, under) {
|
|
167
|
+
return {
|
|
168
|
+
init: () => null,
|
|
169
|
+
fwd: v => nestObj(v, keys, under),
|
|
170
|
+
bwd: t => ({ s: unnestObj(t, keys, under), c: null }),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/** Split one string field into two. Complement-honest: it stores the chosen
|
|
174
|
+
* halves AND the exact original string, so a clean read-write round-trip
|
|
175
|
+
* reproduces the source verbatim (trailing spaces, odd separators) instead
|
|
176
|
+
* of being re-guessed by `join(split(·))`. The lens Cambria couldn't make
|
|
177
|
+
* bidirectional ("firstName/lastName → fullName runs reliably one way"). */
|
|
178
|
+
export function splitV(key, into, spec) {
|
|
179
|
+
const [ka, kb] = into;
|
|
180
|
+
const part = (whole) => {
|
|
181
|
+
const [a, b] = spec.split(whole);
|
|
182
|
+
return { a, b, whole };
|
|
183
|
+
};
|
|
184
|
+
return {
|
|
185
|
+
init: v => part(String(v[key] ?? "")),
|
|
186
|
+
step: (v, c) => {
|
|
187
|
+
const whole = String(v[key] ?? "");
|
|
188
|
+
return whole === c.whole ? c : part(whole);
|
|
189
|
+
},
|
|
190
|
+
fwd: (v, c) => insertPair(v, key, ka, c.a, kb, c.b),
|
|
191
|
+
bwd: (t, _v, c) => {
|
|
192
|
+
const a = String(t[ka] ?? "");
|
|
193
|
+
const b = String(t[kb] ?? "");
|
|
194
|
+
// Parts unchanged ⇒ restore the original whole exactly (GetPut).
|
|
195
|
+
const whole = a === c.a && b === c.b ? c.whole : spec.join(a, b);
|
|
196
|
+
return { s: collapsePair(t, ka, kb, key, whole), c: { a, b, whole } };
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/** Apply a value-lens to a single field, optionally renaming the key. The
|
|
201
|
+
* workhorse behind `mapField`, `wrapField`, and `into`. */
|
|
202
|
+
export function onField(key, viewKey, vl) {
|
|
203
|
+
return {
|
|
204
|
+
init: v => vl.init(v[key]),
|
|
205
|
+
step: (v, c) => (vl.step ? vl.step(v[key], c) : c),
|
|
206
|
+
fwd: (v, c) => replaceKey(v, key, viewKey, vl.fwd(v[key], c)),
|
|
207
|
+
bwd: (t, v, c) => {
|
|
208
|
+
const r = vl.bwd(t[viewKey], v[key], c);
|
|
209
|
+
return { s: replaceKey(t, viewKey, key, r.s), c: r.c };
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function fieldMapToVL(m) {
|
|
214
|
+
return {
|
|
215
|
+
init: m.init,
|
|
216
|
+
step: m.step,
|
|
217
|
+
fwd: m.fwd,
|
|
218
|
+
bwd: (v, s, c) => {
|
|
219
|
+
const r = m.bwd(v, s, c);
|
|
220
|
+
return { s: r.src, c: r.complement };
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/** Scalar → array. Forward wraps the scalar as the array head; the *tail*
|
|
225
|
+
* (everything the older scalar schema can't see) lives in the complement.
|
|
226
|
+
* Writing the scalar replaces only the head and CONSERVES the tail — the
|
|
227
|
+
* consistency Cambria's `head`/`wrap` pair couldn't keep (its Appendix III
|
|
228
|
+
* "defective implementation" clobbered or dropped the unseen elements). */
|
|
229
|
+
export function wrapV() {
|
|
230
|
+
return {
|
|
231
|
+
init: () => ({ tail: [] }),
|
|
232
|
+
fwd: (s, c) => [s, ...c.tail],
|
|
233
|
+
bwd: arr => {
|
|
234
|
+
const a = Array.isArray(arr) ? arr : [arr];
|
|
235
|
+
return { s: a.length ? a[0] : null, c: { tail: a.slice(1) } };
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/** Array → scalar head (the dual of `wrapV`). The tail stays in the source
|
|
240
|
+
* array, so no complement is needed: the head view writes back as
|
|
241
|
+
* `[head, ...tail]`, conserving the rest. Reorder or grow the array and the
|
|
242
|
+
* scalar tracks the new head; edit the scalar and only the head moves. */
|
|
243
|
+
export function headV() {
|
|
244
|
+
return {
|
|
245
|
+
init: () => null,
|
|
246
|
+
fwd: arr => (Array.isArray(arr) && arr.length ? arr[0] : null),
|
|
247
|
+
bwd: (head, arr) => ({ s: [head, ...(Array.isArray(arr) ? arr.slice(1) : [])], c: null }),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
// ── combinators ──────────────────────────────────────────────────────
|
|
251
|
+
/** Compose two value-lenses (the product complement is `[c1, c2]`). */
|
|
252
|
+
function then2(l1, l2) {
|
|
253
|
+
return {
|
|
254
|
+
init: a => {
|
|
255
|
+
const c1 = l1.init(a);
|
|
256
|
+
return [c1, l2.init(l1.fwd(a, c1))];
|
|
257
|
+
},
|
|
258
|
+
step: (a, [c1, c2]) => {
|
|
259
|
+
const c1b = l1.step ? l1.step(a, c1) : c1;
|
|
260
|
+
const b = l1.fwd(a, c1b);
|
|
261
|
+
const c2b = l2.step ? l2.step(b, c2) : c2;
|
|
262
|
+
return [c1b, c2b];
|
|
263
|
+
},
|
|
264
|
+
fwd: (a, [c1, c2]) => l2.fwd(l1.fwd(a, c1), c2),
|
|
265
|
+
bwd: (d, a, [c1, c2]) => {
|
|
266
|
+
const b = l1.fwd(a, c1);
|
|
267
|
+
const r2 = l2.bwd(d, b, c2);
|
|
268
|
+
const r1 = l1.bwd(r2.s, a, c1);
|
|
269
|
+
return { s: r1.s, c: [r1.c, r2.c] };
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/** Left-to-right composition of value-lenses. The complement type is the
|
|
274
|
+
* (heterogeneous) product of the parts', so it's existential here. */
|
|
275
|
+
// biome-ignore lint/suspicious/noExplicitAny: complements compose existentially
|
|
276
|
+
export function seq(...lenses) {
|
|
277
|
+
if (lenses.length === 0) {
|
|
278
|
+
return { init: () => null, fwd: v => v, bwd: t => ({ s: t, c: null }) };
|
|
279
|
+
}
|
|
280
|
+
return lenses.reduce((acc, l) => then2(acc, l));
|
|
281
|
+
}
|
|
282
|
+
/** Descend into a field and apply a value-lens to whatever lives there
|
|
283
|
+
* (a nested object, or an array — see `each`). Cambria's `in`. */
|
|
284
|
+
export function into(key, vl) {
|
|
285
|
+
return onField(key, key, vl);
|
|
286
|
+
}
|
|
287
|
+
/** Apply an element lens to every item of an array, keyed by `by` for stable
|
|
288
|
+
* identity. Inserts (new key), deletes (missing key), and reorders (view
|
|
289
|
+
* order) all round-trip, and each element keeps its OWN complement across
|
|
290
|
+
* the move — the array case Cambria flagged as unbuilt (`mapInto` plus
|
|
291
|
+
* "merging or splitting arrays"). */
|
|
292
|
+
export function eachBy(by,
|
|
293
|
+
// biome-ignore lint/suspicious/noExplicitAny: per-element complement is existential
|
|
294
|
+
elem) {
|
|
295
|
+
return {
|
|
296
|
+
init: arr => {
|
|
297
|
+
const m = new Map();
|
|
298
|
+
for (const el of arr)
|
|
299
|
+
m.set(by(el), elem.init(el));
|
|
300
|
+
return m;
|
|
301
|
+
},
|
|
302
|
+
step: (arr, c) => {
|
|
303
|
+
const m = new Map();
|
|
304
|
+
for (const el of arr) {
|
|
305
|
+
const k = by(el);
|
|
306
|
+
const ci = c.has(k) ? c.get(k) : elem.init(el);
|
|
307
|
+
m.set(k, elem.step ? elem.step(el, ci) : ci);
|
|
308
|
+
}
|
|
309
|
+
return m;
|
|
310
|
+
},
|
|
311
|
+
fwd: (arr, c) => arr.map(el => elem.fwd(el, c.has(by(el)) ? c.get(by(el)) : elem.init(el))),
|
|
312
|
+
bwd: (view, arr, c) => {
|
|
313
|
+
const srcByKey = new Map();
|
|
314
|
+
for (const el of arr)
|
|
315
|
+
srcByKey.set(by(el), el);
|
|
316
|
+
const nc = new Map();
|
|
317
|
+
const s = view.map(vel => {
|
|
318
|
+
const k = by(vel);
|
|
319
|
+
const s0 = srcByKey.get(k) ?? {};
|
|
320
|
+
const ci = c.has(k) ? c.get(k) : elem.init(s0);
|
|
321
|
+
const r = elem.bwd(vel, s0, ci);
|
|
322
|
+
nc.set(k, r.c);
|
|
323
|
+
return r.s;
|
|
324
|
+
});
|
|
325
|
+
return { s, c: nc };
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
/** Positional `each` (element i ↔ element i). Use `eachBy` when elements can
|
|
330
|
+
* reorder; this is fine for stable structures (e.g. recursive trees). */
|
|
331
|
+
// biome-ignore lint/suspicious/noExplicitAny: per-element complement is existential
|
|
332
|
+
export function each(elem) {
|
|
333
|
+
return {
|
|
334
|
+
init: arr => arr.map(el => elem.init(el)),
|
|
335
|
+
step: (arr, c) => arr.map((el, i) => {
|
|
336
|
+
const ci = i < c.length ? c[i] : elem.init(el);
|
|
337
|
+
return elem.step ? elem.step(el, ci) : ci;
|
|
338
|
+
}),
|
|
339
|
+
fwd: (arr, c) => arr.map((el, i) => elem.fwd(el, i < c.length ? c[i] : elem.init(el))),
|
|
340
|
+
bwd: (view, arr, c) => {
|
|
341
|
+
const nc = [];
|
|
342
|
+
const s = view.map((vel, i) => {
|
|
343
|
+
const s0 = arr[i] ?? {};
|
|
344
|
+
const ci = i < c.length ? c[i] : elem.init(s0);
|
|
345
|
+
const r = elem.bwd(vel, s0, ci);
|
|
346
|
+
nc.push(r.c);
|
|
347
|
+
return r.s;
|
|
348
|
+
});
|
|
349
|
+
return { s, c: nc };
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
/** Build a self-referential lens. `recurse(self => …)` lets a lens use itself
|
|
354
|
+
* for nested occurrences — e.g. rename a field at every level of a subtask
|
|
355
|
+
* tree of arbitrary depth. Cambria's open "recursive schemas" case. */
|
|
356
|
+
export function recurse(build) {
|
|
357
|
+
// Lazy thunk: the body references `self`, whose methods defer to the built
|
|
358
|
+
// lens. Construction terminates; recursion only unfolds against finite data.
|
|
359
|
+
let built = null;
|
|
360
|
+
const self = {
|
|
361
|
+
init: s => (built ??= build(self)).init(s),
|
|
362
|
+
step: (s, c) => {
|
|
363
|
+
const b = (built ??= build(self));
|
|
364
|
+
return b.step ? b.step(s, c) : c;
|
|
365
|
+
},
|
|
366
|
+
fwd: (s, c) => (built ??= build(self)).fwd(s, c),
|
|
367
|
+
bwd: (v, s, c) => (built ??= build(self)).bwd(v, s, c),
|
|
368
|
+
};
|
|
369
|
+
return self;
|
|
370
|
+
}
|
|
371
|
+
// ── reactive lifting (cells) ─────────────────────────────────────────
|
|
372
|
+
/** Lift a value-lens onto a reactive cell. */
|
|
373
|
+
export function toStep(vl) {
|
|
374
|
+
return src => lens(src, {
|
|
375
|
+
init: ([v]) => vl.init(v),
|
|
376
|
+
step: ([v], c) => (vl.step ? vl.step(v, c) : c),
|
|
377
|
+
fwd: ([v], c) => vl.fwd(v, c),
|
|
378
|
+
bwd: (t, [v], c) => {
|
|
379
|
+
const r = vl.bwd(t, v, c);
|
|
380
|
+
return { updates: [r.s], complement: r.c };
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
/** Left-to-right composition of reactive steps (chains cells). */
|
|
385
|
+
export function pipe(...steps) {
|
|
386
|
+
return src => steps.reduce((acc, step) => step(acc), src);
|
|
387
|
+
}
|
|
388
|
+
// ── public Step-returning kit (sugar over the value-level core) ──────
|
|
389
|
+
/** Rename a top-level field. */
|
|
390
|
+
export function renameField(from, to) {
|
|
391
|
+
return toStep(renameV(from, to));
|
|
392
|
+
}
|
|
393
|
+
/** Add a field the source can't represent (value parked in the complement). */
|
|
394
|
+
export function addField(key, initial) {
|
|
395
|
+
return toStep(addV(key, initial));
|
|
396
|
+
}
|
|
397
|
+
/** Drop a field (value + position parked in the complement). */
|
|
398
|
+
export function removeField(key) {
|
|
399
|
+
return toStep(removeV(key));
|
|
400
|
+
}
|
|
401
|
+
/** Move several top-level fields into a sub-object. */
|
|
402
|
+
export function nestFields(keys, under) {
|
|
403
|
+
return toStep(nestV(keys, under));
|
|
404
|
+
}
|
|
405
|
+
/** Split one string field into two (complement-honest; see {@link splitV}). */
|
|
406
|
+
export function splitField(key, into, spec) {
|
|
407
|
+
return toStep(splitV(key, into, spec));
|
|
408
|
+
}
|
|
409
|
+
/** Scalar → array on a field, optionally renaming it (see {@link wrapV}). */
|
|
410
|
+
export function wrapField(key, rename) {
|
|
411
|
+
return toStep(onField(key, rename ?? key, wrapV()));
|
|
412
|
+
}
|
|
413
|
+
/** Array → scalar head on a field, optionally renaming it (see {@link headV}). */
|
|
414
|
+
export function headField(key, rename) {
|
|
415
|
+
return toStep(onField(key, rename ?? key, headV()));
|
|
416
|
+
}
|
|
417
|
+
/** A stateful 1→1 value transform on a single field, optionally renaming it. */
|
|
418
|
+
export function mapField(key, m) {
|
|
419
|
+
return toStep(onField(key, m.rename ?? key, fieldMapToVL(m)));
|
|
420
|
+
}
|
|
421
|
+
/** Apply a whole sub-migration inside a nested object field. Cambria's `in`. */
|
|
422
|
+
export function inField(key, vl) {
|
|
423
|
+
return toStep(into(key, vl));
|
|
424
|
+
}
|
|
425
|
+
/** Apply an element migration to every item of an array field, keyed by `by`. */
|
|
426
|
+
// biome-ignore lint/suspicious/noExplicitAny: per-element complement is existential
|
|
427
|
+
export function mapElems(key, by, elem) {
|
|
428
|
+
return toStep(into(key, eachBy(by, elem)));
|
|
429
|
+
}
|
|
@@ -7,10 +7,10 @@ export class AnnularSector extends Shape {
|
|
|
7
7
|
a0;
|
|
8
8
|
a1;
|
|
9
9
|
constructor(center, rOuter, rInner, a0, a1, opts = {}) {
|
|
10
|
-
const ro = Num.
|
|
11
|
-
const ri = Num.
|
|
12
|
-
const a0s = Num.
|
|
13
|
-
const a1s = Num.
|
|
10
|
+
const ro = Num.coerce(rOuter);
|
|
11
|
+
const ri = Num.coerce(rInner);
|
|
12
|
+
const a0s = Num.coerce(a0);
|
|
13
|
+
const a1s = Num.coerce(a1);
|
|
14
14
|
super("path", () => ({
|
|
15
15
|
x: center.x.value - ro.value,
|
|
16
16
|
y: center.y.value - ro.value,
|
package/dist/shapes/button.js
CHANGED
|
@@ -11,7 +11,7 @@ import { tokens } from "./tokens.js";
|
|
|
11
11
|
export function button(pos, content, onClick, opts = {}) {
|
|
12
12
|
const w = opts.width ?? 80;
|
|
13
13
|
const h = opts.height ?? 26;
|
|
14
|
-
const size = Num.
|
|
14
|
+
const size = Num.coerce(opts.size ?? 11);
|
|
15
15
|
const hovered = opts.hovered ?? cell(false);
|
|
16
16
|
// Hover tint behind the border so outline weight stays constant.
|
|
17
17
|
const g = group({ translate: pos }, rect(0, 0, w, h, {
|
package/dist/shapes/circle.js
CHANGED
|
@@ -4,7 +4,7 @@ import { Shape } from "./shape.js";
|
|
|
4
4
|
export class Circle extends Shape {
|
|
5
5
|
radius;
|
|
6
6
|
constructor(center, radius, opts = {}) {
|
|
7
|
-
const r = Num.
|
|
7
|
+
const r = Num.coerce(radius);
|
|
8
8
|
super(opts.dashed ? "path" : "circle", () => ({
|
|
9
9
|
x: center.x.value - r.value,
|
|
10
10
|
y: center.y.value - r.value,
|
package/dist/shapes/handle.js
CHANGED
|
@@ -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 [
|
|
54
|
+
return [SKIP, { x: k, y: k }];
|
|
55
55
|
});
|
|
56
56
|
return handleFn(pos, { cursor: "ew-resize", ...opts });
|
|
57
57
|
};
|
package/dist/shapes/label.js
CHANGED
|
@@ -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.
|
|
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);
|
package/dist/shapes/layout.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Spatial composition primitives.
|
|
2
|
-
import { Box,
|
|
2
|
+
import { Box, boxExpand, 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(() =>
|
|
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
|
*
|
package/dist/shapes/rect.js
CHANGED
|
@@ -9,10 +9,10 @@ export class Rect extends Shape {
|
|
|
9
9
|
h;
|
|
10
10
|
corner;
|
|
11
11
|
constructor(x, y, w, h, opts = {}) {
|
|
12
|
-
const xs = Num.
|
|
13
|
-
const ys = Num.
|
|
14
|
-
const ws = Num.
|
|
15
|
-
const hs = Num.
|
|
12
|
+
const xs = Num.coerce(x);
|
|
13
|
+
const ys = Num.coerce(y);
|
|
14
|
+
const ws = Num.coerce(w);
|
|
15
|
+
const hs = Num.coerce(h);
|
|
16
16
|
super(opts.dashed ? "path" : "rect", () => ({ x: xs.value, y: ys.value, w: ws.value, h: hs.value }), opts, {
|
|
17
17
|
origin: derive(() => ({
|
|
18
18
|
x: xs.value + ws.value / 2,
|
|
@@ -23,7 +23,7 @@ export class Rect extends Shape {
|
|
|
23
23
|
this.y = ys;
|
|
24
24
|
this.w = ws;
|
|
25
25
|
this.h = hs;
|
|
26
|
-
this.corner = Num.
|
|
26
|
+
this.corner = Num.coerce(opts.corner ?? tokens.corner);
|
|
27
27
|
this.stroke(opts, true, {
|
|
28
28
|
x: xs,
|
|
29
29
|
y: ys,
|
|
@@ -123,8 +123,8 @@ export function rect(a, b, c, d, e) {
|
|
|
123
123
|
if (a instanceof Vec) {
|
|
124
124
|
const w = b;
|
|
125
125
|
const h = c;
|
|
126
|
-
const ws = Num.
|
|
127
|
-
const hs = Num.
|
|
126
|
+
const ws = Num.coerce(w);
|
|
127
|
+
const hs = Num.coerce(h);
|
|
128
128
|
return new Rect(derive(() => a.x.value - ws.value / 2), derive(() => a.y.value - hs.value / 2), ws, hs, d);
|
|
129
129
|
}
|
|
130
130
|
return new Rect(a, b, c, d, e);
|