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,209 @@
|
|
|
1
|
+
// Role -> style mapping. `createPalette` takes style functions, so the pi adapter passes theme colors and
|
|
2
|
+
// the gallery scripts pass raw ANSI. A role is "name" or "name.variant"; ".focus" reverses the cell and
|
|
3
|
+
// ".hover" underlines it; a bare "focus"/"hover" is the same on an unstyled cell.
|
|
4
|
+
|
|
5
|
+
import type { Palette } from "./canvas.ts";
|
|
6
|
+
|
|
7
|
+
export type Style = (s: string) => string;
|
|
8
|
+
|
|
9
|
+
export interface PaletteTokens {
|
|
10
|
+
good: Style;
|
|
11
|
+
bad: Style;
|
|
12
|
+
warn: Style;
|
|
13
|
+
accent: Style;
|
|
14
|
+
info: Style;
|
|
15
|
+
muted: Style;
|
|
16
|
+
dim: Style;
|
|
17
|
+
bold: Style;
|
|
18
|
+
italic: Style;
|
|
19
|
+
underline: Style;
|
|
20
|
+
strike: Style;
|
|
21
|
+
code: Style;
|
|
22
|
+
keyword: Style;
|
|
23
|
+
string: Style;
|
|
24
|
+
number: Style;
|
|
25
|
+
comment: Style;
|
|
26
|
+
fn: Style;
|
|
27
|
+
type: Style;
|
|
28
|
+
link: Style;
|
|
29
|
+
/** categorical series colors 0..7 */
|
|
30
|
+
series: Style[];
|
|
31
|
+
/** the hover bubble background (and its text) */
|
|
32
|
+
bubble: Style;
|
|
33
|
+
/** two-colour pixel for the pie: top and bottom slice (null = empty) */
|
|
34
|
+
pie: (top: number | null, bottom: number | null) => Style;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const reverse: Style = (s) => `\x1b[7m${s}\x1b[27m`;
|
|
38
|
+
const underline: Style = (s) => `\x1b[4m${s}\x1b[24m`;
|
|
39
|
+
const compose = (...fs: Array<Style | undefined>): Style | undefined => {
|
|
40
|
+
const list = fs.filter(Boolean) as Style[];
|
|
41
|
+
return list.length ? (s) => list.reduce((acc, f) => f(acc), s) : undefined;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export function createPalette(t: PaletteTokens): Palette {
|
|
45
|
+
const status: Record<string, Style> = { done: t.good, up: t.good, running: t.accent, failed: t.bad, down: t.bad, degraded: t.warn, pending: t.muted, skipped: t.muted, unknown: t.muted };
|
|
46
|
+
const tone: Record<string, Style> = { neutral: t.muted, info: t.info, good: t.good, warn: t.warn, bad: t.bad, success: t.good, error: t.bad };
|
|
47
|
+
const ser = (i: number) => t.series[((i % 8) + 8) % 8];
|
|
48
|
+
return (role) => {
|
|
49
|
+
if (!role) return undefined;
|
|
50
|
+
const parts = role.split(".");
|
|
51
|
+
let focus = false;
|
|
52
|
+
let hover = false;
|
|
53
|
+
while (parts.length > 1 && (parts[parts.length - 1] === "focus" || parts[parts.length - 1] === "hover")) {
|
|
54
|
+
if (parts.pop() === "focus") focus = true;
|
|
55
|
+
else hover = true;
|
|
56
|
+
}
|
|
57
|
+
if (parts.length === 1 && (parts[0] === "focus" || parts[0] === "hover")) {
|
|
58
|
+
if (parts[0] === "focus") focus = true;
|
|
59
|
+
else hover = true;
|
|
60
|
+
parts.length = 0;
|
|
61
|
+
}
|
|
62
|
+
const [base, a, b] = parts;
|
|
63
|
+
let f: Style | undefined;
|
|
64
|
+
switch (base) {
|
|
65
|
+
// diagrams (existing)
|
|
66
|
+
case "box":
|
|
67
|
+
case "icon":
|
|
68
|
+
case "status":
|
|
69
|
+
f = a ? status[a] : undefined;
|
|
70
|
+
break;
|
|
71
|
+
case "zone":
|
|
72
|
+
f = a === "title" ? compose(b ? tone[b] : undefined, t.bold) : a ? tone[a] : t.muted;
|
|
73
|
+
break;
|
|
74
|
+
case "wire":
|
|
75
|
+
f = t.muted;
|
|
76
|
+
break;
|
|
77
|
+
case "arrow":
|
|
78
|
+
case "mark":
|
|
79
|
+
f = t.accent;
|
|
80
|
+
break;
|
|
81
|
+
case "label":
|
|
82
|
+
f = a === "edge" ? t.dim : undefined;
|
|
83
|
+
break;
|
|
84
|
+
case "elabel":
|
|
85
|
+
case "detail":
|
|
86
|
+
case "legend":
|
|
87
|
+
f = t.dim;
|
|
88
|
+
break;
|
|
89
|
+
case "erd":
|
|
90
|
+
f = a === "title" ? t.bold : a === "pk" || a === "key" ? t.warn : a === "type" || a === "flag" ? t.dim : undefined;
|
|
91
|
+
break;
|
|
92
|
+
// text and layout
|
|
93
|
+
case "text":
|
|
94
|
+
f = a === "title" ? compose(t.accent, t.bold) : a === "heading" ? t.bold : a === "muted" ? t.dim : a === "code" ? t.code : a === "warn" ? t.warn : undefined;
|
|
95
|
+
break;
|
|
96
|
+
case "title":
|
|
97
|
+
f = t.bold;
|
|
98
|
+
break;
|
|
99
|
+
case "card":
|
|
100
|
+
f = a === "title" ? t.bold : t.muted;
|
|
101
|
+
break;
|
|
102
|
+
case "tile":
|
|
103
|
+
f = t.muted;
|
|
104
|
+
break;
|
|
105
|
+
case "metric":
|
|
106
|
+
f = a === "label" ? t.dim : a === "value" ? t.bold : a ? tone[a] : undefined;
|
|
107
|
+
break;
|
|
108
|
+
case "callout":
|
|
109
|
+
f = a ? tone[a] : undefined;
|
|
110
|
+
break;
|
|
111
|
+
case "btn":
|
|
112
|
+
f = compose(t.accent, t.bold);
|
|
113
|
+
break;
|
|
114
|
+
case "choice":
|
|
115
|
+
f = a === "q" ? t.bold : a === "picked" ? compose(t.accent, t.bold) : undefined;
|
|
116
|
+
break;
|
|
117
|
+
case "tab":
|
|
118
|
+
f = a === "on" ? t.accent : a === "label" ? compose(t.accent, t.bold) : a === "off" ? t.dim : a === "rule" ? t.muted : undefined;
|
|
119
|
+
break;
|
|
120
|
+
case "faq":
|
|
121
|
+
f = a === "q" ? t.bold : t.accent;
|
|
122
|
+
break;
|
|
123
|
+
case "image":
|
|
124
|
+
f = a === "alt" ? t.bold : a === "border" ? t.muted : t.dim;
|
|
125
|
+
break;
|
|
126
|
+
case "tip":
|
|
127
|
+
f = a === "bubble" ? compose(b ? tone[b] : undefined, t.bubble) : a === "hint" ? undefined : a ? tone[a] : t.info;
|
|
128
|
+
break;
|
|
129
|
+
case "note":
|
|
130
|
+
f = a === "pin" ? compose(t.accent, t.bold, reverse) : compose(a ? tone[a] : t.info, t.bold);
|
|
131
|
+
break;
|
|
132
|
+
case "notes":
|
|
133
|
+
f = a === "on" ? compose(t.accent, t.bold, reverse) : a === "title" ? compose(t.accent, t.bold) : a === "border" ? t.muted : a === "where" ? t.dim : undefined;
|
|
134
|
+
break;
|
|
135
|
+
case "foot":
|
|
136
|
+
f = a === "mark" ? t.accent : t.dim;
|
|
137
|
+
break;
|
|
138
|
+
// markdown
|
|
139
|
+
case "md":
|
|
140
|
+
f = a === "h1" ? compose(t.accent, t.bold) : a === "h2" ? t.bold : a === "h3" ? compose(t.dim, t.bold) : a === "bold" ? t.bold : a === "italic" ? t.italic : a === "code" ? t.code : a === "quote" ? compose(t.dim, t.italic) : a === "quoteBar" || a === "hr" ? t.muted : a === "link" ? compose(t.link, t.underline) : a === "url" ? t.dim : a === "strike" ? t.strike : a === "bullet" ? t.accent : undefined;
|
|
141
|
+
break;
|
|
142
|
+
case "code":
|
|
143
|
+
f = a === "kw" ? t.keyword : a === "str" ? t.string : a === "num" ? t.number : a === "com" ? t.comment : a === "fn" ? t.fn : a === "type" ? t.type : a === "key" ? t.accent : a === "border" ? t.muted : a === "title" ? t.dim : a === "copied" ? t.good : undefined;
|
|
144
|
+
break;
|
|
145
|
+
case "table":
|
|
146
|
+
f = a === "head" ? t.bold : t.muted;
|
|
147
|
+
break;
|
|
148
|
+
// charts
|
|
149
|
+
case "chart":
|
|
150
|
+
f = a === "title" ? t.bold : a === "axis" || a === "grid" ? t.muted : a === "axisLabel" || a === "label" ? t.dim : undefined;
|
|
151
|
+
break;
|
|
152
|
+
case "series":
|
|
153
|
+
f = ser(Number(a ?? 0));
|
|
154
|
+
break;
|
|
155
|
+
case "pie": {
|
|
156
|
+
const top = a === "x" || a === undefined ? null : Number(a);
|
|
157
|
+
const bot = b === "x" || b === undefined ? null : Number(b);
|
|
158
|
+
f = top === null && bot === null ? undefined : t.pie(top, bot);
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
// map
|
|
162
|
+
case "map":
|
|
163
|
+
f = a === "land" ? t.dim : a === "coast" ? t.muted : a === "link" ? t.warn : a === "label" ? t.bold : a === "pin" ? tone[b ?? "info"] : undefined;
|
|
164
|
+
break;
|
|
165
|
+
// sequence
|
|
166
|
+
case "seq":
|
|
167
|
+
f = a === "life" ? t.dim : a === "act" ? t.accent : a === "head" || a === "group" ? t.muted : a === "icon" ? t.accent : a === "title" ? t.bold : a === "num" ? t.dim : a === "noteBorder" ? t.warn : a === "groupKind" ? compose(t.accent, t.bold) : a === "groupLabel" ? t.dim : undefined;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
// Border characters are line art: underlining or reversing them draws a tick or a block on every row of a vertical
|
|
171
|
+
// edge. Those cells light up in the accent colour instead; text cells keep underline (hover) and reverse (focus).
|
|
172
|
+
const lineArt = base === "box" || base === "tile" || base === "card" || base === "zone" || a === "border";
|
|
173
|
+
if (lineArt && (focus || hover)) return (s) => t.bold(t.accent(s));
|
|
174
|
+
if (focus) return (s) => reverse(f ? f(s) : s);
|
|
175
|
+
if (hover) return (s) => underline(f ? f(s) : s);
|
|
176
|
+
return f;
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const sgr = (open: string, close: string): Style => (s) => `\x1b[${open}m${s}\x1b[${close}m`;
|
|
181
|
+
const C256 = [36, 32, 33, 31, 35, 34, 96, 93];
|
|
182
|
+
export const ansiPalette: Palette = createPalette({
|
|
183
|
+
good: sgr("32", "39"),
|
|
184
|
+
bad: sgr("31", "39"),
|
|
185
|
+
warn: sgr("33", "39"),
|
|
186
|
+
accent: sgr("36", "39"),
|
|
187
|
+
info: sgr("34", "39"),
|
|
188
|
+
muted: sgr("90", "39"),
|
|
189
|
+
dim: sgr("2", "22"),
|
|
190
|
+
bold: sgr("1", "22"),
|
|
191
|
+
italic: sgr("3", "23"),
|
|
192
|
+
underline: sgr("4", "24"),
|
|
193
|
+
strike: sgr("9", "29"),
|
|
194
|
+
code: sgr("33", "39"),
|
|
195
|
+
keyword: sgr("35", "39"),
|
|
196
|
+
string: sgr("32", "39"),
|
|
197
|
+
number: sgr("33", "39"),
|
|
198
|
+
comment: sgr("90", "39"),
|
|
199
|
+
fn: sgr("34", "39"),
|
|
200
|
+
type: sgr("36", "39"),
|
|
201
|
+
link: sgr("34", "39"),
|
|
202
|
+
series: C256.map((c) => sgr(String(c), "39")),
|
|
203
|
+
bubble: (s) => `\x1b[30;47m${s}\x1b[0m`,
|
|
204
|
+
pie: (a, b) => (s) => {
|
|
205
|
+
const fg = a !== null ? C256[a % 8] : C256[(b ?? 0) % 8];
|
|
206
|
+
const bg = a !== null && b !== null ? C256[b % 8] + 10 : null;
|
|
207
|
+
return `\x1b[${fg}${bg ? `;${bg}` : ""}m${s}\x1b[0m`;
|
|
208
|
+
},
|
|
209
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// DiagramResult -> terminal lines (legend, footnotes, focus, horizontal crop).
|
|
2
|
+
|
|
3
|
+
import { rowsToLines, type Cell, type Palette } from "./canvas.ts";
|
|
4
|
+
import type { DiagramResult } from "./layeredDiagram.ts";
|
|
5
|
+
|
|
6
|
+
export interface RenderOpts {
|
|
7
|
+
palette?: Palette;
|
|
8
|
+
/** node id to highlight */
|
|
9
|
+
focusId?: string | null;
|
|
10
|
+
hoverId?: string | null;
|
|
11
|
+
/** crop to this many columns starting at panX */
|
|
12
|
+
maxWidth?: number;
|
|
13
|
+
panX?: number;
|
|
14
|
+
title?: string;
|
|
15
|
+
legend?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function textRow(text: string, role: string): Cell[] {
|
|
19
|
+
return Array.from(text).map((ch) => ({ ch, role }));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function toLines(res: DiagramResult, opts: RenderOpts = {}): string[] {
|
|
23
|
+
let rows: Cell[][] = res.rows.map((r) => r.map((c) => ({ ...c })));
|
|
24
|
+
const mark = (id: string | null | undefined, suffix: string) => {
|
|
25
|
+
if (!id) return;
|
|
26
|
+
const n = res.nodes.find((x) => x.id === id);
|
|
27
|
+
if (!n) return;
|
|
28
|
+
for (let y = n.rect.y; y < n.rect.y + n.rect.h; y++)
|
|
29
|
+
for (let x = n.rect.x; x < n.rect.x + n.rect.w; x++) {
|
|
30
|
+
const c = rows[y]?.[x];
|
|
31
|
+
if (c && c.role) c.role = `${c.role}.${suffix}`;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
mark(opts.hoverId, "hover");
|
|
35
|
+
mark(opts.focusId, "focus");
|
|
36
|
+
const pad = (r: Cell[], w: number) => (r.length >= w ? r : r.concat(Array.from({ length: w - r.length }, () => ({ ch: " ", role: "" }))));
|
|
37
|
+
if (opts.maxWidth !== undefined && res.width > opts.maxWidth) {
|
|
38
|
+
const px = Math.max(0, Math.min(opts.panX ?? 0, res.width - opts.maxWidth));
|
|
39
|
+
rows = rows.map((r) => {
|
|
40
|
+
let seg = pad(r, res.width).slice(px, px + opts.maxWidth!);
|
|
41
|
+
// do not start or end in the middle of a wide character
|
|
42
|
+
if (seg.length && seg[0].ch === "") seg[0] = { ch: " ", role: "" };
|
|
43
|
+
if (seg.length && px + opts.maxWidth! < res.width && seg[seg.length - 1].ch !== "" && false) seg = seg.slice();
|
|
44
|
+
return seg;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const out: Cell[][] = [];
|
|
48
|
+
if (opts.title) out.push(textRow(opts.title, "title"));
|
|
49
|
+
out.push(...rows);
|
|
50
|
+
if (opts.legend !== false && res.legend.length) {
|
|
51
|
+
const parts: Cell[] = [];
|
|
52
|
+
res.legend.forEach((e, i) => {
|
|
53
|
+
if (i) parts.push(...textRow(" ", "legend"));
|
|
54
|
+
parts.push(...textRow(e.glyph, e.role), ...textRow(` ${e.text}`, "legend"));
|
|
55
|
+
});
|
|
56
|
+
out.push([], parts);
|
|
57
|
+
}
|
|
58
|
+
for (const f of res.footnotes) out.push(textRow(f, "legend"));
|
|
59
|
+
return rowsToLines(out, opts.palette);
|
|
60
|
+
}
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
// Global grid router: one A* search per edge over a character grid.
|
|
2
|
+
//
|
|
3
|
+
// Rules that make the picture unambiguous:
|
|
4
|
+
// * every edge owns its own cells, wires never share a cell
|
|
5
|
+
// * two wires may cross only at a right angle, going straight through (drawn as a plus)
|
|
6
|
+
// * corners and end cells are exclusive
|
|
7
|
+
// * boxes are obstacles; zone borders may be crossed at a right angle
|
|
8
|
+
// Directions: 0 = north, 1 = east, 2 = south, 3 = west.
|
|
9
|
+
|
|
10
|
+
import type { Rect } from "./canvas.ts";
|
|
11
|
+
|
|
12
|
+
export const DX = [0, 1, 0, -1];
|
|
13
|
+
export const DY = [-1, 0, 1, 0];
|
|
14
|
+
export const OPP = [2, 3, 0, 1];
|
|
15
|
+
export const ZONE = -2;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Search budget per edge is W*H*guardScale expansions; tests set it to 0 to force failures. `budget` is the total for one
|
|
19
|
+
* diagram: every edge, every retry and every candidate layout (dag, erd and infra try up to ten) draw on it. Without it the cost
|
|
20
|
+
* grows with edges x retries x candidates x grid size, and a 72-node map froze pi for seconds. When it is spent, the remaining
|
|
21
|
+
* edges are not routed and are listed as omitted. Counted in expansions, not time, so the result does not depend on the machine:
|
|
22
|
+
* about 4,000 expansions a millisecond, so the default is roughly half a second at its worst.
|
|
23
|
+
*/
|
|
24
|
+
export const tuning = { guardScale: 8, budget: 2_000_000 };
|
|
25
|
+
|
|
26
|
+
/** The work left for the diagram being laid out. Infinity outside `budgeted`, so a Router used on its own is not limited. */
|
|
27
|
+
export const work = { left: Infinity, spent: 0 };
|
|
28
|
+
|
|
29
|
+
/** Run one diagram layout (all its candidates) against the shared budget. */
|
|
30
|
+
export function budgeted<R>(layout: () => R): R {
|
|
31
|
+
work.left = tuning.budget;
|
|
32
|
+
work.spent = 0;
|
|
33
|
+
try {
|
|
34
|
+
return layout();
|
|
35
|
+
} finally {
|
|
36
|
+
work.left = Infinity;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const STEP = 10;
|
|
41
|
+
const TURN = 12;
|
|
42
|
+
const CROSS = 30;
|
|
43
|
+
const CROSS_ZONE = 4;
|
|
44
|
+
const ADJ = 3;
|
|
45
|
+
const OVER = 400;
|
|
46
|
+
|
|
47
|
+
export interface Waypoint {
|
|
48
|
+
x: number;
|
|
49
|
+
y: number;
|
|
50
|
+
dir: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface RouteReq {
|
|
54
|
+
id: number;
|
|
55
|
+
from: Rect;
|
|
56
|
+
to: Rect;
|
|
57
|
+
/** node ids (as given to addSolid) of the two boxes, so their port cells stay usable */
|
|
58
|
+
fromId?: number;
|
|
59
|
+
toId?: number;
|
|
60
|
+
/** Penalty per side (N, E, S, W) for leaving `from` / entering `to`. Negative forbids the side. */
|
|
61
|
+
fromPen: number[];
|
|
62
|
+
toPen: number[];
|
|
63
|
+
waypoints: Waypoint[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** `dir` is the direction of travel used to enter this cell. For the first cell it points away from the box. */
|
|
67
|
+
export interface Step {
|
|
68
|
+
x: number;
|
|
69
|
+
y: number;
|
|
70
|
+
dir: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface Routed {
|
|
74
|
+
id: number;
|
|
75
|
+
steps: Step[];
|
|
76
|
+
ok: boolean;
|
|
77
|
+
forced: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
class Heap {
|
|
81
|
+
private f: number[] = [];
|
|
82
|
+
private v: number[] = [];
|
|
83
|
+
get size(): number {
|
|
84
|
+
return this.f.length;
|
|
85
|
+
}
|
|
86
|
+
push(f: number, v: number): void {
|
|
87
|
+
const F = this.f;
|
|
88
|
+
const V = this.v;
|
|
89
|
+
let i = F.length;
|
|
90
|
+
F.push(f);
|
|
91
|
+
V.push(v);
|
|
92
|
+
while (i > 0) {
|
|
93
|
+
const p = (i - 1) >> 1;
|
|
94
|
+
if (F[p] <= f) break;
|
|
95
|
+
F[i] = F[p];
|
|
96
|
+
V[i] = V[p];
|
|
97
|
+
i = p;
|
|
98
|
+
}
|
|
99
|
+
F[i] = f;
|
|
100
|
+
V[i] = v;
|
|
101
|
+
}
|
|
102
|
+
pop(): number {
|
|
103
|
+
const F = this.f;
|
|
104
|
+
const V = this.v;
|
|
105
|
+
const top = V[0];
|
|
106
|
+
const lf = F.pop()!;
|
|
107
|
+
const lv = V.pop()!;
|
|
108
|
+
const n = F.length;
|
|
109
|
+
if (n > 0) {
|
|
110
|
+
let i = 0;
|
|
111
|
+
for (;;) {
|
|
112
|
+
let c = 2 * i + 1;
|
|
113
|
+
if (c >= n) break;
|
|
114
|
+
if (c + 1 < n && F[c + 1] < F[c]) c++;
|
|
115
|
+
if (F[c] >= lf) break;
|
|
116
|
+
F[i] = F[c];
|
|
117
|
+
V[i] = V[c];
|
|
118
|
+
i = c;
|
|
119
|
+
}
|
|
120
|
+
F[i] = lf;
|
|
121
|
+
V[i] = lv;
|
|
122
|
+
}
|
|
123
|
+
return top;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
interface Source {
|
|
128
|
+
x: number;
|
|
129
|
+
y: number;
|
|
130
|
+
dir: number;
|
|
131
|
+
cost: number;
|
|
132
|
+
/** Port cells must be free. Waypoint cells may already be a crossing. */
|
|
133
|
+
strict?: boolean;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export class Router {
|
|
137
|
+
readonly W: number;
|
|
138
|
+
readonly H: number;
|
|
139
|
+
readonly solid: Uint8Array;
|
|
140
|
+
readonly vOwn: Int32Array;
|
|
141
|
+
readonly hOwn: Int32Array;
|
|
142
|
+
readonly full: Int32Array;
|
|
143
|
+
/** cells next to a box side belong to that box: only its own edges may use them */
|
|
144
|
+
readonly portNode: Int32Array;
|
|
145
|
+
private curFrom = -1;
|
|
146
|
+
private curTo = -1;
|
|
147
|
+
private gen = 0;
|
|
148
|
+
/** the net being routed: its own reserved cells count as free */
|
|
149
|
+
private cur = -2;
|
|
150
|
+
private readonly stamp: Int32Array;
|
|
151
|
+
private readonly best: Int32Array;
|
|
152
|
+
private readonly prev: Int32Array;
|
|
153
|
+
|
|
154
|
+
constructor(W: number, H: number) {
|
|
155
|
+
this.W = W;
|
|
156
|
+
this.H = H;
|
|
157
|
+
this.solid = new Uint8Array(W * H);
|
|
158
|
+
this.vOwn = new Int32Array(W * H).fill(-1);
|
|
159
|
+
this.hOwn = new Int32Array(W * H).fill(-1);
|
|
160
|
+
this.full = new Int32Array(W * H).fill(-1);
|
|
161
|
+
this.portNode = new Int32Array(W * H).fill(-1);
|
|
162
|
+
this.stamp = new Int32Array(W * H * 4);
|
|
163
|
+
this.best = new Int32Array(W * H * 4);
|
|
164
|
+
this.prev = new Int32Array(W * H * 4);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
addSolid(r: Rect, id = -1): void {
|
|
168
|
+
for (let y = r.y; y < r.y + r.h; y++) for (let x = r.x; x < r.x + r.w; x++) if (x >= 0 && y >= 0 && x < this.W && y < this.H) this.solid[y * this.W + x] = 1;
|
|
169
|
+
if (id < 0) return;
|
|
170
|
+
for (let side = 0; side < 4; side++)
|
|
171
|
+
for (const c of this.sideCells(r, side)) {
|
|
172
|
+
if (c.x < 0 || c.y < 0 || c.x >= this.W || c.y >= this.H) continue;
|
|
173
|
+
const k = c.y * this.W + c.x;
|
|
174
|
+
if (this.portNode[k] === -1) this.portNode[k] = id;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** A zone outline: wires may cross it at a right angle but never run along it. */
|
|
179
|
+
addZoneBorder(r: Rect): void {
|
|
180
|
+
const W = this.W;
|
|
181
|
+
for (let x = r.x; x < r.x + r.w; x++) {
|
|
182
|
+
for (const y of [r.y, r.y + r.h - 1]) {
|
|
183
|
+
if (x < 0 || y < 0 || x >= W || y >= this.H) continue;
|
|
184
|
+
const corner = x === r.x || x === r.x + r.w - 1;
|
|
185
|
+
if (corner) this.full[y * W + x] = ZONE;
|
|
186
|
+
else this.hOwn[y * W + x] = ZONE;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
for (let y = r.y + 1; y < r.y + r.h - 1; y++) {
|
|
190
|
+
for (const x of [r.x, r.x + r.w - 1]) {
|
|
191
|
+
if (x < 0 || y < 0 || x >= W || y >= this.H) continue;
|
|
192
|
+
this.vOwn[y * W + x] = ZONE;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private cellFree(x: number, y: number): boolean {
|
|
198
|
+
if (x < 0 || y < 0 || x >= this.W || y >= this.H) return false;
|
|
199
|
+
const c = y * this.W + x;
|
|
200
|
+
const k = this.cur;
|
|
201
|
+
return !this.solid[c] && (this.vOwn[c] === -1 || this.vOwn[c] === k) && (this.hOwn[c] === -1 || this.hOwn[c] === k) && (this.full[c] === -1 || this.full[c] === k);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private adjacency(x: number, y: number, fromX: number, fromY: number): number {
|
|
205
|
+
let n = 0;
|
|
206
|
+
for (let d = 0; d < 4; d++) {
|
|
207
|
+
const ax = x + DX[d];
|
|
208
|
+
const ay = y + DY[d];
|
|
209
|
+
if (ax === fromX && ay === fromY) continue;
|
|
210
|
+
if (ax < 0 || ay < 0 || ax >= this.W || ay >= this.H) continue;
|
|
211
|
+
const c = ay * this.W + ax;
|
|
212
|
+
const k = this.cur;
|
|
213
|
+
const o = (this.vOwn[c] > -1 && this.vOwn[c] !== k) || (this.hOwn[c] > -1 && this.hOwn[c] !== k) || (this.full[c] > -1 && this.full[c] !== k);
|
|
214
|
+
if (o) n++;
|
|
215
|
+
}
|
|
216
|
+
return n;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private sideCells(r: Rect, side: number): Array<{ x: number; y: number }> {
|
|
220
|
+
const out: Array<{ x: number; y: number }> = [];
|
|
221
|
+
if (side === 0 || side === 2) {
|
|
222
|
+
const y = side === 0 ? r.y - 1 : r.y + r.h;
|
|
223
|
+
for (let x = r.x + 1; x <= r.x + r.w - 2; x++) out.push({ x, y });
|
|
224
|
+
} else {
|
|
225
|
+
const x = side === 1 ? r.x + r.w : r.x - 1;
|
|
226
|
+
for (let y = r.y + 1; y <= r.y + r.h - 2; y++) out.push({ x, y });
|
|
227
|
+
}
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private search(
|
|
232
|
+
sources: Source[],
|
|
233
|
+
isTarget: (x: number, y: number, d: number) => number,
|
|
234
|
+
bx0: number,
|
|
235
|
+
by0: number,
|
|
236
|
+
bx1: number,
|
|
237
|
+
by1: number,
|
|
238
|
+
relax: boolean,
|
|
239
|
+
straightStart = false,
|
|
240
|
+
): Step[] | null {
|
|
241
|
+
const W = this.W;
|
|
242
|
+
const H = this.H;
|
|
243
|
+
const gen = ++this.gen;
|
|
244
|
+
const heap = new Heap();
|
|
245
|
+
const hOf = (x: number, y: number): number => (Math.max(0, bx0 - x, x - bx1) + Math.max(0, by0 - y, y - by1)) * STEP;
|
|
246
|
+
for (const s of sources) {
|
|
247
|
+
if (s.x < 0 || s.y < 0 || s.x >= W || s.y >= H || this.solid[s.y * W + s.x]) continue;
|
|
248
|
+
if (s.strict !== false && !relax && !this.cellFree(s.x, s.y)) continue;
|
|
249
|
+
const st = (s.y * W + s.x) * 4 + s.dir;
|
|
250
|
+
if (this.stamp[st] === gen && this.best[st] <= s.cost) continue;
|
|
251
|
+
this.stamp[st] = gen;
|
|
252
|
+
this.best[st] = s.cost;
|
|
253
|
+
this.prev[st] = -1;
|
|
254
|
+
heap.push(s.cost + hOf(s.x, s.y), st);
|
|
255
|
+
}
|
|
256
|
+
let guard = Math.min(W * H * tuning.guardScale, work.left);
|
|
257
|
+
const start = guard;
|
|
258
|
+
while (heap.size > 0 && guard-- > 0) {
|
|
259
|
+
const st = heap.pop();
|
|
260
|
+
const d = st & 3;
|
|
261
|
+
const c = st >> 2;
|
|
262
|
+
const x = c % W;
|
|
263
|
+
const y = (c - x) / W;
|
|
264
|
+
const g = this.best[st];
|
|
265
|
+
if (isTarget(x, y, d) >= 0) {
|
|
266
|
+
work.left -= start - guard;
|
|
267
|
+
work.spent += start - guard;
|
|
268
|
+
const rev: Step[] = [];
|
|
269
|
+
let cur = st;
|
|
270
|
+
while (cur !== -1) {
|
|
271
|
+
const cd = cur & 3;
|
|
272
|
+
const cc = cur >> 2;
|
|
273
|
+
rev.push({ x: cc % W, y: Math.floor(cc / W), dir: cd });
|
|
274
|
+
cur = this.prev[cur];
|
|
275
|
+
}
|
|
276
|
+
return rev.reverse();
|
|
277
|
+
}
|
|
278
|
+
const cur = this.cur;
|
|
279
|
+
const O = (v: number) => (v === cur ? -1 : v);
|
|
280
|
+
const owned = O(this.vOwn[c]) !== -1 || O(this.hOwn[c]) !== -1;
|
|
281
|
+
const firstCell = straightStart && this.prev[st] === -1;
|
|
282
|
+
for (let k = 0; k < 3; k++) {
|
|
283
|
+
const nd = k === 0 ? d : k === 1 ? (d + 1) & 3 : (d + 3) & 3;
|
|
284
|
+
if (nd !== d && (owned || firstCell)) continue;
|
|
285
|
+
const nx = x + DX[nd];
|
|
286
|
+
const ny = y + DY[nd];
|
|
287
|
+
if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
|
|
288
|
+
const q = ny * W + nx;
|
|
289
|
+
if (this.solid[q]) continue;
|
|
290
|
+
const pn = this.portNode[q];
|
|
291
|
+
if (pn !== -1 && pn !== this.curFrom && pn !== this.curTo) continue;
|
|
292
|
+
let cost = STEP;
|
|
293
|
+
if (O(this.full[q]) !== -1) {
|
|
294
|
+
if (!relax || this.full[q] === ZONE) continue;
|
|
295
|
+
cost += OVER;
|
|
296
|
+
}
|
|
297
|
+
const vertical = nd === 0 || nd === 2;
|
|
298
|
+
const same = O(vertical ? this.vOwn[q] : this.hOwn[q]);
|
|
299
|
+
const other = O(vertical ? this.hOwn[q] : this.vOwn[q]);
|
|
300
|
+
if (same !== -1) {
|
|
301
|
+
if (!relax || same === ZONE) continue;
|
|
302
|
+
cost += OVER;
|
|
303
|
+
}
|
|
304
|
+
if (other !== -1) cost += other === ZONE ? CROSS_ZONE : CROSS;
|
|
305
|
+
if (nd !== d) cost += TURN;
|
|
306
|
+
cost += ADJ * this.adjacency(nx, ny, x, y);
|
|
307
|
+
const tx = isTarget(nx, ny, nd);
|
|
308
|
+
if (tx > 0) cost += tx;
|
|
309
|
+
const ns = q * 4 + nd;
|
|
310
|
+
const ng = g + cost;
|
|
311
|
+
if (this.stamp[ns] === gen && this.best[ns] <= ng) continue;
|
|
312
|
+
this.stamp[ns] = gen;
|
|
313
|
+
this.best[ns] = ng;
|
|
314
|
+
this.prev[ns] = st;
|
|
315
|
+
heap.push(ng + hOf(nx, ny), ns);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const used = start - Math.max(guard, 0);
|
|
319
|
+
work.left -= used;
|
|
320
|
+
work.spent += used;
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Route one edge. Does not commit; call `commit` with the result. */
|
|
325
|
+
route(req: RouteReq, relax = false): Routed {
|
|
326
|
+
this.cur = req.id;
|
|
327
|
+
this.curFrom = req.fromId ?? -1;
|
|
328
|
+
this.curTo = req.toId ?? -1;
|
|
329
|
+
const attempt = (useWaypoints: boolean): Step[] | null => {
|
|
330
|
+
let sources: Source[] = [];
|
|
331
|
+
for (let side = 0; side < 4; side++) {
|
|
332
|
+
const pen = req.fromPen[side];
|
|
333
|
+
if (pen < 0) continue;
|
|
334
|
+
for (const c of this.sideCells(req.from, side)) sources.push({ x: c.x, y: c.y, dir: side, cost: pen });
|
|
335
|
+
}
|
|
336
|
+
let steps: Step[] = [];
|
|
337
|
+
let portLeg = true;
|
|
338
|
+
const wps = useWaypoints ? req.waypoints : [];
|
|
339
|
+
for (const wp of wps) {
|
|
340
|
+
const path = this.search(sources, (x, y, d) => (x === wp.x && y === wp.y && d === wp.dir ? 0 : -1), wp.x, wp.y, wp.x, wp.y, relax, portLeg);
|
|
341
|
+
portLeg = false;
|
|
342
|
+
if (!path) return null;
|
|
343
|
+
steps = steps.length ? steps.concat(path.slice(1)) : path;
|
|
344
|
+
sources = [{ x: wp.x, y: wp.y, dir: wp.dir, cost: 0, strict: false }];
|
|
345
|
+
}
|
|
346
|
+
const tmap = new Map<number, [number, number]>();
|
|
347
|
+
for (let side = 0; side < 4; side++) {
|
|
348
|
+
const pen = req.toPen[side];
|
|
349
|
+
if (pen < 0) continue;
|
|
350
|
+
for (const c of this.sideCells(req.to, side)) {
|
|
351
|
+
if (c.x < 0 || c.y < 0 || c.x >= this.W || c.y >= this.H) continue;
|
|
352
|
+
tmap.set(c.y * this.W + c.x, [OPP[side], pen]);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const t = req.to;
|
|
356
|
+
const path = this.search(
|
|
357
|
+
sources,
|
|
358
|
+
(x, y, d) => {
|
|
359
|
+
const e = tmap.get(y * this.W + x);
|
|
360
|
+
// an end cell is exclusive: it must not already carry a crossing wire
|
|
361
|
+
return e && e[0] === d && (relax || this.cellFree(x, y)) ? e[1] : -1;
|
|
362
|
+
},
|
|
363
|
+
t.x - 1,
|
|
364
|
+
t.y - 1,
|
|
365
|
+
t.x + t.w,
|
|
366
|
+
t.y + t.h,
|
|
367
|
+
relax,
|
|
368
|
+
portLeg,
|
|
369
|
+
);
|
|
370
|
+
if (!path) return null;
|
|
371
|
+
return steps.length ? steps.concat(path.slice(1)) : path;
|
|
372
|
+
};
|
|
373
|
+
let steps = attempt(true);
|
|
374
|
+
if (!steps && req.waypoints.length) steps = attempt(false);
|
|
375
|
+
if (!steps) return { id: req.id, steps: [], ok: false, forced: relax };
|
|
376
|
+
return { id: req.id, steps, ok: true, forced: relax };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Reserve straight cells for a net before routing. Others may cross them at a right angle only. */
|
|
380
|
+
reserve(id: number, cells: Array<{ x: number; y: number }>, vertical: boolean): void {
|
|
381
|
+
for (const c of cells) {
|
|
382
|
+
if (c.x < 0 || c.y < 0 || c.x >= this.W || c.y >= this.H) continue;
|
|
383
|
+
const k = c.y * this.W + c.x;
|
|
384
|
+
if (vertical) this.vOwn[k] = id;
|
|
385
|
+
else this.hOwn[k] = id;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
commit(r: Routed): void {
|
|
390
|
+
const W = this.W;
|
|
391
|
+
const n = r.steps.length;
|
|
392
|
+
for (let i = 0; i < n; i++) {
|
|
393
|
+
const s = r.steps[i];
|
|
394
|
+
const c = s.y * W + s.x;
|
|
395
|
+
const inD = s.dir;
|
|
396
|
+
const outD = i + 1 < n ? r.steps[i + 1].dir : -1;
|
|
397
|
+
if (i === 0 || i === n - 1 || outD !== inD) this.full[c] = r.id;
|
|
398
|
+
else if (inD === 0 || inD === 2) this.vOwn[c] = r.id;
|
|
399
|
+
else this.hOwn[c] = r.id;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|