pi-openui 0.0.0-stage → 0.1.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/CHANGELOG.md +35 -0
- package/LICENSE +21 -0
- package/README.md +200 -2
- package/package.json +54 -4
- package/src/blocks/braille.ts +121 -0
- package/src/blocks/bubble.ts +30 -0
- package/src/blocks/charts.ts +370 -0
- package/src/blocks/code.ts +101 -0
- package/src/blocks/core.ts +343 -0
- package/src/blocks/diagrams.ts +142 -0
- package/src/blocks/geo.ts +206 -0
- package/src/blocks/interact.ts +252 -0
- package/src/blocks/markdown.ts +135 -0
- package/src/blocks/notes.ts +277 -0
- package/src/blocks/render.ts +148 -0
- package/src/blocks/sequence.ts +266 -0
- package/src/blocks/table.ts +70 -0
- package/src/blocks/types.ts +126 -0
- package/src/blocks/util.ts +171 -0
- package/src/data/land.ts +3 -0
- package/src/data/sample-image.ts +67 -0
- package/src/diagram/canvas.ts +182 -0
- package/src/diagram/dag.ts +174 -0
- package/src/diagram/decode.ts +241 -0
- package/src/diagram/erd.ts +190 -0
- package/src/diagram/infra.ts +444 -0
- package/src/diagram/layered.ts +334 -0
- package/src/diagram/layeredDiagram.ts +350 -0
- package/src/diagram/palette.ts +209 -0
- package/src/diagram/render.ts +60 -0
- package/src/diagram/router.ts +402 -0
- package/src/diagram/wires.ts +271 -0
- package/src/lang/docs.ts +575 -0
- package/src/lang/library.ts +613 -0
- package/src/lang/md.ts +47 -0
- package/src/lang/parse.ts +230 -0
- package/src/lang/samples.ts +227 -0
- package/src/pi/extension.ts +190 -0
- package/src/pi/images.ts +185 -0
- package/src/pi/widget.ts +562 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
// Layered graph placement (Sugiyama style), deterministic.
|
|
2
|
+
// 1. break cycles (DFS feedback edges are reversed for layout only)
|
|
3
|
+
// 2. longest-path layering, sources pulled next to their consumers
|
|
4
|
+
// 3. dummy items for edges that span several layers
|
|
5
|
+
// 4. crossing minimisation: barycenter sweeps + adjacent transposition
|
|
6
|
+
// 5. cross-axis coordinates: per-layer exact solve (weighted isotonic regression), a few sweeps
|
|
7
|
+
|
|
8
|
+
export interface EdgeIn {
|
|
9
|
+
a: number;
|
|
10
|
+
b: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface LEdge {
|
|
14
|
+
/** layout direction a -> b, always from a lower layer to a higher one */
|
|
15
|
+
a: number;
|
|
16
|
+
b: number;
|
|
17
|
+
/** index into the input edge list */
|
|
18
|
+
src: number;
|
|
19
|
+
/** true when the input edge ran b -> a and was reversed to break a cycle */
|
|
20
|
+
rev: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface Layered {
|
|
24
|
+
n: number;
|
|
25
|
+
edges: LEdge[];
|
|
26
|
+
nLayers: number;
|
|
27
|
+
itemLayer: number[];
|
|
28
|
+
/** real node index, or -1 for a dummy */
|
|
29
|
+
itemNode: number[];
|
|
30
|
+
/** per layout edge: item ids from a to b, adjacent layers */
|
|
31
|
+
chains: number[][];
|
|
32
|
+
segs: Array<{ u: number; v: number; e: number }>;
|
|
33
|
+
up: number[][];
|
|
34
|
+
down: number[][];
|
|
35
|
+
order: number[][];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function breakCycles(n: number, edges: EdgeIn[]): boolean[] {
|
|
39
|
+
const out: number[][] = Array.from({ length: n }, () => []);
|
|
40
|
+
const indeg = new Array<number>(n).fill(0);
|
|
41
|
+
edges.forEach((e, i) => {
|
|
42
|
+
out[e.a].push(i);
|
|
43
|
+
indeg[e.b]++;
|
|
44
|
+
});
|
|
45
|
+
const state = new Uint8Array(n);
|
|
46
|
+
const rev = new Array<boolean>(edges.length).fill(false);
|
|
47
|
+
const starts = Array.from({ length: n }, (_, i) => i).sort((x, y) => (indeg[x] === 0 ? 0 : 1) - (indeg[y] === 0 ? 0 : 1) || x - y);
|
|
48
|
+
for (const s of starts) {
|
|
49
|
+
if (state[s]) continue;
|
|
50
|
+
const stack: Array<{ v: number; i: number }> = [{ v: s, i: 0 }];
|
|
51
|
+
state[s] = 1;
|
|
52
|
+
while (stack.length) {
|
|
53
|
+
const top = stack[stack.length - 1];
|
|
54
|
+
if (top.i < out[top.v].length) {
|
|
55
|
+
const e = out[top.v][top.i++];
|
|
56
|
+
const w = edges[e].b;
|
|
57
|
+
if (state[w] === 0) {
|
|
58
|
+
state[w] = 1;
|
|
59
|
+
stack.push({ v: w, i: 0 });
|
|
60
|
+
} else if (state[w] === 1) rev[e] = true;
|
|
61
|
+
} else {
|
|
62
|
+
state[top.v] = 2;
|
|
63
|
+
stack.pop();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return rev;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function layerize(n: number, edgesIn: EdgeIn[]): Layered {
|
|
71
|
+
const rev = breakCycles(n, edgesIn);
|
|
72
|
+
const edges: LEdge[] = edgesIn.map((e, i) => (rev[i] ? { a: e.b, b: e.a, src: i, rev: true } : { a: e.a, b: e.b, src: i, rev: false }));
|
|
73
|
+
|
|
74
|
+
// longest-path layering (Kahn, stable)
|
|
75
|
+
const outE: number[][] = Array.from({ length: n }, () => []);
|
|
76
|
+
const indeg = new Array<number>(n).fill(0);
|
|
77
|
+
for (const [i, e] of edges.entries()) {
|
|
78
|
+
outE[e.a].push(i);
|
|
79
|
+
indeg[e.b]++;
|
|
80
|
+
}
|
|
81
|
+
const layer = new Array<number>(n).fill(0);
|
|
82
|
+
const hasPred = indeg.map((d) => d > 0);
|
|
83
|
+
const deg = indeg.slice();
|
|
84
|
+
const queue: number[] = [];
|
|
85
|
+
for (let v = 0; v < n; v++) if (deg[v] === 0) queue.push(v);
|
|
86
|
+
for (let qi = 0; qi < queue.length; qi++) {
|
|
87
|
+
const v = queue[qi];
|
|
88
|
+
for (const ei of outE[v]) {
|
|
89
|
+
const w = edges[ei].b;
|
|
90
|
+
layer[w] = Math.max(layer[w], layer[v] + 1);
|
|
91
|
+
if (--deg[w] === 0) queue.push(w);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (let v = 0; v < n; v++) {
|
|
95
|
+
if (!hasPred[v] && outE[v].length) layer[v] = Math.min(...outE[v].map((ei) => layer[edges[ei].b])) - 1;
|
|
96
|
+
}
|
|
97
|
+
// compress unused layer indexes
|
|
98
|
+
const used = Array.from(new Set(layer)).sort((x, y) => x - y);
|
|
99
|
+
const remap = new Map(used.map((l, i) => [l, i]));
|
|
100
|
+
for (let v = 0; v < n; v++) layer[v] = remap.get(layer[v])!;
|
|
101
|
+
const nLayers = used.length;
|
|
102
|
+
|
|
103
|
+
const itemLayer = layer.slice();
|
|
104
|
+
const itemNode = Array.from({ length: n }, (_, i) => i);
|
|
105
|
+
const chains: number[][] = [];
|
|
106
|
+
const segs: Layered["segs"] = [];
|
|
107
|
+
for (const [ei, e] of edges.entries()) {
|
|
108
|
+
const chain = [e.a];
|
|
109
|
+
for (let l = layer[e.a] + 1; l < layer[e.b]; l++) {
|
|
110
|
+
itemLayer.push(l);
|
|
111
|
+
itemNode.push(-1);
|
|
112
|
+
chain.push(itemLayer.length - 1);
|
|
113
|
+
}
|
|
114
|
+
chain.push(e.b);
|
|
115
|
+
chains.push(chain);
|
|
116
|
+
for (let k = 0; k + 1 < chain.length; k++) segs.push({ u: chain[k], v: chain[k + 1], e: ei });
|
|
117
|
+
}
|
|
118
|
+
const N = itemLayer.length;
|
|
119
|
+
const up: number[][] = Array.from({ length: N }, () => []);
|
|
120
|
+
const down: number[][] = Array.from({ length: N }, () => []);
|
|
121
|
+
for (const s of segs) {
|
|
122
|
+
down[s.u].push(s.v);
|
|
123
|
+
up[s.v].push(s.u);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// initial order: DFS pre-order from the first layer on
|
|
127
|
+
const order: number[][] = Array.from({ length: nLayers }, () => []);
|
|
128
|
+
const seen = new Uint8Array(N);
|
|
129
|
+
const byLayer = Array.from({ length: N }, (_, i) => i).sort((x, y) => itemLayer[x] - itemLayer[y] || x - y);
|
|
130
|
+
for (const root of byLayer) {
|
|
131
|
+
if (seen[root]) continue;
|
|
132
|
+
const stack = [root];
|
|
133
|
+
while (stack.length) {
|
|
134
|
+
const v = stack.pop()!;
|
|
135
|
+
if (seen[v]) continue;
|
|
136
|
+
seen[v] = 1;
|
|
137
|
+
order[itemLayer[v]].push(v);
|
|
138
|
+
for (let k = down[v].length - 1; k >= 0; k--) stack.push(down[v][k]);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const L: Layered = { n, edges, nLayers, itemLayer, itemNode, chains, segs, up, down, order };
|
|
143
|
+
minimizeCrossings(L);
|
|
144
|
+
return L;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function positions(L: Layered): number[] {
|
|
148
|
+
const pos = new Array<number>(L.itemLayer.length).fill(0);
|
|
149
|
+
for (const ord of L.order) ord.forEach((it, i) => (pos[it] = i));
|
|
150
|
+
return pos;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function crossingsBetween(L: Layered, pos: number[], l: number, segsAt: Array<{ u: number; v: number }>): number {
|
|
154
|
+
const m = segsAt.length;
|
|
155
|
+
if (m < 2) return 0;
|
|
156
|
+
const arr = segsAt.map((s) => [pos[s.u], pos[s.v]] as const).sort((p, q) => p[0] - q[0] || p[1] - q[1]);
|
|
157
|
+
const size = L.order[l + 1].length + 2;
|
|
158
|
+
const bit = new Int32Array(size + 1);
|
|
159
|
+
let inv = 0;
|
|
160
|
+
let seen = 0;
|
|
161
|
+
for (const [, pv] of arr) {
|
|
162
|
+
// strictly greater earlier values
|
|
163
|
+
let le = 0;
|
|
164
|
+
for (let i = pv + 1; i > 0; i -= i & -i) le += bit[i];
|
|
165
|
+
inv += seen - le;
|
|
166
|
+
for (let i = pv + 1; i <= size; i += i & -i) bit[i]++;
|
|
167
|
+
seen++;
|
|
168
|
+
}
|
|
169
|
+
return inv;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function totalCrossings(L: Layered, pos: number[], segsByLayer: Array<Array<{ u: number; v: number }>>): number {
|
|
173
|
+
let t = 0;
|
|
174
|
+
for (let l = 0; l + 1 < L.nLayers; l++) t += crossingsBetween(L, pos, l, segsByLayer[l]);
|
|
175
|
+
return t;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function minimizeCrossings(L: Layered): void {
|
|
179
|
+
if (L.nLayers < 2) return;
|
|
180
|
+
const segsByLayer: Array<Array<{ u: number; v: number }>> = Array.from({ length: L.nLayers - 1 }, () => []);
|
|
181
|
+
for (const s of L.segs) segsByLayer[L.itemLayer[s.u]].push({ u: s.u, v: s.v });
|
|
182
|
+
let pos = positions(L);
|
|
183
|
+
let bestC = totalCrossings(L, pos, segsByLayer);
|
|
184
|
+
let best = L.order.map((o) => o.slice());
|
|
185
|
+
if (bestC === 0) return;
|
|
186
|
+
const reorder = (l: number, nb: number[][]) => {
|
|
187
|
+
const ord = L.order[l];
|
|
188
|
+
const key = new Map<number, number>();
|
|
189
|
+
ord.forEach((it, i) => {
|
|
190
|
+
const ns = nb[it];
|
|
191
|
+
key.set(it, ns.length ? ns.reduce((a, j) => a + pos[j], 0) / ns.length : i);
|
|
192
|
+
});
|
|
193
|
+
ord.sort((x, y) => key.get(x)! - key.get(y)! || pos[x] - pos[y]);
|
|
194
|
+
ord.forEach((it, i) => (pos[it] = i));
|
|
195
|
+
};
|
|
196
|
+
const pairCross = (a: number, b: number): number => {
|
|
197
|
+
let c = 0;
|
|
198
|
+
for (const ua of L.up[a]) for (const ub of L.up[b]) if (pos[ua] > pos[ub]) c++;
|
|
199
|
+
for (const da of L.down[a]) for (const db of L.down[b]) if (pos[da] > pos[db]) c++;
|
|
200
|
+
return c;
|
|
201
|
+
};
|
|
202
|
+
const transpose = () => {
|
|
203
|
+
for (let pass = 0; pass < 8; pass++) {
|
|
204
|
+
let improved = false;
|
|
205
|
+
for (let l = 0; l < L.nLayers; l++) {
|
|
206
|
+
const ord = L.order[l];
|
|
207
|
+
for (let i = 0; i + 1 < ord.length; i++) {
|
|
208
|
+
const a = ord[i];
|
|
209
|
+
const b = ord[i + 1];
|
|
210
|
+
if (pairCross(b, a) < pairCross(a, b)) {
|
|
211
|
+
ord[i] = b;
|
|
212
|
+
ord[i + 1] = a;
|
|
213
|
+
pos[a] = i + 1;
|
|
214
|
+
pos[b] = i;
|
|
215
|
+
improved = true;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (!improved) break;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
let stagnant = 0;
|
|
223
|
+
for (let iter = 0; iter < 30 && stagnant < 6 && bestC > 0; iter++) {
|
|
224
|
+
if (iter % 2 === 0) for (let l = 1; l < L.nLayers; l++) reorder(l, L.up);
|
|
225
|
+
else for (let l = L.nLayers - 2; l >= 0; l--) reorder(l, L.down);
|
|
226
|
+
transpose();
|
|
227
|
+
pos = positions(L);
|
|
228
|
+
const c = totalCrossings(L, pos, segsByLayer);
|
|
229
|
+
if (c < bestC) {
|
|
230
|
+
bestC = c;
|
|
231
|
+
best = L.order.map((o) => o.slice());
|
|
232
|
+
stagnant = 0;
|
|
233
|
+
} else stagnant++;
|
|
234
|
+
}
|
|
235
|
+
for (let l = 0; l < L.nLayers; l++) L.order[l] = best[l];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function countCrossings(L: Layered): number {
|
|
239
|
+
const segsByLayer: Array<Array<{ u: number; v: number }>> = Array.from({ length: Math.max(0, L.nLayers - 1) }, () => []);
|
|
240
|
+
for (const s of L.segs) segsByLayer[L.itemLayer[s.u]].push({ u: s.u, v: s.v });
|
|
241
|
+
return totalCrossings(L, positions(L), segsByLayer);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Weighted isotonic regression (pool adjacent violators). Returns the fitted value for each input. */
|
|
245
|
+
function pav(t: number[], w: number[]): number[] {
|
|
246
|
+
const sum: number[] = [];
|
|
247
|
+
const wsum: number[] = [];
|
|
248
|
+
const cnt: number[] = [];
|
|
249
|
+
for (let i = 0; i < t.length; i++) {
|
|
250
|
+
sum.push(t[i] * w[i]);
|
|
251
|
+
wsum.push(w[i]);
|
|
252
|
+
cnt.push(1);
|
|
253
|
+
while (sum.length >= 2) {
|
|
254
|
+
const k = sum.length - 1;
|
|
255
|
+
if (sum[k - 1] / wsum[k - 1] <= sum[k] / wsum[k]) break;
|
|
256
|
+
sum[k - 1] += sum[k];
|
|
257
|
+
wsum[k - 1] += wsum[k];
|
|
258
|
+
cnt[k - 1] += cnt[k];
|
|
259
|
+
sum.pop();
|
|
260
|
+
wsum.pop();
|
|
261
|
+
cnt.pop();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const out: number[] = [];
|
|
265
|
+
for (let b = 0; b < sum.length; b++) for (let j = 0; j < cnt[b]; j++) out.push(sum[b] / wsum[b]);
|
|
266
|
+
return out;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Cross-axis coordinates (left edge per item), minimum 0.
|
|
271
|
+
* `cs` is the item size along the cross axis, `gap(a, b)` the blank cells required between neighbours.
|
|
272
|
+
*/
|
|
273
|
+
export function assignCross(L: Layered, cs: number[], gap: (a: number, b: number) => number, weight: (u: number, v: number) => number): number[] {
|
|
274
|
+
const N = L.itemLayer.length;
|
|
275
|
+
const left = new Array<number>(N).fill(0);
|
|
276
|
+
const layerWidth = L.order.map((ord) => ord.reduce((acc, it, i) => acc + cs[it] + (i ? gap(ord[i - 1], it) : 0), 0));
|
|
277
|
+
const maxW = Math.max(0, ...layerWidth);
|
|
278
|
+
for (let l = 0; l < L.nLayers; l++) {
|
|
279
|
+
let x = Math.floor((maxW - layerWidth[l]) / 2);
|
|
280
|
+
L.order[l].forEach((it, i, ord) => {
|
|
281
|
+
if (i) x += gap(ord[i - 1], it);
|
|
282
|
+
left[it] = x;
|
|
283
|
+
x += cs[it];
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
const center = (i: number) => left[i] + (cs[i] - 1) / 2;
|
|
287
|
+
const relayer = (l: number, useUp: boolean, useDown: boolean) => {
|
|
288
|
+
const ord = L.order[l];
|
|
289
|
+
const k = ord.length;
|
|
290
|
+
if (!k) return;
|
|
291
|
+
const off: number[] = [];
|
|
292
|
+
const t: number[] = [];
|
|
293
|
+
const wt: number[] = [];
|
|
294
|
+
for (let i = 0; i < k; i++) {
|
|
295
|
+
const it = ord[i];
|
|
296
|
+
off.push(i ? off[i - 1] + cs[ord[i - 1]] + gap(ord[i - 1], it) : 0);
|
|
297
|
+
let sw = 0;
|
|
298
|
+
let sc = 0;
|
|
299
|
+
if (useUp)
|
|
300
|
+
for (const u of L.up[it]) {
|
|
301
|
+
const w = weight(u, it);
|
|
302
|
+
sw += w;
|
|
303
|
+
sc += w * center(u);
|
|
304
|
+
}
|
|
305
|
+
if (useDown)
|
|
306
|
+
for (const d of L.down[it]) {
|
|
307
|
+
const w = weight(it, d);
|
|
308
|
+
sw += w;
|
|
309
|
+
sc += w * center(d);
|
|
310
|
+
}
|
|
311
|
+
const desired = sw > 0 ? sc / sw - (cs[it] - 1) / 2 : left[it];
|
|
312
|
+
t.push(desired - off[i]);
|
|
313
|
+
wt.push(sw > 0 ? sw : 0.01);
|
|
314
|
+
}
|
|
315
|
+
const fit = pav(t, wt);
|
|
316
|
+
for (let i = 0; i < k; i++) left[ord[i]] = fit[i] + off[i];
|
|
317
|
+
};
|
|
318
|
+
for (let r = 0; r < 5; r++) {
|
|
319
|
+
for (let l = 1; l < L.nLayers; l++) relayer(l, true, false);
|
|
320
|
+
for (let l = L.nLayers - 2; l >= 0; l--) relayer(l, false, true);
|
|
321
|
+
}
|
|
322
|
+
for (let r = 0; r < 2; r++) for (let l = 0; l < L.nLayers; l++) relayer(l, true, true);
|
|
323
|
+
// integer positions with the separation enforced
|
|
324
|
+
for (let l = 0; l < L.nLayers; l++) {
|
|
325
|
+
const ord = L.order[l];
|
|
326
|
+
for (let i = 0; i < ord.length; i++) {
|
|
327
|
+
let v = Math.round(left[ord[i]]);
|
|
328
|
+
if (i) v = Math.max(v, left[ord[i - 1]] + cs[ord[i - 1]] + gap(ord[i - 1], ord[i]));
|
|
329
|
+
left[ord[i]] = v;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const min = Math.min(...left);
|
|
333
|
+
return left.map((v) => v - min);
|
|
334
|
+
}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
// Shared builder for layered diagrams (DAG, ERD): placement -> routing -> raster.
|
|
2
|
+
|
|
3
|
+
import { Canvas, textWidth, type Cell, type Rect } from "./canvas.ts";
|
|
4
|
+
import { assignCross, layerize } from "./layered.ts";
|
|
5
|
+
import { Router, type RouteReq, type Routed, type Waypoint } from "./router.ts";
|
|
6
|
+
import { drawWires, type Mark, type WireSpec } from "./wires.ts";
|
|
7
|
+
|
|
8
|
+
export interface PlacedNode {
|
|
9
|
+
id: string;
|
|
10
|
+
index: number;
|
|
11
|
+
label: string;
|
|
12
|
+
status: string | null;
|
|
13
|
+
rect: Rect;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PlacedEdge {
|
|
17
|
+
/** index in the normalised input edge list */
|
|
18
|
+
index: number;
|
|
19
|
+
from: string;
|
|
20
|
+
to: string;
|
|
21
|
+
reversed: boolean;
|
|
22
|
+
ok: boolean;
|
|
23
|
+
forced: boolean;
|
|
24
|
+
label?: string;
|
|
25
|
+
cells: Array<[number, number]>;
|
|
26
|
+
labelCells: Array<[number, number]>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DiagramResult {
|
|
30
|
+
kind: string;
|
|
31
|
+
direction: "LR" | "TB";
|
|
32
|
+
width: number;
|
|
33
|
+
height: number;
|
|
34
|
+
rows: Cell[][];
|
|
35
|
+
nodes: PlacedNode[];
|
|
36
|
+
edges: PlacedEdge[];
|
|
37
|
+
warnings: string[];
|
|
38
|
+
footnotes: string[];
|
|
39
|
+
degraded: boolean;
|
|
40
|
+
overflow: boolean;
|
|
41
|
+
/** statuses in use, for tests and the pi adapter */
|
|
42
|
+
statuses: string[];
|
|
43
|
+
/** legend entries drawn under the diagram */
|
|
44
|
+
legend: LegendEntry[];
|
|
45
|
+
/** edges that could not be routed cleanly. They are not drawn and are listed in the footnotes. */
|
|
46
|
+
omitted?: Array<{ from: string; to: string; label?: string }>;
|
|
47
|
+
/** zones (InfraMap), for hit-testing */
|
|
48
|
+
zones?: Array<{ name: string; tone: string; rect: Rect }>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface LegendEntry {
|
|
52
|
+
glyph: string;
|
|
53
|
+
role: string;
|
|
54
|
+
text: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface LNodeSpec {
|
|
58
|
+
id: string;
|
|
59
|
+
label: string;
|
|
60
|
+
status: string | null;
|
|
61
|
+
baseW: number;
|
|
62
|
+
baseH: number;
|
|
63
|
+
paint: (c: Canvas, r: Rect) => void;
|
|
64
|
+
/** rows (relative to the box top) where no wire may attach, e.g. a table separator */
|
|
65
|
+
noPortRows?: number[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface LEdgeSpec {
|
|
69
|
+
a: number;
|
|
70
|
+
b: number;
|
|
71
|
+
label?: string;
|
|
72
|
+
dashed?: boolean;
|
|
73
|
+
fromMark: Mark;
|
|
74
|
+
toMark: Mark;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface LayeredSpec {
|
|
78
|
+
nodes: LNodeSpec[];
|
|
79
|
+
edges: LEdgeSpec[];
|
|
80
|
+
/** spacing between ports on one side, default 2 */
|
|
81
|
+
portSpacing?: number;
|
|
82
|
+
/** draw edges that were reversed to break a cycle dashed */
|
|
83
|
+
dashReversed?: boolean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface Built {
|
|
87
|
+
canvas: Canvas;
|
|
88
|
+
nodes: PlacedNode[];
|
|
89
|
+
edges: PlacedEdge[];
|
|
90
|
+
failed: number;
|
|
91
|
+
forced: number;
|
|
92
|
+
footnotes: string[];
|
|
93
|
+
omitted: Array<{ from: string; to: string; label?: string }>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const MARGIN = 3;
|
|
97
|
+
|
|
98
|
+
function build(spec: LayeredSpec, dir: "LR" | "TB", spacing: number, relax: boolean): Built | { failed: number } {
|
|
99
|
+
const n = spec.nodes.length;
|
|
100
|
+
const TB = dir === "TB";
|
|
101
|
+
const sp = spec.portSpacing ?? 2;
|
|
102
|
+
const L = layerize(
|
|
103
|
+
n,
|
|
104
|
+
spec.edges.map((e) => ({ a: e.a, b: e.b })),
|
|
105
|
+
);
|
|
106
|
+
const N = L.itemLayer.length;
|
|
107
|
+
const inDeg = new Array<number>(n).fill(0);
|
|
108
|
+
const outDeg = new Array<number>(n).fill(0);
|
|
109
|
+
for (const e of L.edges) {
|
|
110
|
+
outDeg[e.a]++;
|
|
111
|
+
inDeg[e.b]++;
|
|
112
|
+
}
|
|
113
|
+
const w = new Array<number>(N).fill(1);
|
|
114
|
+
const h = new Array<number>(N).fill(1);
|
|
115
|
+
for (let v = 0; v < n; v++) {
|
|
116
|
+
const k = Math.max(inDeg[v], outDeg[v]);
|
|
117
|
+
const need = k > 0 ? (k - 1) * sp + 3 : 1;
|
|
118
|
+
if (TB) {
|
|
119
|
+
w[v] = Math.max(spec.nodes[v].baseW, need);
|
|
120
|
+
h[v] = spec.nodes[v].baseH;
|
|
121
|
+
} else {
|
|
122
|
+
w[v] = spec.nodes[v].baseW;
|
|
123
|
+
h[v] = Math.max(spec.nodes[v].baseH, need);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const cs = (i: number) => (TB ? w[i] : h[i]);
|
|
127
|
+
const fs = (i: number) => (TB ? h[i] : w[i]);
|
|
128
|
+
const csArr = Array.from({ length: N }, (_, i) => cs(i));
|
|
129
|
+
const nodeGap = (TB ? 4 : 2) + spacing;
|
|
130
|
+
const cross = assignCross(
|
|
131
|
+
L,
|
|
132
|
+
csArr,
|
|
133
|
+
(a, b) => (a < n && b < n ? nodeGap : 2),
|
|
134
|
+
(u, v) => (u >= n && v >= n ? 6 : u >= n || v >= n ? 3 : 1),
|
|
135
|
+
);
|
|
136
|
+
const center = (i: number) => cross[i] + (csArr[i] - 1) / 2;
|
|
137
|
+
// band thickness and channel gaps along the flow axis
|
|
138
|
+
const T = new Array<number>(L.nLayers).fill(1);
|
|
139
|
+
for (let i = 0; i < N; i++) T[L.itemLayer[i]] = Math.max(T[L.itemLayer[i]], fs(i));
|
|
140
|
+
// tracks a channel needs: the largest number of horizontal spans that overlap at one position
|
|
141
|
+
const nonStraight = new Array<number>(Math.max(0, L.nLayers - 1)).fill(0);
|
|
142
|
+
{
|
|
143
|
+
const spans: Array<Array<[number, number]>> = Array.from({ length: Math.max(0, L.nLayers - 1) }, () => []);
|
|
144
|
+
for (const s of L.segs) {
|
|
145
|
+
const a = center(s.u);
|
|
146
|
+
const b = center(s.v);
|
|
147
|
+
if (Math.abs(a - b) > 1) spans[L.itemLayer[s.u]].push([Math.min(a, b), Math.max(a, b)]);
|
|
148
|
+
}
|
|
149
|
+
spans.forEach((sp, l) => {
|
|
150
|
+
const ev: Array<[number, number]> = [];
|
|
151
|
+
for (const [lo, hi] of sp) {
|
|
152
|
+
ev.push([lo, 1]);
|
|
153
|
+
ev.push([hi + 0.5, -1]);
|
|
154
|
+
}
|
|
155
|
+
ev.sort((p, q) => p[0] - q[0] || p[1] - q[1]);
|
|
156
|
+
let cur = 0;
|
|
157
|
+
let best = 0;
|
|
158
|
+
for (const [, d] of ev) {
|
|
159
|
+
cur += d;
|
|
160
|
+
best = Math.max(best, cur);
|
|
161
|
+
}
|
|
162
|
+
nonStraight[l] = best;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
const labelNeed = new Array<number>(Math.max(0, L.nLayers - 1)).fill(0);
|
|
166
|
+
for (const [ei, e] of L.edges.entries()) {
|
|
167
|
+
const lab = spec.edges[e.src].label;
|
|
168
|
+
if (lab) labelNeed[L.itemLayer[L.chains[ei][0]]] = Math.max(labelNeed[L.itemLayer[L.chains[ei][0]]], textWidth(lab));
|
|
169
|
+
}
|
|
170
|
+
const gapFlow = (l: number) => {
|
|
171
|
+
const ns = nonStraight[l];
|
|
172
|
+
let g = (TB ? 2 : 5) + ns + spacing * 2;
|
|
173
|
+
if (labelNeed[l]) g = Math.max(g, TB ? 3 : labelNeed[l] + 4);
|
|
174
|
+
return g;
|
|
175
|
+
};
|
|
176
|
+
const F = new Array<number>(L.nLayers).fill(0);
|
|
177
|
+
F[0] = MARGIN;
|
|
178
|
+
for (let l = 0; l + 1 < L.nLayers; l++) F[l + 1] = F[l] + T[l] + gapFlow(l);
|
|
179
|
+
const rect: Rect[] = [];
|
|
180
|
+
let W = 0;
|
|
181
|
+
let H = 0;
|
|
182
|
+
for (let i = 0; i < N; i++) {
|
|
183
|
+
const l = L.itemLayer[i];
|
|
184
|
+
const off = Math.floor((T[l] - fs(i)) / 2);
|
|
185
|
+
const r = TB ? { x: MARGIN + cross[i], y: F[l] + off, w: w[i], h: h[i] } : { x: F[l] + off, y: MARGIN + cross[i], w: w[i], h: h[i] };
|
|
186
|
+
rect.push(r);
|
|
187
|
+
W = Math.max(W, r.x + r.w);
|
|
188
|
+
H = Math.max(H, r.y + r.h);
|
|
189
|
+
}
|
|
190
|
+
W += MARGIN;
|
|
191
|
+
H += MARGIN;
|
|
192
|
+
const router = new Router(W, H);
|
|
193
|
+
for (let v = 0; v < n; v++) {
|
|
194
|
+
router.addSolid(rect[v], v);
|
|
195
|
+
for (const row of spec.nodes[v].noPortRows ?? []) {
|
|
196
|
+
router.addSolid({ x: rect[v].x - 1, y: rect[v].y + row, w: 1, h: 1 });
|
|
197
|
+
router.addSolid({ x: rect[v].x + rect[v].w, y: rect[v].y + row, w: 1, h: 1 });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// reserve the lanes of long edges so no other wire can turn or end on them
|
|
202
|
+
for (const [ei, chain] of L.chains.entries()) {
|
|
203
|
+
for (let k = 1; k + 1 < chain.length; k++) {
|
|
204
|
+
const it = chain[k];
|
|
205
|
+
const l = L.itemLayer[it];
|
|
206
|
+
const r = rect[it];
|
|
207
|
+
const cells = Array.from({ length: T[l] }, (_, d) => (TB ? { x: r.x, y: F[l] + d } : { x: F[l] + d, y: r.y }));
|
|
208
|
+
router.reserve(ei, cells, TB);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const fromPen = TB ? [300, 120, 0, 120] : [120, 0, 120, 300];
|
|
212
|
+
const toPen = TB ? [0, 120, 300, 120] : [120, 300, 120, 0];
|
|
213
|
+
const order = L.edges.map((_, i) => i).sort((p, q) => {
|
|
214
|
+
const sp1 = L.chains[p].length;
|
|
215
|
+
const sp2 = L.chains[q].length;
|
|
216
|
+
return sp2 - sp1 || p - q;
|
|
217
|
+
});
|
|
218
|
+
const routed = new Map<number, Routed>();
|
|
219
|
+
let failed = 0;
|
|
220
|
+
const forced = 0;
|
|
221
|
+
const omitted: Built["omitted"] = [];
|
|
222
|
+
for (const ei of order) {
|
|
223
|
+
const e = L.edges[ei];
|
|
224
|
+
const chain = L.chains[ei];
|
|
225
|
+
const wps: Waypoint[] = [];
|
|
226
|
+
for (let k = 1; k + 1 < chain.length; k++) {
|
|
227
|
+
const it = chain[k];
|
|
228
|
+
const l = L.itemLayer[it];
|
|
229
|
+
const r = rect[it];
|
|
230
|
+
const pts: Waypoint[] = TB
|
|
231
|
+
? [
|
|
232
|
+
{ x: r.x, y: F[l], dir: 2 },
|
|
233
|
+
{ x: r.x, y: F[l] + T[l] - 1, dir: 2 },
|
|
234
|
+
]
|
|
235
|
+
: [
|
|
236
|
+
{ x: F[l], y: r.y, dir: 1 },
|
|
237
|
+
{ x: F[l] + T[l] - 1, y: r.y, dir: 1 },
|
|
238
|
+
];
|
|
239
|
+
for (const p of pts) if (!wps.length || wps[wps.length - 1].x !== p.x || wps[wps.length - 1].y !== p.y) wps.push(p);
|
|
240
|
+
}
|
|
241
|
+
const req: RouteReq = { id: ei, from: rect[e.a], to: rect[e.b], fromId: e.a, toId: e.b, fromPen, toPen, waypoints: wps };
|
|
242
|
+
const r = router.route(req, false);
|
|
243
|
+
if (!r.ok) {
|
|
244
|
+
if (!relax) return { failed: failed + 1 };
|
|
245
|
+
failed++;
|
|
246
|
+
const o = spec.edges[e.src];
|
|
247
|
+
omitted.push({ from: spec.nodes[o.a].id, to: spec.nodes[o.b].id, label: o.label });
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
router.commit(r);
|
|
251
|
+
routed.set(ei, r);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// raster
|
|
255
|
+
const canvas = new Canvas(W, H);
|
|
256
|
+
for (let v = 0; v < n; v++) spec.nodes[v].paint(canvas, rect[v]);
|
|
257
|
+
const wires: WireSpec[] = [];
|
|
258
|
+
for (const [ei, r] of routed) {
|
|
259
|
+
const e = L.edges[ei];
|
|
260
|
+
const o = spec.edges[e.src];
|
|
261
|
+
wires.push({
|
|
262
|
+
id: ei,
|
|
263
|
+
steps: r.steps,
|
|
264
|
+
dashed: !!o.dashed || (!!spec.dashReversed && e.rev),
|
|
265
|
+
startMark: e.rev ? o.toMark : o.fromMark,
|
|
266
|
+
endMark: e.rev ? o.fromMark : o.toMark,
|
|
267
|
+
label: o.label,
|
|
268
|
+
desc: `${spec.nodes[o.a].id} -> ${spec.nodes[o.b].id}`,
|
|
269
|
+
role: "wire",
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
const { footnotes, labelCells } = drawWires(canvas, wires, (x, y) => router.solid[y * W + x] === 1);
|
|
273
|
+
|
|
274
|
+
// crop to content
|
|
275
|
+
let minX = W;
|
|
276
|
+
let minY = H;
|
|
277
|
+
let maxX = -1;
|
|
278
|
+
let maxY = -1;
|
|
279
|
+
for (let y = 0; y < H; y++)
|
|
280
|
+
for (let x = 0; x < W; x++)
|
|
281
|
+
if (canvas.getCh(x, y) !== " " || canvas.getRole(x, y) !== "") {
|
|
282
|
+
minX = Math.min(minX, x);
|
|
283
|
+
maxX = Math.max(maxX, x);
|
|
284
|
+
minY = Math.min(minY, y);
|
|
285
|
+
maxY = Math.max(maxY, y);
|
|
286
|
+
}
|
|
287
|
+
if (maxX < 0) {
|
|
288
|
+
minX = minY = 0;
|
|
289
|
+
maxX = maxY = 0;
|
|
290
|
+
}
|
|
291
|
+
const out = new Canvas(maxX - minX + 1, maxY - minY + 1);
|
|
292
|
+
for (let y = 0; y < out.h; y++) for (let x = 0; x < out.w; x++) out.set(x, y, canvas.getCh(x + minX, y + minY), canvas.getRole(x + minX, y + minY));
|
|
293
|
+
const nodes: PlacedNode[] = spec.nodes.map((s, i) => ({
|
|
294
|
+
id: s.id,
|
|
295
|
+
index: i,
|
|
296
|
+
label: s.label,
|
|
297
|
+
status: s.status,
|
|
298
|
+
rect: { x: rect[i].x - minX, y: rect[i].y - minY, w: rect[i].w, h: rect[i].h },
|
|
299
|
+
}));
|
|
300
|
+
const edges: PlacedEdge[] = [];
|
|
301
|
+
for (const [ei, r] of [...routed.entries()].sort((a, b) => a[0] - b[0])) {
|
|
302
|
+
const e = L.edges[ei];
|
|
303
|
+
const o = spec.edges[e.src];
|
|
304
|
+
edges.push({
|
|
305
|
+
index: e.src,
|
|
306
|
+
from: spec.nodes[o.a].id,
|
|
307
|
+
to: spec.nodes[o.b].id,
|
|
308
|
+
reversed: e.rev,
|
|
309
|
+
ok: true,
|
|
310
|
+
forced: r.forced,
|
|
311
|
+
label: o.label,
|
|
312
|
+
cells: r.steps.map((s) => [s.x - minX, s.y - minY] as [number, number]),
|
|
313
|
+
labelCells: (labelCells.get(ei) ?? []).map(([x, y]) => [x - minX, y - minY] as [number, number]),
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
for (const o of omitted) footnotes.push(`${o.from} -> ${o.to}${o.label ? ` (${o.label})` : ""} (not drawn)`);
|
|
317
|
+
return { canvas: out, nodes, edges, failed, forced, footnotes, omitted };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function layeredDiagram(kind: string, spec: LayeredSpec, dir: "LR" | "TB"): DiagramResult {
|
|
321
|
+
let built: Built | null = null;
|
|
322
|
+
for (const spacing of [0, 1, 2, 4, 7]) {
|
|
323
|
+
const r = build(spec, dir, spacing, false);
|
|
324
|
+
if ("canvas" in r) {
|
|
325
|
+
built = r;
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (!built) {
|
|
330
|
+
const r = build(spec, dir, 4, true);
|
|
331
|
+
if ("canvas" in r) built = r;
|
|
332
|
+
}
|
|
333
|
+
if (!built) throw new Error("layeredDiagram: could not place diagram");
|
|
334
|
+
return {
|
|
335
|
+
kind,
|
|
336
|
+
direction: dir,
|
|
337
|
+
width: built.canvas.w,
|
|
338
|
+
height: built.canvas.h,
|
|
339
|
+
rows: built.canvas.rows(),
|
|
340
|
+
nodes: built.nodes,
|
|
341
|
+
edges: built.edges,
|
|
342
|
+
warnings: [],
|
|
343
|
+
footnotes: built.footnotes,
|
|
344
|
+
degraded: built.omitted.length > 0,
|
|
345
|
+
overflow: false,
|
|
346
|
+
statuses: [],
|
|
347
|
+
legend: [],
|
|
348
|
+
omitted: built.omitted,
|
|
349
|
+
};
|
|
350
|
+
}
|