bireactive 0.2.4 → 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/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/web/diagram.js +2 -2
- package/package.json +1 -1
- 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,543 @@
|
|
|
1
|
+
// graph.ts — graph layout on the interval atoms.
|
|
2
|
+
//
|
|
3
|
+
// The headline is `rank`: layer assignment is longest-path, and
|
|
4
|
+
// longest-path IS interval narrowing. For every edge u→v we assert
|
|
5
|
+
// `order(layer(u), layer(v), 1)` (v sits at least one layer below u);
|
|
6
|
+
// the solver narrows each layer cell's lower bound to the length of the
|
|
7
|
+
// longest path reaching it. A merge node — many edges into one cell —
|
|
8
|
+
// is fan-in narrowing, the thing a lens can't express.
|
|
9
|
+
//
|
|
10
|
+
// Everything after ranking (crossing reduction, coordinate assignment)
|
|
11
|
+
// is the standard Sugiyama machinery: barycenter sweeps to order each
|
|
12
|
+
// layer, then pool-adjacent-violators to place nodes as close to their
|
|
13
|
+
// neighbours' barycenter as non-overlap allows. Those are heuristics,
|
|
14
|
+
// not lattice operations, and are written as plain functions.
|
|
15
|
+
//
|
|
16
|
+
// Layouts share one engine: `layered` (4 directions), `tree`
|
|
17
|
+
// (parents centred over children), `radial` (layers as rings), and
|
|
18
|
+
// `lanes` (git-style column packing).
|
|
19
|
+
import { intervalCell } from "./lattice.js";
|
|
20
|
+
import { order } from "./numeric.js";
|
|
21
|
+
import { solve } from "./solver.js";
|
|
22
|
+
const DEFAULT_SIZE = { w: 46, h: 34 };
|
|
23
|
+
// ── ranking: longest-path via interval narrowing ────────────────────
|
|
24
|
+
/** Assign each node an integer layer so every edge increases layer by
|
|
25
|
+
* ≥1. Cycles are broken (back edges reversed for ranking only). The
|
|
26
|
+
* value is the longest path from a source — computed as the lower
|
|
27
|
+
* bound the `order` atoms narrow each layer cell to. */
|
|
28
|
+
export function rank(g) {
|
|
29
|
+
const acyclic = breakCycles(g);
|
|
30
|
+
const cell = new Map();
|
|
31
|
+
const hi = Math.max(1, g.nodes.length);
|
|
32
|
+
for (const n of g.nodes)
|
|
33
|
+
cell.set(n, intervalCell(0, hi));
|
|
34
|
+
const props = [];
|
|
35
|
+
for (const [u, v] of acyclic)
|
|
36
|
+
props.push(...order(cell.get(u), cell.get(v), 1));
|
|
37
|
+
const s = solve(...props);
|
|
38
|
+
const layer = new Map();
|
|
39
|
+
for (const n of g.nodes)
|
|
40
|
+
layer.set(n, Math.round(cell.get(n).value[0]));
|
|
41
|
+
s.dispose();
|
|
42
|
+
return layer;
|
|
43
|
+
}
|
|
44
|
+
/** Strongly-connected components (Tarjan), each a list of mutually
|
|
45
|
+
* reachable nodes, in reverse-topological order of the condensation.
|
|
46
|
+
* Singletons are size-1 components; a self-loop still reports size 1.
|
|
47
|
+
* The cyclic cores of a graph are exactly the components of size > 1. */
|
|
48
|
+
export function scc(g) {
|
|
49
|
+
const adj = new Map();
|
|
50
|
+
for (const n of g.nodes)
|
|
51
|
+
adj.set(n, []);
|
|
52
|
+
for (const [u, v] of g.edges)
|
|
53
|
+
adj.get(u)?.push(v);
|
|
54
|
+
let idx = 0;
|
|
55
|
+
const index = new Map();
|
|
56
|
+
const low = new Map();
|
|
57
|
+
const onStack = new Set();
|
|
58
|
+
const stack = [];
|
|
59
|
+
const out = [];
|
|
60
|
+
const connect = (v) => {
|
|
61
|
+
index.set(v, idx);
|
|
62
|
+
low.set(v, idx);
|
|
63
|
+
idx++;
|
|
64
|
+
stack.push(v);
|
|
65
|
+
onStack.add(v);
|
|
66
|
+
for (const w of adj.get(v)) {
|
|
67
|
+
if (!index.has(w)) {
|
|
68
|
+
connect(w);
|
|
69
|
+
low.set(v, Math.min(low.get(v), low.get(w)));
|
|
70
|
+
}
|
|
71
|
+
else if (onStack.has(w)) {
|
|
72
|
+
low.set(v, Math.min(low.get(v), index.get(w)));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (low.get(v) === index.get(v)) {
|
|
76
|
+
const comp = [];
|
|
77
|
+
let w;
|
|
78
|
+
do {
|
|
79
|
+
w = stack.pop();
|
|
80
|
+
onStack.delete(w);
|
|
81
|
+
comp.push(w);
|
|
82
|
+
} while (w !== v);
|
|
83
|
+
out.push(comp);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
for (const n of g.nodes)
|
|
87
|
+
if (!index.has(n))
|
|
88
|
+
connect(n);
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
/** Reverse edges that close a cycle (DFS back edges), returning an
|
|
92
|
+
* acyclic edge list. Order of `g.edges` is otherwise preserved. */
|
|
93
|
+
function breakCycles(g) {
|
|
94
|
+
const adj = new Map();
|
|
95
|
+
for (const n of g.nodes)
|
|
96
|
+
adj.set(n, []);
|
|
97
|
+
for (const [u, v] of g.edges)
|
|
98
|
+
adj.get(u)?.push(v);
|
|
99
|
+
const WHITE = 0;
|
|
100
|
+
const GREY = 1;
|
|
101
|
+
const BLACK = 2;
|
|
102
|
+
const color = new Map(g.nodes.map(n => [n, WHITE]));
|
|
103
|
+
const back = new Set();
|
|
104
|
+
const key = (u, v) => `${g.nodes.indexOf(u)}->${g.nodes.indexOf(v)}`;
|
|
105
|
+
const visit = (u) => {
|
|
106
|
+
color.set(u, GREY);
|
|
107
|
+
for (const v of adj.get(u) ?? []) {
|
|
108
|
+
const c = color.get(v);
|
|
109
|
+
if (c === GREY)
|
|
110
|
+
back.add(key(u, v)); // edge into the active stack
|
|
111
|
+
else if (c === WHITE)
|
|
112
|
+
visit(v);
|
|
113
|
+
}
|
|
114
|
+
color.set(u, BLACK);
|
|
115
|
+
};
|
|
116
|
+
for (const n of g.nodes)
|
|
117
|
+
if (color.get(n) === WHITE)
|
|
118
|
+
visit(n);
|
|
119
|
+
return g.edges.map(([u, v]) => (back.has(key(u, v)) ? [v, u] : [u, v]));
|
|
120
|
+
}
|
|
121
|
+
// ── crossing reduction ──────────────────────────────────────────────
|
|
122
|
+
/** Group nodes by layer, ordered to reduce edge crossings via
|
|
123
|
+
* barycenter sweeps. Returns the per-layer node arrays. */
|
|
124
|
+
function ordered(g, layer, sweeps) {
|
|
125
|
+
const numLayers = Math.max(0, ...[...layer.values()].map(l => l + 1));
|
|
126
|
+
const layers = Array.from({ length: numLayers }, () => []);
|
|
127
|
+
for (const n of g.nodes)
|
|
128
|
+
layers[layer.get(n)].push(n);
|
|
129
|
+
const down = new Map(); // node → neighbours in layer below
|
|
130
|
+
const up = new Map(); // node → neighbours in layer above
|
|
131
|
+
for (const n of g.nodes) {
|
|
132
|
+
down.set(n, []);
|
|
133
|
+
up.set(n, []);
|
|
134
|
+
}
|
|
135
|
+
for (const [u, v] of g.edges) {
|
|
136
|
+
const lu = layer.get(u);
|
|
137
|
+
const lv = layer.get(v);
|
|
138
|
+
if (lu === lv)
|
|
139
|
+
continue;
|
|
140
|
+
const [hiNode, loNode] = lu < lv ? [u, v] : [v, u];
|
|
141
|
+
down.get(hiNode).push(loNode);
|
|
142
|
+
up.get(loNode).push(hiNode);
|
|
143
|
+
}
|
|
144
|
+
const indexIn = (arr) => new Map(arr.map((n, i) => [n, i]));
|
|
145
|
+
const sweep = (from, to, step, side) => {
|
|
146
|
+
for (let l = from; l !== to; l += step) {
|
|
147
|
+
const ref = indexIn(layers[l - step]);
|
|
148
|
+
const bary = (n) => {
|
|
149
|
+
const nb = side.get(n);
|
|
150
|
+
if (nb.length === 0)
|
|
151
|
+
return Number.POSITIVE_INFINITY; // keep relative order
|
|
152
|
+
let acc = 0;
|
|
153
|
+
for (const m of nb)
|
|
154
|
+
acc += ref.get(m);
|
|
155
|
+
return acc / nb.length;
|
|
156
|
+
};
|
|
157
|
+
const b = new Map(layers[l].map(n => [n, bary(n)]));
|
|
158
|
+
// Stable sort; nodes with no neighbours (Infinity) hold position.
|
|
159
|
+
layers[l].sort((p, q) => {
|
|
160
|
+
const bp = b.get(p);
|
|
161
|
+
const bq = b.get(q);
|
|
162
|
+
if (!Number.isFinite(bp) || !Number.isFinite(bq))
|
|
163
|
+
return 0;
|
|
164
|
+
return bp - bq;
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
for (let s = 0; s < sweeps; s++) {
|
|
169
|
+
if (numLayers > 1)
|
|
170
|
+
sweep(1, numLayers, 1, up); // top→bottom by parents
|
|
171
|
+
if (numLayers > 1)
|
|
172
|
+
sweep(numLayers - 2, -1, -1, down); // bottom→top by children
|
|
173
|
+
}
|
|
174
|
+
return layers;
|
|
175
|
+
}
|
|
176
|
+
/** Count edge crossings given per-layer orderings (for tests / quality). */
|
|
177
|
+
export function crossings(g, layer, layers) {
|
|
178
|
+
const pos = new Map();
|
|
179
|
+
for (const arr of layers)
|
|
180
|
+
arr.forEach((n, i) => pos.set(n, i));
|
|
181
|
+
let total = 0;
|
|
182
|
+
const byLayerPair = new Map();
|
|
183
|
+
for (const [u, v] of g.edges) {
|
|
184
|
+
const lu = layer.get(u);
|
|
185
|
+
const lv = layer.get(v);
|
|
186
|
+
if (Math.abs(lu - lv) !== 1)
|
|
187
|
+
continue;
|
|
188
|
+
const top = lu < lv ? lu : lv;
|
|
189
|
+
const [a, b] = lu < lv ? [u, v] : [v, u];
|
|
190
|
+
const list = byLayerPair.get(top) ?? byLayerPair.set(top, []).get(top);
|
|
191
|
+
list.push([pos.get(a), pos.get(b)]);
|
|
192
|
+
}
|
|
193
|
+
for (const list of byLayerPair.values()) {
|
|
194
|
+
for (let i = 0; i < list.length; i++) {
|
|
195
|
+
for (let j = i + 1; j < list.length; j++) {
|
|
196
|
+
const [a1, b1] = list[i];
|
|
197
|
+
const [a2, b2] = list[j];
|
|
198
|
+
if ((a1 < a2 && b1 > b2) || (a1 > a2 && b1 < b2))
|
|
199
|
+
total++;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return total;
|
|
204
|
+
}
|
|
205
|
+
// ── coordinate assignment ───────────────────────────────────────────
|
|
206
|
+
/** Pool-adjacent-violators: the non-decreasing sequence closest (least
|
|
207
|
+
* squares) to `y`. The kernel of minimum-displacement, ordered,
|
|
208
|
+
* non-overlapping placement. */
|
|
209
|
+
function isotonic(y) {
|
|
210
|
+
const blocks = [];
|
|
211
|
+
for (const yi of y) {
|
|
212
|
+
let b = { sum: yi, count: 1 };
|
|
213
|
+
while (blocks.length > 0 &&
|
|
214
|
+
blocks[blocks.length - 1].sum / blocks[blocks.length - 1].count > b.sum / b.count) {
|
|
215
|
+
const prev = blocks.pop();
|
|
216
|
+
b = { sum: prev.sum + b.sum, count: prev.count + b.count };
|
|
217
|
+
}
|
|
218
|
+
blocks.push(b);
|
|
219
|
+
}
|
|
220
|
+
const out = [];
|
|
221
|
+
for (const b of blocks) {
|
|
222
|
+
const avg = b.sum / b.count;
|
|
223
|
+
for (let k = 0; k < b.count; k++)
|
|
224
|
+
out.push(avg);
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
/** Place ordered nodes at centres as close to `desired` as possible
|
|
229
|
+
* subject to `gap` edge-to-edge separation. */
|
|
230
|
+
function placeRow(sizes, desired, gap) {
|
|
231
|
+
const n = sizes.length;
|
|
232
|
+
if (n === 0)
|
|
233
|
+
return [];
|
|
234
|
+
const cumSep = new Array(n);
|
|
235
|
+
cumSep[0] = 0;
|
|
236
|
+
for (let i = 1; i < n; i++)
|
|
237
|
+
cumSep[i] = cumSep[i - 1] + sizes[i - 1] / 2 + gap + sizes[i] / 2;
|
|
238
|
+
const q = desired.map((d, i) => d - cumSep[i]);
|
|
239
|
+
const qhat = isotonic(q);
|
|
240
|
+
return qhat.map((v, i) => v + cumSep[i]);
|
|
241
|
+
}
|
|
242
|
+
/** Full layered layout → top-left placement per node. */
|
|
243
|
+
export function layered(g, opts = {}) {
|
|
244
|
+
const dir = opts.direction ?? "TB";
|
|
245
|
+
const layerGap = opts.layerGap ?? 90;
|
|
246
|
+
const nodeGap = opts.nodeGap ?? 28;
|
|
247
|
+
const sizeOf = opts.sizeOf ?? (() => DEFAULT_SIZE);
|
|
248
|
+
const sweeps = opts.sweeps ?? 6;
|
|
249
|
+
const align = opts.align ?? "both";
|
|
250
|
+
const layer = rank(g);
|
|
251
|
+
const layers = ordered(g, layer, sweeps);
|
|
252
|
+
const horizontal = dir === "LR" || dir === "RL";
|
|
253
|
+
// Cross-axis size of a node (the extent we pack along within a layer).
|
|
254
|
+
const cross = (n) => (horizontal ? sizeOf(n).h : sizeOf(n).w);
|
|
255
|
+
// Neighbour sets for barycenter, per `align`. `hi` is the upper
|
|
256
|
+
// (smaller-layer) endpoint, `lo` the lower. "down" centres a node on
|
|
257
|
+
// its lower neighbours (parents over children); "up" the reverse.
|
|
258
|
+
const nbrs = new Map();
|
|
259
|
+
for (const n of g.nodes)
|
|
260
|
+
nbrs.set(n, []);
|
|
261
|
+
for (const [u, v] of g.edges) {
|
|
262
|
+
const lu = layer.get(u);
|
|
263
|
+
const lv = layer.get(v);
|
|
264
|
+
if (lu === lv)
|
|
265
|
+
continue;
|
|
266
|
+
const hiNode = lu < lv ? u : v;
|
|
267
|
+
const loNode = lu < lv ? v : u;
|
|
268
|
+
if (align !== "up")
|
|
269
|
+
nbrs.get(hiNode).push(loNode); // hi centred on its lo neighbours
|
|
270
|
+
if (align !== "down")
|
|
271
|
+
nbrs.get(loNode).push(hiNode); // lo centred on its hi neighbours
|
|
272
|
+
}
|
|
273
|
+
// Initial centres: pack each layer from 0.
|
|
274
|
+
const center = new Map();
|
|
275
|
+
for (const arr of layers) {
|
|
276
|
+
let c = 0;
|
|
277
|
+
for (let i = 0; i < arr.length; i++) {
|
|
278
|
+
const half = cross(arr[i]) / 2;
|
|
279
|
+
c += half;
|
|
280
|
+
center.set(arr[i], c);
|
|
281
|
+
c += half + nodeGap;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// Refinement: pull toward neighbour barycenter, re-resolve overlaps.
|
|
285
|
+
const refine = (order) => {
|
|
286
|
+
for (const li of order) {
|
|
287
|
+
const arr = layers[li];
|
|
288
|
+
if (arr.length === 0)
|
|
289
|
+
continue;
|
|
290
|
+
const desired = arr.map(n => {
|
|
291
|
+
const nb = nbrs.get(n).filter(m => layer.get(m) !== li);
|
|
292
|
+
if (nb.length === 0)
|
|
293
|
+
return center.get(n);
|
|
294
|
+
let acc = 0;
|
|
295
|
+
for (const m of nb)
|
|
296
|
+
acc += center.get(m);
|
|
297
|
+
return acc / nb.length;
|
|
298
|
+
});
|
|
299
|
+
const placed = placeRow(arr.map(cross), desired, nodeGap);
|
|
300
|
+
arr.forEach((n, i) => center.set(n, placed[i]));
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
const numLayers = layers.length;
|
|
304
|
+
const downOrder = Array.from({ length: numLayers }, (_, i) => i);
|
|
305
|
+
const upOrder = [...downOrder].reverse();
|
|
306
|
+
for (let s = 0; s < sweeps; s++) {
|
|
307
|
+
refine(downOrder);
|
|
308
|
+
refine(upOrder);
|
|
309
|
+
}
|
|
310
|
+
// Layer-axis coordinate (centre of each layer).
|
|
311
|
+
const layerCenter = (l) => l * layerGap;
|
|
312
|
+
// Assemble placements in (layerAxis, crossAxis) then orient.
|
|
313
|
+
const maxLayer = numLayers - 1;
|
|
314
|
+
const out = new Map();
|
|
315
|
+
for (const n of g.nodes) {
|
|
316
|
+
const { w, h } = sizeOf(n);
|
|
317
|
+
const l = layer.get(n);
|
|
318
|
+
const cc = center.get(n); // cross-axis centre
|
|
319
|
+
let lc = layerCenter(l); // layer-axis centre
|
|
320
|
+
if (dir === "BT" || dir === "RL")
|
|
321
|
+
lc = layerCenter(maxLayer) - lc;
|
|
322
|
+
const cx = horizontal ? lc : cc;
|
|
323
|
+
const cy = horizontal ? cc : lc;
|
|
324
|
+
out.set(n, { x: cx - w / 2, y: cy - h / 2, w, h });
|
|
325
|
+
}
|
|
326
|
+
return normalize(out, 0);
|
|
327
|
+
}
|
|
328
|
+
/** Tree layout: parents centred over their children. A `layered` with
|
|
329
|
+
* downward barycenter and tighter defaults. */
|
|
330
|
+
export function tree(g, opts = {}) {
|
|
331
|
+
return layered(g, { align: "down", sweeps: 8, layerGap: 80, ...opts });
|
|
332
|
+
}
|
|
333
|
+
/** Radial layout: layers become concentric rings, cross-axis position
|
|
334
|
+
* becomes angle. Reads a `layered` (TB) result and re-maps to polar. */
|
|
335
|
+
export function radial(g, opts = {}) {
|
|
336
|
+
const flat = layered(g, { ...opts, direction: "TB", align: "down" });
|
|
337
|
+
const layer = rank(g);
|
|
338
|
+
const ringGap = opts.layerGap ?? 80;
|
|
339
|
+
// Cross-axis span per ring → map to [0, 2π) (or a fan for the root ring).
|
|
340
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
341
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
342
|
+
for (const p of flat.values()) {
|
|
343
|
+
minX = Math.min(minX, p.x + p.w / 2);
|
|
344
|
+
maxX = Math.max(maxX, p.x + p.w / 2);
|
|
345
|
+
}
|
|
346
|
+
const span = Math.max(1, maxX - minX);
|
|
347
|
+
const out = new Map();
|
|
348
|
+
for (const n of g.nodes) {
|
|
349
|
+
const p = flat.get(n);
|
|
350
|
+
const cxFlat = p.x + p.w / 2;
|
|
351
|
+
const l = layer.get(n);
|
|
352
|
+
const radius = l * ringGap;
|
|
353
|
+
const theta = l === 0 ? 0 : ((cxFlat - minX) / span) * Math.PI * 1.8 - Math.PI * 0.9;
|
|
354
|
+
const cx = Math.sin(theta) * radius;
|
|
355
|
+
const cy = -Math.cos(theta) * radius;
|
|
356
|
+
out.set(n, { x: cx - p.w / 2, y: cy - p.h / 2, w: p.w, h: p.h });
|
|
357
|
+
}
|
|
358
|
+
return normalize(out, 0);
|
|
359
|
+
}
|
|
360
|
+
// ── recurrent (cyclic) layout ───────────────────────────────────────
|
|
361
|
+
/** A traversal order of an SCC's members that follows edges where it
|
|
362
|
+
* can — a greedy walk. For a simple cycle this is the cycle order, so
|
|
363
|
+
* laying the members out in this order around a circle traces the loop. */
|
|
364
|
+
function ringOrder(members, adj) {
|
|
365
|
+
const set = new Set(members);
|
|
366
|
+
const seen = new Set();
|
|
367
|
+
const out = [];
|
|
368
|
+
let cur = members[0];
|
|
369
|
+
while (out.length < members.length && cur !== undefined) {
|
|
370
|
+
out.push(cur);
|
|
371
|
+
seen.add(cur);
|
|
372
|
+
const here = cur;
|
|
373
|
+
const next = (adj.get(here) ?? []).find(w => set.has(w) && !seen.has(w));
|
|
374
|
+
cur = next ?? members.find(m => !seen.has(m));
|
|
375
|
+
}
|
|
376
|
+
return out;
|
|
377
|
+
}
|
|
378
|
+
/** Recurrent-hierarchy layout: decompose into strongly-connected
|
|
379
|
+
* components, lay the condensation (a DAG of SCCs) out hierarchically,
|
|
380
|
+
* and draw every cyclic component as a ring centred on its condensation
|
|
381
|
+
* slot. The cycle closes by going around the circle — no backward edge.
|
|
382
|
+
* Singleton components are placed as ordinary nodes. */
|
|
383
|
+
export function recurrent(g, opts = {}) {
|
|
384
|
+
const sizeOf = opts.sizeOf ?? (() => DEFAULT_SIZE);
|
|
385
|
+
const ringGap = 16;
|
|
386
|
+
const comps = scc(g);
|
|
387
|
+
const compOf = new Map();
|
|
388
|
+
comps.forEach((c, i) => c.forEach(n => compOf.set(n, i)));
|
|
389
|
+
const adj = new Map();
|
|
390
|
+
for (const n of g.nodes)
|
|
391
|
+
adj.set(n, []);
|
|
392
|
+
for (const [u, v] of g.edges)
|
|
393
|
+
adj.get(u)?.push(v);
|
|
394
|
+
// Ring radius for a component: chord between adjacent members ≥ their
|
|
395
|
+
// widest extent plus a gap.
|
|
396
|
+
const radiusOf = (members) => {
|
|
397
|
+
if (members.length < 2)
|
|
398
|
+
return 0;
|
|
399
|
+
let widest = 0;
|
|
400
|
+
for (const n of members)
|
|
401
|
+
widest = Math.max(widest, sizeOf(n).w, sizeOf(n).h);
|
|
402
|
+
return (widest + ringGap) / (2 * Math.sin(Math.PI / members.length));
|
|
403
|
+
};
|
|
404
|
+
// Each SCC is a meta-node sized to its ring's bounding box.
|
|
405
|
+
const ringSize = (i) => {
|
|
406
|
+
const members = comps[i];
|
|
407
|
+
if (members.length < 2)
|
|
408
|
+
return sizeOf(members[0]);
|
|
409
|
+
const r = radiusOf(members);
|
|
410
|
+
let widest = 0;
|
|
411
|
+
for (const n of members)
|
|
412
|
+
widest = Math.max(widest, sizeOf(n).w, sizeOf(n).h);
|
|
413
|
+
return { w: 2 * r + widest, h: 2 * r + widest };
|
|
414
|
+
};
|
|
415
|
+
const metaEdges = new Set();
|
|
416
|
+
const edges = [];
|
|
417
|
+
for (const [u, v] of g.edges) {
|
|
418
|
+
const cu = compOf.get(u);
|
|
419
|
+
const cv = compOf.get(v);
|
|
420
|
+
const key = `${cu}->${cv}`;
|
|
421
|
+
if (cu !== cv && !metaEdges.has(key)) {
|
|
422
|
+
metaEdges.add(key);
|
|
423
|
+
edges.push([cu, cv]);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const horizontal = opts.direction === "LR" || opts.direction === "RL";
|
|
427
|
+
let maxAlong = 0;
|
|
428
|
+
for (let i = 0; i < comps.length; i++) {
|
|
429
|
+
const sz = ringSize(i);
|
|
430
|
+
maxAlong = Math.max(maxAlong, horizontal ? sz.w : sz.h);
|
|
431
|
+
}
|
|
432
|
+
const metaPlace = layered({ nodes: comps.map((_, i) => i), edges }, {
|
|
433
|
+
direction: opts.direction ?? "TB",
|
|
434
|
+
sizeOf: ringSize,
|
|
435
|
+
layerGap: maxAlong + (opts.layerGap ?? 60),
|
|
436
|
+
nodeGap: opts.nodeGap ?? 40,
|
|
437
|
+
});
|
|
438
|
+
const out = new Map();
|
|
439
|
+
for (let i = 0; i < comps.length; i++) {
|
|
440
|
+
const members = comps[i];
|
|
441
|
+
const slot = metaPlace.get(i);
|
|
442
|
+
const cx = slot.x + slot.w / 2;
|
|
443
|
+
const cy = slot.y + slot.h / 2;
|
|
444
|
+
if (members.length < 2) {
|
|
445
|
+
const { w, h } = sizeOf(members[0]);
|
|
446
|
+
out.set(members[0], { x: cx - w / 2, y: cy - h / 2, w, h });
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
const order = ringOrder(members, adj);
|
|
450
|
+
const r = radiusOf(members);
|
|
451
|
+
order.forEach((n, k) => {
|
|
452
|
+
const theta = -Math.PI / 2 + (k / order.length) * Math.PI * 2;
|
|
453
|
+
const { w, h } = sizeOf(n);
|
|
454
|
+
out.set(n, {
|
|
455
|
+
x: cx + Math.cos(theta) * r - w / 2,
|
|
456
|
+
y: cy + Math.sin(theta) * r - h / 2,
|
|
457
|
+
w,
|
|
458
|
+
h,
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
return normalize(out, 0);
|
|
463
|
+
}
|
|
464
|
+
/** Git-style lane packing: nodes flow top-to-bottom in topological
|
|
465
|
+
* order (one row each), each assigned the first free column ("lane")
|
|
466
|
+
* that no live edge occupies. Branch points open lanes, merges free
|
|
467
|
+
* them. Expects a DAG whose `nodes` are in commit order (parents
|
|
468
|
+
* before children). */
|
|
469
|
+
export function lanes(g, opts = {}) {
|
|
470
|
+
const rowGap = opts.rowGap ?? 56;
|
|
471
|
+
const laneGap = opts.laneGap ?? 46;
|
|
472
|
+
const sizeOf = opts.sizeOf ?? (() => DEFAULT_SIZE);
|
|
473
|
+
const layer = rank(g);
|
|
474
|
+
const rowOf = new Map();
|
|
475
|
+
[...g.nodes]
|
|
476
|
+
.map((n, i) => [n, i])
|
|
477
|
+
.sort((a, b) => layer.get(a[0]) - layer.get(b[0]) || a[1] - b[1])
|
|
478
|
+
.forEach(([n], row) => rowOf.set(n, row));
|
|
479
|
+
const parentsOf = new Map(g.nodes.map(n => [n, []]));
|
|
480
|
+
const unplacedKids = new Map(g.nodes.map(n => [n, 0]));
|
|
481
|
+
for (const [u, v] of g.edges) {
|
|
482
|
+
parentsOf.get(v).push(u);
|
|
483
|
+
unplacedKids.set(u, unplacedKids.get(u) + 1);
|
|
484
|
+
}
|
|
485
|
+
// Greedy lane assignment in row order: continue a parent's lane where
|
|
486
|
+
// possible (the branch stays in its column), else take the first free
|
|
487
|
+
// lane. A lane frees once its owner's last child is placed.
|
|
488
|
+
const laneOf = new Map();
|
|
489
|
+
const owner = [];
|
|
490
|
+
for (const n of [...g.nodes].sort((a, b) => rowOf.get(a) - rowOf.get(b))) {
|
|
491
|
+
const parentLane = owner.findIndex(o => o !== null && parentsOf.get(n).includes(o));
|
|
492
|
+
let lane = parentLane;
|
|
493
|
+
if (lane === -1) {
|
|
494
|
+
lane = owner.findIndex(o => o === null);
|
|
495
|
+
if (lane === -1) {
|
|
496
|
+
lane = owner.length;
|
|
497
|
+
owner.push(null);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
laneOf.set(n, lane);
|
|
501
|
+
for (const p of parentsOf.get(n)) {
|
|
502
|
+
unplacedKids.set(p, unplacedKids.get(p) - 1);
|
|
503
|
+
if (unplacedKids.get(p) === 0) {
|
|
504
|
+
const pl = owner.indexOf(p);
|
|
505
|
+
if (pl !== -1)
|
|
506
|
+
owner[pl] = null;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
owner[lane] = unplacedKids.get(n) > 0 ? n : null;
|
|
510
|
+
}
|
|
511
|
+
const out = new Map();
|
|
512
|
+
for (const n of g.nodes) {
|
|
513
|
+
const { w, h } = sizeOf(n);
|
|
514
|
+
const cx = laneOf.get(n) * laneGap;
|
|
515
|
+
const cy = rowOf.get(n) * rowGap;
|
|
516
|
+
out.set(n, { x: cx - w / 2, y: cy - h / 2, w, h });
|
|
517
|
+
}
|
|
518
|
+
return normalize(out, 0);
|
|
519
|
+
}
|
|
520
|
+
/** Shift placements so the bounding box starts at (pad, pad). */
|
|
521
|
+
function normalize(p, pad) {
|
|
522
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
523
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
524
|
+
for (const b of p.values()) {
|
|
525
|
+
minX = Math.min(minX, b.x);
|
|
526
|
+
minY = Math.min(minY, b.y);
|
|
527
|
+
}
|
|
528
|
+
if (!Number.isFinite(minX))
|
|
529
|
+
return p;
|
|
530
|
+
for (const [n, b] of p)
|
|
531
|
+
p.set(n, { ...b, x: b.x - minX + pad, y: b.y - minY + pad });
|
|
532
|
+
return p;
|
|
533
|
+
}
|
|
534
|
+
/** Bounding size of a placement map. */
|
|
535
|
+
export function extent(p) {
|
|
536
|
+
let w = 0;
|
|
537
|
+
let h = 0;
|
|
538
|
+
for (const b of p.values()) {
|
|
539
|
+
w = Math.max(w, b.x + b.w);
|
|
540
|
+
h = Math.max(h, b.y + b.h);
|
|
541
|
+
}
|
|
542
|
+
return { w, h };
|
|
543
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
export { Box, box } from "../core/
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export { type
|
|
5
|
-
export {
|
|
6
|
-
export {
|
|
1
|
+
export { Box, box } from "../core/index.js";
|
|
2
|
+
export { allDifferent, restrict, same } from "./csp.js";
|
|
3
|
+
export { col, type FlexOpts, type Item, row } from "./flex.js";
|
|
4
|
+
export { crossings, type Direction, extent, type Graph, type LanesOpts, type LayeredOpts, lanes, layered, type Placement, radial, rank, recurrent, type Size, scc, tree, } from "./graph.js";
|
|
5
|
+
export { type Interval, interval, intervalCell, isContradiction, isTop, type Lattice, type LatticeCell, latticeCell, latticeFor, merge, point, set, setCell, width, } from "./lattice.js";
|
|
6
|
+
export { attach, centerInside, follow, type GridOpts, grid, inset, lockSize, pinEdge, type Side, } from "./layout.js";
|
|
7
|
+
export { add, bound, equal, fix, order, total } from "./numeric.js";
|
|
8
|
+
export { type Propagator, propagator, Solver, type SolverOpts, solve, solver } from "./solver.js";
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
// Propagators — the monotone, partial-information solver layer.
|
|
2
|
+
//
|
|
3
|
+
// Cells hold partial knowledge on a lattice; propagators only ever
|
|
4
|
+
// narrow it (`merge`). Fixpoint iteration terminates by the structure
|
|
5
|
+
// of the lattice, so there is no divergence panic and no fuel cap that
|
|
6
|
+
// can lie. Two lattices cover the surface: intervals (layout, ranges,
|
|
7
|
+
// graph layering) and finite sets (CSP, sudoku, type inference).
|
|
8
|
+
export { Box, box } from "../core/index.js";
|
|
9
|
+
export { allDifferent, restrict, same } from "./csp.js";
|
|
10
|
+
export { col, row } from "./flex.js";
|
|
11
|
+
export { crossings, extent, lanes, layered, radial, rank, recurrent, scc, tree, } from "./graph.js";
|
|
12
|
+
export { interval, intervalCell, isContradiction, isTop, latticeCell, latticeFor, merge, point, set, setCell, width, } from "./lattice.js";
|
|
13
|
+
export { attach, centerInside, follow, grid, inset, lockSize, pinEdge, } from "./layout.js";
|
|
14
|
+
export { add, bound, equal, fix, order, total } from "./numeric.js";
|
|
15
|
+
export { propagator, Solver, solve, solver } from "./solver.js";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type Cell, type Writable } from "../core/index.js";
|
|
2
|
+
/** A meet-semilattice with a greatest element and a contradiction test.
|
|
3
|
+
* `meet` is the only way information enters a cell, so it must be
|
|
4
|
+
* commutative, associative, idempotent, and ≤ both inputs. */
|
|
5
|
+
export interface Lattice<T> {
|
|
6
|
+
/** No information — the identity for `meet`. */
|
|
7
|
+
readonly top: T;
|
|
8
|
+
/** Greatest lower bound: combine two pieces of partial knowledge. */
|
|
9
|
+
meet(a: T, b: T): T;
|
|
10
|
+
/** Value equality — drives change detection (and cell de-duping). */
|
|
11
|
+
equals(a: T, b: T): boolean;
|
|
12
|
+
/** Self-contradiction: the empty interval / empty candidate set. */
|
|
13
|
+
isBottom(a: T): boolean;
|
|
14
|
+
}
|
|
15
|
+
/** A `cell<T>` carrying a `Lattice<T>` so `merge` can narrow generically. */
|
|
16
|
+
export type LatticeCell<T> = Writable<Cell<T>>;
|
|
17
|
+
/** The lattice a cell was minted with, or `undefined` for a plain cell. */
|
|
18
|
+
export declare function latticeFor<T>(c: Cell<T>): Lattice<T> | undefined;
|
|
19
|
+
/** Mint a cell over `lat`, seeded at `top` (no information) by default. */
|
|
20
|
+
export declare function latticeCell<T>(lat: Lattice<T>, init?: T): LatticeCell<T>;
|
|
21
|
+
/** Narrow `c` by `info` (monotone meet). No-ops when nothing sharpens, so
|
|
22
|
+
* it's safe to call in any order and any number of times. Returns true
|
|
23
|
+
* iff the cell actually narrowed. */
|
|
24
|
+
export declare function merge<T>(c: LatticeCell<T>, info: T): boolean;
|
|
25
|
+
/** True when the cell's knowledge has collapsed to a contradiction. */
|
|
26
|
+
export declare function isContradiction<T>(c: Cell<T>): boolean;
|
|
27
|
+
/** True when the cell still holds no information (its lattice `top`). */
|
|
28
|
+
export declare function isTop<T>(c: Cell<T>): boolean;
|
|
29
|
+
/** An inclusive numeric interval `[lo, hi]`. `top` is `[-∞, ∞]`;
|
|
30
|
+
* `lo > hi` is bottom (the empty interval). */
|
|
31
|
+
export type Interval = readonly [number, number];
|
|
32
|
+
export declare const interval: Lattice<Interval>;
|
|
33
|
+
/** Interval cell, optionally seeded with bounds (defaults to `top`). */
|
|
34
|
+
export declare function intervalCell(lo?: number, hi?: number): LatticeCell<Interval>;
|
|
35
|
+
/** Width of an interval (`Infinity` if unbounded, negative if bottom). */
|
|
36
|
+
export declare function width(i: Interval): number;
|
|
37
|
+
/** A single value, or `undefined` when the interval isn't a point. */
|
|
38
|
+
export declare function point(i: Interval): number | undefined;
|
|
39
|
+
/** A finite candidate-set lattice over `universe`. `top` is the whole
|
|
40
|
+
* universe (all candidates open); `meet` intersects; bottom is empty.
|
|
41
|
+
* Height is `|universe|`, so narrowing always terminates. */
|
|
42
|
+
export declare function set<E>(universe: Iterable<E>): Lattice<ReadonlySet<E>>;
|
|
43
|
+
/** Candidate-set cell over `universe`, seeded with `init` (defaults to
|
|
44
|
+
* the whole universe). */
|
|
45
|
+
export declare function setCell<E>(universe: Iterable<E>, init?: Iterable<E>): LatticeCell<ReadonlySet<E>>;
|