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.
@@ -0,0 +1,271 @@
1
+ // Draw routed wires, end markers and labels onto a Canvas.
2
+
3
+ import { Canvas, textWidth } from "./canvas.ts";
4
+ import { DX, DY, OPP, type Step } from "./router.ts";
5
+
6
+ export type Mark = "none" | "arrow" | "dot" | "one" | "many";
7
+
8
+ export interface WireSpec {
9
+ id: number;
10
+ steps: Step[];
11
+ dashed: boolean;
12
+ /** mark at the start box / end box */
13
+ startMark: Mark;
14
+ endMark: Mark;
15
+ label?: string;
16
+ /** short description for a footnote, e.g. "a -> b" */
17
+ desc?: string;
18
+ /** role prefix, e.g. "wire" */
19
+ role: string;
20
+ }
21
+
22
+ const BIT = [1, 2, 4, 8]; // N E S W
23
+ const GLYPH: Record<number, string> = {
24
+ 0: " ",
25
+ 1: "│",
26
+ 2: "─",
27
+ 3: "╰",
28
+ 4: "│",
29
+ 5: "│",
30
+ 6: "╭",
31
+ 7: "├",
32
+ 8: "─",
33
+ 9: "╯",
34
+ 10: "─",
35
+ 11: "┴",
36
+ 12: "╮",
37
+ 13: "┤",
38
+ 14: "┬",
39
+ 15: "┼",
40
+ };
41
+ const ARROW = ["▲", "▶", "▼", "◀"];
42
+ const JUNCTION = ["┴", "├", "┬", "┤"]; // by outward normal of the box side
43
+ /** end glyphs by the direction that points INTO the box: 0 north, 1 east, 2 south, 3 west */
44
+ export const ONE_END = ["┬", "┤", "┴", "├"];
45
+ export const MANY_END = ["V", "<", "Λ", ">"];
46
+
47
+ export interface WireDrawResult {
48
+ footnotes: string[];
49
+ /** cells that carry the label text of each wire id, for tests */
50
+ labelCells: Map<number, Array<[number, number]>>;
51
+ }
52
+
53
+ function straightCells(w: WireSpec): Array<{ i: number; horizontal: boolean }> {
54
+ const out: Array<{ i: number; horizontal: boolean }> = [];
55
+ const s = w.steps;
56
+ for (let i = 1; i + 1 < s.length; i++) if (s[i].dir === s[i + 1].dir) out.push({ i, horizontal: s[i].dir === 1 || s[i].dir === 3 });
57
+ return out;
58
+ }
59
+
60
+ export function drawWires(c: Canvas, wires: WireSpec[], reserved?: (x: number, y: number) => boolean): WireDrawResult {
61
+ const W = c.w;
62
+ const mask = new Uint8Array(c.w * c.h);
63
+ const solidN = new Uint8Array(c.w * c.h);
64
+ const dashN = new Uint8Array(c.w * c.h);
65
+ const roleAt = new Array<string>(c.w * c.h).fill("");
66
+ const wireCell = new Uint8Array(c.w * c.h);
67
+ for (const w of wires) {
68
+ const n = w.steps.length;
69
+ for (let i = 0; i < n; i++) {
70
+ const s = w.steps[i];
71
+ if (!c.inb(s.x, s.y)) continue;
72
+ const k = s.y * W + s.x;
73
+ mask[k] |= BIT[OPP[s.dir]];
74
+ if (i + 1 < n) mask[k] |= BIT[w.steps[i + 1].dir];
75
+ if (w.dashed) dashN[k]++;
76
+ else solidN[k]++;
77
+ roleAt[k] = w.role;
78
+ wireCell[k] = 1;
79
+ }
80
+ }
81
+ for (let y = 0; y < c.h; y++) {
82
+ for (let x = 0; x < c.w; x++) {
83
+ const k = y * W + x;
84
+ if (!wireCell[k]) continue;
85
+ let m = mask[k];
86
+ if (c.getRole(x, y).startsWith("zone")) {
87
+ const zc = c.getCh(x, y);
88
+ m |= zc === "│" ? 5 : zc === "─" ? 10 : 0;
89
+ }
90
+ let g = GLYPH[m];
91
+ if (dashN[k] > 0 && solidN[k] === 0) {
92
+ if (m === 5 || m === 1 || m === 4) g = "┆";
93
+ else if (m === 10 || m === 2 || m === 8) g = "┄";
94
+ }
95
+ c.set(x, y, g, roleAt[k]);
96
+ }
97
+ }
98
+
99
+ // markers and attached ends
100
+ for (const w of wires) {
101
+ const s = w.steps;
102
+ if (!s.length) continue;
103
+ const first = s[0];
104
+ const last = s[s.length - 1];
105
+ const startDirIn = OPP[first.dir]; // pointing into the start box
106
+ const endDirIn = last.dir; // pointing into the end box
107
+ applyEnd(c, first, startDirIn, w.startMark, w.role, true);
108
+ applyEnd(c, last, endDirIn, w.endMark, w.role, false);
109
+ }
110
+
111
+ // labels
112
+ const footnotes: string[] = [];
113
+ const labelCells = new Map<number, Array<[number, number]>>();
114
+ const taken = new Set<number>();
115
+ for (const w of wires) {
116
+ if (!w.label) continue;
117
+ const text = w.label;
118
+ const tw = textWidth(text);
119
+ const placed = placeLabel(c, w, text, tw, reserved, taken);
120
+ if (placed) labelCells.set(w.id, placed);
121
+ else {
122
+ const idx = footnotes.length + 1;
123
+ const mark = String(idx);
124
+ const cand = straightCells(w);
125
+ const mid = cand.length ? cand[Math.floor(cand.length / 2)] : null;
126
+ if (mid) {
127
+ const st = w.steps[mid.i];
128
+ c.set(st.x, st.y, mark.length === 1 ? mark : mark[0], "elabel");
129
+ labelCells.set(w.id, [[st.x, st.y]]);
130
+ footnotes.push(`${idx} ${text}`);
131
+ } else footnotes.push(`${w.desc ? w.desc + ": " : ""}${text}`);
132
+ }
133
+ }
134
+ return { footnotes, labelCells };
135
+ }
136
+
137
+ function applyEnd(c: Canvas, st: Step, dirIn: number, mark: Mark, role: string, isStart: boolean): void {
138
+ const outward = OPP[dirIn];
139
+ if (mark === "none") {
140
+ // attach: turn the box border cell into a junction
141
+ const bx = st.x + DX[dirIn];
142
+ const by = st.y + DY[dirIn];
143
+ c.set(bx, by, JUNCTION[outward], c.getRole(bx, by) || role);
144
+ return;
145
+ }
146
+ if (mark === "arrow") {
147
+ c.set(st.x, st.y, ARROW[dirIn], "arrow");
148
+ return;
149
+ }
150
+ if (mark === "dot") {
151
+ c.set(st.x, st.y, "●", "mark");
152
+ return;
153
+ }
154
+ // Relation ends, one cell each so they stay small and touch the box:
155
+ // one = a bar across the wire, fused with it (├ ┤ ┬ ┴), which reads as the "||" next to the box border
156
+ // many = a crow's foot whose toes face the box (< > for a side entry, V and Λ for a top or bottom entry)
157
+ c.set(st.x, st.y, mark === "one" ? ONE_END[dirIn] : MANY_END[dirIn], "mark");
158
+ void isStart;
159
+ void role;
160
+ }
161
+
162
+ function placeLabel(c: Canvas, w: WireSpec, text: string, tw: number, reserved: ((x: number, y: number) => boolean) | undefined, taken: Set<number>): Array<[number, number]> | null {
163
+ const s = w.steps;
164
+ // maximal runs of straight cells (same direction, consecutive steps)
165
+ type Run = { cells: number[]; horizontal: boolean };
166
+ const contiguous: Run[] = [];
167
+ let cur: Run | null = null;
168
+ for (let i = 1; i + 1 < s.length; i++) {
169
+ if (s[i].dir !== s[i + 1].dir) {
170
+ cur = null;
171
+ continue;
172
+ }
173
+ if (cur && cur.cells[cur.cells.length - 1] === i - 1) cur.cells.push(i);
174
+ else {
175
+ cur = { cells: [i], horizontal: s[i].dir === 1 || s[i].dir === 3 };
176
+ contiguous.push(cur);
177
+ }
178
+ }
179
+ const free = (x: number, y: number) => c.isBlank(x, y) && !taken.has(y * c.w + x) && !(reserved && reserved(x, y));
180
+ // 1. inline on the longest horizontal run
181
+ const hor = contiguous.filter((r) => r.horizontal).sort((a, b) => b.cells.length - a.cells.length);
182
+ for (const r of hor) {
183
+ if (r.cells.length < tw + 2) continue;
184
+ const xs = r.cells.map((i) => s[i].x).sort((p, q) => p - q);
185
+ const x0 = xs[Math.floor((xs.length - tw) / 2)];
186
+ const y = s[r.cells[0]].y;
187
+ const cells: Array<[number, number]> = [];
188
+ let ok = true;
189
+ for (let k = 0; k < tw; k++) {
190
+ const x = x0 + k;
191
+ const cell = c.getCh(x, y);
192
+ if (!"─┄".includes(cell)) {
193
+ ok = false;
194
+ break;
195
+ }
196
+ cells.push([x, y]);
197
+ }
198
+ if (!ok) continue;
199
+ c.text(x0, y, text, "elabel");
200
+ for (const [x, y2] of cells) taken.add(y2 * c.w + x);
201
+ return cells;
202
+ }
203
+ // 2. beside a horizontal run (above, then below)
204
+ for (const r of hor) {
205
+ if (r.cells.length < Math.min(tw, 4)) continue;
206
+ const mid = s[r.cells[Math.floor(r.cells.length / 2)]];
207
+ const x0 = mid.x - Math.floor(tw / 2);
208
+ for (const dy of [-1, 1]) {
209
+ const y = mid.y + dy;
210
+ let ok = true;
211
+ for (let k = 0; k < tw; k++) if (!free(x0 + k, y)) ok = false;
212
+ if (!ok) continue;
213
+ c.text(x0, y, text, "label.edge");
214
+ const cells: Array<[number, number]> = [];
215
+ for (let k = 0; k < tw; k++) {
216
+ taken.add(y * c.w + x0 + k);
217
+ cells.push([x0 + k, y]);
218
+ }
219
+ return cells;
220
+ }
221
+ }
222
+ // 3. beside a vertical run (right, then left)
223
+ const ver = contiguous.filter((r) => !r.horizontal).sort((a, b) => b.cells.length - a.cells.length);
224
+ for (const r of ver) {
225
+ for (const ci of [Math.floor(r.cells.length / 2), 0, r.cells.length - 1]) {
226
+ const st = s[r.cells[ci]];
227
+ for (const side of [1, -1]) {
228
+ const x0 = side === 1 ? st.x + 2 : st.x - 1 - tw;
229
+ let ok = true;
230
+ for (let k = 0; k < tw; k++) if (!free(x0 + k, st.y)) ok = false;
231
+ if (!ok) continue;
232
+ c.text(x0, st.y, text, "label.edge");
233
+ const cells: Array<[number, number]> = [];
234
+ for (let k = 0; k < tw; k++) {
235
+ taken.add(st.y * c.w + x0 + k);
236
+ cells.push([x0 + k, st.y]);
237
+ }
238
+ return cells;
239
+ }
240
+ }
241
+ }
242
+ // 4. beside any cell of the wire, ends included (very short wires)
243
+ const pick = [Math.floor(s.length / 2), ...s.map((_, i) => i)];
244
+ for (const i of pick) {
245
+ const st = s[i];
246
+ if (st === undefined) continue;
247
+ const vertical = st.dir === 0 || st.dir === 2;
248
+ const spots: Array<[number, number]> = vertical
249
+ ? [
250
+ [st.x + 2, st.y],
251
+ [st.x - 1 - tw, st.y],
252
+ ]
253
+ : [
254
+ [st.x - Math.floor(tw / 2), st.y - 1],
255
+ [st.x - Math.floor(tw / 2), st.y + 1],
256
+ ];
257
+ for (const [x0, y] of spots) {
258
+ let ok = true;
259
+ for (let k = 0; k < tw; k++) if (!free(x0 + k, y)) ok = false;
260
+ if (!ok) continue;
261
+ c.text(x0, y, text, "label.edge");
262
+ const cells: Array<[number, number]> = [];
263
+ for (let k = 0; k < tw; k++) {
264
+ taken.add(y * c.w + x0 + k);
265
+ cells.push([x0 + k, y]);
266
+ }
267
+ return cells;
268
+ }
269
+ }
270
+ return null;
271
+ }