@mermkit/render 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/dist/ascii.d.ts +17 -0
- package/dist/ascii.d.ts.map +1 -0
- package/dist/ascii.js +2098 -0
- package/dist/ascii.js.map +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +482 -0
- package/dist/index.js.map +1 -0
- package/package.json +25 -0
package/dist/ascii.js
ADDED
|
@@ -0,0 +1,2098 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
2
|
+
var ArrowType;
|
|
3
|
+
(function (ArrowType) {
|
|
4
|
+
ArrowType["Solid"] = "solid";
|
|
5
|
+
ArrowType["Dotted"] = "dotted";
|
|
6
|
+
})(ArrowType || (ArrowType = {}));
|
|
7
|
+
const sequenceDiagramKeyword = "sequenceDiagram";
|
|
8
|
+
const solidArrowSyntax = "->>";
|
|
9
|
+
const dottedArrowSyntax = "-->>";
|
|
10
|
+
const defaultConfig = {
|
|
11
|
+
useAscii: false,
|
|
12
|
+
showCoords: false,
|
|
13
|
+
verbose: false,
|
|
14
|
+
boxBorderPadding: 1,
|
|
15
|
+
paddingBetweenX: 5,
|
|
16
|
+
paddingBetweenY: 5,
|
|
17
|
+
graphDirection: "LR",
|
|
18
|
+
styleType: "cli",
|
|
19
|
+
sequenceParticipantSpacing: 5,
|
|
20
|
+
sequenceMessageSpacing: 1,
|
|
21
|
+
sequenceSelfMessageWidth: 4
|
|
22
|
+
};
|
|
23
|
+
const junctionChars = new Set([
|
|
24
|
+
"─",
|
|
25
|
+
"│",
|
|
26
|
+
"┌",
|
|
27
|
+
"┐",
|
|
28
|
+
"└",
|
|
29
|
+
"┘",
|
|
30
|
+
"├",
|
|
31
|
+
"┤",
|
|
32
|
+
"┬",
|
|
33
|
+
"┴",
|
|
34
|
+
"┼",
|
|
35
|
+
"╴",
|
|
36
|
+
"╵",
|
|
37
|
+
"╶",
|
|
38
|
+
"╷"
|
|
39
|
+
]);
|
|
40
|
+
const asciiChars = {
|
|
41
|
+
topLeft: "+",
|
|
42
|
+
topRight: "+",
|
|
43
|
+
bottomLeft: "+",
|
|
44
|
+
bottomRight: "+",
|
|
45
|
+
horizontal: "-",
|
|
46
|
+
vertical: "|",
|
|
47
|
+
teeDown: "+",
|
|
48
|
+
teeRight: "+",
|
|
49
|
+
teeLeft: "+",
|
|
50
|
+
cross: "+",
|
|
51
|
+
arrowRight: ">",
|
|
52
|
+
arrowLeft: "<",
|
|
53
|
+
solidLine: "-",
|
|
54
|
+
dottedLine: ".",
|
|
55
|
+
selfTopRight: "+",
|
|
56
|
+
selfBottom: "+"
|
|
57
|
+
};
|
|
58
|
+
const unicodeChars = {
|
|
59
|
+
topLeft: "┌",
|
|
60
|
+
topRight: "┐",
|
|
61
|
+
bottomLeft: "└",
|
|
62
|
+
bottomRight: "┘",
|
|
63
|
+
horizontal: "─",
|
|
64
|
+
vertical: "│",
|
|
65
|
+
teeDown: "┬",
|
|
66
|
+
teeRight: "├",
|
|
67
|
+
teeLeft: "┤",
|
|
68
|
+
cross: "┼",
|
|
69
|
+
arrowRight: "►",
|
|
70
|
+
arrowLeft: "◄",
|
|
71
|
+
solidLine: "─",
|
|
72
|
+
dottedLine: "┈",
|
|
73
|
+
selfTopRight: "┐",
|
|
74
|
+
selfBottom: "┘"
|
|
75
|
+
};
|
|
76
|
+
const directions = {
|
|
77
|
+
up: { x: 1, y: 0 },
|
|
78
|
+
down: { x: 1, y: 2 },
|
|
79
|
+
left: { x: 0, y: 1 },
|
|
80
|
+
right: { x: 2, y: 1 },
|
|
81
|
+
upperRight: { x: 2, y: 0 },
|
|
82
|
+
upperLeft: { x: 0, y: 0 },
|
|
83
|
+
lowerRight: { x: 2, y: 2 },
|
|
84
|
+
lowerLeft: { x: 0, y: 2 },
|
|
85
|
+
middle: { x: 1, y: 1 }
|
|
86
|
+
};
|
|
87
|
+
function normalizeConfig(options = {}) {
|
|
88
|
+
const config = {
|
|
89
|
+
...defaultConfig,
|
|
90
|
+
...options
|
|
91
|
+
};
|
|
92
|
+
if (config.graphDirection !== "LR" && config.graphDirection !== "TD") {
|
|
93
|
+
throw new Error(`invalid graphDirection: ${config.graphDirection}`);
|
|
94
|
+
}
|
|
95
|
+
if (config.styleType !== "cli" && config.styleType !== "html") {
|
|
96
|
+
throw new Error(`invalid styleType: ${config.styleType}`);
|
|
97
|
+
}
|
|
98
|
+
if (config.boxBorderPadding < 0) {
|
|
99
|
+
throw new Error("boxBorderPadding must be non-negative");
|
|
100
|
+
}
|
|
101
|
+
if (config.paddingBetweenX < 0 || config.paddingBetweenY < 0) {
|
|
102
|
+
throw new Error("paddingBetweenX/paddingBetweenY must be non-negative");
|
|
103
|
+
}
|
|
104
|
+
if (config.sequenceParticipantSpacing < 0 || config.sequenceMessageSpacing < 0) {
|
|
105
|
+
throw new Error("sequence spacing must be non-negative");
|
|
106
|
+
}
|
|
107
|
+
if (config.sequenceSelfMessageWidth < 2) {
|
|
108
|
+
throw new Error("sequenceSelfMessageWidth must be at least 2");
|
|
109
|
+
}
|
|
110
|
+
return config;
|
|
111
|
+
}
|
|
112
|
+
function makeLogger(verbose) {
|
|
113
|
+
return {
|
|
114
|
+
debug: (...args) => {
|
|
115
|
+
if (verbose) {
|
|
116
|
+
// eslint-disable-next-line no-console
|
|
117
|
+
console.debug(...args);
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
warn: (...args) => {
|
|
121
|
+
if (verbose) {
|
|
122
|
+
// eslint-disable-next-line no-console
|
|
123
|
+
console.warn(...args);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function min(a, b) {
|
|
129
|
+
return a < b ? a : b;
|
|
130
|
+
}
|
|
131
|
+
function max(a, b) {
|
|
132
|
+
return a > b ? a : b;
|
|
133
|
+
}
|
|
134
|
+
function abs(a) {
|
|
135
|
+
return a < 0 ? -a : a;
|
|
136
|
+
}
|
|
137
|
+
function ceilDiv(x, y) {
|
|
138
|
+
if (y === 0)
|
|
139
|
+
return 0;
|
|
140
|
+
return x % y === 0 ? x / y : Math.floor(x / y) + 1;
|
|
141
|
+
}
|
|
142
|
+
function gridKey(coord) {
|
|
143
|
+
return `${coord.x},${coord.y}`;
|
|
144
|
+
}
|
|
145
|
+
function gridEquals(a, b) {
|
|
146
|
+
return a.x === b.x && a.y === b.y;
|
|
147
|
+
}
|
|
148
|
+
function drawingEquals(a, b) {
|
|
149
|
+
return a.x === b.x && a.y === b.y;
|
|
150
|
+
}
|
|
151
|
+
function directionOpposite(dir) {
|
|
152
|
+
if (dir === directions.up)
|
|
153
|
+
return directions.down;
|
|
154
|
+
if (dir === directions.down)
|
|
155
|
+
return directions.up;
|
|
156
|
+
if (dir === directions.left)
|
|
157
|
+
return directions.right;
|
|
158
|
+
if (dir === directions.right)
|
|
159
|
+
return directions.left;
|
|
160
|
+
if (dir === directions.upperRight)
|
|
161
|
+
return directions.lowerLeft;
|
|
162
|
+
if (dir === directions.upperLeft)
|
|
163
|
+
return directions.lowerRight;
|
|
164
|
+
if (dir === directions.lowerRight)
|
|
165
|
+
return directions.upperLeft;
|
|
166
|
+
if (dir === directions.lowerLeft)
|
|
167
|
+
return directions.upperRight;
|
|
168
|
+
return directions.middle;
|
|
169
|
+
}
|
|
170
|
+
function gridDirection(coord, dir) {
|
|
171
|
+
return { x: coord.x + dir.x, y: coord.y + dir.y };
|
|
172
|
+
}
|
|
173
|
+
function drawingDirection(coord, dir) {
|
|
174
|
+
return { x: coord.x + dir.x, y: coord.y + dir.y };
|
|
175
|
+
}
|
|
176
|
+
function determineDirection(from, to) {
|
|
177
|
+
if (from.x === to.x) {
|
|
178
|
+
return from.y < to.y ? directions.down : directions.up;
|
|
179
|
+
}
|
|
180
|
+
if (from.y === to.y) {
|
|
181
|
+
return from.x < to.x ? directions.right : directions.left;
|
|
182
|
+
}
|
|
183
|
+
if (from.x < to.x) {
|
|
184
|
+
return from.y < to.y ? directions.lowerRight : directions.upperRight;
|
|
185
|
+
}
|
|
186
|
+
return from.y < to.y ? directions.lowerLeft : directions.upperLeft;
|
|
187
|
+
}
|
|
188
|
+
function splitLines(input) {
|
|
189
|
+
return input.split(/\n|\\n/);
|
|
190
|
+
}
|
|
191
|
+
function removeComments(lines) {
|
|
192
|
+
const cleaned = [];
|
|
193
|
+
for (const line of lines) {
|
|
194
|
+
const trimmed = line.trim();
|
|
195
|
+
if (trimmed.startsWith("%%"))
|
|
196
|
+
continue;
|
|
197
|
+
const idx = line.indexOf("%%");
|
|
198
|
+
let updated = line;
|
|
199
|
+
if (idx !== -1) {
|
|
200
|
+
updated = line.slice(0, idx).trim();
|
|
201
|
+
}
|
|
202
|
+
if (updated.trim().length > 0) {
|
|
203
|
+
cleaned.push(updated);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return cleaned;
|
|
207
|
+
}
|
|
208
|
+
function isSequenceDiagram(input) {
|
|
209
|
+
const lines = input.split("\n");
|
|
210
|
+
for (const line of lines) {
|
|
211
|
+
const trimmed = line.trim();
|
|
212
|
+
if (trimmed === "" || trimmed.startsWith("%%"))
|
|
213
|
+
continue;
|
|
214
|
+
return trimmed.startsWith(sequenceDiagramKeyword);
|
|
215
|
+
}
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
function parseNode(line) {
|
|
219
|
+
const trimmed = line.trim();
|
|
220
|
+
const match = trimmed.match(/^(.+):::(.+)$/);
|
|
221
|
+
if (match) {
|
|
222
|
+
return { name: match[1].trim(), styleClass: match[2].trim() };
|
|
223
|
+
}
|
|
224
|
+
return { name: trimmed, styleClass: "" };
|
|
225
|
+
}
|
|
226
|
+
function parseStyleClass(match) {
|
|
227
|
+
const className = match[1];
|
|
228
|
+
const styles = match[2];
|
|
229
|
+
const styleMap = {};
|
|
230
|
+
for (const style of styles.split(",")) {
|
|
231
|
+
const [key, value] = style.split(":");
|
|
232
|
+
if (key) {
|
|
233
|
+
styleMap[key] = value ?? "";
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return { name: className, styles: styleMap };
|
|
237
|
+
}
|
|
238
|
+
function addNode(node, data) {
|
|
239
|
+
if (!data.has(node.name)) {
|
|
240
|
+
data.set(node.name, []);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function setData(parent, edge, data) {
|
|
244
|
+
const existing = data.get(parent.name);
|
|
245
|
+
if (existing) {
|
|
246
|
+
existing.push(edge);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
data.set(parent.name, [edge]);
|
|
250
|
+
}
|
|
251
|
+
if (!data.has(edge.child.name)) {
|
|
252
|
+
data.set(edge.child.name, []);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function setArrowWithLabel(lhs, rhs, label, data) {
|
|
256
|
+
for (const l of lhs) {
|
|
257
|
+
for (const r of rhs) {
|
|
258
|
+
setData(l, { parent: l, child: r, label }, data);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return rhs;
|
|
262
|
+
}
|
|
263
|
+
function setArrow(lhs, rhs, data) {
|
|
264
|
+
return setArrowWithLabel(lhs, rhs, "", data);
|
|
265
|
+
}
|
|
266
|
+
function mermaidFileToMap(mermaid, config) {
|
|
267
|
+
const rawLines = splitLines(mermaid).flatMap((line) => line.split(";"));
|
|
268
|
+
const lines = [];
|
|
269
|
+
for (const raw of rawLines) {
|
|
270
|
+
if (raw === "---")
|
|
271
|
+
break;
|
|
272
|
+
const trimmed = raw.trim();
|
|
273
|
+
if (trimmed.startsWith("%%"))
|
|
274
|
+
continue;
|
|
275
|
+
const idx = raw.indexOf("%%");
|
|
276
|
+
const line = idx === -1 ? raw : raw.slice(0, idx).trim();
|
|
277
|
+
if (line.trim().length > 0) {
|
|
278
|
+
lines.push(line);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const data = new Map();
|
|
282
|
+
const styleClasses = new Map();
|
|
283
|
+
const props = {
|
|
284
|
+
data,
|
|
285
|
+
styleClasses,
|
|
286
|
+
graphDirection: "",
|
|
287
|
+
styleType: config.styleType,
|
|
288
|
+
paddingX: config.paddingBetweenX,
|
|
289
|
+
paddingY: config.paddingBetweenY,
|
|
290
|
+
subgraphs: [],
|
|
291
|
+
useAscii: config.useAscii,
|
|
292
|
+
boxBorderPadding: config.boxBorderPadding
|
|
293
|
+
};
|
|
294
|
+
const paddingRegex = /^padding([xy])\s*=\s*(\d+)$/i;
|
|
295
|
+
let remaining = [...lines];
|
|
296
|
+
while (remaining.length > 0) {
|
|
297
|
+
const trimmed = remaining[0].trim();
|
|
298
|
+
if (trimmed === "") {
|
|
299
|
+
remaining = remaining.slice(1);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const match = trimmed.match(paddingRegex);
|
|
303
|
+
if (match) {
|
|
304
|
+
const value = Number.parseInt(match[2], 10);
|
|
305
|
+
if (Number.isNaN(value)) {
|
|
306
|
+
throw new Error(`invalid padding value: ${match[2]}`);
|
|
307
|
+
}
|
|
308
|
+
if (match[1].toLowerCase() === "x") {
|
|
309
|
+
props.paddingX = value;
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
props.paddingY = value;
|
|
313
|
+
}
|
|
314
|
+
remaining = remaining.slice(1);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
if (remaining.length === 0) {
|
|
320
|
+
throw new Error("missing graph definition");
|
|
321
|
+
}
|
|
322
|
+
switch (remaining[0].trim()) {
|
|
323
|
+
case "graph LR":
|
|
324
|
+
case "flowchart LR":
|
|
325
|
+
props.graphDirection = "LR";
|
|
326
|
+
break;
|
|
327
|
+
case "graph TD":
|
|
328
|
+
case "flowchart TD":
|
|
329
|
+
case "graph TB":
|
|
330
|
+
case "flowchart TB":
|
|
331
|
+
props.graphDirection = "TD";
|
|
332
|
+
break;
|
|
333
|
+
default:
|
|
334
|
+
throw new Error(`unsupported graph type '${remaining[0]}'. Supported types: graph TD, graph TB, graph LR, flowchart TD, flowchart TB, flowchart LR`);
|
|
335
|
+
}
|
|
336
|
+
remaining = remaining.slice(1);
|
|
337
|
+
const subgraphStack = [];
|
|
338
|
+
const subgraphRegex = /^\s*subgraph\s+(.+)$/i;
|
|
339
|
+
const endRegex = /^\s*end\s*$/i;
|
|
340
|
+
for (const line of remaining) {
|
|
341
|
+
const trimmedLine = line.trim();
|
|
342
|
+
const subMatch = trimmedLine.match(subgraphRegex);
|
|
343
|
+
if (subMatch) {
|
|
344
|
+
const subgraphName = subMatch[1].trim();
|
|
345
|
+
const newSubgraph = {
|
|
346
|
+
name: subgraphName,
|
|
347
|
+
nodes: [],
|
|
348
|
+
children: []
|
|
349
|
+
};
|
|
350
|
+
if (subgraphStack.length > 0) {
|
|
351
|
+
const parent = subgraphStack[subgraphStack.length - 1];
|
|
352
|
+
newSubgraph.parent = parent;
|
|
353
|
+
parent.children.push(newSubgraph);
|
|
354
|
+
}
|
|
355
|
+
subgraphStack.push(newSubgraph);
|
|
356
|
+
props.subgraphs.push(newSubgraph);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (endRegex.test(trimmedLine)) {
|
|
360
|
+
if (subgraphStack.length > 0) {
|
|
361
|
+
subgraphStack.pop();
|
|
362
|
+
}
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
const existingNodes = new Set();
|
|
366
|
+
for (const [key] of data) {
|
|
367
|
+
existingNodes.add(key);
|
|
368
|
+
}
|
|
369
|
+
const nodes = parseGraphLine(line, props);
|
|
370
|
+
if (!nodes) {
|
|
371
|
+
const node = parseNode(line);
|
|
372
|
+
addNode(node, data);
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
for (const node of nodes) {
|
|
376
|
+
addNode(node, data);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (subgraphStack.length > 0) {
|
|
380
|
+
for (const [nodeName] of data) {
|
|
381
|
+
if (!existingNodes.has(nodeName)) {
|
|
382
|
+
for (const sg of subgraphStack) {
|
|
383
|
+
if (!sg.nodes.includes(nodeName)) {
|
|
384
|
+
sg.nodes.push(nodeName);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return props;
|
|
392
|
+
}
|
|
393
|
+
function parseGraphLine(line, props) {
|
|
394
|
+
const trimmed = line.trim();
|
|
395
|
+
if (trimmed === "")
|
|
396
|
+
return [];
|
|
397
|
+
const arrowLabel = trimmed.match(/^(.+?)\s*-->\|(.+)\|\s*(.+)$/);
|
|
398
|
+
if (arrowLabel) {
|
|
399
|
+
const lhs = parseGraphLine(arrowLabel[1], props) ?? [parseNode(arrowLabel[1])];
|
|
400
|
+
const rhs = parseGraphLine(arrowLabel[3], props) ?? [parseNode(arrowLabel[3])];
|
|
401
|
+
return setArrowWithLabel(lhs, rhs, arrowLabel[2], props.data);
|
|
402
|
+
}
|
|
403
|
+
const arrow = trimmed.match(/^(.+?)\s*-->\s*(.+)$/);
|
|
404
|
+
if (arrow) {
|
|
405
|
+
const lhs = parseGraphLine(arrow[1], props) ?? [parseNode(arrow[1])];
|
|
406
|
+
const rhs = parseGraphLine(arrow[2], props) ?? [parseNode(arrow[2])];
|
|
407
|
+
return setArrow(lhs, rhs, props.data);
|
|
408
|
+
}
|
|
409
|
+
const classDef = trimmed.match(/^classDef\s+(.+)\s+(.+)$/);
|
|
410
|
+
if (classDef) {
|
|
411
|
+
const style = parseStyleClass(classDef);
|
|
412
|
+
props.styleClasses.set(style.name, style);
|
|
413
|
+
return [];
|
|
414
|
+
}
|
|
415
|
+
const andMatch = trimmed.match(/^(.+) & (.+)$/);
|
|
416
|
+
if (andMatch) {
|
|
417
|
+
const lhs = parseGraphLine(andMatch[1], props) ?? [parseNode(andMatch[1])];
|
|
418
|
+
const rhs = parseGraphLine(andMatch[2], props) ?? [parseNode(andMatch[2])];
|
|
419
|
+
return [...lhs, ...rhs];
|
|
420
|
+
}
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
class Graph {
|
|
424
|
+
nodes = [];
|
|
425
|
+
edges = [];
|
|
426
|
+
drawing;
|
|
427
|
+
grid = new Map();
|
|
428
|
+
columnWidth = new Map();
|
|
429
|
+
rowHeight = new Map();
|
|
430
|
+
styleClasses = new Map();
|
|
431
|
+
styleType;
|
|
432
|
+
paddingX;
|
|
433
|
+
paddingY;
|
|
434
|
+
subgraphs = [];
|
|
435
|
+
offsetX = 0;
|
|
436
|
+
offsetY = 0;
|
|
437
|
+
useAscii = false;
|
|
438
|
+
graphDirection;
|
|
439
|
+
boxBorderPadding;
|
|
440
|
+
logger;
|
|
441
|
+
constructor(props, logger) {
|
|
442
|
+
this.drawing = mkDrawing(0, 0);
|
|
443
|
+
this.styleType = props.styleType;
|
|
444
|
+
this.paddingX = props.paddingX;
|
|
445
|
+
this.paddingY = props.paddingY;
|
|
446
|
+
this.graphDirection = props.graphDirection === "" ? "LR" : props.graphDirection;
|
|
447
|
+
this.useAscii = props.useAscii;
|
|
448
|
+
this.boxBorderPadding = props.boxBorderPadding;
|
|
449
|
+
this.logger = logger;
|
|
450
|
+
let index = 0;
|
|
451
|
+
for (const [nodeName, children] of props.data) {
|
|
452
|
+
let parentNode = this.getNode(nodeName);
|
|
453
|
+
if (!parentNode) {
|
|
454
|
+
parentNode = this.createNode(nodeName, index, "");
|
|
455
|
+
index += 1;
|
|
456
|
+
}
|
|
457
|
+
for (const textEdge of children) {
|
|
458
|
+
let childNode = this.getNode(textEdge.child.name);
|
|
459
|
+
if (!childNode) {
|
|
460
|
+
childNode = this.createNode(textEdge.child.name, index, textEdge.child.styleClass);
|
|
461
|
+
parentNode.styleClassName = textEdge.parent.styleClass;
|
|
462
|
+
index += 1;
|
|
463
|
+
}
|
|
464
|
+
const e = {
|
|
465
|
+
from: parentNode,
|
|
466
|
+
to: childNode,
|
|
467
|
+
text: textEdge.label,
|
|
468
|
+
path: [],
|
|
469
|
+
labelLine: [],
|
|
470
|
+
startDir: directions.right,
|
|
471
|
+
endDir: directions.left
|
|
472
|
+
};
|
|
473
|
+
this.edges.push(e);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
this.setStyleClasses(props);
|
|
477
|
+
this.setSubgraphs(props.subgraphs);
|
|
478
|
+
}
|
|
479
|
+
createNode(name, index, styleClassName) {
|
|
480
|
+
const node = {
|
|
481
|
+
name,
|
|
482
|
+
drawn: false,
|
|
483
|
+
index,
|
|
484
|
+
styleClassName,
|
|
485
|
+
styleClass: { name: "", styles: {} }
|
|
486
|
+
};
|
|
487
|
+
this.nodes.push(node);
|
|
488
|
+
return node;
|
|
489
|
+
}
|
|
490
|
+
getNode(name) {
|
|
491
|
+
return this.nodes.find((node) => node.name === name);
|
|
492
|
+
}
|
|
493
|
+
getColumnWidth(index) {
|
|
494
|
+
return this.columnWidth.get(index) ?? 0;
|
|
495
|
+
}
|
|
496
|
+
getRowHeight(index) {
|
|
497
|
+
return this.rowHeight.get(index) ?? 0;
|
|
498
|
+
}
|
|
499
|
+
setColumnWidthValue(index, value) {
|
|
500
|
+
this.columnWidth.set(index, value);
|
|
501
|
+
}
|
|
502
|
+
setRowHeightValue(index, value) {
|
|
503
|
+
this.rowHeight.set(index, value);
|
|
504
|
+
}
|
|
505
|
+
setStyleClasses(props) {
|
|
506
|
+
this.styleClasses = props.styleClasses;
|
|
507
|
+
this.styleType = props.styleType;
|
|
508
|
+
this.paddingX = props.paddingX;
|
|
509
|
+
this.paddingY = props.paddingY;
|
|
510
|
+
for (const node of this.nodes) {
|
|
511
|
+
if (node.styleClassName) {
|
|
512
|
+
const style = this.styleClasses.get(node.styleClassName);
|
|
513
|
+
if (style)
|
|
514
|
+
node.styleClass = style;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
setSubgraphs(textSubgraphs) {
|
|
519
|
+
this.subgraphs = [];
|
|
520
|
+
for (const tsg of textSubgraphs) {
|
|
521
|
+
const sg = {
|
|
522
|
+
name: tsg.name,
|
|
523
|
+
nodes: [],
|
|
524
|
+
children: [],
|
|
525
|
+
minX: 0,
|
|
526
|
+
minY: 0,
|
|
527
|
+
maxX: 0,
|
|
528
|
+
maxY: 0
|
|
529
|
+
};
|
|
530
|
+
for (const nodeName of tsg.nodes) {
|
|
531
|
+
const node = this.getNode(nodeName);
|
|
532
|
+
if (node)
|
|
533
|
+
sg.nodes.push(node);
|
|
534
|
+
}
|
|
535
|
+
this.subgraphs.push(sg);
|
|
536
|
+
}
|
|
537
|
+
for (let i = 0; i < textSubgraphs.length; i += 1) {
|
|
538
|
+
const tsg = textSubgraphs[i];
|
|
539
|
+
const sg = this.subgraphs[i];
|
|
540
|
+
if (tsg.parent) {
|
|
541
|
+
const parentIndex = textSubgraphs.indexOf(tsg.parent);
|
|
542
|
+
if (parentIndex !== -1) {
|
|
543
|
+
sg.parent = this.subgraphs[parentIndex];
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
for (const child of tsg.children) {
|
|
547
|
+
const childIndex = textSubgraphs.indexOf(child);
|
|
548
|
+
if (childIndex !== -1) {
|
|
549
|
+
sg.children.push(this.subgraphs[childIndex]);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
createMapping() {
|
|
555
|
+
const highestPositionPerLevel = Array.from({ length: 100 }, () => 0);
|
|
556
|
+
const nodesFound = new Set();
|
|
557
|
+
const rootNodes = [];
|
|
558
|
+
for (const node of this.nodes) {
|
|
559
|
+
if (!nodesFound.has(node.name)) {
|
|
560
|
+
rootNodes.push(node);
|
|
561
|
+
}
|
|
562
|
+
nodesFound.add(node.name);
|
|
563
|
+
for (const child of this.getChildren(node)) {
|
|
564
|
+
nodesFound.add(child.name);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
let hasExternalRoots = false;
|
|
568
|
+
let hasSubgraphRootsWithEdges = false;
|
|
569
|
+
for (const node of rootNodes) {
|
|
570
|
+
if (this.isNodeInAnySubgraph(node)) {
|
|
571
|
+
if (this.getChildren(node).length > 0) {
|
|
572
|
+
hasSubgraphRootsWithEdges = true;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
hasExternalRoots = true;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
const shouldSeparate = this.graphDirection === "LR" && hasExternalRoots && hasSubgraphRootsWithEdges;
|
|
580
|
+
const externalRootNodes = [];
|
|
581
|
+
const subgraphRootNodes = [];
|
|
582
|
+
if (shouldSeparate) {
|
|
583
|
+
for (const node of rootNodes) {
|
|
584
|
+
if (this.isNodeInAnySubgraph(node)) {
|
|
585
|
+
subgraphRootNodes.push(node);
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
externalRootNodes.push(node);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
externalRootNodes.push(...rootNodes);
|
|
594
|
+
}
|
|
595
|
+
for (const node of externalRootNodes) {
|
|
596
|
+
let mappingCoord;
|
|
597
|
+
if (this.graphDirection === "LR") {
|
|
598
|
+
mappingCoord = this.reserveSpotInGrid(node, { x: 0, y: highestPositionPerLevel[0] });
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
mappingCoord = this.reserveSpotInGrid(node, { x: highestPositionPerLevel[0], y: 0 });
|
|
602
|
+
}
|
|
603
|
+
node.gridCoord = mappingCoord;
|
|
604
|
+
highestPositionPerLevel[0] += 4;
|
|
605
|
+
}
|
|
606
|
+
if (shouldSeparate && subgraphRootNodes.length > 0) {
|
|
607
|
+
const subgraphLevel = 4;
|
|
608
|
+
for (const node of subgraphRootNodes) {
|
|
609
|
+
let mappingCoord;
|
|
610
|
+
if (this.graphDirection === "LR") {
|
|
611
|
+
mappingCoord = this.reserveSpotInGrid(node, { x: subgraphLevel, y: highestPositionPerLevel[subgraphLevel] });
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
mappingCoord = this.reserveSpotInGrid(node, { x: highestPositionPerLevel[subgraphLevel], y: subgraphLevel });
|
|
615
|
+
}
|
|
616
|
+
node.gridCoord = mappingCoord;
|
|
617
|
+
highestPositionPerLevel[subgraphLevel] += 4;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
for (const node of this.nodes) {
|
|
621
|
+
const childLevel = this.graphDirection === "LR" ? (node.gridCoord?.x ?? 0) + 4 : (node.gridCoord?.y ?? 0) + 4;
|
|
622
|
+
let highestPosition = highestPositionPerLevel[childLevel];
|
|
623
|
+
for (const child of this.getChildren(node)) {
|
|
624
|
+
if (child.gridCoord)
|
|
625
|
+
continue;
|
|
626
|
+
let mappingCoord;
|
|
627
|
+
if (this.graphDirection === "LR") {
|
|
628
|
+
mappingCoord = this.reserveSpotInGrid(child, { x: childLevel, y: highestPosition });
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
mappingCoord = this.reserveSpotInGrid(child, { x: highestPosition, y: childLevel });
|
|
632
|
+
}
|
|
633
|
+
child.gridCoord = mappingCoord;
|
|
634
|
+
highestPositionPerLevel[childLevel] = highestPosition + 4;
|
|
635
|
+
highestPosition = highestPositionPerLevel[childLevel];
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
for (const node of this.nodes) {
|
|
639
|
+
this.setColumnWidth(node);
|
|
640
|
+
}
|
|
641
|
+
for (const edge of this.edges) {
|
|
642
|
+
this.determinePath(edge);
|
|
643
|
+
this.increaseGridSizeForPath(edge.path);
|
|
644
|
+
this.determineLabelLine(edge);
|
|
645
|
+
}
|
|
646
|
+
for (const node of this.nodes) {
|
|
647
|
+
if (!node.gridCoord)
|
|
648
|
+
continue;
|
|
649
|
+
const dc = this.gridToDrawingCoord(node.gridCoord);
|
|
650
|
+
node.drawingCoord = dc;
|
|
651
|
+
node.drawing = drawBox(node, this);
|
|
652
|
+
}
|
|
653
|
+
this.setDrawingSizeToGridConstraints();
|
|
654
|
+
this.calculateSubgraphBoundingBoxes();
|
|
655
|
+
this.offsetDrawingForSubgraphs();
|
|
656
|
+
}
|
|
657
|
+
reserveSpotInGrid(node, requested) {
|
|
658
|
+
if (this.grid.has(gridKey(requested))) {
|
|
659
|
+
if (this.graphDirection === "LR") {
|
|
660
|
+
return this.reserveSpotInGrid(node, { x: requested.x, y: requested.y + 4 });
|
|
661
|
+
}
|
|
662
|
+
return this.reserveSpotInGrid(node, { x: requested.x + 4, y: requested.y });
|
|
663
|
+
}
|
|
664
|
+
for (let x = 0; x < 3; x += 1) {
|
|
665
|
+
for (let y = 0; y < 3; y += 1) {
|
|
666
|
+
const reserved = { x: requested.x + x, y: requested.y + y };
|
|
667
|
+
this.grid.set(gridKey(reserved), node);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return requested;
|
|
671
|
+
}
|
|
672
|
+
setColumnWidth(node) {
|
|
673
|
+
if (!node.gridCoord)
|
|
674
|
+
return;
|
|
675
|
+
const col1 = 1;
|
|
676
|
+
const col2 = 2 * this.boxBorderPadding + stringWidth(node.name);
|
|
677
|
+
const col3 = 1;
|
|
678
|
+
const cols = [col1, col2, col3];
|
|
679
|
+
const rows = [1, 1 + 2 * this.boxBorderPadding, 1];
|
|
680
|
+
for (let idx = 0; idx < cols.length; idx += 1) {
|
|
681
|
+
const xCoord = node.gridCoord.x + idx;
|
|
682
|
+
this.setColumnWidthValue(xCoord, max(this.getColumnWidth(xCoord), cols[idx]));
|
|
683
|
+
}
|
|
684
|
+
for (let idx = 0; idx < rows.length; idx += 1) {
|
|
685
|
+
const yCoord = node.gridCoord.y + idx;
|
|
686
|
+
this.setRowHeightValue(yCoord, max(this.getRowHeight(yCoord), rows[idx]));
|
|
687
|
+
}
|
|
688
|
+
if (node.gridCoord.x > 0) {
|
|
689
|
+
this.setColumnWidthValue(node.gridCoord.x - 1, this.paddingX);
|
|
690
|
+
}
|
|
691
|
+
if (node.gridCoord.y > 0) {
|
|
692
|
+
let basePadding = this.paddingY;
|
|
693
|
+
if (this.hasIncomingEdgeFromOutsideSubgraph(node)) {
|
|
694
|
+
basePadding += 4;
|
|
695
|
+
}
|
|
696
|
+
this.setRowHeightValue(node.gridCoord.y - 1, max(this.getRowHeight(node.gridCoord.y - 1), basePadding));
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
increaseGridSizeForPath(path) {
|
|
700
|
+
for (const coord of path) {
|
|
701
|
+
if (!this.columnWidth.has(coord.x)) {
|
|
702
|
+
this.setColumnWidthValue(coord.x, Math.floor(this.paddingX / 2));
|
|
703
|
+
}
|
|
704
|
+
if (!this.rowHeight.has(coord.y)) {
|
|
705
|
+
this.setRowHeightValue(coord.y, Math.floor(this.paddingY / 2));
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
determinePath(edge) {
|
|
710
|
+
const [preferredDir, preferredOpposite, alternativeDir, alternativeOpposite] = determineStartAndEndDir(edge, this.graphDirection);
|
|
711
|
+
let preferredPath = [];
|
|
712
|
+
let alternativePath = [];
|
|
713
|
+
let from = gridDirection(edge.from.gridCoord, preferredDir);
|
|
714
|
+
let to = gridDirection(edge.to.gridCoord, preferredOpposite);
|
|
715
|
+
preferredPath = this.getPath(from, to) ?? [];
|
|
716
|
+
preferredPath = mergePath(preferredPath);
|
|
717
|
+
from = gridDirection(edge.from.gridCoord, alternativeDir);
|
|
718
|
+
to = gridDirection(edge.to.gridCoord, alternativeOpposite);
|
|
719
|
+
alternativePath = this.getPath(from, to) ?? [];
|
|
720
|
+
alternativePath = mergePath(alternativePath);
|
|
721
|
+
if (preferredPath.length === 0 && alternativePath.length === 0) {
|
|
722
|
+
edge.startDir = preferredDir;
|
|
723
|
+
edge.endDir = preferredOpposite;
|
|
724
|
+
edge.path = [];
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
if (alternativePath.length === 0 || preferredPath.length <= alternativePath.length) {
|
|
728
|
+
edge.startDir = preferredDir;
|
|
729
|
+
edge.endDir = preferredOpposite;
|
|
730
|
+
edge.path = preferredPath;
|
|
731
|
+
}
|
|
732
|
+
else {
|
|
733
|
+
edge.startDir = alternativeDir;
|
|
734
|
+
edge.endDir = alternativeOpposite;
|
|
735
|
+
edge.path = alternativePath;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
determineLabelLine(edge) {
|
|
739
|
+
const labelLength = stringWidth(edge.text);
|
|
740
|
+
if (labelLength === 0 || edge.path.length < 2)
|
|
741
|
+
return;
|
|
742
|
+
let prev = edge.path[0];
|
|
743
|
+
let largestLine = [edge.path[0], edge.path[1]];
|
|
744
|
+
let largestSize = 0;
|
|
745
|
+
for (const step of edge.path.slice(1)) {
|
|
746
|
+
const line = [prev, step];
|
|
747
|
+
const lineWidth = this.calculateLineWidth(line);
|
|
748
|
+
if (lineWidth >= labelLength) {
|
|
749
|
+
largestLine = line;
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
if (lineWidth > largestSize) {
|
|
753
|
+
largestSize = lineWidth;
|
|
754
|
+
largestLine = line;
|
|
755
|
+
}
|
|
756
|
+
prev = step;
|
|
757
|
+
}
|
|
758
|
+
const [a, b] = largestLine;
|
|
759
|
+
const minX = Math.min(a.x, b.x);
|
|
760
|
+
const maxX = Math.max(a.x, b.x);
|
|
761
|
+
const middleX = minX + Math.floor((maxX - minX) / 2);
|
|
762
|
+
const current = this.getColumnWidth(middleX);
|
|
763
|
+
this.setColumnWidthValue(middleX, max(current, labelLength + 2));
|
|
764
|
+
edge.labelLine = largestLine;
|
|
765
|
+
}
|
|
766
|
+
calculateLineWidth(line) {
|
|
767
|
+
let total = 0;
|
|
768
|
+
for (const coord of line) {
|
|
769
|
+
total += this.getColumnWidth(coord.x);
|
|
770
|
+
}
|
|
771
|
+
return total;
|
|
772
|
+
}
|
|
773
|
+
isFreeInGrid(coord) {
|
|
774
|
+
if (coord.x < 0 || coord.y < 0)
|
|
775
|
+
return false;
|
|
776
|
+
return !this.grid.has(gridKey(coord));
|
|
777
|
+
}
|
|
778
|
+
getPath(from, to) {
|
|
779
|
+
const heap = new MinHeap();
|
|
780
|
+
heap.push({ coord: from, priority: 0 });
|
|
781
|
+
const costSoFar = new Map();
|
|
782
|
+
const cameFrom = new Map();
|
|
783
|
+
costSoFar.set(gridKey(from), 0);
|
|
784
|
+
cameFrom.set(gridKey(from), null);
|
|
785
|
+
const dirs = [
|
|
786
|
+
{ x: 1, y: 0 },
|
|
787
|
+
{ x: -1, y: 0 },
|
|
788
|
+
{ x: 0, y: 1 },
|
|
789
|
+
{ x: 0, y: -1 }
|
|
790
|
+
];
|
|
791
|
+
while (heap.size() > 0) {
|
|
792
|
+
const current = heap.pop().coord;
|
|
793
|
+
if (gridEquals(current, to)) {
|
|
794
|
+
const path = [];
|
|
795
|
+
let cursor = current;
|
|
796
|
+
while (cursor) {
|
|
797
|
+
path.unshift(cursor);
|
|
798
|
+
cursor = cameFrom.get(gridKey(cursor)) ?? null;
|
|
799
|
+
}
|
|
800
|
+
return path;
|
|
801
|
+
}
|
|
802
|
+
for (const dir of dirs) {
|
|
803
|
+
const next = { x: current.x + dir.x, y: current.y + dir.y };
|
|
804
|
+
if (!this.isFreeInGrid(next) && !gridEquals(next, to))
|
|
805
|
+
continue;
|
|
806
|
+
const newCost = (costSoFar.get(gridKey(current)) ?? 0) + 1;
|
|
807
|
+
const existing = costSoFar.get(gridKey(next));
|
|
808
|
+
if (existing === undefined || newCost < existing) {
|
|
809
|
+
costSoFar.set(gridKey(next), newCost);
|
|
810
|
+
const priority = newCost + heuristic(next, to);
|
|
811
|
+
heap.push({ coord: next, priority });
|
|
812
|
+
cameFrom.set(gridKey(next), current);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
return null;
|
|
817
|
+
}
|
|
818
|
+
draw() {
|
|
819
|
+
this.drawSubgraphs();
|
|
820
|
+
for (const node of this.nodes) {
|
|
821
|
+
if (!node.drawn) {
|
|
822
|
+
this.drawNode(node);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
const lineDrawings = [];
|
|
826
|
+
const cornerDrawings = [];
|
|
827
|
+
const arrowHeadDrawings = [];
|
|
828
|
+
const boxStartDrawings = [];
|
|
829
|
+
const labelDrawings = [];
|
|
830
|
+
for (const edge of this.edges) {
|
|
831
|
+
const { line, boxStart, arrowHead, corners, label } = this.drawEdge(edge);
|
|
832
|
+
if (line)
|
|
833
|
+
lineDrawings.push(line);
|
|
834
|
+
if (boxStart)
|
|
835
|
+
boxStartDrawings.push(boxStart);
|
|
836
|
+
if (arrowHead)
|
|
837
|
+
arrowHeadDrawings.push(arrowHead);
|
|
838
|
+
if (corners)
|
|
839
|
+
cornerDrawings.push(corners);
|
|
840
|
+
if (label)
|
|
841
|
+
labelDrawings.push(label);
|
|
842
|
+
}
|
|
843
|
+
this.drawing = this.mergeDrawings(this.drawing, { x: 0, y: 0 }, ...lineDrawings);
|
|
844
|
+
this.drawing = this.mergeDrawings(this.drawing, { x: 0, y: 0 }, ...cornerDrawings);
|
|
845
|
+
this.drawing = this.mergeDrawings(this.drawing, { x: 0, y: 0 }, ...arrowHeadDrawings);
|
|
846
|
+
this.drawing = this.mergeDrawings(this.drawing, { x: 0, y: 0 }, ...boxStartDrawings);
|
|
847
|
+
this.drawing = this.mergeDrawings(this.drawing, { x: 0, y: 0 }, ...labelDrawings);
|
|
848
|
+
this.drawSubgraphLabels();
|
|
849
|
+
return this.drawing;
|
|
850
|
+
}
|
|
851
|
+
drawNode(node) {
|
|
852
|
+
if (!node.drawing || !node.drawingCoord)
|
|
853
|
+
return;
|
|
854
|
+
this.drawing = this.mergeDrawings(this.drawing, node.drawingCoord, node.drawing);
|
|
855
|
+
}
|
|
856
|
+
drawEdge(edge) {
|
|
857
|
+
if (!edge.path.length) {
|
|
858
|
+
return { line: null, boxStart: null, arrowHead: null, corners: null, label: null };
|
|
859
|
+
}
|
|
860
|
+
const line = this.drawPath(edge.path);
|
|
861
|
+
const boxStart = this.drawBoxStart(edge.path, line.linesDrawn[0]);
|
|
862
|
+
const arrowHead = this.drawArrowHead(line.linesDrawn[line.linesDrawn.length - 1], line.lineDirs[line.lineDirs.length - 1]);
|
|
863
|
+
const corners = this.drawCorners(edge.path);
|
|
864
|
+
const label = this.drawArrowLabel(edge);
|
|
865
|
+
return { line: line.drawing, boxStart, arrowHead, corners, label };
|
|
866
|
+
}
|
|
867
|
+
drawPath(path) {
|
|
868
|
+
const d = copyCanvas(this.drawing);
|
|
869
|
+
const linesDrawn = [];
|
|
870
|
+
const lineDirs = [];
|
|
871
|
+
let previous = path[0];
|
|
872
|
+
for (const next of path.slice(1)) {
|
|
873
|
+
const previousDrawing = this.gridToDrawingCoord(previous);
|
|
874
|
+
const nextDrawing = this.gridToDrawingCoord(next);
|
|
875
|
+
if (drawingEquals(previousDrawing, nextDrawing)) {
|
|
876
|
+
previous = next;
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
const dir = determineDirection(previous, next);
|
|
880
|
+
let drawn = this.drawLine(d, previousDrawing, nextDrawing, 1, -1);
|
|
881
|
+
if (drawn.length === 0) {
|
|
882
|
+
drawn = [previousDrawing];
|
|
883
|
+
}
|
|
884
|
+
linesDrawn.push(drawn);
|
|
885
|
+
lineDirs.push(dir);
|
|
886
|
+
previous = next;
|
|
887
|
+
}
|
|
888
|
+
return { drawing: d, linesDrawn, lineDirs };
|
|
889
|
+
}
|
|
890
|
+
drawLine(d, from, to, offsetFrom, offsetTo) {
|
|
891
|
+
const drawn = [];
|
|
892
|
+
const dir = determineDirection(from, to);
|
|
893
|
+
const draw = (coord, char) => {
|
|
894
|
+
ensureSize(d, coord.x, coord.y);
|
|
895
|
+
d[coord.x][coord.y] = char;
|
|
896
|
+
};
|
|
897
|
+
const lineChar = this.useAscii ? "-" : "─";
|
|
898
|
+
const vertChar = this.useAscii ? "|" : "│";
|
|
899
|
+
if (!this.useAscii) {
|
|
900
|
+
switch (dir) {
|
|
901
|
+
case directions.up:
|
|
902
|
+
for (let y = from.y - offsetFrom; y >= to.y - offsetTo; y -= 1) {
|
|
903
|
+
drawn.push({ x: from.x, y });
|
|
904
|
+
draw({ x: from.x, y }, vertChar);
|
|
905
|
+
}
|
|
906
|
+
break;
|
|
907
|
+
case directions.down:
|
|
908
|
+
for (let y = from.y + offsetFrom; y <= to.y + offsetTo; y += 1) {
|
|
909
|
+
drawn.push({ x: from.x, y });
|
|
910
|
+
draw({ x: from.x, y }, vertChar);
|
|
911
|
+
}
|
|
912
|
+
break;
|
|
913
|
+
case directions.left:
|
|
914
|
+
for (let x = from.x - offsetFrom; x >= to.x - offsetTo; x -= 1) {
|
|
915
|
+
drawn.push({ x, y: from.y });
|
|
916
|
+
draw({ x, y: from.y }, lineChar);
|
|
917
|
+
}
|
|
918
|
+
break;
|
|
919
|
+
case directions.right:
|
|
920
|
+
for (let x = from.x + offsetFrom; x <= to.x + offsetTo; x += 1) {
|
|
921
|
+
drawn.push({ x, y: from.y });
|
|
922
|
+
draw({ x, y: from.y }, lineChar);
|
|
923
|
+
}
|
|
924
|
+
break;
|
|
925
|
+
case directions.upperLeft:
|
|
926
|
+
for (let x = from.x, y = from.y - offsetFrom; x >= to.x - offsetTo && y >= to.y - offsetTo; x -= 1, y -= 1) {
|
|
927
|
+
drawn.push({ x, y });
|
|
928
|
+
draw({ x, y }, "╲");
|
|
929
|
+
}
|
|
930
|
+
break;
|
|
931
|
+
case directions.upperRight:
|
|
932
|
+
for (let x = from.x, y = from.y - offsetFrom; x <= to.x + offsetTo && y >= to.y - offsetTo; x += 1, y -= 1) {
|
|
933
|
+
drawn.push({ x, y });
|
|
934
|
+
draw({ x, y }, "╱");
|
|
935
|
+
}
|
|
936
|
+
break;
|
|
937
|
+
case directions.lowerLeft:
|
|
938
|
+
for (let x = from.x, y = from.y + offsetFrom; x >= to.x - offsetTo && y <= to.y + offsetTo; x -= 1, y += 1) {
|
|
939
|
+
drawn.push({ x, y });
|
|
940
|
+
draw({ x, y }, "╱");
|
|
941
|
+
}
|
|
942
|
+
break;
|
|
943
|
+
case directions.lowerRight:
|
|
944
|
+
for (let x = from.x, y = from.y + offsetFrom; x <= to.x + offsetTo && y <= to.y + offsetTo; x += 1, y += 1) {
|
|
945
|
+
drawn.push({ x, y });
|
|
946
|
+
draw({ x, y }, "╲");
|
|
947
|
+
}
|
|
948
|
+
break;
|
|
949
|
+
default:
|
|
950
|
+
break;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
else {
|
|
954
|
+
switch (dir) {
|
|
955
|
+
case directions.up:
|
|
956
|
+
for (let y = from.y - offsetFrom; y >= to.y - offsetTo; y -= 1) {
|
|
957
|
+
drawn.push({ x: from.x, y });
|
|
958
|
+
draw({ x: from.x, y }, "|");
|
|
959
|
+
}
|
|
960
|
+
break;
|
|
961
|
+
case directions.down:
|
|
962
|
+
for (let y = from.y + offsetFrom; y <= to.y + offsetTo; y += 1) {
|
|
963
|
+
drawn.push({ x: from.x, y });
|
|
964
|
+
draw({ x: from.x, y }, "|");
|
|
965
|
+
}
|
|
966
|
+
break;
|
|
967
|
+
case directions.left:
|
|
968
|
+
for (let x = from.x - offsetFrom; x >= to.x - offsetTo; x -= 1) {
|
|
969
|
+
drawn.push({ x, y: from.y });
|
|
970
|
+
draw({ x, y: from.y }, "-");
|
|
971
|
+
}
|
|
972
|
+
break;
|
|
973
|
+
case directions.right:
|
|
974
|
+
for (let x = from.x + offsetFrom; x <= to.x + offsetTo; x += 1) {
|
|
975
|
+
drawn.push({ x, y: from.y });
|
|
976
|
+
draw({ x, y: from.y }, "-");
|
|
977
|
+
}
|
|
978
|
+
break;
|
|
979
|
+
case directions.upperLeft:
|
|
980
|
+
for (let x = from.x, y = from.y - offsetFrom; x >= to.x - offsetTo && y >= to.y - offsetTo; x -= 1, y -= 1) {
|
|
981
|
+
drawn.push({ x, y });
|
|
982
|
+
draw({ x, y }, "\\");
|
|
983
|
+
}
|
|
984
|
+
break;
|
|
985
|
+
case directions.upperRight:
|
|
986
|
+
for (let x = from.x, y = from.y - offsetFrom; x <= to.x + offsetTo && y >= to.y - offsetTo; x += 1, y -= 1) {
|
|
987
|
+
drawn.push({ x, y });
|
|
988
|
+
draw({ x, y }, "/");
|
|
989
|
+
}
|
|
990
|
+
break;
|
|
991
|
+
case directions.lowerLeft:
|
|
992
|
+
for (let x = from.x, y = from.y + offsetFrom; x >= to.x - offsetTo && y <= to.y + offsetTo; x -= 1, y += 1) {
|
|
993
|
+
drawn.push({ x, y });
|
|
994
|
+
draw({ x, y }, "/");
|
|
995
|
+
}
|
|
996
|
+
break;
|
|
997
|
+
case directions.lowerRight:
|
|
998
|
+
for (let x = from.x, y = from.y + offsetFrom; x <= to.x + offsetTo && y <= to.y + offsetTo; x += 1, y += 1) {
|
|
999
|
+
drawn.push({ x, y });
|
|
1000
|
+
draw({ x, y }, "\\");
|
|
1001
|
+
}
|
|
1002
|
+
break;
|
|
1003
|
+
default:
|
|
1004
|
+
break;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
return drawn;
|
|
1008
|
+
}
|
|
1009
|
+
drawBoxStart(path, firstLine) {
|
|
1010
|
+
const d = copyCanvas(this.drawing);
|
|
1011
|
+
if (this.useAscii)
|
|
1012
|
+
return d;
|
|
1013
|
+
const from = firstLine[0];
|
|
1014
|
+
const dir = determineDirection(path[0], path[1]);
|
|
1015
|
+
switch (dir) {
|
|
1016
|
+
case directions.up:
|
|
1017
|
+
d[from.x][from.y + 1] = "┴";
|
|
1018
|
+
break;
|
|
1019
|
+
case directions.down:
|
|
1020
|
+
d[from.x][from.y - 1] = "┬";
|
|
1021
|
+
break;
|
|
1022
|
+
case directions.left:
|
|
1023
|
+
d[from.x + 1][from.y] = "┤";
|
|
1024
|
+
break;
|
|
1025
|
+
case directions.right:
|
|
1026
|
+
d[from.x - 1][from.y] = "├";
|
|
1027
|
+
break;
|
|
1028
|
+
default:
|
|
1029
|
+
break;
|
|
1030
|
+
}
|
|
1031
|
+
return d;
|
|
1032
|
+
}
|
|
1033
|
+
drawArrowHead(line, fallback) {
|
|
1034
|
+
const d = copyCanvas(this.drawing);
|
|
1035
|
+
if (line.length === 0)
|
|
1036
|
+
return d;
|
|
1037
|
+
const from = line[0];
|
|
1038
|
+
const last = line[line.length - 1];
|
|
1039
|
+
let dir = determineDirection(from, last);
|
|
1040
|
+
if (line.length === 1 || dir === directions.middle) {
|
|
1041
|
+
dir = fallback;
|
|
1042
|
+
}
|
|
1043
|
+
let char = "";
|
|
1044
|
+
if (!this.useAscii) {
|
|
1045
|
+
switch (dir) {
|
|
1046
|
+
case directions.up:
|
|
1047
|
+
char = "▲";
|
|
1048
|
+
break;
|
|
1049
|
+
case directions.down:
|
|
1050
|
+
char = "▼";
|
|
1051
|
+
break;
|
|
1052
|
+
case directions.left:
|
|
1053
|
+
char = "◄";
|
|
1054
|
+
break;
|
|
1055
|
+
case directions.right:
|
|
1056
|
+
char = "►";
|
|
1057
|
+
break;
|
|
1058
|
+
case directions.upperRight:
|
|
1059
|
+
char = "◥";
|
|
1060
|
+
break;
|
|
1061
|
+
case directions.upperLeft:
|
|
1062
|
+
char = "◤";
|
|
1063
|
+
break;
|
|
1064
|
+
case directions.lowerRight:
|
|
1065
|
+
char = "◢";
|
|
1066
|
+
break;
|
|
1067
|
+
case directions.lowerLeft:
|
|
1068
|
+
char = "◣";
|
|
1069
|
+
break;
|
|
1070
|
+
default:
|
|
1071
|
+
char = "●";
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
else {
|
|
1075
|
+
switch (dir) {
|
|
1076
|
+
case directions.up:
|
|
1077
|
+
char = "^";
|
|
1078
|
+
break;
|
|
1079
|
+
case directions.down:
|
|
1080
|
+
char = "v";
|
|
1081
|
+
break;
|
|
1082
|
+
case directions.left:
|
|
1083
|
+
char = "<";
|
|
1084
|
+
break;
|
|
1085
|
+
case directions.right:
|
|
1086
|
+
char = ">";
|
|
1087
|
+
break;
|
|
1088
|
+
default:
|
|
1089
|
+
char = "*";
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
d[last.x][last.y] = char;
|
|
1093
|
+
return d;
|
|
1094
|
+
}
|
|
1095
|
+
drawCorners(path) {
|
|
1096
|
+
const d = copyCanvas(this.drawing);
|
|
1097
|
+
for (let idx = 0; idx < path.length; idx += 1) {
|
|
1098
|
+
if (idx === 0 || idx === path.length - 1)
|
|
1099
|
+
continue;
|
|
1100
|
+
const coord = path[idx];
|
|
1101
|
+
const drawingCoord = this.gridToDrawingCoord(coord);
|
|
1102
|
+
const prevDir = determineDirection(path[idx - 1], coord);
|
|
1103
|
+
const nextDir = determineDirection(coord, path[idx + 1]);
|
|
1104
|
+
let corner = "+";
|
|
1105
|
+
if (!this.useAscii) {
|
|
1106
|
+
if ((prevDir === directions.right && nextDir === directions.down) || (prevDir === directions.up && nextDir === directions.left)) {
|
|
1107
|
+
corner = "┐";
|
|
1108
|
+
}
|
|
1109
|
+
else if ((prevDir === directions.right && nextDir === directions.up) ||
|
|
1110
|
+
(prevDir === directions.down && nextDir === directions.left)) {
|
|
1111
|
+
corner = "┘";
|
|
1112
|
+
}
|
|
1113
|
+
else if ((prevDir === directions.left && nextDir === directions.down) ||
|
|
1114
|
+
(prevDir === directions.up && nextDir === directions.right)) {
|
|
1115
|
+
corner = "┌";
|
|
1116
|
+
}
|
|
1117
|
+
else if ((prevDir === directions.left && nextDir === directions.up) ||
|
|
1118
|
+
(prevDir === directions.down && nextDir === directions.right)) {
|
|
1119
|
+
corner = "└";
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
d[drawingCoord.x][drawingCoord.y] = corner;
|
|
1123
|
+
}
|
|
1124
|
+
return d;
|
|
1125
|
+
}
|
|
1126
|
+
drawArrowLabel(edge) {
|
|
1127
|
+
if (edge.text.length === 0 || edge.labelLine.length === 0)
|
|
1128
|
+
return null;
|
|
1129
|
+
const d = copyCanvas(this.drawing);
|
|
1130
|
+
dDrawTextOnLine(d, this.lineToDrawing(edge.labelLine), edge.text);
|
|
1131
|
+
return d;
|
|
1132
|
+
}
|
|
1133
|
+
drawSubgraphs() {
|
|
1134
|
+
const sorted = this.sortSubgraphsByDepth();
|
|
1135
|
+
for (const sg of sorted) {
|
|
1136
|
+
const sgDrawing = drawSubgraph(sg, this);
|
|
1137
|
+
const offset = { x: sg.minX, y: sg.minY };
|
|
1138
|
+
this.drawing = this.mergeDrawings(this.drawing, offset, sgDrawing);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
drawSubgraphLabels() {
|
|
1142
|
+
for (const sg of this.subgraphs) {
|
|
1143
|
+
if (sg.nodes.length === 0)
|
|
1144
|
+
continue;
|
|
1145
|
+
const { drawing, offset } = drawSubgraphLabel(sg, this);
|
|
1146
|
+
this.drawing = this.mergeDrawings(this.drawing, offset, drawing);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
sortSubgraphsByDepth() {
|
|
1150
|
+
const depths = new Map();
|
|
1151
|
+
for (const sg of this.subgraphs) {
|
|
1152
|
+
depths.set(sg, this.getSubgraphDepth(sg));
|
|
1153
|
+
}
|
|
1154
|
+
const sorted = [...this.subgraphs];
|
|
1155
|
+
sorted.sort((a, b) => (depths.get(a) ?? 0) - (depths.get(b) ?? 0));
|
|
1156
|
+
return sorted;
|
|
1157
|
+
}
|
|
1158
|
+
getSubgraphDepth(sg) {
|
|
1159
|
+
if (!sg.parent)
|
|
1160
|
+
return 0;
|
|
1161
|
+
return 1 + this.getSubgraphDepth(sg.parent);
|
|
1162
|
+
}
|
|
1163
|
+
getChildren(node) {
|
|
1164
|
+
const children = [];
|
|
1165
|
+
for (const edge of this.edges) {
|
|
1166
|
+
if (edge.from.name === node.name) {
|
|
1167
|
+
children.push(edge.to);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
return children;
|
|
1171
|
+
}
|
|
1172
|
+
gridToDrawingCoord(coord) {
|
|
1173
|
+
let x = 0;
|
|
1174
|
+
let y = 0;
|
|
1175
|
+
for (let column = 0; column < coord.x; column += 1) {
|
|
1176
|
+
x += this.getColumnWidth(column);
|
|
1177
|
+
}
|
|
1178
|
+
for (let row = 0; row < coord.y; row += 1) {
|
|
1179
|
+
y += this.getRowHeight(row);
|
|
1180
|
+
}
|
|
1181
|
+
return {
|
|
1182
|
+
x: x + Math.floor(this.getColumnWidth(coord.x) / 2) + this.offsetX,
|
|
1183
|
+
y: y + Math.floor(this.getRowHeight(coord.y) / 2) + this.offsetY
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
setDrawingSizeToGridConstraints() {
|
|
1187
|
+
let maxX = 0;
|
|
1188
|
+
let maxY = 0;
|
|
1189
|
+
for (const width of this.columnWidth.values()) {
|
|
1190
|
+
maxX += width;
|
|
1191
|
+
}
|
|
1192
|
+
for (const height of this.rowHeight.values()) {
|
|
1193
|
+
maxY += height;
|
|
1194
|
+
}
|
|
1195
|
+
this.drawing = ensureDrawingSize(this.drawing, maxX - 1, maxY - 1);
|
|
1196
|
+
}
|
|
1197
|
+
calculateSubgraphBoundingBoxes() {
|
|
1198
|
+
for (const sg of this.subgraphs) {
|
|
1199
|
+
this.calculateSubgraphBoundingBox(sg);
|
|
1200
|
+
}
|
|
1201
|
+
this.ensureSubgraphSpacing();
|
|
1202
|
+
}
|
|
1203
|
+
calculateSubgraphBoundingBox(sg) {
|
|
1204
|
+
if (sg.nodes.length === 0)
|
|
1205
|
+
return;
|
|
1206
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
1207
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
1208
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
1209
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
1210
|
+
for (const child of sg.children) {
|
|
1211
|
+
this.calculateSubgraphBoundingBox(child);
|
|
1212
|
+
if (child.nodes.length > 0) {
|
|
1213
|
+
minX = min(minX, child.minX);
|
|
1214
|
+
minY = min(minY, child.minY);
|
|
1215
|
+
maxX = max(maxX, child.maxX);
|
|
1216
|
+
maxY = max(maxY, child.maxY);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
for (const node of sg.nodes) {
|
|
1220
|
+
if (!node.drawingCoord || !node.drawing)
|
|
1221
|
+
continue;
|
|
1222
|
+
const nodeMinX = node.drawingCoord.x;
|
|
1223
|
+
const nodeMinY = node.drawingCoord.y;
|
|
1224
|
+
const nodeMaxX = nodeMinX + node.drawing.length - 1;
|
|
1225
|
+
const nodeMaxY = nodeMinY + node.drawing[0].length - 1;
|
|
1226
|
+
minX = min(minX, nodeMinX);
|
|
1227
|
+
minY = min(minY, nodeMinY);
|
|
1228
|
+
maxX = max(maxX, nodeMaxX);
|
|
1229
|
+
maxY = max(maxY, nodeMaxY);
|
|
1230
|
+
}
|
|
1231
|
+
const subgraphPadding = 2;
|
|
1232
|
+
const subgraphLabelSpace = 2;
|
|
1233
|
+
sg.minX = minX - subgraphPadding;
|
|
1234
|
+
sg.minY = minY - subgraphPadding - subgraphLabelSpace;
|
|
1235
|
+
sg.maxX = maxX + subgraphPadding;
|
|
1236
|
+
sg.maxY = maxY + subgraphPadding;
|
|
1237
|
+
}
|
|
1238
|
+
ensureSubgraphSpacing() {
|
|
1239
|
+
const minSpacing = 1;
|
|
1240
|
+
const rootSubgraphs = this.subgraphs.filter((sg) => !sg.parent && sg.nodes.length > 0);
|
|
1241
|
+
for (let i = 0; i < rootSubgraphs.length; i += 1) {
|
|
1242
|
+
for (let j = i + 1; j < rootSubgraphs.length; j += 1) {
|
|
1243
|
+
const sg1 = rootSubgraphs[i];
|
|
1244
|
+
const sg2 = rootSubgraphs[j];
|
|
1245
|
+
if (sg1.minX < sg2.maxX && sg1.maxX > sg2.minX) {
|
|
1246
|
+
if (sg1.maxY >= sg2.minY - minSpacing && sg1.minY < sg2.minY) {
|
|
1247
|
+
sg2.minY = sg1.maxY + minSpacing + 1;
|
|
1248
|
+
}
|
|
1249
|
+
else if (sg2.maxY >= sg1.minY - minSpacing && sg2.minY < sg1.minY) {
|
|
1250
|
+
sg1.minY = sg2.maxY + minSpacing + 1;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
if (sg1.minY < sg2.maxY && sg1.maxY > sg2.minY) {
|
|
1254
|
+
if (sg1.maxX >= sg2.minX - minSpacing && sg1.minX < sg2.minX) {
|
|
1255
|
+
sg2.minX = sg1.maxX + minSpacing + 1;
|
|
1256
|
+
}
|
|
1257
|
+
else if (sg2.maxX >= sg1.minX - minSpacing && sg2.minX < sg1.minX) {
|
|
1258
|
+
sg1.minX = sg2.maxX + minSpacing + 1;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
offsetDrawingForSubgraphs() {
|
|
1265
|
+
if (this.subgraphs.length === 0)
|
|
1266
|
+
return;
|
|
1267
|
+
let minX = 0;
|
|
1268
|
+
let minY = 0;
|
|
1269
|
+
for (const sg of this.subgraphs) {
|
|
1270
|
+
minX = min(minX, sg.minX);
|
|
1271
|
+
minY = min(minY, sg.minY);
|
|
1272
|
+
}
|
|
1273
|
+
const offsetX = -minX;
|
|
1274
|
+
const offsetY = -minY;
|
|
1275
|
+
if (offsetX === 0 && offsetY === 0)
|
|
1276
|
+
return;
|
|
1277
|
+
this.offsetX = offsetX;
|
|
1278
|
+
this.offsetY = offsetY;
|
|
1279
|
+
for (const sg of this.subgraphs) {
|
|
1280
|
+
sg.minX += offsetX;
|
|
1281
|
+
sg.minY += offsetY;
|
|
1282
|
+
sg.maxX += offsetX;
|
|
1283
|
+
sg.maxY += offsetY;
|
|
1284
|
+
}
|
|
1285
|
+
for (const node of this.nodes) {
|
|
1286
|
+
if (node.drawingCoord) {
|
|
1287
|
+
node.drawingCoord.x += offsetX;
|
|
1288
|
+
node.drawingCoord.y += offsetY;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
hasIncomingEdgeFromOutsideSubgraph(node) {
|
|
1293
|
+
const nodeSubgraph = this.getNodeSubgraph(node);
|
|
1294
|
+
if (!nodeSubgraph)
|
|
1295
|
+
return false;
|
|
1296
|
+
let hasExternal = false;
|
|
1297
|
+
for (const edge of this.edges) {
|
|
1298
|
+
if (edge.to === node) {
|
|
1299
|
+
const sourceSubgraph = this.getNodeSubgraph(edge.from);
|
|
1300
|
+
if (sourceSubgraph !== nodeSubgraph) {
|
|
1301
|
+
hasExternal = true;
|
|
1302
|
+
break;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
if (!hasExternal)
|
|
1307
|
+
return false;
|
|
1308
|
+
for (const other of nodeSubgraph.nodes) {
|
|
1309
|
+
if (other === node || !other.gridCoord)
|
|
1310
|
+
continue;
|
|
1311
|
+
let otherHasExternal = false;
|
|
1312
|
+
for (const edge of this.edges) {
|
|
1313
|
+
if (edge.to === other) {
|
|
1314
|
+
const sourceSubgraph = this.getNodeSubgraph(edge.from);
|
|
1315
|
+
if (sourceSubgraph !== nodeSubgraph) {
|
|
1316
|
+
otherHasExternal = true;
|
|
1317
|
+
break;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
if (otherHasExternal && other.gridCoord.y < (node.gridCoord?.y ?? 0)) {
|
|
1322
|
+
return false;
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
return true;
|
|
1326
|
+
}
|
|
1327
|
+
getNodeSubgraph(node) {
|
|
1328
|
+
for (const sg of this.subgraphs) {
|
|
1329
|
+
if (sg.nodes.includes(node))
|
|
1330
|
+
return sg;
|
|
1331
|
+
}
|
|
1332
|
+
return undefined;
|
|
1333
|
+
}
|
|
1334
|
+
isNodeInAnySubgraph(node) {
|
|
1335
|
+
return this.getNodeSubgraph(node) !== undefined;
|
|
1336
|
+
}
|
|
1337
|
+
lineToDrawing(line) {
|
|
1338
|
+
return line.map((coord) => this.gridToDrawingCoord(coord));
|
|
1339
|
+
}
|
|
1340
|
+
mergeDrawings(base, mergeCoord, ...drawings) {
|
|
1341
|
+
let maxX = base.length - 1;
|
|
1342
|
+
let maxY = base[0]?.length ? base[0].length - 1 : 0;
|
|
1343
|
+
for (const d of drawings) {
|
|
1344
|
+
maxX = max(maxX, d.length - 1 + mergeCoord.x);
|
|
1345
|
+
maxY = max(maxY, (d[0]?.length ?? 1) - 1 + mergeCoord.y);
|
|
1346
|
+
}
|
|
1347
|
+
const merged = mkDrawing(maxX, maxY);
|
|
1348
|
+
for (let x = 0; x <= maxX; x += 1) {
|
|
1349
|
+
for (let y = 0; y <= maxY; y += 1) {
|
|
1350
|
+
if (x < base.length && y < base[0].length) {
|
|
1351
|
+
merged[x][y] = base[x][y];
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
for (const d of drawings) {
|
|
1356
|
+
for (let x = 0; x < d.length; x += 1) {
|
|
1357
|
+
for (let y = 0; y < d[0].length; y += 1) {
|
|
1358
|
+
const char = d[x][y];
|
|
1359
|
+
if (char !== " ") {
|
|
1360
|
+
const current = merged[x + mergeCoord.x][y + mergeCoord.y];
|
|
1361
|
+
if (!this.useAscii && junctionChars.has(char) && junctionChars.has(current)) {
|
|
1362
|
+
merged[x + mergeCoord.x][y + mergeCoord.y] = mergeJunctions(current, char);
|
|
1363
|
+
}
|
|
1364
|
+
else {
|
|
1365
|
+
merged[x + mergeCoord.x][y + mergeCoord.y] = char;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
return merged;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
class MinHeap {
|
|
1375
|
+
items = [];
|
|
1376
|
+
size() {
|
|
1377
|
+
return this.items.length;
|
|
1378
|
+
}
|
|
1379
|
+
push(item) {
|
|
1380
|
+
this.items.push(item);
|
|
1381
|
+
this.bubbleUp(this.items.length - 1);
|
|
1382
|
+
}
|
|
1383
|
+
pop() {
|
|
1384
|
+
if (this.items.length === 0)
|
|
1385
|
+
return undefined;
|
|
1386
|
+
const top = this.items[0];
|
|
1387
|
+
const end = this.items.pop();
|
|
1388
|
+
if (this.items.length > 0 && end) {
|
|
1389
|
+
this.items[0] = end;
|
|
1390
|
+
this.bubbleDown(0);
|
|
1391
|
+
}
|
|
1392
|
+
return top;
|
|
1393
|
+
}
|
|
1394
|
+
bubbleUp(index) {
|
|
1395
|
+
while (index > 0) {
|
|
1396
|
+
const parent = Math.floor((index - 1) / 2);
|
|
1397
|
+
if (this.items[parent].priority <= this.items[index].priority)
|
|
1398
|
+
break;
|
|
1399
|
+
[this.items[parent], this.items[index]] = [this.items[index], this.items[parent]];
|
|
1400
|
+
index = parent;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
bubbleDown(index) {
|
|
1404
|
+
const length = this.items.length;
|
|
1405
|
+
while (true) {
|
|
1406
|
+
let left = 2 * index + 1;
|
|
1407
|
+
let right = left + 1;
|
|
1408
|
+
let smallest = index;
|
|
1409
|
+
if (left < length && this.items[left].priority < this.items[smallest].priority) {
|
|
1410
|
+
smallest = left;
|
|
1411
|
+
}
|
|
1412
|
+
if (right < length && this.items[right].priority < this.items[smallest].priority) {
|
|
1413
|
+
smallest = right;
|
|
1414
|
+
}
|
|
1415
|
+
if (smallest === index)
|
|
1416
|
+
break;
|
|
1417
|
+
[this.items[index], this.items[smallest]] = [this.items[smallest], this.items[index]];
|
|
1418
|
+
index = smallest;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
function heuristic(a, b) {
|
|
1423
|
+
const absX = abs(a.x - b.x);
|
|
1424
|
+
const absY = abs(a.y - b.y);
|
|
1425
|
+
if (absX === 0 || absY === 0) {
|
|
1426
|
+
return absX + absY;
|
|
1427
|
+
}
|
|
1428
|
+
return absX + absY + 1;
|
|
1429
|
+
}
|
|
1430
|
+
function determineStartAndEndDir(edge, graphDirection) {
|
|
1431
|
+
if (edge.from === edge.to) {
|
|
1432
|
+
if (graphDirection === "LR") {
|
|
1433
|
+
return [directions.right, directions.down, directions.down, directions.right];
|
|
1434
|
+
}
|
|
1435
|
+
return [directions.down, directions.right, directions.right, directions.down];
|
|
1436
|
+
}
|
|
1437
|
+
const d = determineDirection(edge.from.gridCoord, edge.to.gridCoord);
|
|
1438
|
+
let preferredDir = d;
|
|
1439
|
+
let preferredOpposite = directionOpposite(preferredDir);
|
|
1440
|
+
let alternativeDir = d;
|
|
1441
|
+
let alternativeOpposite = preferredOpposite;
|
|
1442
|
+
const isBackwards = graphDirection === "LR"
|
|
1443
|
+
? d === directions.left || d === directions.upperLeft || d === directions.lowerLeft
|
|
1444
|
+
: d === directions.up || d === directions.upperLeft || d === directions.upperRight;
|
|
1445
|
+
switch (d) {
|
|
1446
|
+
case directions.lowerRight:
|
|
1447
|
+
if (graphDirection === "LR") {
|
|
1448
|
+
preferredDir = directions.down;
|
|
1449
|
+
preferredOpposite = directions.left;
|
|
1450
|
+
alternativeDir = directions.right;
|
|
1451
|
+
alternativeOpposite = directions.up;
|
|
1452
|
+
}
|
|
1453
|
+
else {
|
|
1454
|
+
preferredDir = directions.right;
|
|
1455
|
+
preferredOpposite = directions.up;
|
|
1456
|
+
alternativeDir = directions.down;
|
|
1457
|
+
alternativeOpposite = directions.left;
|
|
1458
|
+
}
|
|
1459
|
+
break;
|
|
1460
|
+
case directions.upperRight:
|
|
1461
|
+
if (graphDirection === "LR") {
|
|
1462
|
+
preferredDir = directions.up;
|
|
1463
|
+
preferredOpposite = directions.left;
|
|
1464
|
+
alternativeDir = directions.right;
|
|
1465
|
+
alternativeOpposite = directions.down;
|
|
1466
|
+
}
|
|
1467
|
+
else {
|
|
1468
|
+
preferredDir = directions.right;
|
|
1469
|
+
preferredOpposite = directions.down;
|
|
1470
|
+
alternativeDir = directions.up;
|
|
1471
|
+
alternativeOpposite = directions.left;
|
|
1472
|
+
}
|
|
1473
|
+
break;
|
|
1474
|
+
case directions.lowerLeft:
|
|
1475
|
+
if (graphDirection === "LR") {
|
|
1476
|
+
preferredDir = directions.down;
|
|
1477
|
+
preferredOpposite = directions.down;
|
|
1478
|
+
alternativeDir = directions.left;
|
|
1479
|
+
alternativeOpposite = directions.up;
|
|
1480
|
+
}
|
|
1481
|
+
else {
|
|
1482
|
+
preferredDir = directions.left;
|
|
1483
|
+
preferredOpposite = directions.up;
|
|
1484
|
+
alternativeDir = directions.down;
|
|
1485
|
+
alternativeOpposite = directions.right;
|
|
1486
|
+
}
|
|
1487
|
+
break;
|
|
1488
|
+
case directions.upperLeft:
|
|
1489
|
+
if (graphDirection === "LR") {
|
|
1490
|
+
preferredDir = directions.down;
|
|
1491
|
+
preferredOpposite = directions.down;
|
|
1492
|
+
alternativeDir = directions.left;
|
|
1493
|
+
alternativeOpposite = directions.down;
|
|
1494
|
+
}
|
|
1495
|
+
else {
|
|
1496
|
+
preferredDir = directions.right;
|
|
1497
|
+
preferredOpposite = directions.right;
|
|
1498
|
+
alternativeDir = directions.up;
|
|
1499
|
+
alternativeOpposite = directions.right;
|
|
1500
|
+
}
|
|
1501
|
+
break;
|
|
1502
|
+
default:
|
|
1503
|
+
if (isBackwards) {
|
|
1504
|
+
if (graphDirection === "LR" && d === directions.left) {
|
|
1505
|
+
preferredDir = directions.down;
|
|
1506
|
+
preferredOpposite = directions.down;
|
|
1507
|
+
alternativeDir = directions.left;
|
|
1508
|
+
alternativeOpposite = directions.right;
|
|
1509
|
+
}
|
|
1510
|
+
else if (graphDirection === "TD" && d === directions.up) {
|
|
1511
|
+
preferredDir = directions.right;
|
|
1512
|
+
preferredOpposite = directions.right;
|
|
1513
|
+
alternativeDir = directions.up;
|
|
1514
|
+
alternativeOpposite = directions.down;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
return [preferredDir, preferredOpposite, alternativeDir, alternativeOpposite];
|
|
1519
|
+
}
|
|
1520
|
+
function mergePath(path) {
|
|
1521
|
+
if (path.length <= 2)
|
|
1522
|
+
return path;
|
|
1523
|
+
const toRemove = new Set();
|
|
1524
|
+
let step0 = path[0];
|
|
1525
|
+
let step1 = path[1];
|
|
1526
|
+
for (let idx = 0; idx < path.slice(2).length; idx += 1) {
|
|
1527
|
+
const step2 = path[idx + 2];
|
|
1528
|
+
const prevDir = determineDirection(step0, step1);
|
|
1529
|
+
const dir = determineDirection(step1, step2);
|
|
1530
|
+
if (prevDir === dir) {
|
|
1531
|
+
toRemove.add(idx + 1);
|
|
1532
|
+
}
|
|
1533
|
+
step0 = step1;
|
|
1534
|
+
step1 = step2;
|
|
1535
|
+
}
|
|
1536
|
+
return path.filter((_, idx) => !toRemove.has(idx));
|
|
1537
|
+
}
|
|
1538
|
+
function mergeJunctions(c1, c2) {
|
|
1539
|
+
const map = {
|
|
1540
|
+
"─": { "│": "┼", "┌": "┬", "┐": "┬", "└": "┴", "┘": "┴", "├": "┼", "┤": "┼", "┬": "┬", "┴": "┴" },
|
|
1541
|
+
"│": { "─": "┼", "┌": "├", "┐": "┤", "└": "├", "┘": "┤", "├": "├", "┤": "┤", "┬": "┼", "┴": "┼" },
|
|
1542
|
+
"┌": { "─": "┬", "│": "├", "┐": "┬", "└": "├", "┘": "┼", "├": "├", "┤": "┼", "┬": "┬", "┴": "┼" },
|
|
1543
|
+
"┐": { "─": "┬", "│": "┤", "┌": "┬", "└": "┼", "┘": "┤", "├": "┼", "┤": "┤", "┬": "┬", "┴": "┼" },
|
|
1544
|
+
"└": { "─": "┴", "│": "├", "┌": "├", "┐": "┼", "┘": "┴", "├": "├", "┤": "┼", "┬": "┼", "┴": "┴" },
|
|
1545
|
+
"┘": { "─": "┴", "│": "┤", "┌": "┼", "┐": "┤", "└": "┴", "├": "┼", "┤": "┤", "┬": "┼", "┴": "┴" },
|
|
1546
|
+
"├": { "─": "┼", "│": "├", "┌": "├", "┐": "┼", "└": "├", "┘": "┼", "┤": "┼", "┬": "┼", "┴": "┼" },
|
|
1547
|
+
"┤": { "─": "┼", "│": "┤", "┌": "┼", "┐": "┤", "└": "┼", "┘": "┤", "├": "┼", "┬": "┼", "┴": "┼" },
|
|
1548
|
+
"┬": { "─": "┬", "│": "┼", "┌": "┬", "┐": "┬", "└": "┼", "┘": "┼", "├": "┼", "┤": "┼", "┴": "┼" },
|
|
1549
|
+
"┴": { "─": "┴", "│": "┼", "┌": "┼", "┐": "┼", "└": "┴", "┘": "┴", "├": "┼", "┤": "┼", "┬": "┼" }
|
|
1550
|
+
};
|
|
1551
|
+
return map[c1]?.[c2] ?? c1;
|
|
1552
|
+
}
|
|
1553
|
+
function mkDrawing(x, y) {
|
|
1554
|
+
const drawing = [];
|
|
1555
|
+
for (let i = 0; i <= x; i += 1) {
|
|
1556
|
+
drawing[i] = [];
|
|
1557
|
+
for (let j = 0; j <= y; j += 1) {
|
|
1558
|
+
drawing[i][j] = " ";
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
return drawing;
|
|
1562
|
+
}
|
|
1563
|
+
function ensureSize(d, x, y) {
|
|
1564
|
+
while (d.length <= x) {
|
|
1565
|
+
d.push([]);
|
|
1566
|
+
}
|
|
1567
|
+
for (let i = 0; i < d.length; i += 1) {
|
|
1568
|
+
if (!d[i])
|
|
1569
|
+
d[i] = [];
|
|
1570
|
+
while (d[i].length <= y) {
|
|
1571
|
+
d[i].push(" ");
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
function ensureDrawingSize(d, x, y) {
|
|
1576
|
+
const width = max(x, d.length - 1);
|
|
1577
|
+
const height = max(y, d[0]?.length ? d[0].length - 1 : 0);
|
|
1578
|
+
const resized = mkDrawing(width, height);
|
|
1579
|
+
for (let i = 0; i < d.length; i += 1) {
|
|
1580
|
+
for (let j = 0; j < d[0].length; j += 1) {
|
|
1581
|
+
resized[i][j] = d[i][j];
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
return resized;
|
|
1585
|
+
}
|
|
1586
|
+
function copyCanvas(d) {
|
|
1587
|
+
return mkDrawing(d.length - 1, d[0].length - 1);
|
|
1588
|
+
}
|
|
1589
|
+
function drawingToString(d) {
|
|
1590
|
+
const maxX = d.length - 1;
|
|
1591
|
+
const maxY = d[0].length - 1;
|
|
1592
|
+
let result = "";
|
|
1593
|
+
for (let y = 0; y <= maxY; y += 1) {
|
|
1594
|
+
for (let x = 0; x <= maxX; x += 1) {
|
|
1595
|
+
result += d[x][y];
|
|
1596
|
+
}
|
|
1597
|
+
if (y !== maxY)
|
|
1598
|
+
result += "\n";
|
|
1599
|
+
}
|
|
1600
|
+
return result;
|
|
1601
|
+
}
|
|
1602
|
+
function drawBox(node, graph) {
|
|
1603
|
+
if (!node.gridCoord)
|
|
1604
|
+
return mkDrawing(0, 0);
|
|
1605
|
+
let w = 0;
|
|
1606
|
+
let h = 0;
|
|
1607
|
+
for (let i = 0; i < 2; i += 1) {
|
|
1608
|
+
w += graph.getColumnWidth(node.gridCoord.x + i);
|
|
1609
|
+
h += graph.getRowHeight(node.gridCoord.y + i);
|
|
1610
|
+
}
|
|
1611
|
+
const from = { x: 0, y: 0 };
|
|
1612
|
+
const to = { x: w, y: h };
|
|
1613
|
+
const box = mkDrawing(max(from.x, to.x), max(from.y, to.y));
|
|
1614
|
+
const horizontal = graph.useAscii ? "-" : "─";
|
|
1615
|
+
const vertical = graph.useAscii ? "|" : "│";
|
|
1616
|
+
const topLeft = graph.useAscii ? "+" : "┌";
|
|
1617
|
+
const topRight = graph.useAscii ? "+" : "┐";
|
|
1618
|
+
const bottomLeft = graph.useAscii ? "+" : "└";
|
|
1619
|
+
const bottomRight = graph.useAscii ? "+" : "┘";
|
|
1620
|
+
for (let x = from.x + 1; x < to.x; x += 1) {
|
|
1621
|
+
box[x][from.y] = horizontal;
|
|
1622
|
+
box[x][to.y] = horizontal;
|
|
1623
|
+
}
|
|
1624
|
+
for (let y = from.y + 1; y < to.y; y += 1) {
|
|
1625
|
+
box[from.x][y] = vertical;
|
|
1626
|
+
box[to.x][y] = vertical;
|
|
1627
|
+
}
|
|
1628
|
+
box[from.x][from.y] = topLeft;
|
|
1629
|
+
box[to.x][from.y] = topRight;
|
|
1630
|
+
box[from.x][to.y] = bottomLeft;
|
|
1631
|
+
box[to.x][to.y] = bottomRight;
|
|
1632
|
+
const textY = from.y + Math.floor(h / 2);
|
|
1633
|
+
const label = node.name;
|
|
1634
|
+
const labelWidth = stringWidth(label);
|
|
1635
|
+
const textX = from.x + Math.floor(w / 2) - ceilDiv(labelWidth, 2) + 1;
|
|
1636
|
+
const chars = Array.from(label);
|
|
1637
|
+
for (let i = 0; i < chars.length; i += 1) {
|
|
1638
|
+
box[textX + i][textY] = wrapTextInColor(chars[i], node.styleClass.styles["color"], graph.styleType);
|
|
1639
|
+
}
|
|
1640
|
+
return box;
|
|
1641
|
+
}
|
|
1642
|
+
function drawSubgraph(sg, graph) {
|
|
1643
|
+
const width = sg.maxX - sg.minX;
|
|
1644
|
+
const height = sg.maxY - sg.minY;
|
|
1645
|
+
if (width <= 0 || height <= 0)
|
|
1646
|
+
return mkDrawing(0, 0);
|
|
1647
|
+
const from = { x: 0, y: 0 };
|
|
1648
|
+
const to = { x: width, y: height };
|
|
1649
|
+
const subgraph = mkDrawing(width, height);
|
|
1650
|
+
const horizontal = graph.useAscii ? "-" : "─";
|
|
1651
|
+
const vertical = graph.useAscii ? "|" : "│";
|
|
1652
|
+
const topLeft = graph.useAscii ? "+" : "┌";
|
|
1653
|
+
const topRight = graph.useAscii ? "+" : "┐";
|
|
1654
|
+
const bottomLeft = graph.useAscii ? "+" : "└";
|
|
1655
|
+
const bottomRight = graph.useAscii ? "+" : "┘";
|
|
1656
|
+
for (let x = from.x + 1; x < to.x; x += 1) {
|
|
1657
|
+
subgraph[x][from.y] = horizontal;
|
|
1658
|
+
subgraph[x][to.y] = horizontal;
|
|
1659
|
+
}
|
|
1660
|
+
for (let y = from.y + 1; y < to.y; y += 1) {
|
|
1661
|
+
subgraph[from.x][y] = vertical;
|
|
1662
|
+
subgraph[to.x][y] = vertical;
|
|
1663
|
+
}
|
|
1664
|
+
subgraph[from.x][from.y] = topLeft;
|
|
1665
|
+
subgraph[to.x][from.y] = topRight;
|
|
1666
|
+
subgraph[from.x][to.y] = bottomLeft;
|
|
1667
|
+
subgraph[to.x][to.y] = bottomRight;
|
|
1668
|
+
return subgraph;
|
|
1669
|
+
}
|
|
1670
|
+
function drawSubgraphLabel(sg, graph) {
|
|
1671
|
+
const width = sg.maxX - sg.minX;
|
|
1672
|
+
const height = sg.maxY - sg.minY;
|
|
1673
|
+
if (width <= 0 || height <= 0) {
|
|
1674
|
+
return { drawing: mkDrawing(0, 0), offset: { x: 0, y: 0 } };
|
|
1675
|
+
}
|
|
1676
|
+
const drawing = mkDrawing(width, height);
|
|
1677
|
+
const labelY = 1;
|
|
1678
|
+
let labelX = Math.floor(width / 2) - Math.floor(stringWidth(sg.name) / 2);
|
|
1679
|
+
if (labelX < 1)
|
|
1680
|
+
labelX = 1;
|
|
1681
|
+
const chars = Array.from(sg.name);
|
|
1682
|
+
for (let i = 0; i < chars.length; i += 1) {
|
|
1683
|
+
if (labelX + i < width) {
|
|
1684
|
+
drawing[labelX + i][labelY] = chars[i];
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
return { drawing, offset: { x: sg.minX, y: sg.minY } };
|
|
1688
|
+
}
|
|
1689
|
+
function wrapTextInColor(text, color, styleType) {
|
|
1690
|
+
if (!color)
|
|
1691
|
+
return text;
|
|
1692
|
+
if (styleType === "html") {
|
|
1693
|
+
return `<span style='color: ${color}'>${text}</span>`;
|
|
1694
|
+
}
|
|
1695
|
+
const rgb = parseHexColor(color);
|
|
1696
|
+
if (!rgb)
|
|
1697
|
+
return text;
|
|
1698
|
+
return `\u001b[38;2;${rgb.r};${rgb.g};${rgb.b}m${text}\u001b[0m`;
|
|
1699
|
+
}
|
|
1700
|
+
function parseHexColor(input) {
|
|
1701
|
+
const trimmed = input.trim();
|
|
1702
|
+
if (!trimmed.startsWith("#"))
|
|
1703
|
+
return null;
|
|
1704
|
+
const hex = trimmed.slice(1);
|
|
1705
|
+
if (hex.length === 3) {
|
|
1706
|
+
const r = Number.parseInt(hex[0] + hex[0], 16);
|
|
1707
|
+
const g = Number.parseInt(hex[1] + hex[1], 16);
|
|
1708
|
+
const b = Number.parseInt(hex[2] + hex[2], 16);
|
|
1709
|
+
if ([r, g, b].some((v) => Number.isNaN(v)))
|
|
1710
|
+
return null;
|
|
1711
|
+
return { r, g, b };
|
|
1712
|
+
}
|
|
1713
|
+
if (hex.length === 6) {
|
|
1714
|
+
const r = Number.parseInt(hex.slice(0, 2), 16);
|
|
1715
|
+
const g = Number.parseInt(hex.slice(2, 4), 16);
|
|
1716
|
+
const b = Number.parseInt(hex.slice(4, 6), 16);
|
|
1717
|
+
if ([r, g, b].some((v) => Number.isNaN(v)))
|
|
1718
|
+
return null;
|
|
1719
|
+
return { r, g, b };
|
|
1720
|
+
}
|
|
1721
|
+
return null;
|
|
1722
|
+
}
|
|
1723
|
+
function dDrawTextOnLine(d, line, label) {
|
|
1724
|
+
if (line.length < 2)
|
|
1725
|
+
return;
|
|
1726
|
+
let minX = Math.min(line[0].x, line[1].x);
|
|
1727
|
+
let maxX = Math.max(line[0].x, line[1].x);
|
|
1728
|
+
let minY = Math.min(line[0].y, line[1].y);
|
|
1729
|
+
let maxY = Math.max(line[0].y, line[1].y);
|
|
1730
|
+
const middleX = minX + Math.floor((maxX - minX) / 2);
|
|
1731
|
+
const middleY = minY + Math.floor((maxY - minY) / 2);
|
|
1732
|
+
const start = { x: middleX - Math.floor(stringWidth(label) / 2), y: middleY };
|
|
1733
|
+
drawText(d, start, label);
|
|
1734
|
+
}
|
|
1735
|
+
function drawText(d, start, text) {
|
|
1736
|
+
ensureSize(d, start.x + stringWidth(text), start.y);
|
|
1737
|
+
const chars = Array.from(text);
|
|
1738
|
+
for (let i = 0; i < chars.length; i += 1) {
|
|
1739
|
+
d[start.x + i][start.y] = chars[i];
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
function drawingWithCoordsWrapper(d) {
|
|
1743
|
+
const maxX = d.length - 1;
|
|
1744
|
+
const maxY = d[0].length - 1;
|
|
1745
|
+
const debug = mkDrawing(maxX + 2, maxY + 1);
|
|
1746
|
+
for (let x = 0; x <= maxX; x += 1) {
|
|
1747
|
+
debug[x + 2][0] = String(x % 10);
|
|
1748
|
+
}
|
|
1749
|
+
for (let y = 0; y <= maxY; y += 1) {
|
|
1750
|
+
debug[0][y + 1] = String(y % 10).padStart(2, " ");
|
|
1751
|
+
}
|
|
1752
|
+
for (let x = 0; x < debug.length; x += 1) {
|
|
1753
|
+
for (let y = 0; y < debug[0].length; y += 1) {
|
|
1754
|
+
if (x >= 2 && y >= 1 && x - 2 < d.length && y - 1 < d[0].length) {
|
|
1755
|
+
debug[x][y] = d[x - 2][y - 1];
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
return debug;
|
|
1760
|
+
}
|
|
1761
|
+
function drawingCoordWrapper(d, graph) {
|
|
1762
|
+
const maxX = d.length - 1;
|
|
1763
|
+
const maxY = d[0].length - 1;
|
|
1764
|
+
const debug = mkDrawing(maxX + 2, maxY + 1);
|
|
1765
|
+
let currX = 3;
|
|
1766
|
+
for (let x = 0; currX <= maxX + graph.getColumnWidth(x); x += 1) {
|
|
1767
|
+
const w = graph.getColumnWidth(x);
|
|
1768
|
+
const debugPos = currX;
|
|
1769
|
+
debug[debugPos][0] = String(x % 10);
|
|
1770
|
+
currX += w;
|
|
1771
|
+
}
|
|
1772
|
+
let currY = 2;
|
|
1773
|
+
for (let y = 0; currY <= maxY + graph.getRowHeight(y); y += 1) {
|
|
1774
|
+
const h = graph.getRowHeight(y);
|
|
1775
|
+
const debugPos = currY + Math.floor(h / 2);
|
|
1776
|
+
debug[0][debugPos] = String(y % 10);
|
|
1777
|
+
currY += h;
|
|
1778
|
+
}
|
|
1779
|
+
return graph.mergeDrawings(debug, { x: 1, y: 1 }, d);
|
|
1780
|
+
}
|
|
1781
|
+
function renderGraph(input, config) {
|
|
1782
|
+
const logger = makeLogger(config.verbose);
|
|
1783
|
+
const props = mermaidFileToMap(input, config);
|
|
1784
|
+
const graph = new Graph(props, logger);
|
|
1785
|
+
graph.createMapping();
|
|
1786
|
+
let drawing = graph.draw();
|
|
1787
|
+
if (config.showCoords) {
|
|
1788
|
+
drawing = drawingWithCoordsWrapper(drawing);
|
|
1789
|
+
drawing = drawingCoordWrapper(drawing, graph);
|
|
1790
|
+
}
|
|
1791
|
+
return drawingToString(drawing);
|
|
1792
|
+
}
|
|
1793
|
+
function parseSequence(input) {
|
|
1794
|
+
const rawLines = splitLines(input);
|
|
1795
|
+
const lines = removeComments(rawLines);
|
|
1796
|
+
if (lines.length === 0)
|
|
1797
|
+
throw new Error("no content found");
|
|
1798
|
+
if (!lines[0].trim().startsWith(sequenceDiagramKeyword)) {
|
|
1799
|
+
throw new Error(`expected ${sequenceDiagramKeyword}`);
|
|
1800
|
+
}
|
|
1801
|
+
const sd = { participants: [], messages: [], autonumber: false };
|
|
1802
|
+
const participantMap = new Map();
|
|
1803
|
+
for (let i = 1; i < lines.length; i += 1) {
|
|
1804
|
+
const trimmed = lines[i].trim();
|
|
1805
|
+
if (trimmed === "")
|
|
1806
|
+
continue;
|
|
1807
|
+
if (/^\s*autonumber\s*$/i.test(trimmed)) {
|
|
1808
|
+
sd.autonumber = true;
|
|
1809
|
+
continue;
|
|
1810
|
+
}
|
|
1811
|
+
if (parseParticipant(trimmed, sd, participantMap))
|
|
1812
|
+
continue;
|
|
1813
|
+
if (parseMessage(trimmed, sd, participantMap))
|
|
1814
|
+
continue;
|
|
1815
|
+
throw new Error(`line ${i + 1}: invalid syntax: ${trimmed}`);
|
|
1816
|
+
}
|
|
1817
|
+
if (sd.participants.length === 0) {
|
|
1818
|
+
throw new Error("no participants found");
|
|
1819
|
+
}
|
|
1820
|
+
return sd;
|
|
1821
|
+
}
|
|
1822
|
+
function parseParticipant(line, sd, participants) {
|
|
1823
|
+
const match = line.match(/^\s*participant\s+(?:"([^"]+)"|(\S+))(?:\s+as\s+(.+))?$/i);
|
|
1824
|
+
if (!match)
|
|
1825
|
+
return false;
|
|
1826
|
+
const id = match[1] || match[2];
|
|
1827
|
+
if (!id)
|
|
1828
|
+
return false;
|
|
1829
|
+
let label = match[3] || id;
|
|
1830
|
+
label = label.replace(/^"|"$/g, "");
|
|
1831
|
+
if (participants.has(id)) {
|
|
1832
|
+
throw new Error(`duplicate participant ${id}`);
|
|
1833
|
+
}
|
|
1834
|
+
const participant = { id, label, index: sd.participants.length };
|
|
1835
|
+
sd.participants.push(participant);
|
|
1836
|
+
participants.set(id, participant);
|
|
1837
|
+
return true;
|
|
1838
|
+
}
|
|
1839
|
+
function parseMessage(line, sd, participants) {
|
|
1840
|
+
const match = line.match(/^\s*(?:"([^"]+)"|([^\s\->]+))\s*(-->>|->>)\s*(?:"([^"]+)"|([^\s\->]+))\s*:\s*(.*)$/);
|
|
1841
|
+
if (!match)
|
|
1842
|
+
return false;
|
|
1843
|
+
const fromId = match[1] || match[2];
|
|
1844
|
+
const arrow = match[3];
|
|
1845
|
+
const toId = match[4] || match[5];
|
|
1846
|
+
const label = match[6]?.trim() ?? "";
|
|
1847
|
+
if (!fromId || !toId)
|
|
1848
|
+
return false;
|
|
1849
|
+
const from = getParticipant(fromId, sd, participants);
|
|
1850
|
+
const to = getParticipant(toId, sd, participants);
|
|
1851
|
+
const arrowType = arrow === solidArrowSyntax ? ArrowType.Solid : ArrowType.Dotted;
|
|
1852
|
+
const number = sd.autonumber ? sd.messages.length + 1 : 0;
|
|
1853
|
+
sd.messages.push({ from, to, label, arrowType, number });
|
|
1854
|
+
return true;
|
|
1855
|
+
}
|
|
1856
|
+
function getParticipant(id, sd, participants) {
|
|
1857
|
+
const existing = participants.get(id);
|
|
1858
|
+
if (existing)
|
|
1859
|
+
return existing;
|
|
1860
|
+
const participant = { id, label: id, index: sd.participants.length };
|
|
1861
|
+
sd.participants.push(participant);
|
|
1862
|
+
participants.set(id, participant);
|
|
1863
|
+
return participant;
|
|
1864
|
+
}
|
|
1865
|
+
function renderSequence(sd, config) {
|
|
1866
|
+
const chars = config.useAscii ? asciiChars : unicodeChars;
|
|
1867
|
+
const layout = calculateSequenceLayout(sd, config);
|
|
1868
|
+
const lines = [];
|
|
1869
|
+
lines.push(buildSequenceLine(sd.participants, layout, (i) => {
|
|
1870
|
+
return chars.topLeft + chars.horizontal.repeat(layout.participantWidths[i]) + chars.topRight;
|
|
1871
|
+
}));
|
|
1872
|
+
lines.push(buildSequenceLine(sd.participants, layout, (i) => {
|
|
1873
|
+
const w = layout.participantWidths[i];
|
|
1874
|
+
const labelLen = stringWidth(sd.participants[i].label);
|
|
1875
|
+
const pad = Math.floor((w - labelLen) / 2);
|
|
1876
|
+
return chars.vertical + " ".repeat(pad) + sd.participants[i].label + " ".repeat(w - pad - labelLen) + chars.vertical;
|
|
1877
|
+
}));
|
|
1878
|
+
lines.push(buildSequenceLine(sd.participants, layout, (i) => {
|
|
1879
|
+
const w = layout.participantWidths[i];
|
|
1880
|
+
return chars.bottomLeft + chars.horizontal.repeat(Math.floor(w / 2)) + chars.teeDown +
|
|
1881
|
+
chars.horizontal.repeat(w - Math.floor(w / 2) - 1) + chars.bottomRight;
|
|
1882
|
+
}));
|
|
1883
|
+
for (const msg of sd.messages) {
|
|
1884
|
+
for (let i = 0; i < layout.messageSpacing; i += 1) {
|
|
1885
|
+
lines.push(buildLifeline(layout, chars));
|
|
1886
|
+
}
|
|
1887
|
+
if (msg.from === msg.to) {
|
|
1888
|
+
lines.push(...renderSelfMessage(msg, layout, chars));
|
|
1889
|
+
}
|
|
1890
|
+
else {
|
|
1891
|
+
lines.push(...renderMessage(msg, layout, chars));
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
lines.push(buildLifeline(layout, chars));
|
|
1895
|
+
return lines.join("\n") + "\n";
|
|
1896
|
+
}
|
|
1897
|
+
function calculateSequenceLayout(sd, config) {
|
|
1898
|
+
const participantSpacing = config.sequenceParticipantSpacing || defaultConfig.sequenceParticipantSpacing;
|
|
1899
|
+
const widths = sd.participants.map((p) => {
|
|
1900
|
+
const w = stringWidth(p.label) + 2;
|
|
1901
|
+
return w < 3 ? 3 : w;
|
|
1902
|
+
});
|
|
1903
|
+
const centers = [];
|
|
1904
|
+
let currentX = 0;
|
|
1905
|
+
for (let i = 0; i < sd.participants.length; i += 1) {
|
|
1906
|
+
const boxWidth = widths[i] + 2;
|
|
1907
|
+
if (i === 0) {
|
|
1908
|
+
centers[i] = Math.floor(boxWidth / 2);
|
|
1909
|
+
currentX = boxWidth;
|
|
1910
|
+
}
|
|
1911
|
+
else {
|
|
1912
|
+
currentX += participantSpacing;
|
|
1913
|
+
centers[i] = currentX + Math.floor(boxWidth / 2);
|
|
1914
|
+
currentX += boxWidth;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
const last = sd.participants.length - 1;
|
|
1918
|
+
const totalWidth = centers[last] + Math.floor((widths[last] + 2) / 2);
|
|
1919
|
+
return {
|
|
1920
|
+
participantWidths: widths,
|
|
1921
|
+
participantCenters: centers,
|
|
1922
|
+
totalWidth,
|
|
1923
|
+
messageSpacing: config.sequenceMessageSpacing || defaultConfig.sequenceMessageSpacing,
|
|
1924
|
+
selfMessageWidth: config.sequenceSelfMessageWidth || defaultConfig.sequenceSelfMessageWidth
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
function buildSequenceLine(participants, layout, draw) {
|
|
1928
|
+
let line = "";
|
|
1929
|
+
for (let i = 0; i < participants.length; i += 1) {
|
|
1930
|
+
const boxWidth = layout.participantWidths[i] + 2;
|
|
1931
|
+
const left = layout.participantCenters[i] - Math.floor(boxWidth / 2);
|
|
1932
|
+
const needed = left - stringWidth(line);
|
|
1933
|
+
if (needed > 0) {
|
|
1934
|
+
line += " ".repeat(needed);
|
|
1935
|
+
}
|
|
1936
|
+
line += draw(i);
|
|
1937
|
+
}
|
|
1938
|
+
return line;
|
|
1939
|
+
}
|
|
1940
|
+
function buildLifeline(layout, chars) {
|
|
1941
|
+
const line = Array.from({ length: layout.totalWidth + 1 }, () => " ");
|
|
1942
|
+
for (const c of layout.participantCenters) {
|
|
1943
|
+
if (c < line.length)
|
|
1944
|
+
line[c] = chars.vertical;
|
|
1945
|
+
}
|
|
1946
|
+
return line.join("").replace(/\s+$/, "");
|
|
1947
|
+
}
|
|
1948
|
+
function renderMessage(msg, layout, chars) {
|
|
1949
|
+
const lines = [];
|
|
1950
|
+
const from = layout.participantCenters[msg.from.index];
|
|
1951
|
+
const to = layout.participantCenters[msg.to.index];
|
|
1952
|
+
let label = msg.label;
|
|
1953
|
+
if (msg.number > 0) {
|
|
1954
|
+
label = `${msg.number}. ${label}`;
|
|
1955
|
+
}
|
|
1956
|
+
if (label) {
|
|
1957
|
+
const start = min(from, to) + 2;
|
|
1958
|
+
const labelWidth = stringWidth(label);
|
|
1959
|
+
const w = max(layout.totalWidth, start + labelWidth) + 10;
|
|
1960
|
+
let line = buildLifeline(layout, chars);
|
|
1961
|
+
if (stringWidth(line) < w) {
|
|
1962
|
+
line = line + " ".repeat(w - stringWidth(line));
|
|
1963
|
+
}
|
|
1964
|
+
const arr = Array.from(line);
|
|
1965
|
+
let col = start;
|
|
1966
|
+
for (const r of Array.from(label)) {
|
|
1967
|
+
if (col < arr.length) {
|
|
1968
|
+
arr[col] = r;
|
|
1969
|
+
col += 1;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
lines.push(arr.join("").replace(/\s+$/, ""));
|
|
1973
|
+
}
|
|
1974
|
+
let line = Array.from(buildLifeline(layout, chars));
|
|
1975
|
+
const style = msg.arrowType === ArrowType.Dotted ? chars.dottedLine : chars.solidLine;
|
|
1976
|
+
if (from < to) {
|
|
1977
|
+
line[from] = chars.teeRight;
|
|
1978
|
+
for (let i = from + 1; i < to; i += 1) {
|
|
1979
|
+
line[i] = style;
|
|
1980
|
+
}
|
|
1981
|
+
line[to - 1] = chars.arrowRight;
|
|
1982
|
+
line[to] = chars.vertical;
|
|
1983
|
+
}
|
|
1984
|
+
else {
|
|
1985
|
+
line[to] = chars.vertical;
|
|
1986
|
+
line[to + 1] = chars.arrowLeft;
|
|
1987
|
+
for (let i = to + 2; i < from; i += 1) {
|
|
1988
|
+
line[i] = style;
|
|
1989
|
+
}
|
|
1990
|
+
line[from] = chars.teeLeft;
|
|
1991
|
+
}
|
|
1992
|
+
lines.push(line.join("").replace(/\s+$/, ""));
|
|
1993
|
+
return lines;
|
|
1994
|
+
}
|
|
1995
|
+
function renderSelfMessage(msg, layout, chars) {
|
|
1996
|
+
const lines = [];
|
|
1997
|
+
const center = layout.participantCenters[msg.from.index];
|
|
1998
|
+
const width = layout.selfMessageWidth;
|
|
1999
|
+
const ensureWidth = (line) => {
|
|
2000
|
+
const target = layout.totalWidth + width + 1;
|
|
2001
|
+
const arr = Array.from(line);
|
|
2002
|
+
if (arr.length < target) {
|
|
2003
|
+
arr.push(...Array.from({ length: target - arr.length }, () => " "));
|
|
2004
|
+
}
|
|
2005
|
+
return arr;
|
|
2006
|
+
};
|
|
2007
|
+
let label = msg.label;
|
|
2008
|
+
if (msg.number > 0) {
|
|
2009
|
+
label = `${msg.number}. ${label}`;
|
|
2010
|
+
}
|
|
2011
|
+
if (label) {
|
|
2012
|
+
let line = ensureWidth(buildLifeline(layout, chars));
|
|
2013
|
+
const start = center + 2;
|
|
2014
|
+
const labelWidth = stringWidth(label);
|
|
2015
|
+
const needed = start + labelWidth + 10;
|
|
2016
|
+
if (line.length < needed) {
|
|
2017
|
+
line.push(...Array.from({ length: needed - line.length }, () => " "));
|
|
2018
|
+
}
|
|
2019
|
+
let col = start;
|
|
2020
|
+
for (const c of Array.from(label)) {
|
|
2021
|
+
if (col < line.length) {
|
|
2022
|
+
line[col] = c;
|
|
2023
|
+
col += 1;
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
lines.push(line.join("").replace(/\s+$/, ""));
|
|
2027
|
+
}
|
|
2028
|
+
const l1 = ensureWidth(buildLifeline(layout, chars));
|
|
2029
|
+
l1[center] = chars.teeRight;
|
|
2030
|
+
for (let i = 1; i < width; i += 1) {
|
|
2031
|
+
l1[center + i] = chars.horizontal;
|
|
2032
|
+
}
|
|
2033
|
+
l1[center + width - 1] = chars.selfTopRight;
|
|
2034
|
+
lines.push(l1.join("").replace(/\s+$/, ""));
|
|
2035
|
+
const l2 = ensureWidth(buildLifeline(layout, chars));
|
|
2036
|
+
l2[center + width - 1] = chars.vertical;
|
|
2037
|
+
lines.push(l2.join("").replace(/\s+$/, ""));
|
|
2038
|
+
const l3 = ensureWidth(buildLifeline(layout, chars));
|
|
2039
|
+
l3[center] = chars.vertical;
|
|
2040
|
+
l3[center + 1] = chars.arrowLeft;
|
|
2041
|
+
for (let i = 2; i < width - 1; i += 1) {
|
|
2042
|
+
l3[center + i] = chars.horizontal;
|
|
2043
|
+
}
|
|
2044
|
+
l3[center + width - 1] = chars.selfBottom;
|
|
2045
|
+
lines.push(l3.join("").replace(/\s+$/, ""));
|
|
2046
|
+
return lines;
|
|
2047
|
+
}
|
|
2048
|
+
function stringWidth(input) {
|
|
2049
|
+
let width = 0;
|
|
2050
|
+
for (const char of Array.from(input)) {
|
|
2051
|
+
const codePoint = char.codePointAt(0) ?? 0;
|
|
2052
|
+
width += isFullwidthCodePoint(codePoint) ? 2 : 1;
|
|
2053
|
+
}
|
|
2054
|
+
return width;
|
|
2055
|
+
}
|
|
2056
|
+
function isFullwidthCodePoint(codePoint) {
|
|
2057
|
+
if (codePoint >= 0x1100 && (codePoint <= 0x115f ||
|
|
2058
|
+
codePoint === 0x2329 ||
|
|
2059
|
+
codePoint === 0x232a ||
|
|
2060
|
+
(codePoint >= 0x2e80 && codePoint <= 0x3247 && codePoint !== 0x303f) ||
|
|
2061
|
+
(codePoint >= 0x3250 && codePoint <= 0x4dbf) ||
|
|
2062
|
+
(codePoint >= 0x4e00 && codePoint <= 0xa4c6) ||
|
|
2063
|
+
(codePoint >= 0xa960 && codePoint <= 0xa97c) ||
|
|
2064
|
+
(codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
|
|
2065
|
+
(codePoint >= 0xf900 && codePoint <= 0xfaff) ||
|
|
2066
|
+
(codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
|
|
2067
|
+
(codePoint >= 0xfe30 && codePoint <= 0xfe6b) ||
|
|
2068
|
+
(codePoint >= 0xff01 && codePoint <= 0xff60) ||
|
|
2069
|
+
(codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
|
|
2070
|
+
(codePoint >= 0x1b000 && codePoint <= 0x1b001) ||
|
|
2071
|
+
(codePoint >= 0x1f200 && codePoint <= 0x1f251) ||
|
|
2072
|
+
(codePoint >= 0x20000 && codePoint <= 0x3fffd))) {
|
|
2073
|
+
return true;
|
|
2074
|
+
}
|
|
2075
|
+
return false;
|
|
2076
|
+
}
|
|
2077
|
+
export function renderAscii(input, options = {}) {
|
|
2078
|
+
if (!input || input.trim() === "")
|
|
2079
|
+
return null;
|
|
2080
|
+
let config;
|
|
2081
|
+
try {
|
|
2082
|
+
config = normalizeConfig(options);
|
|
2083
|
+
}
|
|
2084
|
+
catch (error) {
|
|
2085
|
+
return null;
|
|
2086
|
+
}
|
|
2087
|
+
try {
|
|
2088
|
+
if (isSequenceDiagram(input)) {
|
|
2089
|
+
const sd = parseSequence(input);
|
|
2090
|
+
return renderSequence(sd, config);
|
|
2091
|
+
}
|
|
2092
|
+
return renderGraph(input, config);
|
|
2093
|
+
}
|
|
2094
|
+
catch (error) {
|
|
2095
|
+
return null;
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
//# sourceMappingURL=ascii.js.map
|