@shibayama/pdgkit 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/LICENSE +35 -0
- package/README.md +709 -0
- package/assets/FONT-LICENSE.md +9 -0
- package/assets/IPA_Font_License_Agreement_v1.0.txt +117 -0
- package/assets/ipaexg.ttf +0 -0
- package/dist/core.cjs +2378 -0
- package/dist/core.d.cts +130 -0
- package/dist/core.d.ts +130 -0
- package/dist/core.js +2338 -0
- package/dist/index.cjs +3539 -0
- package/dist/index.d.cts +214 -0
- package/dist/index.d.ts +214 -0
- package/dist/index.js +3475 -0
- package/dist/pdgkit-mcp.cjs +3220 -0
- package/dist/pdgkit-mcp.d.cts +1 -0
- package/dist/pdgkit-mcp.d.ts +1 -0
- package/dist/pdgkit-mcp.js +3196 -0
- package/dist/pdgkit.cjs +3558 -0
- package/dist/pdgkit.d.cts +1 -0
- package/dist/pdgkit.d.ts +1 -0
- package/dist/pdgkit.global.js +247 -0
- package/dist/pdgkit.js +3534 -0
- package/docs/ai-authoring-guide.md +507 -0
- package/docs/spec.md +592 -0
- package/examples/01-block.pdg +14 -0
- package/examples/02-flow.pdg +14 -0
- package/examples/03-state.pdg +13 -0
- package/examples/04-seq.pdg +10 -0
- package/examples/05-system.pdg +19 -0
- package/examples/06-iot-cloud.pdg +27 -0
- package/examples/07-image-pipeline.pdg +17 -0
- package/examples/08-control-loop.pdg +20 -0
- package/examples/09-handshake.pdg +12 -0
- package/package.json +91 -0
package/dist/core.js
ADDED
|
@@ -0,0 +1,2338 @@
|
|
|
1
|
+
// src/core/parser.ts
|
|
2
|
+
var ID_PATTERN = /[A-Za-z0-9_*]+/.source;
|
|
3
|
+
var OP_TABLE = [
|
|
4
|
+
{ lit: "<->", kind: "bidir" },
|
|
5
|
+
{ lit: "=>", kind: "thick" },
|
|
6
|
+
{ lit: "->", kind: "arrow" },
|
|
7
|
+
{ lit: "<-", kind: "arrow", reverse: true },
|
|
8
|
+
{ lit: ".>", kind: "dashed-arrow" },
|
|
9
|
+
{ lit: "..", kind: "dashed" },
|
|
10
|
+
{ lit: "-", kind: "line" }
|
|
11
|
+
];
|
|
12
|
+
var DEF_RE = new RegExp(`^(${ID_PATTERN})\\s*=(?!>)\\s*(.*)$`);
|
|
13
|
+
var CONN_RE = new RegExp(
|
|
14
|
+
`^(${ID_PATTERN})\\s+(<->|=>|->|<-|\\.>|\\.\\.|-)\\s+(${ID_PATTERN})\\s*(?::\\s*(.*))?$`
|
|
15
|
+
);
|
|
16
|
+
var CONT_RE = new RegExp(`^(${ID_PATTERN})\\s*:\\s*(.+)$`);
|
|
17
|
+
function parse(source) {
|
|
18
|
+
const doc = {
|
|
19
|
+
nodes: /* @__PURE__ */ new Map(),
|
|
20
|
+
containments: [],
|
|
21
|
+
edges: [],
|
|
22
|
+
diagnostics: [],
|
|
23
|
+
kind: "block"
|
|
24
|
+
};
|
|
25
|
+
const lines = source.split(/\r?\n/);
|
|
26
|
+
for (let i = 0; i < lines.length; i++) {
|
|
27
|
+
const raw = stripComment(lines[i]);
|
|
28
|
+
const line = raw.trim();
|
|
29
|
+
if (!line) continue;
|
|
30
|
+
if (handleDef(line, i + 1, doc)) continue;
|
|
31
|
+
if (handleConn(line, i + 1, doc)) continue;
|
|
32
|
+
if (handleCont(line, i + 1, doc)) continue;
|
|
33
|
+
doc.diagnostics.push({
|
|
34
|
+
severity: "error",
|
|
35
|
+
line: i + 1,
|
|
36
|
+
col: 1,
|
|
37
|
+
message: `\u69CB\u6587\u4E0D\u660E: "${line}"`
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
doc.kind = inferKind(doc);
|
|
41
|
+
return doc;
|
|
42
|
+
}
|
|
43
|
+
function handleDef(line, lineNum, doc) {
|
|
44
|
+
const m = line.match(DEF_RE);
|
|
45
|
+
if (!m) return false;
|
|
46
|
+
const id = m[1];
|
|
47
|
+
const tail = m[2];
|
|
48
|
+
const label = splitBilingual(tail);
|
|
49
|
+
const existing = doc.nodes.get(id);
|
|
50
|
+
if (existing) {
|
|
51
|
+
doc.diagnostics.push({
|
|
52
|
+
severity: "warning",
|
|
53
|
+
line: lineNum,
|
|
54
|
+
col: 1,
|
|
55
|
+
message: `\u7B26\u53F7 "${id}" \u306F\u518D\u5B9A\u7FA9\u3055\u308C\u307E\u3057\u305F`
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
doc.nodes.set(id, {
|
|
59
|
+
id,
|
|
60
|
+
label,
|
|
61
|
+
implicit: false
|
|
62
|
+
});
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
function handleConn(line, lineNum, doc) {
|
|
66
|
+
const m = line.match(CONN_RE);
|
|
67
|
+
if (!m) return false;
|
|
68
|
+
const opLit = m[2];
|
|
69
|
+
const entry = OP_TABLE.find((o) => o.lit === opLit);
|
|
70
|
+
const from = entry.reverse ? m[3] : m[1];
|
|
71
|
+
const to = entry.reverse ? m[1] : m[3];
|
|
72
|
+
const labelText = m[4] ?? "";
|
|
73
|
+
const edge = {
|
|
74
|
+
from,
|
|
75
|
+
to,
|
|
76
|
+
op: entry.kind,
|
|
77
|
+
label: labelText.trim() ? splitBilingual(labelText) : void 0,
|
|
78
|
+
line: lineNum
|
|
79
|
+
};
|
|
80
|
+
doc.edges.push(edge);
|
|
81
|
+
ensureNode(doc, from);
|
|
82
|
+
ensureNode(doc, to);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
function handleCont(line, lineNum, doc) {
|
|
86
|
+
const m = line.match(CONT_RE);
|
|
87
|
+
if (!m) return false;
|
|
88
|
+
const parent = m[1];
|
|
89
|
+
const rest = m[2].trim();
|
|
90
|
+
const children = rest.split(/\s+/).filter(Boolean);
|
|
91
|
+
const idRe = new RegExp(`^${ID_PATTERN}$`);
|
|
92
|
+
for (const c of children) {
|
|
93
|
+
if (!idRe.test(c)) {
|
|
94
|
+
doc.diagnostics.push({
|
|
95
|
+
severity: "error",
|
|
96
|
+
line: lineNum,
|
|
97
|
+
col: 1,
|
|
98
|
+
message: `\u5305\u542B\u306E\u5B50\u3068\u3057\u3066\u4E0D\u6B63\u306A\u30C8\u30FC\u30AF\u30F3: "${c}"`
|
|
99
|
+
});
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
doc.containments.push({ parent, children, line: lineNum });
|
|
104
|
+
ensureNode(doc, parent);
|
|
105
|
+
for (const c of children) ensureNode(doc, c);
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
function ensureNode(doc, id) {
|
|
109
|
+
let n = doc.nodes.get(id);
|
|
110
|
+
if (n) return n;
|
|
111
|
+
n = { id, label: {}, implicit: true };
|
|
112
|
+
doc.nodes.set(id, n);
|
|
113
|
+
return n;
|
|
114
|
+
}
|
|
115
|
+
function stripComment(line) {
|
|
116
|
+
let inQuote = false;
|
|
117
|
+
for (let i = 0; i < line.length; i++) {
|
|
118
|
+
const c = line[i];
|
|
119
|
+
if (c === '"') inQuote = !inQuote;
|
|
120
|
+
else if (!inQuote && c === "#") return line.slice(0, i);
|
|
121
|
+
}
|
|
122
|
+
return line;
|
|
123
|
+
}
|
|
124
|
+
function splitBilingual(text) {
|
|
125
|
+
const s = text.trim();
|
|
126
|
+
if (!s) return {};
|
|
127
|
+
const slashIdx = findBilingualSeparator(s);
|
|
128
|
+
if (slashIdx === -1) {
|
|
129
|
+
return { ja: stripQuotes(s) };
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
ja: stripQuotes(s.slice(0, slashIdx).trim()),
|
|
133
|
+
en: stripQuotes(s.slice(slashIdx + 1).trim())
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function findBilingualSeparator(s) {
|
|
137
|
+
let inQuote = false;
|
|
138
|
+
for (let i = 0; i < s.length; i++) {
|
|
139
|
+
if (s[i] === '"') {
|
|
140
|
+
inQuote = !inQuote;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (!inQuote && s[i] === "/" && isSpace(s[i - 1]) && isSpace(s[i + 1])) return i;
|
|
144
|
+
}
|
|
145
|
+
return -1;
|
|
146
|
+
}
|
|
147
|
+
function isSpace(ch) {
|
|
148
|
+
return ch === " " || ch === " ";
|
|
149
|
+
}
|
|
150
|
+
function stripQuotes(s) {
|
|
151
|
+
if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) {
|
|
152
|
+
return s.slice(1, -1);
|
|
153
|
+
}
|
|
154
|
+
return s;
|
|
155
|
+
}
|
|
156
|
+
function inferKind(doc) {
|
|
157
|
+
if (doc.containments.length > 0) return "block";
|
|
158
|
+
for (const n of doc.nodes.values()) {
|
|
159
|
+
const ja = n.label.ja ?? "";
|
|
160
|
+
const en = n.label.en ?? "";
|
|
161
|
+
if (ja.endsWith("?") || en.endsWith("?")) return "flow";
|
|
162
|
+
}
|
|
163
|
+
if (doc.nodes.has("*")) return "state";
|
|
164
|
+
const pairs = /* @__PURE__ */ new Set();
|
|
165
|
+
for (const e of doc.edges) {
|
|
166
|
+
if (e.op === "bidir") return "seq";
|
|
167
|
+
const fwd = `${e.from}|${e.to}`;
|
|
168
|
+
const rev = `${e.to}|${e.from}`;
|
|
169
|
+
if (pairs.has(rev)) return "seq";
|
|
170
|
+
pairs.add(fwd);
|
|
171
|
+
}
|
|
172
|
+
return "flow";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/core/layout.ts
|
|
176
|
+
var NODE_W = 36;
|
|
177
|
+
var NODE_H = 14;
|
|
178
|
+
var PAD = 8;
|
|
179
|
+
var GRID_GAP = 24;
|
|
180
|
+
var TITLE_H = 5;
|
|
181
|
+
var PARENT_EDGE_LANE_H = 14;
|
|
182
|
+
var MARGIN = 8;
|
|
183
|
+
var ROOT_GAP = MARGIN * 4;
|
|
184
|
+
var ROUTE_GAP = PAD * 1.5;
|
|
185
|
+
var OBSTACLE_PAD = 0.8;
|
|
186
|
+
var BORDER_CLEARANCE = 4;
|
|
187
|
+
var ARROW_TERMINAL_CLEARANCE = 4.6;
|
|
188
|
+
var THICK_ARROW_TERMINAL_CLEARANCE = 5.4;
|
|
189
|
+
var VERTICAL_PORT_RATIO = 0.25;
|
|
190
|
+
var PORT_STUB = 6;
|
|
191
|
+
var MAX_ROUTE_LANES = 18;
|
|
192
|
+
var EPS = 1e-3;
|
|
193
|
+
function layout(doc) {
|
|
194
|
+
switch (doc.kind) {
|
|
195
|
+
case "block":
|
|
196
|
+
return layoutBlock(doc);
|
|
197
|
+
case "flow":
|
|
198
|
+
return layoutFlow(doc);
|
|
199
|
+
case "state":
|
|
200
|
+
return layoutState(doc);
|
|
201
|
+
case "seq":
|
|
202
|
+
return layoutSeq(doc);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function layoutBlock(doc) {
|
|
206
|
+
const childMap = /* @__PURE__ */ new Map();
|
|
207
|
+
for (const c of doc.containments) childMap.set(c.parent, c.children);
|
|
208
|
+
const parentMap = /* @__PURE__ */ new Map();
|
|
209
|
+
for (const c of doc.containments) {
|
|
210
|
+
for (const child of c.children) parentMap.set(child, c.parent);
|
|
211
|
+
}
|
|
212
|
+
const childIds = /* @__PURE__ */ new Set();
|
|
213
|
+
for (const cs of childMap.values()) for (const c of cs) childIds.add(c);
|
|
214
|
+
const allIds = [...doc.nodes.keys()];
|
|
215
|
+
const roots = allIds.filter((id) => !childIds.has(id));
|
|
216
|
+
const placed = [];
|
|
217
|
+
const positions = /* @__PURE__ */ new Map();
|
|
218
|
+
function size(id) {
|
|
219
|
+
const children = childMap.get(id);
|
|
220
|
+
if (!children || children.length === 0) return { w: NODE_W, h: NODE_H };
|
|
221
|
+
const sizes = children.map(size);
|
|
222
|
+
if (arrangementOf(id) === "grid") {
|
|
223
|
+
const cols = gridColsOf(id, children);
|
|
224
|
+
const colW = Math.max(...sizes.map((s) => s.w));
|
|
225
|
+
const rowHeights = gridRowHeights(sizes, cols);
|
|
226
|
+
return {
|
|
227
|
+
w: cols * colW + 2 * PAD + (cols - 1) * GRID_GAP,
|
|
228
|
+
h: titleHeightOf(id) + rowHeights.reduce((sum, h) => sum + h, 0) + 2 * PAD + Math.max(0, rowHeights.length - 1) * GRID_GAP
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
const maxW = Math.max(...sizes.map((s) => s.w));
|
|
232
|
+
const totH = sizes.reduce((a, b) => a + b.h, 0);
|
|
233
|
+
return {
|
|
234
|
+
w: maxW + 2 * PAD,
|
|
235
|
+
h: titleHeightOf(id) + totH + 2 * PAD + (children.length - 1) * GRID_GAP
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function arrangementOf(id) {
|
|
239
|
+
const children = childMap.get(id);
|
|
240
|
+
if (!children || children.length <= 2) return "stack";
|
|
241
|
+
if (hasParentToChildEdges(id, children)) return "grid";
|
|
242
|
+
return hasLinearChildFlow(children, doc.edges, childMap) ? "stack" : "grid";
|
|
243
|
+
}
|
|
244
|
+
function gridColsOf(id, children) {
|
|
245
|
+
if (hasParentToChildEdges(id, children)) return Math.min(children.length, 4);
|
|
246
|
+
if (children.length >= 7) return 4;
|
|
247
|
+
if (children.length >= 5) return 3;
|
|
248
|
+
return 2;
|
|
249
|
+
}
|
|
250
|
+
function gridRowHeights(sizes, cols) {
|
|
251
|
+
const rows = Math.ceil(sizes.length / cols);
|
|
252
|
+
const heights = [];
|
|
253
|
+
for (let row = 0; row < rows; row++) {
|
|
254
|
+
const rowSizes = sizes.slice(row * cols, row * cols + cols);
|
|
255
|
+
heights.push(Math.max(...rowSizes.map((s) => s.h)));
|
|
256
|
+
}
|
|
257
|
+
return heights;
|
|
258
|
+
}
|
|
259
|
+
function titleHeightOf(id) {
|
|
260
|
+
const children = childMap.get(id);
|
|
261
|
+
return children && hasParentToChildEdges(id, children) ? TITLE_H + PARENT_EDGE_LANE_H : TITLE_H;
|
|
262
|
+
}
|
|
263
|
+
function hasParentToChildEdges(id, children) {
|
|
264
|
+
return doc.edges.some((edge) => edge.from === id && children.some((child) => child === edge.to || containsDescendant(child, edge.to, childMap)));
|
|
265
|
+
}
|
|
266
|
+
function place(id, ox, oy) {
|
|
267
|
+
const s = size(id);
|
|
268
|
+
positions.set(id, { x: ox, y: oy, w: s.w, h: s.h });
|
|
269
|
+
const node = doc.nodes.get(id);
|
|
270
|
+
const children = childMap.get(id);
|
|
271
|
+
if (!children || children.length === 0) {
|
|
272
|
+
placed.push({
|
|
273
|
+
id,
|
|
274
|
+
x: ox,
|
|
275
|
+
y: oy,
|
|
276
|
+
w: s.w,
|
|
277
|
+
h: s.h,
|
|
278
|
+
label: node?.label ?? {},
|
|
279
|
+
shape: "box",
|
|
280
|
+
isContainer: false
|
|
281
|
+
});
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const sizes = children.map(size);
|
|
285
|
+
if (arrangementOf(id) === "grid") {
|
|
286
|
+
const cols = gridColsOf(id, children);
|
|
287
|
+
const colW = Math.max(...sizes.map((s2) => s2.w));
|
|
288
|
+
const rowHeights = gridRowHeights(sizes, cols);
|
|
289
|
+
const rowTops = [];
|
|
290
|
+
let rowTop = oy + titleHeightOf(id) + PAD;
|
|
291
|
+
for (let row = 0; row < rowHeights.length; row++) {
|
|
292
|
+
rowTops[row] = rowTop;
|
|
293
|
+
rowTop += rowHeights[row] + GRID_GAP;
|
|
294
|
+
}
|
|
295
|
+
for (let i = 0; i < children.length; i++) {
|
|
296
|
+
const r = Math.floor(i / cols);
|
|
297
|
+
const cc = i % cols;
|
|
298
|
+
const cx = ox + PAD + cc * (colW + GRID_GAP) + (colW - sizes[i].w) / 2;
|
|
299
|
+
const cy = rowTops[r] + (rowHeights[r] - sizes[i].h) / 2;
|
|
300
|
+
place(children[i], cx, cy);
|
|
301
|
+
}
|
|
302
|
+
alignGridRows(children, sizes, rowHeights, rowTops, cols, childMap, doc.edges, positions, placed);
|
|
303
|
+
} else {
|
|
304
|
+
const maxW = Math.max(...sizes.map((s2) => s2.w));
|
|
305
|
+
let yy = oy + titleHeightOf(id) + PAD;
|
|
306
|
+
for (let i = 0; i < children.length; i++) {
|
|
307
|
+
const cx = ox + PAD + (maxW - sizes[i].w) / 2;
|
|
308
|
+
place(children[i], cx, yy);
|
|
309
|
+
yy += sizes[i].h + GRID_GAP;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
placed.push({
|
|
313
|
+
id,
|
|
314
|
+
x: ox,
|
|
315
|
+
y: oy,
|
|
316
|
+
w: s.w,
|
|
317
|
+
h: s.h,
|
|
318
|
+
label: node?.label ?? {},
|
|
319
|
+
shape: "box",
|
|
320
|
+
isContainer: true
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
let cur = MARGIN;
|
|
324
|
+
for (const r of roots) {
|
|
325
|
+
const previousIds = new Set(positions.keys());
|
|
326
|
+
place(r, cur, MARGIN);
|
|
327
|
+
const subtreeIds = collectSubtreeIds(r, childMap);
|
|
328
|
+
const dy = rootAlignmentDelta(r, subtreeIds, previousIds, doc.edges, positions);
|
|
329
|
+
if (Math.abs(dy) >= EPS) shiftSubtree(subtreeIds, dy, positions, placed);
|
|
330
|
+
const sz = size(r);
|
|
331
|
+
cur += sz.w + ROOT_GAP;
|
|
332
|
+
}
|
|
333
|
+
const edges = makeBlockEdges(doc.edges, positions, placed, parentMap);
|
|
334
|
+
placed.sort((a, b) => {
|
|
335
|
+
if (a.isContainer !== b.isContainer) return a.isContainer ? -1 : 1;
|
|
336
|
+
if (a.isContainer && b.isContainer) {
|
|
337
|
+
const depthDiff = depthOf(a.id, parentMap) - depthOf(b.id, parentMap);
|
|
338
|
+
if (depthDiff !== 0) return depthDiff;
|
|
339
|
+
return b.w * b.h - a.w * a.h;
|
|
340
|
+
}
|
|
341
|
+
return a.y - b.y || a.x - b.x;
|
|
342
|
+
});
|
|
343
|
+
const boxes = [...positions.values()];
|
|
344
|
+
const edgePoints = edges.flatMap((edge) => edge.points);
|
|
345
|
+
const maxBoxX = boxes.length ? Math.max(...boxes.map((b) => b.x + b.w)) : MARGIN;
|
|
346
|
+
const maxBoxY = boxes.length ? Math.max(...boxes.map((b) => b.y + b.h)) : MARGIN;
|
|
347
|
+
const maxEdgeX = edgePoints.length ? Math.max(...edgePoints.map(([x]) => x)) : MARGIN;
|
|
348
|
+
const maxEdgeY = edgePoints.length ? Math.max(...edgePoints.map(([, y]) => y)) : MARGIN;
|
|
349
|
+
const width = Math.max(maxBoxX, maxEdgeX) + MARGIN;
|
|
350
|
+
const height = Math.max(maxBoxY, maxEdgeY) + MARGIN;
|
|
351
|
+
return { nodes: placed, edges, width, height, kind: "block" };
|
|
352
|
+
}
|
|
353
|
+
function layoutFlow(doc) {
|
|
354
|
+
const ids = [...doc.nodes.keys()];
|
|
355
|
+
const { byRank } = computeRanks(doc);
|
|
356
|
+
function shapeOf(id) {
|
|
357
|
+
const n = doc.nodes.get(id);
|
|
358
|
+
const ja = n?.label.ja ?? "";
|
|
359
|
+
const en = n?.label.en ?? "";
|
|
360
|
+
if (ja.endsWith("?") || en.endsWith("?")) return "diamond";
|
|
361
|
+
const inc = doc.edges.filter((e) => e.to === id).length;
|
|
362
|
+
const out = doc.edges.filter((e) => e.from === id).length;
|
|
363
|
+
if (inc === 0 || out === 0) return "round";
|
|
364
|
+
return "box";
|
|
365
|
+
}
|
|
366
|
+
const V_GAP = 14;
|
|
367
|
+
const H_GAP = 10;
|
|
368
|
+
const positions = /* @__PURE__ */ new Map();
|
|
369
|
+
const sortedRanks = [...byRank.keys()].sort((a, b) => a - b);
|
|
370
|
+
let y = MARGIN;
|
|
371
|
+
let maxX = 0;
|
|
372
|
+
for (const r of sortedRanks) {
|
|
373
|
+
const lane = byRank.get(r);
|
|
374
|
+
const widths = lane.map((id) => shapeOf(id) === "diamond" ? NODE_W * 1.2 : NODE_W);
|
|
375
|
+
const totalW = widths.reduce((a, b) => a + b, 0) + (lane.length - 1) * H_GAP;
|
|
376
|
+
let x = MARGIN;
|
|
377
|
+
const canvasW = Math.max(totalW + 2 * MARGIN, 200);
|
|
378
|
+
x = (canvasW - totalW) / 2;
|
|
379
|
+
for (let i = 0; i < lane.length; i++) {
|
|
380
|
+
positions.set(lane[i], { x, y, w: widths[i], h: NODE_H });
|
|
381
|
+
x += widths[i] + H_GAP;
|
|
382
|
+
}
|
|
383
|
+
if (x > maxX) maxX = x;
|
|
384
|
+
y += NODE_H + V_GAP;
|
|
385
|
+
}
|
|
386
|
+
for (const id of ids) {
|
|
387
|
+
if (!positions.has(id)) {
|
|
388
|
+
positions.set(id, { x: MARGIN, y, w: NODE_W, h: NODE_H });
|
|
389
|
+
y += NODE_H + V_GAP;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const placed = [];
|
|
393
|
+
for (const [id, b] of positions) {
|
|
394
|
+
placed.push({
|
|
395
|
+
id,
|
|
396
|
+
...b,
|
|
397
|
+
label: doc.nodes.get(id)?.label ?? {},
|
|
398
|
+
shape: shapeOf(id),
|
|
399
|
+
isContainer: false
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
const edges = makeEdges(doc.edges, positions);
|
|
403
|
+
return { nodes: placed, edges, width: maxX + MARGIN, height: y + MARGIN, kind: "flow" };
|
|
404
|
+
}
|
|
405
|
+
function layoutState(doc) {
|
|
406
|
+
const { byRank } = computeRanks(doc);
|
|
407
|
+
function shapeOf(id) {
|
|
408
|
+
if (id === "*") return "circle";
|
|
409
|
+
return "round";
|
|
410
|
+
}
|
|
411
|
+
const V_GAP = 14, H_GAP = 10;
|
|
412
|
+
const positions = /* @__PURE__ */ new Map();
|
|
413
|
+
const sortedRanks = [...byRank.keys()].sort((a, b) => a - b);
|
|
414
|
+
let y = MARGIN;
|
|
415
|
+
let maxX = 0;
|
|
416
|
+
for (const r of sortedRanks) {
|
|
417
|
+
const lane = byRank.get(r);
|
|
418
|
+
const widths = lane.map((id) => shapeOf(id) === "circle" ? 6 : NODE_W);
|
|
419
|
+
const heights = lane.map((id) => shapeOf(id) === "circle" ? 6 : NODE_H);
|
|
420
|
+
const totalW = widths.reduce((a, b) => a + b, 0) + (lane.length - 1) * H_GAP;
|
|
421
|
+
const canvasW = Math.max(totalW + 2 * MARGIN, 200);
|
|
422
|
+
let x = (canvasW - totalW) / 2;
|
|
423
|
+
for (let i = 0; i < lane.length; i++) {
|
|
424
|
+
positions.set(lane[i], { x, y: y + (NODE_H - heights[i]) / 2, w: widths[i], h: heights[i] });
|
|
425
|
+
x += widths[i] + H_GAP;
|
|
426
|
+
}
|
|
427
|
+
if (x > maxX) maxX = x;
|
|
428
|
+
y += NODE_H + V_GAP;
|
|
429
|
+
}
|
|
430
|
+
for (const id of doc.nodes.keys()) {
|
|
431
|
+
if (!positions.has(id)) {
|
|
432
|
+
positions.set(id, { x: MARGIN, y, w: NODE_W, h: NODE_H });
|
|
433
|
+
y += NODE_H + V_GAP;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const placed = [];
|
|
437
|
+
for (const [id, b] of positions) {
|
|
438
|
+
placed.push({
|
|
439
|
+
id,
|
|
440
|
+
...b,
|
|
441
|
+
label: doc.nodes.get(id)?.label ?? {},
|
|
442
|
+
shape: shapeOf(id),
|
|
443
|
+
isContainer: false
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
const edges = makeEdges(doc.edges, positions);
|
|
447
|
+
return { nodes: placed, edges, width: maxX + MARGIN, height: y + MARGIN, kind: "state" };
|
|
448
|
+
}
|
|
449
|
+
function layoutSeq(doc) {
|
|
450
|
+
const seen = /* @__PURE__ */ new Set();
|
|
451
|
+
const actors = [];
|
|
452
|
+
for (const e of doc.edges) {
|
|
453
|
+
for (const id of [e.from, e.to]) {
|
|
454
|
+
if (!seen.has(id)) {
|
|
455
|
+
seen.add(id);
|
|
456
|
+
actors.push(id);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
for (const id of doc.nodes.keys()) {
|
|
461
|
+
if (!seen.has(id)) {
|
|
462
|
+
seen.add(id);
|
|
463
|
+
actors.push(id);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const ACTOR_W = 40, ACTOR_H = 12, COL_GAP = 28, MSG_GAP = 12;
|
|
467
|
+
const xOf = /* @__PURE__ */ new Map();
|
|
468
|
+
const placed = [];
|
|
469
|
+
let x = MARGIN;
|
|
470
|
+
for (const id of actors) {
|
|
471
|
+
xOf.set(id, x + ACTOR_W / 2);
|
|
472
|
+
placed.push({
|
|
473
|
+
id,
|
|
474
|
+
x,
|
|
475
|
+
y: MARGIN,
|
|
476
|
+
w: ACTOR_W,
|
|
477
|
+
h: ACTOR_H,
|
|
478
|
+
label: doc.nodes.get(id)?.label ?? {},
|
|
479
|
+
shape: "actor",
|
|
480
|
+
isContainer: false
|
|
481
|
+
});
|
|
482
|
+
x += ACTOR_W + COL_GAP;
|
|
483
|
+
}
|
|
484
|
+
let y = MARGIN + ACTOR_H + MSG_GAP;
|
|
485
|
+
const msgEdges = [];
|
|
486
|
+
for (const e of doc.edges) {
|
|
487
|
+
const xa = xOf.get(e.from);
|
|
488
|
+
const xb = xOf.get(e.to);
|
|
489
|
+
if (xa === void 0 || xb === void 0) continue;
|
|
490
|
+
msgEdges.push({
|
|
491
|
+
from: e.from,
|
|
492
|
+
to: e.to,
|
|
493
|
+
points: [[xa, y], [xb, y]],
|
|
494
|
+
label: e.label,
|
|
495
|
+
op: e.op
|
|
496
|
+
});
|
|
497
|
+
y += MSG_GAP;
|
|
498
|
+
}
|
|
499
|
+
const lifelines = actors.map((id) => ({
|
|
500
|
+
from: id,
|
|
501
|
+
to: id,
|
|
502
|
+
points: [[xOf.get(id), MARGIN + ACTOR_H], [xOf.get(id), y + MSG_GAP]],
|
|
503
|
+
op: "dashed",
|
|
504
|
+
isLifeline: true
|
|
505
|
+
}));
|
|
506
|
+
return {
|
|
507
|
+
nodes: placed,
|
|
508
|
+
edges: [...lifelines, ...msgEdges],
|
|
509
|
+
width: x,
|
|
510
|
+
height: y + MARGIN * 2,
|
|
511
|
+
kind: "seq"
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
function computeRanks(doc) {
|
|
515
|
+
const ids = [...doc.nodes.keys()];
|
|
516
|
+
const incoming = /* @__PURE__ */ new Map();
|
|
517
|
+
for (const id of ids) incoming.set(id, 0);
|
|
518
|
+
for (const e of doc.edges) incoming.set(e.to, (incoming.get(e.to) ?? 0) + 1);
|
|
519
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
520
|
+
for (const e of doc.edges) {
|
|
521
|
+
if (!outgoing.has(e.from)) outgoing.set(e.from, []);
|
|
522
|
+
outgoing.get(e.from).push(e.to);
|
|
523
|
+
}
|
|
524
|
+
let sources = ids.filter((id) => incoming.get(id) === 0);
|
|
525
|
+
if (sources.length === 0) {
|
|
526
|
+
sources = ids.includes("*") ? ["*"] : ids.length ? [ids[0]] : [];
|
|
527
|
+
}
|
|
528
|
+
const rank = /* @__PURE__ */ new Map();
|
|
529
|
+
const visited = /* @__PURE__ */ new Set();
|
|
530
|
+
for (const s of sources) {
|
|
531
|
+
rank.set(s, 0);
|
|
532
|
+
visited.add(s);
|
|
533
|
+
}
|
|
534
|
+
let frontier = [...sources];
|
|
535
|
+
while (frontier.length) {
|
|
536
|
+
const next = [];
|
|
537
|
+
for (const n of frontier) {
|
|
538
|
+
const r = rank.get(n);
|
|
539
|
+
for (const m of outgoing.get(n) ?? []) {
|
|
540
|
+
if (!visited.has(m)) {
|
|
541
|
+
rank.set(m, r + 1);
|
|
542
|
+
visited.add(m);
|
|
543
|
+
next.push(m);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
frontier = next;
|
|
548
|
+
}
|
|
549
|
+
let maxRank = 0;
|
|
550
|
+
for (const r of rank.values()) if (r > maxRank) maxRank = r;
|
|
551
|
+
for (const id of ids) {
|
|
552
|
+
if (!visited.has(id)) {
|
|
553
|
+
maxRank++;
|
|
554
|
+
rank.set(id, maxRank);
|
|
555
|
+
visited.add(id);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
const byRank = /* @__PURE__ */ new Map();
|
|
559
|
+
for (const [id, r] of rank) {
|
|
560
|
+
if (!byRank.has(r)) byRank.set(r, []);
|
|
561
|
+
byRank.get(r).push(id);
|
|
562
|
+
}
|
|
563
|
+
return { byRank };
|
|
564
|
+
}
|
|
565
|
+
function makeEdges(srcEdges, positions) {
|
|
566
|
+
return srcEdges.map((e) => {
|
|
567
|
+
const a = positions.get(e.from);
|
|
568
|
+
const b = positions.get(e.to);
|
|
569
|
+
if (!a || !b) {
|
|
570
|
+
return { from: e.from, to: e.to, points: [], label: e.label, op: e.op };
|
|
571
|
+
}
|
|
572
|
+
return {
|
|
573
|
+
from: e.from,
|
|
574
|
+
to: e.to,
|
|
575
|
+
points: orthogonalRoute(a, b),
|
|
576
|
+
label: e.label,
|
|
577
|
+
op: e.op
|
|
578
|
+
};
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
function hasLinearChildFlow(children, edges, childMap) {
|
|
582
|
+
const childSet = new Set(children);
|
|
583
|
+
const pairs = /* @__PURE__ */ new Set();
|
|
584
|
+
const incoming = /* @__PURE__ */ new Map();
|
|
585
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
586
|
+
for (const child of children) {
|
|
587
|
+
incoming.set(child, 0);
|
|
588
|
+
outgoing.set(child, 0);
|
|
589
|
+
}
|
|
590
|
+
for (const edge of edges) {
|
|
591
|
+
const from = topChildFor(edge.from, childSet, childMap);
|
|
592
|
+
const to = topChildFor(edge.to, childSet, childMap);
|
|
593
|
+
if (!from || !to || from === to) continue;
|
|
594
|
+
const key = `${from}|${to}`;
|
|
595
|
+
if (pairs.has(key)) continue;
|
|
596
|
+
pairs.add(key);
|
|
597
|
+
outgoing.set(from, (outgoing.get(from) ?? 0) + 1);
|
|
598
|
+
incoming.set(to, (incoming.get(to) ?? 0) + 1);
|
|
599
|
+
}
|
|
600
|
+
if (pairs.size < children.length - 1) return false;
|
|
601
|
+
let starts = 0;
|
|
602
|
+
let ends = 0;
|
|
603
|
+
for (const child of children) {
|
|
604
|
+
const inc = incoming.get(child) ?? 0;
|
|
605
|
+
const out = outgoing.get(child) ?? 0;
|
|
606
|
+
if (inc > 1 || out > 1) return false;
|
|
607
|
+
if (inc === 0 && out === 1) starts++;
|
|
608
|
+
if (inc === 1 && out === 0) ends++;
|
|
609
|
+
}
|
|
610
|
+
return starts === 1 && ends === 1;
|
|
611
|
+
}
|
|
612
|
+
function topChildFor(id, childSet, childMap) {
|
|
613
|
+
if (childSet.has(id)) return id;
|
|
614
|
+
for (const child of childSet) {
|
|
615
|
+
if (containsDescendant(child, id, childMap)) return child;
|
|
616
|
+
}
|
|
617
|
+
return void 0;
|
|
618
|
+
}
|
|
619
|
+
function containsDescendant(ancestor, id, childMap) {
|
|
620
|
+
const children = childMap.get(ancestor);
|
|
621
|
+
if (!children) return false;
|
|
622
|
+
for (const child of children) {
|
|
623
|
+
if (child === id || containsDescendant(child, id, childMap)) return true;
|
|
624
|
+
}
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
function collectSubtreeIds(id, childMap) {
|
|
628
|
+
const ids = /* @__PURE__ */ new Set([id]);
|
|
629
|
+
for (const child of childMap.get(id) ?? []) {
|
|
630
|
+
for (const descendant of collectSubtreeIds(child, childMap)) ids.add(descendant);
|
|
631
|
+
}
|
|
632
|
+
return ids;
|
|
633
|
+
}
|
|
634
|
+
function alignGridRows(children, sizes, rowHeights, rowTops, cols, childMap, edges, positions, placed) {
|
|
635
|
+
const rows = Math.ceil(children.length / cols);
|
|
636
|
+
for (let row = 0; row < rows; row++) {
|
|
637
|
+
const rowChildren = children.slice(row * cols, row * cols + cols);
|
|
638
|
+
const rowTop = rowTops[row];
|
|
639
|
+
const rowH = rowHeights[row];
|
|
640
|
+
if (rowTop === void 0 || rowH === void 0) continue;
|
|
641
|
+
for (const child of rowChildren) {
|
|
642
|
+
const childIndex = children.indexOf(child);
|
|
643
|
+
const childSize = sizes[childIndex];
|
|
644
|
+
if (!childSize || childSize.h >= rowH - EPS) continue;
|
|
645
|
+
const subtreeIds = collectSubtreeIds(child, childMap);
|
|
646
|
+
const siblingIds = /* @__PURE__ */ new Set();
|
|
647
|
+
for (const sibling of rowChildren) {
|
|
648
|
+
if (sibling === child) continue;
|
|
649
|
+
for (const id of collectSubtreeIds(sibling, childMap)) siblingIds.add(id);
|
|
650
|
+
}
|
|
651
|
+
const deltas = [];
|
|
652
|
+
for (const edge of edges) {
|
|
653
|
+
const fromChild = subtreeIds.has(edge.from);
|
|
654
|
+
const toChild = subtreeIds.has(edge.to);
|
|
655
|
+
if (fromChild && siblingIds.has(edge.to)) {
|
|
656
|
+
addWeightedDelta(
|
|
657
|
+
deltas,
|
|
658
|
+
centerY(positions.get(edge.to)) - centerY(positions.get(edge.from)),
|
|
659
|
+
alignmentWeight(edge, false)
|
|
660
|
+
);
|
|
661
|
+
} else if (toChild && siblingIds.has(edge.from)) {
|
|
662
|
+
addWeightedDelta(
|
|
663
|
+
deltas,
|
|
664
|
+
centerY(positions.get(edge.from)) - centerY(positions.get(edge.to)),
|
|
665
|
+
alignmentWeight(edge, true)
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (deltas.length === 0) continue;
|
|
670
|
+
deltas.sort((a, b) => a - b);
|
|
671
|
+
const desired = deltas[Math.floor(deltas.length / 2)];
|
|
672
|
+
const box = positions.get(child);
|
|
673
|
+
if (!box) continue;
|
|
674
|
+
const minDy = rowTop - box.y;
|
|
675
|
+
const maxDy = rowTop + rowH - childSize.h - box.y;
|
|
676
|
+
const dy = Math.min(maxDy, Math.max(minDy, desired));
|
|
677
|
+
if (Math.abs(dy) >= EPS) shiftSubtree(subtreeIds, dy, positions, placed);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
function rootAlignmentDelta(rootId, subtreeIds, previousIds, edges, positions) {
|
|
682
|
+
const deltas = [];
|
|
683
|
+
for (const edge of edges) {
|
|
684
|
+
const fromCurrent = subtreeIds.has(edge.from);
|
|
685
|
+
const toCurrent = subtreeIds.has(edge.to);
|
|
686
|
+
const fromPrevious = previousIds.has(edge.from);
|
|
687
|
+
const toPrevious = previousIds.has(edge.to);
|
|
688
|
+
if (fromCurrent && toPrevious) {
|
|
689
|
+
addWeightedDelta(
|
|
690
|
+
deltas,
|
|
691
|
+
centerY(positions.get(edge.to)) - centerY(positions.get(edge.from)),
|
|
692
|
+
alignmentWeight(edge, false)
|
|
693
|
+
);
|
|
694
|
+
} else if (toCurrent && fromPrevious) {
|
|
695
|
+
addWeightedDelta(
|
|
696
|
+
deltas,
|
|
697
|
+
centerY(positions.get(edge.from)) - centerY(positions.get(edge.to)),
|
|
698
|
+
alignmentWeight(edge, true)
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (deltas.length === 0) return 0;
|
|
703
|
+
deltas.sort((a, b) => a - b);
|
|
704
|
+
const desired = deltas[Math.floor(deltas.length / 2)];
|
|
705
|
+
const root = positions.get(rootId);
|
|
706
|
+
if (!root) return desired;
|
|
707
|
+
return Math.max(MARGIN - root.y, desired);
|
|
708
|
+
}
|
|
709
|
+
function addWeightedDelta(deltas, delta, weight) {
|
|
710
|
+
if (!Number.isFinite(delta)) return;
|
|
711
|
+
for (let i = 0; i < weight; i++) deltas.push(delta);
|
|
712
|
+
}
|
|
713
|
+
function alignmentWeight(edge, currentIsTarget) {
|
|
714
|
+
const feedback = edge.op === "dashed" || edge.op === "dashed-arrow";
|
|
715
|
+
return (feedback ? 1 : 3) + (currentIsTarget ? 1 : 0);
|
|
716
|
+
}
|
|
717
|
+
function centerY(box) {
|
|
718
|
+
return box ? box.y + box.h / 2 : Number.NaN;
|
|
719
|
+
}
|
|
720
|
+
function shiftSubtree(ids, dy, positions, placed) {
|
|
721
|
+
for (const id of ids) {
|
|
722
|
+
const box = positions.get(id);
|
|
723
|
+
if (box) box.y += dy;
|
|
724
|
+
}
|
|
725
|
+
for (const node of placed) {
|
|
726
|
+
if (ids.has(node.id)) node.y += dy;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
function makeBlockEdges(srcEdges, positions, obstacles, parentMap) {
|
|
730
|
+
const containerIds = new Set(obstacles.filter((o) => o.isContainer).map((o) => o.id));
|
|
731
|
+
const plans = srcEdges.map((edge, index) => {
|
|
732
|
+
const a = positions.get(edge.from);
|
|
733
|
+
const b = positions.get(edge.to);
|
|
734
|
+
if (!a || !b) {
|
|
735
|
+
return { edge, index, endpointBoundaries: [], endpointInteriorBarriers: [] };
|
|
736
|
+
}
|
|
737
|
+
const routeA = routeEndpointBox(edge.from, edge.to, positions, parentMap, containerIds) ?? a;
|
|
738
|
+
const routeB = routeEndpointBox(edge.to, edge.from, positions, parentMap, containerIds) ?? b;
|
|
739
|
+
const endpointBoundaries = [
|
|
740
|
+
...routeA === a ? [] : [a],
|
|
741
|
+
...routeB === b ? [] : [b]
|
|
742
|
+
];
|
|
743
|
+
const endpointInteriorBarriers = [
|
|
744
|
+
...isExternalContainerEndpoint(edge.from, edge.to, parentMap, containerIds) ? [a] : [],
|
|
745
|
+
...isExternalContainerEndpoint(edge.to, edge.from, parentMap, containerIds) ? [b] : []
|
|
746
|
+
];
|
|
747
|
+
return {
|
|
748
|
+
edge,
|
|
749
|
+
index,
|
|
750
|
+
routeA,
|
|
751
|
+
routeB,
|
|
752
|
+
endpointBoundaries,
|
|
753
|
+
endpointInteriorBarriers,
|
|
754
|
+
bounds: commonRoutingBounds(edge.from, edge.to, parentMap, positions)
|
|
755
|
+
};
|
|
756
|
+
});
|
|
757
|
+
return routePlansSequentially(plans, obstacles, parentMap);
|
|
758
|
+
}
|
|
759
|
+
function routePlansSequentially(plans, baseObstacles, parentMap) {
|
|
760
|
+
const routed = [];
|
|
761
|
+
for (const plan of plans) {
|
|
762
|
+
const searchBox = routeSearchBox(plan);
|
|
763
|
+
const routedArrowGuards = routed.flatMap((edge, index) => arrowGuardObstaclesFor(edge, index));
|
|
764
|
+
const routedEdgeGuards = routed.flatMap((edge, index) => edgeGuardObstaclesFor(edge, index));
|
|
765
|
+
const extraArrowGuards = routedArrowGuards.filter((guard) => guard.edgeIndex !== plan.index && !sharesRouteEndpoint(guard, plan.edge) && (!searchBox || boxesOverlap(searchBox, guard)));
|
|
766
|
+
const extraEdgeGuards = routedEdgeGuards.filter((guard) => shouldUseEdgeGuard(guard, plan, searchBox));
|
|
767
|
+
routed.push(routeBlockPlan(plan, [...baseObstacles, ...extraArrowGuards, ...extraEdgeGuards], parentMap));
|
|
768
|
+
}
|
|
769
|
+
return routed;
|
|
770
|
+
}
|
|
771
|
+
function routeBlockPlan(plan, obstacles, parentMap) {
|
|
772
|
+
const { edge, routeA, routeB } = plan;
|
|
773
|
+
if (!routeA || !routeB) {
|
|
774
|
+
return { from: edge.from, to: edge.to, points: [], label: edge.label, op: edge.op };
|
|
775
|
+
}
|
|
776
|
+
return {
|
|
777
|
+
from: edge.from,
|
|
778
|
+
to: edge.to,
|
|
779
|
+
points: avoidObstaclesRoute(
|
|
780
|
+
routeA,
|
|
781
|
+
routeB,
|
|
782
|
+
edge.from,
|
|
783
|
+
edge.to,
|
|
784
|
+
obstacles,
|
|
785
|
+
parentMap,
|
|
786
|
+
plan.bounds,
|
|
787
|
+
plan.endpointBoundaries,
|
|
788
|
+
plan.endpointInteriorBarriers,
|
|
789
|
+
edge.op
|
|
790
|
+
),
|
|
791
|
+
label: edge.label,
|
|
792
|
+
op: edge.op
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
function arrowGuardObstaclesFor(edge, edgeIndex) {
|
|
796
|
+
if (edge.isLifeline || edge.points.length < 2) return [];
|
|
797
|
+
const guards = [];
|
|
798
|
+
if (hasEndArrow(edge.op)) {
|
|
799
|
+
guards.push(makeArrowGuard(edge, edgeIndex, edge.points[edge.points.length - 1], "end"));
|
|
800
|
+
}
|
|
801
|
+
if (edge.op === "bidir") {
|
|
802
|
+
guards.push(makeArrowGuard(edge, edgeIndex, edge.points[0], "start"));
|
|
803
|
+
}
|
|
804
|
+
return guards;
|
|
805
|
+
}
|
|
806
|
+
function hasEndArrow(op) {
|
|
807
|
+
return op !== "line" && op !== "dashed";
|
|
808
|
+
}
|
|
809
|
+
function makeArrowGuard(edge, edgeIndex, tip, side) {
|
|
810
|
+
const half = edge.op === "thick" ? 5 : 4.2;
|
|
811
|
+
return {
|
|
812
|
+
id: `__arrow_guard_${edgeIndex}_${side}`,
|
|
813
|
+
x: tip[0] - half,
|
|
814
|
+
y: tip[1] - half,
|
|
815
|
+
w: half * 2,
|
|
816
|
+
h: half * 2,
|
|
817
|
+
isContainer: false,
|
|
818
|
+
edgeIndex,
|
|
819
|
+
edgeFrom: edge.from,
|
|
820
|
+
edgeTo: edge.to
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
function sharesRouteEndpoint(guard, edge) {
|
|
824
|
+
return guard.edgeFrom === edge.from || guard.edgeFrom === edge.to || guard.edgeTo === edge.from || guard.edgeTo === edge.to;
|
|
825
|
+
}
|
|
826
|
+
function edgeGuardObstaclesFor(edge, edgeIndex) {
|
|
827
|
+
if (edge.points.length < 2) return [];
|
|
828
|
+
const guards = [];
|
|
829
|
+
const clear = 2.2;
|
|
830
|
+
const trim = 1.2;
|
|
831
|
+
for (let i = 0; i < edge.points.length - 1; i++) {
|
|
832
|
+
const a = edge.points[i];
|
|
833
|
+
const b = edge.points[i + 1];
|
|
834
|
+
const len = segmentLength(a, b);
|
|
835
|
+
if (len <= trim * 2) continue;
|
|
836
|
+
if (Math.abs(a[0] - b[0]) < EPS) {
|
|
837
|
+
const y1 = Math.min(a[1], b[1]) + trim;
|
|
838
|
+
const y2 = Math.max(a[1], b[1]) - trim;
|
|
839
|
+
guards.push({
|
|
840
|
+
id: `__edge_guard_${edgeIndex}_${i}`,
|
|
841
|
+
x: a[0] - clear,
|
|
842
|
+
y: y1,
|
|
843
|
+
w: clear * 2,
|
|
844
|
+
h: y2 - y1,
|
|
845
|
+
isContainer: false,
|
|
846
|
+
edgeIndex,
|
|
847
|
+
edgeOp: edge.op,
|
|
848
|
+
orientation: "vertical"
|
|
849
|
+
});
|
|
850
|
+
} else if (Math.abs(a[1] - b[1]) < EPS) {
|
|
851
|
+
const x1 = Math.min(a[0], b[0]) + trim;
|
|
852
|
+
const x2 = Math.max(a[0], b[0]) - trim;
|
|
853
|
+
guards.push({
|
|
854
|
+
id: `__edge_guard_${edgeIndex}_${i}`,
|
|
855
|
+
x: x1,
|
|
856
|
+
y: a[1] - clear,
|
|
857
|
+
w: x2 - x1,
|
|
858
|
+
h: clear * 2,
|
|
859
|
+
isContainer: false,
|
|
860
|
+
edgeIndex,
|
|
861
|
+
edgeOp: edge.op,
|
|
862
|
+
orientation: "horizontal"
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
return guards;
|
|
867
|
+
}
|
|
868
|
+
function shouldUseEdgeGuard(guard, plan, searchBox) {
|
|
869
|
+
if (guard.edgeIndex === plan.index) return false;
|
|
870
|
+
return !searchBox || boxesOverlap(searchBox, guard);
|
|
871
|
+
}
|
|
872
|
+
function routeSearchBox(plan) {
|
|
873
|
+
if (!plan.routeA || !plan.routeB) return void 0;
|
|
874
|
+
const left = Math.min(plan.routeA.x, plan.routeB.x);
|
|
875
|
+
const top = Math.min(plan.routeA.y, plan.routeB.y);
|
|
876
|
+
const right = Math.max(plan.routeA.x + plan.routeA.w, plan.routeB.x + plan.routeB.w);
|
|
877
|
+
const bottom = Math.max(plan.routeA.y + plan.routeA.h, plan.routeB.y + plan.routeB.h);
|
|
878
|
+
const pad = ROUTE_GAP * 4;
|
|
879
|
+
const candidate = {
|
|
880
|
+
x: left - pad,
|
|
881
|
+
y: top - pad,
|
|
882
|
+
w: right - left + pad * 2,
|
|
883
|
+
h: bottom - top + pad * 2
|
|
884
|
+
};
|
|
885
|
+
if (!plan.bounds) return candidate;
|
|
886
|
+
return {
|
|
887
|
+
x: Math.max(candidate.x, plan.bounds.x),
|
|
888
|
+
y: Math.max(candidate.y, plan.bounds.y),
|
|
889
|
+
w: Math.min(candidate.x + candidate.w, plan.bounds.x + plan.bounds.w) - Math.max(candidate.x, plan.bounds.x),
|
|
890
|
+
h: Math.min(candidate.y + candidate.h, plan.bounds.y + plan.bounds.h) - Math.max(candidate.y, plan.bounds.y)
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
function boxesOverlap(a, b) {
|
|
894
|
+
return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
|
|
895
|
+
}
|
|
896
|
+
function routeEndpointBox(id, otherId, positions, parentMap, containerIds) {
|
|
897
|
+
const container = positions.get(id);
|
|
898
|
+
const other = positions.get(otherId);
|
|
899
|
+
if (!container || !other) return void 0;
|
|
900
|
+
if (!isAncestorOf(id, otherId, parentMap)) {
|
|
901
|
+
if (!containerIds.has(id)) return void 0;
|
|
902
|
+
return container;
|
|
903
|
+
}
|
|
904
|
+
const left = container.x + PAD;
|
|
905
|
+
const right = container.x + container.w - PAD;
|
|
906
|
+
const top = container.y + TITLE_H + PAD / 2;
|
|
907
|
+
const x = Math.min(right, Math.max(left, other.x + other.w / 2));
|
|
908
|
+
return { x: x - 0.1, y: top - 0.1, w: 0.2, h: 0.2 };
|
|
909
|
+
}
|
|
910
|
+
function isExternalContainerEndpoint(id, otherId, parentMap, containerIds) {
|
|
911
|
+
return containerIds.has(id) && !isAncestorOf(id, otherId, parentMap);
|
|
912
|
+
}
|
|
913
|
+
function isAncestorOf(ancestor, id, parentMap) {
|
|
914
|
+
let cur = id;
|
|
915
|
+
const seen = /* @__PURE__ */ new Set();
|
|
916
|
+
while (parentMap.has(cur) && !seen.has(cur)) {
|
|
917
|
+
seen.add(cur);
|
|
918
|
+
cur = parentMap.get(cur);
|
|
919
|
+
if (cur === ancestor) return true;
|
|
920
|
+
}
|
|
921
|
+
return false;
|
|
922
|
+
}
|
|
923
|
+
function depthOf(id, parentMap) {
|
|
924
|
+
let depth = 0;
|
|
925
|
+
let cur = id;
|
|
926
|
+
const seen = /* @__PURE__ */ new Set();
|
|
927
|
+
while (parentMap.has(cur) && !seen.has(cur)) {
|
|
928
|
+
seen.add(cur);
|
|
929
|
+
cur = parentMap.get(cur);
|
|
930
|
+
depth++;
|
|
931
|
+
}
|
|
932
|
+
return depth;
|
|
933
|
+
}
|
|
934
|
+
function avoidObstaclesRoute(a, b, from, to, obstacles, parentMap, bounds, endpointBoundaries = [], endpointInteriorBarriers = [], op = "line", strictEdgeGuards = true) {
|
|
935
|
+
const routedObstacles = obstacles.filter((o) => o.id !== from && o.id !== to);
|
|
936
|
+
const leafObstacles = routedObstacles.filter((o) => !o.isContainer);
|
|
937
|
+
const hardLeafObstacles = leafObstacles.filter((o) => o.id.startsWith("__arrow_guard_"));
|
|
938
|
+
const allowedContainers = allowedContainerIds(from, to, parentMap);
|
|
939
|
+
const passableContainerObstacles = routedObstacles.filter((o) => o.isContainer && allowedContainers.has(o.id));
|
|
940
|
+
const blockedContainerObstacles = routedObstacles.filter((o) => o.isContainer && !allowedContainers.has(o.id));
|
|
941
|
+
const externalContainerObstacles = passableContainerObstacles.filter((o) => isAncestorOf(o.id, from, parentMap) !== isAncestorOf(o.id, to, parentMap));
|
|
942
|
+
const boundaryObstacles = endpointBoundaries.map((box, index) => ({
|
|
943
|
+
...box,
|
|
944
|
+
id: `__endpoint_boundary_${index}`,
|
|
945
|
+
isContainer: true
|
|
946
|
+
}));
|
|
947
|
+
const realLeafObstacles = leafObstacles.filter((o) => !o.id.startsWith("__"));
|
|
948
|
+
const edgeGuardObstacles = leafObstacles.filter(isEdgeGuardObstacle);
|
|
949
|
+
const straight = preferredStraightRoute(a, b);
|
|
950
|
+
if (straight && hasArrowTerminalClearance(straight, op) && (!bounds || routeFitsBounds(straight, bounds)) && !routeIntersectsAnyInterior(straight, realLeafObstacles, OBSTACLE_PAD) && !routeIntersectsAnyInterior(straight, hardLeafObstacles, OBSTACLE_PAD) && (!strictEdgeGuards || !routeSharesAnyEdgeGuardLane(straight, edgeGuardObstacles)) && !routeIntersectsAnyInterior(straight, blockedContainerObstacles, OBSTACLE_PAD) && !routeIntersectsAnyInterior(straight, endpointInteriorBarriers, 0) && !routeOverlapsAnyBorder(straight, [
|
|
951
|
+
...endpointBoundaries,
|
|
952
|
+
...passableContainerObstacles,
|
|
953
|
+
...blockedContainerObstacles
|
|
954
|
+
], BORDER_CLEARANCE)) {
|
|
955
|
+
return straight;
|
|
956
|
+
}
|
|
957
|
+
const lanes = routeLanes(a, b, [...routedObstacles, ...boundaryObstacles], bounds);
|
|
958
|
+
let best = orthogonalRoute(a, b);
|
|
959
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
960
|
+
for (const start of portsOf(a)) {
|
|
961
|
+
for (const end of portsOf(b)) {
|
|
962
|
+
const sourcePort = start.point;
|
|
963
|
+
const targetPort = end.point;
|
|
964
|
+
const sp = portLeadPoint(start);
|
|
965
|
+
const ep = portLeadPoint(end);
|
|
966
|
+
const candidates = [
|
|
967
|
+
[sourcePort, sp, [ep[0], sp[1]], ep, targetPort],
|
|
968
|
+
[sourcePort, sp, [sp[0], ep[1]], ep, targetPort]
|
|
969
|
+
];
|
|
970
|
+
if (Math.abs(sp[0] - ep[0]) < EPS || Math.abs(sp[1] - ep[1]) < EPS) {
|
|
971
|
+
candidates.push([sourcePort, sp, ep, targetPort]);
|
|
972
|
+
}
|
|
973
|
+
for (const x of lanes.xs) {
|
|
974
|
+
candidates.push([sourcePort, sp, [x, sp[1]], [x, ep[1]], ep, targetPort]);
|
|
975
|
+
}
|
|
976
|
+
for (const y of lanes.ys) {
|
|
977
|
+
candidates.push([sourcePort, sp, [sp[0], y], [ep[0], y], ep, targetPort]);
|
|
978
|
+
}
|
|
979
|
+
for (const x of lanes.xs) {
|
|
980
|
+
for (const y of lanes.ys) {
|
|
981
|
+
candidates.push([sourcePort, sp, [x, sp[1]], [x, y], [ep[0], y], ep, targetPort]);
|
|
982
|
+
candidates.push([sourcePort, sp, [sp[0], y], [x, y], [x, ep[1]], ep, targetPort]);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
for (const candidate of candidates) {
|
|
986
|
+
const normalized = normalizeRoute(candidate);
|
|
987
|
+
if (!isOrthogonalRoute(normalized)) continue;
|
|
988
|
+
if (!hasArrowTerminalClearance(normalized, op)) continue;
|
|
989
|
+
if (bounds && !routeFitsBounds(normalized, bounds)) continue;
|
|
990
|
+
if (routeOverlapsAnyBorder(normalized, [a, b], 0)) continue;
|
|
991
|
+
if (routeIntersectsAnyInterior(normalized, endpointInteriorBarriers, 0)) continue;
|
|
992
|
+
if (routeIntersectsAnyInterior(normalized, realLeafObstacles, OBSTACLE_PAD)) continue;
|
|
993
|
+
if (routeIntersectsAnyInterior(normalized, hardLeafObstacles, OBSTACLE_PAD)) continue;
|
|
994
|
+
if (strictEdgeGuards && routeSharesAnyEdgeGuardLane(normalized, edgeGuardObstacles)) continue;
|
|
995
|
+
if (routeIntersectsAnyInterior(normalized, blockedContainerObstacles, OBSTACLE_PAD)) continue;
|
|
996
|
+
if (routeOverlapsAnyBorder(normalized, [
|
|
997
|
+
...endpointBoundaries,
|
|
998
|
+
...passableContainerObstacles,
|
|
999
|
+
...blockedContainerObstacles
|
|
1000
|
+
], BORDER_CLEARANCE)) continue;
|
|
1001
|
+
const score = scoreRoute(
|
|
1002
|
+
normalized,
|
|
1003
|
+
a,
|
|
1004
|
+
b,
|
|
1005
|
+
leafObstacles,
|
|
1006
|
+
blockedContainerObstacles,
|
|
1007
|
+
passableContainerObstacles,
|
|
1008
|
+
externalContainerObstacles,
|
|
1009
|
+
endpointBoundaries
|
|
1010
|
+
) + portPairPenalty(a, b, start.side, end.side, op) + start.offsetPenalty + end.offsetPenalty;
|
|
1011
|
+
if (score < bestScore) {
|
|
1012
|
+
best = normalized;
|
|
1013
|
+
bestScore = score;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
if (bestScore === Number.POSITIVE_INFINITY) {
|
|
1019
|
+
if (strictEdgeGuards && edgeGuardObstacles.length > 0) {
|
|
1020
|
+
return avoidObstaclesRoute(
|
|
1021
|
+
a,
|
|
1022
|
+
b,
|
|
1023
|
+
from,
|
|
1024
|
+
to,
|
|
1025
|
+
obstacles,
|
|
1026
|
+
parentMap,
|
|
1027
|
+
bounds,
|
|
1028
|
+
endpointBoundaries,
|
|
1029
|
+
endpointInteriorBarriers,
|
|
1030
|
+
op,
|
|
1031
|
+
false
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
return orthogonalRoute(a, b);
|
|
1035
|
+
}
|
|
1036
|
+
return best;
|
|
1037
|
+
}
|
|
1038
|
+
function preferredStraightRoute(a, b) {
|
|
1039
|
+
const acx = a.x + a.w / 2;
|
|
1040
|
+
const acy = a.y + a.h / 2;
|
|
1041
|
+
const bcx = b.x + b.w / 2;
|
|
1042
|
+
const bcy = b.y + b.h / 2;
|
|
1043
|
+
if (Math.abs(acy - bcy) < EPS) {
|
|
1044
|
+
if (a.x + a.w <= b.x) return [[a.x + a.w, acy], [b.x, bcy]];
|
|
1045
|
+
if (b.x + b.w <= a.x) return [[a.x, acy], [b.x + b.w, bcy]];
|
|
1046
|
+
}
|
|
1047
|
+
if (Math.abs(acx - bcx) < EPS) {
|
|
1048
|
+
if (a.y + a.h <= b.y) return [[acx, a.y + a.h], [bcx, b.y]];
|
|
1049
|
+
if (b.y + b.h <= a.y) return [[acx, a.y], [bcx, b.y + b.h]];
|
|
1050
|
+
}
|
|
1051
|
+
return void 0;
|
|
1052
|
+
}
|
|
1053
|
+
function hasArrowTerminalClearance(points, op) {
|
|
1054
|
+
if (points.length < 2) return false;
|
|
1055
|
+
if (op !== "line" && op !== "dashed") {
|
|
1056
|
+
if (terminalSegmentLength(points, false) < arrowTerminalClearance(op)) return false;
|
|
1057
|
+
}
|
|
1058
|
+
if (op === "bidir") {
|
|
1059
|
+
if (terminalSegmentLength(points, true) < arrowTerminalClearance(op)) return false;
|
|
1060
|
+
}
|
|
1061
|
+
return true;
|
|
1062
|
+
}
|
|
1063
|
+
function terminalSegmentLength(points, atStart) {
|
|
1064
|
+
if (points.length < 2) return 0;
|
|
1065
|
+
const a = atStart ? points[0] : points[points.length - 1];
|
|
1066
|
+
const b = atStart ? points[1] : points[points.length - 2];
|
|
1067
|
+
return segmentLength(a, b);
|
|
1068
|
+
}
|
|
1069
|
+
function arrowTerminalClearance(op) {
|
|
1070
|
+
return op === "thick" ? THICK_ARROW_TERMINAL_CLEARANCE : ARROW_TERMINAL_CLEARANCE;
|
|
1071
|
+
}
|
|
1072
|
+
function routeIntersectsAnyInterior(points, boxes, pad) {
|
|
1073
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
1074
|
+
for (const box of boxes) {
|
|
1075
|
+
if (boxInteriorOverlap(points[i], points[i + 1], box, pad) > EPS) return true;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
return false;
|
|
1079
|
+
}
|
|
1080
|
+
function routeOverlapsAnyBorder(points, boxes, tolerance) {
|
|
1081
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
1082
|
+
for (const box of boxes) {
|
|
1083
|
+
if (boxBorderProximityOverlap(points[i], points[i + 1], box, tolerance) > EPS) return true;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
return false;
|
|
1087
|
+
}
|
|
1088
|
+
function routeSharesAnyEdgeGuardLane(points, guards) {
|
|
1089
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
1090
|
+
for (const guard of guards) {
|
|
1091
|
+
if (segmentSharesEdgeGuardLane(points[i], points[i + 1], guard)) return true;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
return false;
|
|
1095
|
+
}
|
|
1096
|
+
function segmentSharesEdgeGuardLane(a, b, guard) {
|
|
1097
|
+
const segmentVertical = Math.abs(a[0] - b[0]) < EPS;
|
|
1098
|
+
const segmentHorizontal = Math.abs(a[1] - b[1]) < EPS;
|
|
1099
|
+
if (segmentVertical && guard.orientation === "vertical") {
|
|
1100
|
+
const guardX = guard.x + guard.w / 2;
|
|
1101
|
+
if (Math.abs(a[0] - guardX) > guard.w / 2 + EPS) return false;
|
|
1102
|
+
return intervalOverlap(a[1], b[1], guard.y, guard.y + guard.h) > EPS;
|
|
1103
|
+
}
|
|
1104
|
+
if (segmentHorizontal && guard.orientation === "horizontal") {
|
|
1105
|
+
const guardY = guard.y + guard.h / 2;
|
|
1106
|
+
if (Math.abs(a[1] - guardY) > guard.h / 2 + EPS) return false;
|
|
1107
|
+
return intervalOverlap(a[0], b[0], guard.x, guard.x + guard.w) > EPS;
|
|
1108
|
+
}
|
|
1109
|
+
return false;
|
|
1110
|
+
}
|
|
1111
|
+
function allowedContainerIds(from, to, parentMap) {
|
|
1112
|
+
return /* @__PURE__ */ new Set([from, to, ...ancestorsOf(from, parentMap), ...ancestorsOf(to, parentMap)]);
|
|
1113
|
+
}
|
|
1114
|
+
function portsOf(box) {
|
|
1115
|
+
const cx = box.x + box.w / 2;
|
|
1116
|
+
const cy = box.y + box.h / 2;
|
|
1117
|
+
const xOffset = Math.min(box.w * 0.25, Math.max(3, box.w / 2 - 4));
|
|
1118
|
+
const yOffset = Math.min(box.h * 0.25, Math.max(2, box.h / 2 - 3));
|
|
1119
|
+
const sidePenalty = 9;
|
|
1120
|
+
return [
|
|
1121
|
+
{ point: [box.x + box.w, cy], side: "right", offsetPenalty: 0 },
|
|
1122
|
+
{ point: [box.x, cy], side: "left", offsetPenalty: 0 },
|
|
1123
|
+
{ point: [cx, box.y + box.h], side: "bottom", offsetPenalty: 0 },
|
|
1124
|
+
{ point: [cx, box.y], side: "top", offsetPenalty: 0 },
|
|
1125
|
+
{ point: [box.x + box.w, cy - yOffset], side: "right", offsetPenalty: sidePenalty },
|
|
1126
|
+
{ point: [box.x + box.w, cy + yOffset], side: "right", offsetPenalty: sidePenalty },
|
|
1127
|
+
{ point: [box.x, cy - yOffset], side: "left", offsetPenalty: sidePenalty },
|
|
1128
|
+
{ point: [box.x, cy + yOffset], side: "left", offsetPenalty: sidePenalty },
|
|
1129
|
+
{ point: [cx - xOffset, box.y + box.h], side: "bottom", offsetPenalty: sidePenalty },
|
|
1130
|
+
{ point: [cx + xOffset, box.y + box.h], side: "bottom", offsetPenalty: sidePenalty },
|
|
1131
|
+
{ point: [cx - xOffset, box.y], side: "top", offsetPenalty: sidePenalty },
|
|
1132
|
+
{ point: [cx + xOffset, box.y], side: "top", offsetPenalty: sidePenalty }
|
|
1133
|
+
];
|
|
1134
|
+
}
|
|
1135
|
+
function portLeadPoint(port) {
|
|
1136
|
+
const [x, y] = port.point;
|
|
1137
|
+
switch (port.side) {
|
|
1138
|
+
case "right":
|
|
1139
|
+
return [x + PORT_STUB, y];
|
|
1140
|
+
case "left":
|
|
1141
|
+
return [x - PORT_STUB, y];
|
|
1142
|
+
case "bottom":
|
|
1143
|
+
return [x, y + PORT_STUB];
|
|
1144
|
+
case "top":
|
|
1145
|
+
return [x, y - PORT_STUB];
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
function portPairPenalty(source, target, start, end, op) {
|
|
1149
|
+
const sx = source.x + source.w / 2;
|
|
1150
|
+
const sy = source.y + source.h / 2;
|
|
1151
|
+
const tx = target.x + target.w / 2;
|
|
1152
|
+
const ty = target.y + target.h / 2;
|
|
1153
|
+
const dx = tx - sx;
|
|
1154
|
+
const dy = ty - sy;
|
|
1155
|
+
if (op === "dashed") return dashedPortPairPenalty(start, end, dx, dy, source, target);
|
|
1156
|
+
return sourcePortPenalty(start, dx, dy, source) + targetPortPenalty(end, dx, dy, target);
|
|
1157
|
+
}
|
|
1158
|
+
function dashedPortPairPenalty(start, end, dx, dy, source, target) {
|
|
1159
|
+
if (Math.abs(dy) > Math.min(source.h, target.h) * 0.8) {
|
|
1160
|
+
const startDesired = dy > 0 ? "bottom" : "top";
|
|
1161
|
+
const endDesired = dy > 0 ? "top" : "bottom";
|
|
1162
|
+
return dashedSidePenalty(start, startDesired) + dashedSidePenalty(end, endDesired);
|
|
1163
|
+
}
|
|
1164
|
+
if (Math.abs(dx) > Math.min(source.w, target.w) * 0.8) {
|
|
1165
|
+
const startDesired = dx > 0 ? "right" : "left";
|
|
1166
|
+
const endDesired = dx > 0 ? "left" : "right";
|
|
1167
|
+
return dashedSidePenalty(start, startDesired) + dashedSidePenalty(end, endDesired);
|
|
1168
|
+
}
|
|
1169
|
+
return 0;
|
|
1170
|
+
}
|
|
1171
|
+
function dashedSidePenalty(side, desired) {
|
|
1172
|
+
if (side === desired) return 0;
|
|
1173
|
+
if (side === oppositeSide(desired)) return 1400;
|
|
1174
|
+
return 450;
|
|
1175
|
+
}
|
|
1176
|
+
function sourcePortPenalty(side, dx, dy, box) {
|
|
1177
|
+
if (Math.abs(dy) > box.h * 0.8 && Math.abs(dy) >= Math.abs(dx) * VERTICAL_PORT_RATIO) {
|
|
1178
|
+
const desired = dy > 0 ? "bottom" : "top";
|
|
1179
|
+
if (side === desired) return 0;
|
|
1180
|
+
return side === oppositeSide(desired) ? 800 : 180;
|
|
1181
|
+
}
|
|
1182
|
+
if (Math.abs(dx) > box.w * 0.5) {
|
|
1183
|
+
const desired = dx > 0 ? "right" : "left";
|
|
1184
|
+
if (side === desired) return 0;
|
|
1185
|
+
return side === oppositeSide(desired) ? 900 : 90;
|
|
1186
|
+
}
|
|
1187
|
+
if (Math.abs(dy) > box.h * 0.8) {
|
|
1188
|
+
const desired = dy > 0 ? "bottom" : "top";
|
|
1189
|
+
if (side === desired) return 0;
|
|
1190
|
+
return side === oppositeSide(desired) ? 600 : 120;
|
|
1191
|
+
}
|
|
1192
|
+
return 0;
|
|
1193
|
+
}
|
|
1194
|
+
function targetPortPenalty(side, dx, dy, box) {
|
|
1195
|
+
if (Math.abs(dy) > box.h * 0.8 && Math.abs(dy) >= Math.abs(dx) * VERTICAL_PORT_RATIO) {
|
|
1196
|
+
const desired = dy > 0 ? "top" : "bottom";
|
|
1197
|
+
if (side === desired) return 0;
|
|
1198
|
+
return side === oppositeSide(desired) ? 1400 : 360;
|
|
1199
|
+
}
|
|
1200
|
+
if (Math.abs(dx) > box.w * 0.5 && Math.abs(dx) >= Math.abs(dy) * 1.2) {
|
|
1201
|
+
const desired = dx > 0 ? "left" : "right";
|
|
1202
|
+
if (side === desired) return 0;
|
|
1203
|
+
return side === oppositeSide(desired) ? 1200 : 220;
|
|
1204
|
+
}
|
|
1205
|
+
if (Math.abs(dy) > box.h * 0.35) {
|
|
1206
|
+
const desired = dy > 0 ? "top" : "bottom";
|
|
1207
|
+
if (side === desired) return 0;
|
|
1208
|
+
return side === "left" || side === "right" ? 9e3 : 1200;
|
|
1209
|
+
}
|
|
1210
|
+
if (Math.abs(dx) > box.w * 0.5) {
|
|
1211
|
+
const desired = dx > 0 ? "left" : "right";
|
|
1212
|
+
if (side === desired) return 0;
|
|
1213
|
+
return side === oppositeSide(desired) ? 1e3 : 160;
|
|
1214
|
+
}
|
|
1215
|
+
return 0;
|
|
1216
|
+
}
|
|
1217
|
+
function oppositeSide(side) {
|
|
1218
|
+
switch (side) {
|
|
1219
|
+
case "right":
|
|
1220
|
+
return "left";
|
|
1221
|
+
case "left":
|
|
1222
|
+
return "right";
|
|
1223
|
+
case "bottom":
|
|
1224
|
+
return "top";
|
|
1225
|
+
case "top":
|
|
1226
|
+
return "bottom";
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
function routeLanes(a, b, obstacles, bounds) {
|
|
1230
|
+
const boxes = [a, b, ...obstacles];
|
|
1231
|
+
const laneMargin = ROUTE_GAP * 2;
|
|
1232
|
+
const minX = bounds ? bounds.x : Math.max(MARGIN, Math.min(...boxes.map((o) => o.x)) - laneMargin);
|
|
1233
|
+
const maxX = bounds ? bounds.x + bounds.w : Math.max(...boxes.map((o) => o.x + o.w)) + laneMargin;
|
|
1234
|
+
const minY = bounds ? bounds.y : Math.max(MARGIN, Math.min(...boxes.map((o) => o.y)) - laneMargin);
|
|
1235
|
+
const maxY = bounds ? bounds.y + bounds.h : Math.max(...boxes.map((o) => o.y + o.h)) + laneMargin;
|
|
1236
|
+
const xs = [(a.x + a.w / 2 + b.x + b.w / 2) / 2];
|
|
1237
|
+
const ys = [(a.y + a.h / 2 + b.y + b.h / 2) / 2];
|
|
1238
|
+
for (const box of boxes) {
|
|
1239
|
+
for (const gap of [ROUTE_GAP, ROUTE_GAP * 1.5, ROUTE_GAP * 2]) {
|
|
1240
|
+
xs.push(box.x - gap, box.x + box.w + gap);
|
|
1241
|
+
ys.push(box.y - gap, box.y + box.h + gap);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
if (bounds) {
|
|
1245
|
+
xs.push(bounds.x, bounds.x + bounds.w);
|
|
1246
|
+
ys.push(bounds.y, bounds.y + bounds.h);
|
|
1247
|
+
}
|
|
1248
|
+
return {
|
|
1249
|
+
xs: limitRouteLanes(
|
|
1250
|
+
uniqueSorted(xs.filter((x) => x >= minX && x <= maxX)),
|
|
1251
|
+
(a.x + a.w / 2 + b.x + b.w / 2) / 2
|
|
1252
|
+
),
|
|
1253
|
+
ys: limitRouteLanes(
|
|
1254
|
+
uniqueSorted(ys.filter((y) => y >= minY && y <= maxY)),
|
|
1255
|
+
(a.y + a.h / 2 + b.y + b.h / 2) / 2
|
|
1256
|
+
)
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
function limitRouteLanes(values, anchor) {
|
|
1260
|
+
if (values.length <= MAX_ROUTE_LANES) return values;
|
|
1261
|
+
const keep = /* @__PURE__ */ new Set([values[0], values[values.length - 1]]);
|
|
1262
|
+
for (const value of [...values].sort((a, b) => Math.abs(a - anchor) - Math.abs(b - anchor))) {
|
|
1263
|
+
keep.add(value);
|
|
1264
|
+
if (keep.size >= MAX_ROUTE_LANES) break;
|
|
1265
|
+
}
|
|
1266
|
+
return [...keep].sort((a, b) => a - b);
|
|
1267
|
+
}
|
|
1268
|
+
function commonRoutingBounds(from, to, parentMap, positions) {
|
|
1269
|
+
const common = nearestCommonAncestor(from, to, parentMap);
|
|
1270
|
+
if (!common) return void 0;
|
|
1271
|
+
const box = positions.get(common);
|
|
1272
|
+
if (!box) return void 0;
|
|
1273
|
+
const inset = ROUTE_GAP / 2;
|
|
1274
|
+
const top = box.y + TITLE_H + PAD / 2;
|
|
1275
|
+
const bottom = box.y + box.h - inset;
|
|
1276
|
+
return {
|
|
1277
|
+
x: box.x + inset,
|
|
1278
|
+
y: top,
|
|
1279
|
+
w: Math.max(0, box.w - inset * 2),
|
|
1280
|
+
h: Math.max(0, bottom - top)
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
function nearestCommonAncestor(from, to, parentMap) {
|
|
1284
|
+
const toAncestors = new Set(ancestorsOf(to, parentMap));
|
|
1285
|
+
return ancestorsOf(from, parentMap).find((id) => toAncestors.has(id));
|
|
1286
|
+
}
|
|
1287
|
+
function ancestorsOf(id, parentMap) {
|
|
1288
|
+
const out = [];
|
|
1289
|
+
let cur = id;
|
|
1290
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1291
|
+
while (parentMap.has(cur) && !seen.has(cur)) {
|
|
1292
|
+
seen.add(cur);
|
|
1293
|
+
cur = parentMap.get(cur);
|
|
1294
|
+
out.push(cur);
|
|
1295
|
+
}
|
|
1296
|
+
return out;
|
|
1297
|
+
}
|
|
1298
|
+
function routeFitsBounds(points, bounds) {
|
|
1299
|
+
const right = bounds.x + bounds.w;
|
|
1300
|
+
const bottom = bounds.y + bounds.h;
|
|
1301
|
+
return points.every(([x, y]) => x >= bounds.x - EPS && x <= right + EPS && y >= bounds.y - EPS && y <= bottom + EPS);
|
|
1302
|
+
}
|
|
1303
|
+
function uniqueSorted(values) {
|
|
1304
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1305
|
+
const out = [];
|
|
1306
|
+
for (const value of values) {
|
|
1307
|
+
const rounded = Math.round(value * 1e3) / 1e3;
|
|
1308
|
+
const key = rounded.toFixed(3);
|
|
1309
|
+
if (seen.has(key)) continue;
|
|
1310
|
+
seen.add(key);
|
|
1311
|
+
out.push(rounded);
|
|
1312
|
+
}
|
|
1313
|
+
return out.sort((a, b) => a - b);
|
|
1314
|
+
}
|
|
1315
|
+
function normalizeRoute(points) {
|
|
1316
|
+
const deduped = [];
|
|
1317
|
+
for (const point of points) {
|
|
1318
|
+
const prev = deduped[deduped.length - 1];
|
|
1319
|
+
if (!prev || Math.abs(prev[0] - point[0]) >= EPS || Math.abs(prev[1] - point[1]) >= EPS) {
|
|
1320
|
+
deduped.push(point);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
const out = [];
|
|
1324
|
+
for (const point of deduped) {
|
|
1325
|
+
out.push(point);
|
|
1326
|
+
while (out.length >= 3) {
|
|
1327
|
+
const a = out[out.length - 3];
|
|
1328
|
+
const b = out[out.length - 2];
|
|
1329
|
+
const c = out[out.length - 1];
|
|
1330
|
+
const sameX = Math.abs(a[0] - b[0]) < EPS && Math.abs(b[0] - c[0]) < EPS;
|
|
1331
|
+
const sameY = Math.abs(a[1] - b[1]) < EPS && Math.abs(b[1] - c[1]) < EPS;
|
|
1332
|
+
if (!sameX && !sameY) break;
|
|
1333
|
+
out.splice(out.length - 2, 1);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
return out;
|
|
1337
|
+
}
|
|
1338
|
+
function isOrthogonalRoute(points) {
|
|
1339
|
+
if (points.length < 2) return false;
|
|
1340
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
1341
|
+
const a = points[i];
|
|
1342
|
+
const b = points[i + 1];
|
|
1343
|
+
if (Math.abs(a[0] - b[0]) >= EPS && Math.abs(a[1] - b[1]) >= EPS) return false;
|
|
1344
|
+
}
|
|
1345
|
+
return true;
|
|
1346
|
+
}
|
|
1347
|
+
function scoreRoute(points, source, target, leafObstacles, blockedContainerObstacles, passableContainerObstacles, externalContainerObstacles, endpointBoundaries) {
|
|
1348
|
+
let score = 0;
|
|
1349
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
1350
|
+
const a = points[i];
|
|
1351
|
+
const b = points[i + 1];
|
|
1352
|
+
score += segmentLength(a, b);
|
|
1353
|
+
const sourceInterior = boxInteriorOverlap(a, b, source, 0);
|
|
1354
|
+
if (sourceInterior > 0) score += 22e4 + sourceInterior * 1500;
|
|
1355
|
+
const targetInterior = boxInteriorOverlap(a, b, target, 0);
|
|
1356
|
+
if (targetInterior > 0) score += 22e4 + targetInterior * 1500;
|
|
1357
|
+
const sourceBorder = boxBorderOverlap(a, b, source);
|
|
1358
|
+
if (sourceBorder > 0) score += 8e3 + sourceBorder * 120;
|
|
1359
|
+
const targetBorder = boxBorderOverlap(a, b, target);
|
|
1360
|
+
if (targetBorder > 0) score += 8e3 + targetBorder * 120;
|
|
1361
|
+
for (const boundary of endpointBoundaries) {
|
|
1362
|
+
const overlap = boxBorderOverlap(a, b, boundary);
|
|
1363
|
+
if (overlap > 0) score += 12e3 + overlap * 160;
|
|
1364
|
+
}
|
|
1365
|
+
for (const obstacle of leafObstacles) {
|
|
1366
|
+
const overlap = boxInteriorOverlap(a, b, obstacle, OBSTACLE_PAD);
|
|
1367
|
+
if (overlap > 0) score += leafObstaclePenalty(obstacle, a, b, overlap);
|
|
1368
|
+
}
|
|
1369
|
+
for (const obstacle of blockedContainerObstacles) {
|
|
1370
|
+
const overlap = boxInteriorOverlap(a, b, obstacle, OBSTACLE_PAD);
|
|
1371
|
+
if (overlap > 0) score += 18e4 + overlap * 1200;
|
|
1372
|
+
const border = boxBorderOverlap(a, b, obstacle);
|
|
1373
|
+
if (border > 0) score += 18e3 + border * 300;
|
|
1374
|
+
}
|
|
1375
|
+
for (const obstacle of passableContainerObstacles) {
|
|
1376
|
+
const titleOverlap = boxInteriorOverlap(a, b, containerTitleRoutingBand(obstacle), OBSTACLE_PAD);
|
|
1377
|
+
if (titleOverlap > 0) score += 14e3 + titleOverlap * 120;
|
|
1378
|
+
const overlap = boxBorderOverlap(a, b, obstacle);
|
|
1379
|
+
if (overlap > 0) score += 6e3 + overlap * 120;
|
|
1380
|
+
}
|
|
1381
|
+
for (const obstacle of externalContainerObstacles) {
|
|
1382
|
+
const overlap = boxInteriorOverlap(a, b, obstacle, 0);
|
|
1383
|
+
if (overlap > 0) score += overlap * 40;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
score += Math.max(0, points.length - 2) * 3;
|
|
1387
|
+
score += routeExcursionPenalty(points, source, target);
|
|
1388
|
+
return score;
|
|
1389
|
+
}
|
|
1390
|
+
function routeExcursionPenalty(points, source, target) {
|
|
1391
|
+
const pad = ROUTE_GAP * 5;
|
|
1392
|
+
const left = Math.min(source.x, target.x) - pad;
|
|
1393
|
+
const top = Math.min(source.y, target.y) - pad;
|
|
1394
|
+
const right = Math.max(source.x + source.w, target.x + target.w) + pad;
|
|
1395
|
+
const bottom = Math.max(source.y + source.h, target.y + target.h) + pad;
|
|
1396
|
+
let penalty = 0;
|
|
1397
|
+
for (const [x, y] of points) {
|
|
1398
|
+
if (x < left) penalty += (left - x) * 90;
|
|
1399
|
+
if (x > right) penalty += (x - right) * 90;
|
|
1400
|
+
if (y < top) penalty += (top - y) * 90;
|
|
1401
|
+
if (y > bottom) penalty += (y - bottom) * 90;
|
|
1402
|
+
}
|
|
1403
|
+
return penalty;
|
|
1404
|
+
}
|
|
1405
|
+
function leafObstaclePenalty(obstacle, a, b, overlap) {
|
|
1406
|
+
if (obstacle.id.startsWith("__arrow_guard_")) return 7e4 + overlap * 700;
|
|
1407
|
+
if (isEdgeGuardObstacle(obstacle)) return edgeGuardPenalty(obstacle, a, b, overlap);
|
|
1408
|
+
return 16e4 + overlap * 1200;
|
|
1409
|
+
}
|
|
1410
|
+
function edgeGuardPenalty(obstacle, a, b, overlap) {
|
|
1411
|
+
const segmentVertical = Math.abs(a[0] - b[0]) < EPS;
|
|
1412
|
+
const guardVertical = obstacle.orientation === "vertical";
|
|
1413
|
+
if (segmentVertical === guardVertical) return 85e3 + overlap * 1200;
|
|
1414
|
+
return 500 + overlap * 160;
|
|
1415
|
+
}
|
|
1416
|
+
function isEdgeGuardObstacle(obstacle) {
|
|
1417
|
+
return obstacle.id.startsWith("__edge_guard_") && "orientation" in obstacle && (obstacle.orientation === "vertical" || obstacle.orientation === "horizontal");
|
|
1418
|
+
}
|
|
1419
|
+
function containerTitleRoutingBand(container) {
|
|
1420
|
+
return {
|
|
1421
|
+
x: container.x,
|
|
1422
|
+
y: container.y,
|
|
1423
|
+
w: container.w,
|
|
1424
|
+
h: TITLE_H + PAD
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
function segmentLength(a, b) {
|
|
1428
|
+
return Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]);
|
|
1429
|
+
}
|
|
1430
|
+
function boxInteriorOverlap(a, b, box, pad) {
|
|
1431
|
+
const left = box.x - pad;
|
|
1432
|
+
const right = box.x + box.w + pad;
|
|
1433
|
+
const top = box.y - pad;
|
|
1434
|
+
const bottom = box.y + box.h + pad;
|
|
1435
|
+
if (Math.abs(a[0] - b[0]) < EPS) {
|
|
1436
|
+
const x = a[0];
|
|
1437
|
+
if (x <= left || x >= right) return 0;
|
|
1438
|
+
return intervalOverlap(a[1], b[1], top, bottom);
|
|
1439
|
+
}
|
|
1440
|
+
if (Math.abs(a[1] - b[1]) < EPS) {
|
|
1441
|
+
const y = a[1];
|
|
1442
|
+
if (y <= top || y >= bottom) return 0;
|
|
1443
|
+
return intervalOverlap(a[0], b[0], left, right);
|
|
1444
|
+
}
|
|
1445
|
+
return 0;
|
|
1446
|
+
}
|
|
1447
|
+
function boxBorderOverlap(a, b, box) {
|
|
1448
|
+
return boxBorderProximityOverlap(a, b, box, 0);
|
|
1449
|
+
}
|
|
1450
|
+
function boxBorderProximityOverlap(a, b, box, tolerance) {
|
|
1451
|
+
if (Math.abs(a[0] - b[0]) < EPS) {
|
|
1452
|
+
const x = a[0];
|
|
1453
|
+
if (Math.abs(x - box.x) > tolerance + EPS && Math.abs(x - (box.x + box.w)) > tolerance + EPS) return 0;
|
|
1454
|
+
return intervalOverlap(a[1], b[1], box.y, box.y + box.h);
|
|
1455
|
+
}
|
|
1456
|
+
if (Math.abs(a[1] - b[1]) < EPS) {
|
|
1457
|
+
const y = a[1];
|
|
1458
|
+
if (Math.abs(y - box.y) > tolerance + EPS && Math.abs(y - (box.y + box.h)) > tolerance + EPS) return 0;
|
|
1459
|
+
return intervalOverlap(a[0], b[0], box.x, box.x + box.w);
|
|
1460
|
+
}
|
|
1461
|
+
return 0;
|
|
1462
|
+
}
|
|
1463
|
+
function intervalOverlap(a1, a2, b1, b2) {
|
|
1464
|
+
const minA = Math.min(a1, a2);
|
|
1465
|
+
const maxA = Math.max(a1, a2);
|
|
1466
|
+
const minB = Math.min(b1, b2);
|
|
1467
|
+
const maxB = Math.max(b1, b2);
|
|
1468
|
+
return Math.max(0, Math.min(maxA, maxB) - Math.max(minA, minB));
|
|
1469
|
+
}
|
|
1470
|
+
function orthogonalRoute(a, b) {
|
|
1471
|
+
const ca = { x: a.x + a.w / 2, y: a.y + a.h / 2 };
|
|
1472
|
+
const cb = { x: b.x + b.w / 2, y: b.y + b.h / 2 };
|
|
1473
|
+
const dx = cb.x - ca.x;
|
|
1474
|
+
const dy = cb.y - ca.y;
|
|
1475
|
+
let pa, pb;
|
|
1476
|
+
const separatedHorizontally = a.x + a.w <= b.x || b.x + b.w <= a.x;
|
|
1477
|
+
const separatedVertically = a.y + a.h <= b.y || b.y + b.h <= a.y;
|
|
1478
|
+
const horizontal = separatedHorizontally ? true : separatedVertically ? false : Math.abs(dx) > Math.abs(dy);
|
|
1479
|
+
if (horizontal) {
|
|
1480
|
+
pa = [dx > 0 ? a.x + a.w : a.x, ca.y];
|
|
1481
|
+
pb = [dx > 0 ? b.x : b.x + b.w, cb.y];
|
|
1482
|
+
if (Math.abs(pa[1] - pb[1]) < 0.5) return [pa, pb];
|
|
1483
|
+
const midX = (pa[0] + pb[0]) / 2;
|
|
1484
|
+
return [pa, [midX, pa[1]], [midX, pb[1]], pb];
|
|
1485
|
+
} else {
|
|
1486
|
+
pa = [ca.x, dy > 0 ? a.y + a.h : a.y];
|
|
1487
|
+
pb = [cb.x, dy > 0 ? b.y : b.y + b.h];
|
|
1488
|
+
if (Math.abs(pa[0] - pb[0]) < 0.5) return [pa, pb];
|
|
1489
|
+
const midY = (pa[1] + pb[1]) / 2;
|
|
1490
|
+
return [pa, [pa[0], midY], [pb[0], midY], pb];
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// src/core/render.ts
|
|
1495
|
+
var NS = "http://www.w3.org/2000/svg";
|
|
1496
|
+
var FONT_FAMILY = '"Hiragino Sans", "Yu Gothic", "Noto Sans CJK JP", sans-serif';
|
|
1497
|
+
var LABEL_OFFSET = 4;
|
|
1498
|
+
var LABEL_FONT_JA = 2.4;
|
|
1499
|
+
var LABEL_FONT_EN = 2.1;
|
|
1500
|
+
var LABEL_GAP = 0.6;
|
|
1501
|
+
var LABEL_OFFSET_STEPS = [0, 3.2, 6.4];
|
|
1502
|
+
var EPS2 = 1e-3;
|
|
1503
|
+
function render(laid, opts = {}) {
|
|
1504
|
+
const lang = opts.lang ?? "ja";
|
|
1505
|
+
const svg = el("svg", {
|
|
1506
|
+
xmlns: NS,
|
|
1507
|
+
viewBox: `0 0 ${laid.width} ${laid.height}`,
|
|
1508
|
+
"font-family": FONT_FAMILY,
|
|
1509
|
+
"shape-rendering": "geometricPrecision"
|
|
1510
|
+
});
|
|
1511
|
+
const containers = laid.nodes.filter((n) => n.isContainer);
|
|
1512
|
+
const leaves = laid.nodes.filter((n) => !n.isContainer);
|
|
1513
|
+
const labelBoxes = [];
|
|
1514
|
+
for (const c of containers) svg.appendChild(renderContainer(c, lang));
|
|
1515
|
+
for (const e of laid.edges) svg.appendChild(renderEdge(e, lang, laid, labelBoxes));
|
|
1516
|
+
for (const e of laid.edges) svg.appendChild(renderEdgeHeads(e));
|
|
1517
|
+
for (const n of leaves) svg.appendChild(renderNode(n, lang));
|
|
1518
|
+
return svg;
|
|
1519
|
+
}
|
|
1520
|
+
function renderContainer(n, lang) {
|
|
1521
|
+
const g = el("g");
|
|
1522
|
+
g.appendChild(el("rect", {
|
|
1523
|
+
x: n.x,
|
|
1524
|
+
y: n.y,
|
|
1525
|
+
width: n.w,
|
|
1526
|
+
height: n.h,
|
|
1527
|
+
fill: "white",
|
|
1528
|
+
stroke: "#000",
|
|
1529
|
+
"stroke-width": 0.3
|
|
1530
|
+
}));
|
|
1531
|
+
const labels = pickLabel(n.label, lang);
|
|
1532
|
+
if (labels.length || n.id) {
|
|
1533
|
+
const t = el("text", {
|
|
1534
|
+
x: n.x + 2,
|
|
1535
|
+
y: n.y + 3.5,
|
|
1536
|
+
"font-size": 2.6,
|
|
1537
|
+
fill: "#000"
|
|
1538
|
+
});
|
|
1539
|
+
t.textContent = (n.id ? n.id + " " : "") + (labels[0] ?? "");
|
|
1540
|
+
g.appendChild(t);
|
|
1541
|
+
}
|
|
1542
|
+
return g;
|
|
1543
|
+
}
|
|
1544
|
+
function renderNode(n, lang) {
|
|
1545
|
+
const g = el("g");
|
|
1546
|
+
g.appendChild(renderShape(n));
|
|
1547
|
+
const lines = [];
|
|
1548
|
+
if (n.id && n.id !== "*") lines.push(n.id);
|
|
1549
|
+
const labels = pickLabel(n.label, lang);
|
|
1550
|
+
for (const l of labels) lines.push(l);
|
|
1551
|
+
if (lines.length === 0) return g;
|
|
1552
|
+
const fontSize = 2.8;
|
|
1553
|
+
const lineH = fontSize * 1.2;
|
|
1554
|
+
const totalH = lines.length * lineH;
|
|
1555
|
+
const startY = n.y + n.h / 2 - totalH / 2 + lineH * 0.8;
|
|
1556
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1557
|
+
const t = el("text", {
|
|
1558
|
+
x: n.x + n.w / 2,
|
|
1559
|
+
y: startY + i * lineH,
|
|
1560
|
+
"font-size": fontSize,
|
|
1561
|
+
fill: "#000",
|
|
1562
|
+
"text-anchor": "middle"
|
|
1563
|
+
});
|
|
1564
|
+
t.textContent = lines[i];
|
|
1565
|
+
g.appendChild(t);
|
|
1566
|
+
}
|
|
1567
|
+
return g;
|
|
1568
|
+
}
|
|
1569
|
+
function renderShape(n) {
|
|
1570
|
+
const stroke = "#000";
|
|
1571
|
+
const fill = "white";
|
|
1572
|
+
const sw = 0.4;
|
|
1573
|
+
switch (n.shape) {
|
|
1574
|
+
case "round":
|
|
1575
|
+
return el("rect", {
|
|
1576
|
+
x: n.x,
|
|
1577
|
+
y: n.y,
|
|
1578
|
+
width: n.w,
|
|
1579
|
+
height: n.h,
|
|
1580
|
+
rx: Math.min(n.w, n.h) / 2,
|
|
1581
|
+
ry: Math.min(n.w, n.h) / 2,
|
|
1582
|
+
fill,
|
|
1583
|
+
stroke,
|
|
1584
|
+
"stroke-width": sw
|
|
1585
|
+
});
|
|
1586
|
+
case "circle": {
|
|
1587
|
+
const r = Math.min(n.w, n.h) / 2;
|
|
1588
|
+
return el("circle", {
|
|
1589
|
+
cx: n.x + n.w / 2,
|
|
1590
|
+
cy: n.y + n.h / 2,
|
|
1591
|
+
r,
|
|
1592
|
+
fill: "#000",
|
|
1593
|
+
stroke
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
case "diamond": {
|
|
1597
|
+
const cx = n.x + n.w / 2;
|
|
1598
|
+
const cy = n.y + n.h / 2;
|
|
1599
|
+
const pts = [[cx, n.y], [n.x + n.w, cy], [cx, n.y + n.h], [n.x, cy]].map((p) => p.join(",")).join(" ");
|
|
1600
|
+
return el("polygon", { points: pts, fill, stroke, "stroke-width": sw });
|
|
1601
|
+
}
|
|
1602
|
+
case "actor":
|
|
1603
|
+
case "box":
|
|
1604
|
+
default:
|
|
1605
|
+
return el("rect", {
|
|
1606
|
+
x: n.x,
|
|
1607
|
+
y: n.y,
|
|
1608
|
+
width: n.w,
|
|
1609
|
+
height: n.h,
|
|
1610
|
+
fill,
|
|
1611
|
+
stroke,
|
|
1612
|
+
"stroke-width": sw
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
function renderEdge(e, lang, laid, labelBoxes) {
|
|
1617
|
+
const g = el("g");
|
|
1618
|
+
if (e.points.length < 2) return g;
|
|
1619
|
+
const bodyPoints = edgeBodyPoints(e);
|
|
1620
|
+
if (bodyPoints.length < 2) return g;
|
|
1621
|
+
const d = bodyPoints.map((p, i) => i === 0 ? `M ${p[0]} ${p[1]}` : `L ${p[0]} ${p[1]}`).join(" ");
|
|
1622
|
+
const path = el("path", {
|
|
1623
|
+
d,
|
|
1624
|
+
fill: "none",
|
|
1625
|
+
stroke: "#000",
|
|
1626
|
+
"stroke-width": strokeWidth(e.op),
|
|
1627
|
+
"stroke-linecap": "butt",
|
|
1628
|
+
"stroke-linejoin": "miter"
|
|
1629
|
+
});
|
|
1630
|
+
const dash = strokeDash(e.op);
|
|
1631
|
+
if (dash) path.setAttribute("stroke-dasharray", dash);
|
|
1632
|
+
g.appendChild(path);
|
|
1633
|
+
const labels = pickLabel(e.label, lang);
|
|
1634
|
+
if (labels.length) {
|
|
1635
|
+
const ja = labels[0];
|
|
1636
|
+
const en = labels[1];
|
|
1637
|
+
const placement = chooseLabelPlacement(e, labels, laid, labelBoxes);
|
|
1638
|
+
labelBoxes.push(expandBox(placement.box, 0.8));
|
|
1639
|
+
const drawText = (text, x, y, fontSize, anchor, baseline) => {
|
|
1640
|
+
const t = el("text", {
|
|
1641
|
+
x,
|
|
1642
|
+
y,
|
|
1643
|
+
"font-size": fontSize,
|
|
1644
|
+
fill: "#000",
|
|
1645
|
+
"text-anchor": anchor,
|
|
1646
|
+
"dominant-baseline": baseline
|
|
1647
|
+
});
|
|
1648
|
+
t.textContent = text;
|
|
1649
|
+
g.appendChild(t);
|
|
1650
|
+
};
|
|
1651
|
+
drawText(ja, placement.ja.x, placement.ja.y, LABEL_FONT_JA, placement.anchor, placement.ja.baseline);
|
|
1652
|
+
if (en && placement.en) {
|
|
1653
|
+
drawText(en, placement.en.x, placement.en.y, LABEL_FONT_EN, placement.anchor, placement.en.baseline);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
return g;
|
|
1657
|
+
}
|
|
1658
|
+
function renderEdgeHeads(e) {
|
|
1659
|
+
const g = el("g");
|
|
1660
|
+
if (e.isLifeline) return g;
|
|
1661
|
+
const endHead = arrowHeadFor(e, false);
|
|
1662
|
+
const startHead = arrowHeadFor(e, true);
|
|
1663
|
+
if (endHead) g.appendChild(endHead);
|
|
1664
|
+
if (startHead) g.appendChild(startHead);
|
|
1665
|
+
return g;
|
|
1666
|
+
}
|
|
1667
|
+
function strokeWidth(op) {
|
|
1668
|
+
return op === "thick" ? 0.7 : 0.4;
|
|
1669
|
+
}
|
|
1670
|
+
function strokeDash(op) {
|
|
1671
|
+
return op === "dashed" || op === "dashed-arrow" ? "1.4 1.2" : null;
|
|
1672
|
+
}
|
|
1673
|
+
function edgeBodyPoints(e) {
|
|
1674
|
+
let points = e.points.map((p) => [p[0], p[1]]);
|
|
1675
|
+
if (e.op !== "line" && e.op !== "dashed") {
|
|
1676
|
+
points = trimPolyline(points, false, arrowLength(e.op));
|
|
1677
|
+
}
|
|
1678
|
+
if (e.op === "bidir") {
|
|
1679
|
+
points = trimPolyline(points, true, arrowLength(e.op));
|
|
1680
|
+
}
|
|
1681
|
+
return points;
|
|
1682
|
+
}
|
|
1683
|
+
function trimPolyline(points, atStart, amount) {
|
|
1684
|
+
if (points.length < 2 || amount <= 0) return points;
|
|
1685
|
+
const out = atStart ? [...points].reverse() : [...points];
|
|
1686
|
+
let remaining = amount;
|
|
1687
|
+
while (out.length >= 2 && remaining > 0) {
|
|
1688
|
+
const tip = out[out.length - 1];
|
|
1689
|
+
const prev = out[out.length - 2];
|
|
1690
|
+
const dx = prev[0] - tip[0];
|
|
1691
|
+
const dy = prev[1] - tip[1];
|
|
1692
|
+
const len = Math.hypot(dx, dy);
|
|
1693
|
+
if (len < EPS2) {
|
|
1694
|
+
out.pop();
|
|
1695
|
+
continue;
|
|
1696
|
+
}
|
|
1697
|
+
if (len > remaining) {
|
|
1698
|
+
out[out.length - 1] = [
|
|
1699
|
+
tip[0] + dx / len * remaining,
|
|
1700
|
+
tip[1] + dy / len * remaining
|
|
1701
|
+
];
|
|
1702
|
+
remaining = 0;
|
|
1703
|
+
} else {
|
|
1704
|
+
out.pop();
|
|
1705
|
+
remaining -= len;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
return atStart ? out.reverse() : out;
|
|
1709
|
+
}
|
|
1710
|
+
function arrowLength(op) {
|
|
1711
|
+
return op === "thick" ? 3.4 : 2.8;
|
|
1712
|
+
}
|
|
1713
|
+
function arrowHeadFor(e, atStart) {
|
|
1714
|
+
if (e.points.length < 2) return null;
|
|
1715
|
+
if (atStart && e.op !== "bidir") return null;
|
|
1716
|
+
if (!atStart && (e.op === "line" || e.op === "dashed")) return null;
|
|
1717
|
+
const points = atStart ? e.points : [...e.points].reverse();
|
|
1718
|
+
return renderArrowHead(points[0], points[1], e.op === "thick");
|
|
1719
|
+
}
|
|
1720
|
+
function renderArrowHead(tip, next, bold) {
|
|
1721
|
+
const dx = next[0] - tip[0];
|
|
1722
|
+
const dy = next[1] - tip[1];
|
|
1723
|
+
const len = Math.hypot(dx, dy);
|
|
1724
|
+
if (len < 1e-3) return null;
|
|
1725
|
+
const ux = dx / len;
|
|
1726
|
+
const uy = dy / len;
|
|
1727
|
+
const arrowLen = bold ? 3.4 : 2.8;
|
|
1728
|
+
const halfW = bold ? 1.6 : 1.25;
|
|
1729
|
+
const bx = tip[0] + ux * arrowLen;
|
|
1730
|
+
const by = tip[1] + uy * arrowLen;
|
|
1731
|
+
const px = -uy;
|
|
1732
|
+
const py = ux;
|
|
1733
|
+
const pts = [
|
|
1734
|
+
tip,
|
|
1735
|
+
[bx + px * halfW, by + py * halfW],
|
|
1736
|
+
[bx - px * halfW, by - py * halfW]
|
|
1737
|
+
].map((p) => p.join(",")).join(" ");
|
|
1738
|
+
return el("polygon", {
|
|
1739
|
+
points: pts,
|
|
1740
|
+
fill: "#000",
|
|
1741
|
+
stroke: "#000",
|
|
1742
|
+
"stroke-width": 0
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
function el(tag, attrs = {}) {
|
|
1746
|
+
const node = document.createElementNS(NS, tag);
|
|
1747
|
+
for (const [k, v] of Object.entries(attrs)) node.setAttribute(k, String(v));
|
|
1748
|
+
return node;
|
|
1749
|
+
}
|
|
1750
|
+
function pickLabel(b, lang) {
|
|
1751
|
+
if (!b) return [];
|
|
1752
|
+
if (lang === "ja") return b.ja ? [b.ja] : b.en ? [b.en] : [];
|
|
1753
|
+
if (lang === "en") return b.en ? [b.en] : b.ja ? [b.ja] : [];
|
|
1754
|
+
const out = [];
|
|
1755
|
+
if (b.ja) out.push(b.ja);
|
|
1756
|
+
if (b.en) out.push(b.en);
|
|
1757
|
+
return out;
|
|
1758
|
+
}
|
|
1759
|
+
function chooseLabelPlacement(edge, labels, laid, occupiedLabels = []) {
|
|
1760
|
+
const candidates = [];
|
|
1761
|
+
const textW = Math.max(...labels.map((label, index) => estimateTextWidth(
|
|
1762
|
+
label,
|
|
1763
|
+
index === 0 ? LABEL_FONT_JA : LABEL_FONT_EN
|
|
1764
|
+
)));
|
|
1765
|
+
const textH = labels.length > 1 ? LABEL_FONT_JA + LABEL_FONT_EN + LABEL_GAP : LABEL_FONT_JA;
|
|
1766
|
+
for (let i = 0; i < edge.points.length - 1; i++) {
|
|
1767
|
+
const a = edge.points[i];
|
|
1768
|
+
const b = edge.points[i + 1];
|
|
1769
|
+
const dx = b[0] - a[0];
|
|
1770
|
+
const dy = b[1] - a[1];
|
|
1771
|
+
const len = Math.abs(dx) + Math.abs(dy);
|
|
1772
|
+
if (len < 8) continue;
|
|
1773
|
+
for (const t of [0.5, 0.35, 0.65, 0.22, 0.78]) {
|
|
1774
|
+
const x = a[0] + dx * t;
|
|
1775
|
+
const y = a[1] + dy * t;
|
|
1776
|
+
for (const offsetStep of LABEL_OFFSET_STEPS) {
|
|
1777
|
+
const offset = LABEL_OFFSET + offsetStep;
|
|
1778
|
+
const offsetPenalty = offsetStep * 9;
|
|
1779
|
+
if (Math.abs(dy) < EPS2) {
|
|
1780
|
+
candidates.push(makeHorizontalLabel(edge, laid, occupiedLabels, labels, textW, textH, a, b, x, y, "above", offset, offsetPenalty));
|
|
1781
|
+
candidates.push(makeHorizontalLabel(edge, laid, occupiedLabels, labels, textW, textH, a, b, x, y, "below", offset, offsetPenalty));
|
|
1782
|
+
} else if (Math.abs(dx) < EPS2) {
|
|
1783
|
+
candidates.push(makeVerticalLabel(edge, laid, occupiedLabels, labels, textW, textH, a, b, x, y, "right", offset, offsetPenalty));
|
|
1784
|
+
candidates.push(makeVerticalLabel(edge, laid, occupiedLabels, labels, textW, textH, a, b, x, y, "left", offset, offsetPenalty));
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
if (candidates.length === 0) {
|
|
1790
|
+
const [a, b] = longestSegment(edge.points);
|
|
1791
|
+
const x = (a[0] + b[0]) / 2;
|
|
1792
|
+
const y = (a[1] + b[1]) / 2;
|
|
1793
|
+
return Math.abs(b[1] - a[1]) >= Math.abs(b[0] - a[0]) ? makeVerticalLabel(edge, laid, occupiedLabels, labels, textW, textH, a, b, x, y, "right", LABEL_OFFSET, 0) : makeHorizontalLabel(edge, laid, occupiedLabels, labels, textW, textH, a, b, x, y, "above", LABEL_OFFSET, 0);
|
|
1794
|
+
}
|
|
1795
|
+
candidates.sort((a, b) => a.score - b.score);
|
|
1796
|
+
return candidates[0];
|
|
1797
|
+
}
|
|
1798
|
+
function makeHorizontalLabel(edge, laid, occupiedLabels, labels, textW, textH, lineA, lineB, x, lineY, side, offset, offsetPenalty) {
|
|
1799
|
+
const top = side === "above" ? lineY - offset - textH : lineY + offset;
|
|
1800
|
+
const box = { x: x - textW / 2, y: top, w: textW, h: textH };
|
|
1801
|
+
let jaY;
|
|
1802
|
+
let enY;
|
|
1803
|
+
if (labels.length > 1) {
|
|
1804
|
+
jaY = top + LABEL_FONT_JA;
|
|
1805
|
+
enY = jaY + LABEL_GAP + LABEL_FONT_EN;
|
|
1806
|
+
} else {
|
|
1807
|
+
jaY = top + LABEL_FONT_JA;
|
|
1808
|
+
}
|
|
1809
|
+
const placement = {
|
|
1810
|
+
box,
|
|
1811
|
+
lineA,
|
|
1812
|
+
lineB,
|
|
1813
|
+
score: 0,
|
|
1814
|
+
vertical: false,
|
|
1815
|
+
anchor: "middle",
|
|
1816
|
+
ja: { x, y: jaY, baseline: "alphabetic" },
|
|
1817
|
+
en: enY === void 0 ? void 0 : { x, y: enY, baseline: "alphabetic" }
|
|
1818
|
+
};
|
|
1819
|
+
placement.score = labelPlacementScore(placement, edge, laid, occupiedLabels) + (side === "above" ? 0 : 8) + offsetPenalty;
|
|
1820
|
+
return placement;
|
|
1821
|
+
}
|
|
1822
|
+
function makeVerticalLabel(edge, laid, occupiedLabels, labels, textW, textH, lineA, lineB, lineX, y, side, offset, offsetPenalty) {
|
|
1823
|
+
const x = side === "right" ? lineX + offset : lineX - offset;
|
|
1824
|
+
const box = {
|
|
1825
|
+
x: side === "right" ? x : x - textW,
|
|
1826
|
+
y: y - textH / 2,
|
|
1827
|
+
w: textW,
|
|
1828
|
+
h: textH
|
|
1829
|
+
};
|
|
1830
|
+
const anchor = side === "right" ? "start" : "end";
|
|
1831
|
+
const jaY = labels.length > 1 ? y - LABEL_FONT_JA / 2 - LABEL_GAP / 2 : y;
|
|
1832
|
+
const enY = labels.length > 1 ? y + LABEL_FONT_EN / 2 + LABEL_GAP / 2 : void 0;
|
|
1833
|
+
const placement = {
|
|
1834
|
+
box,
|
|
1835
|
+
lineA,
|
|
1836
|
+
lineB,
|
|
1837
|
+
score: 0,
|
|
1838
|
+
vertical: true,
|
|
1839
|
+
anchor,
|
|
1840
|
+
ja: { x, y: jaY, baseline: "middle" },
|
|
1841
|
+
en: enY === void 0 ? void 0 : { x, y: enY, baseline: "middle" }
|
|
1842
|
+
};
|
|
1843
|
+
placement.score = labelPlacementScore(placement, edge, laid, occupiedLabels) + (side === "right" ? 0 : 8) + offsetPenalty;
|
|
1844
|
+
return placement;
|
|
1845
|
+
}
|
|
1846
|
+
function labelPlacementScore(placement, edge, laid, occupiedLabels) {
|
|
1847
|
+
let score = placement.vertical ? 5 : 0;
|
|
1848
|
+
const box = placement.box;
|
|
1849
|
+
if (box.x < 0) score += Math.abs(box.x) * 300;
|
|
1850
|
+
if (box.y < 0) score += Math.abs(box.y) * 300;
|
|
1851
|
+
if (box.x + box.w > laid.width) score += (box.x + box.w - laid.width) * 300;
|
|
1852
|
+
if (box.y + box.h > laid.height) score += (box.y + box.h - laid.height) * 300;
|
|
1853
|
+
for (const node of laid.nodes) {
|
|
1854
|
+
if (node.isContainer) {
|
|
1855
|
+
const title = { x: node.x, y: node.y, w: node.w, h: 6 };
|
|
1856
|
+
score += rectOverlapArea(box, title) * 900;
|
|
1857
|
+
score += rectBorderOverlapPenalty(box, node) * 1800;
|
|
1858
|
+
score += rectBorderBandOverlapArea(box, node, 1.8) * 9e3;
|
|
1859
|
+
} else {
|
|
1860
|
+
const bodyOverlap = rectOverlapArea(box, node);
|
|
1861
|
+
if (bodyOverlap > 0) score += 1e6 + bodyOverlap * 5e4;
|
|
1862
|
+
score += rectOverlapArea(box, expandBox(node, 1.8)) * 5200;
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
for (const other of laid.edges) {
|
|
1866
|
+
for (let i = 0; i < other.points.length - 1; i++) {
|
|
1867
|
+
const a = other.points[i];
|
|
1868
|
+
const b = other.points[i + 1];
|
|
1869
|
+
const overlap = segmentIntersectsRect(a, b, expandBox(box, 0.8));
|
|
1870
|
+
if (!overlap) continue;
|
|
1871
|
+
score += other === edge ? 35 : 380;
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
for (const occupied of occupiedLabels) {
|
|
1875
|
+
score += rectOverlapArea(box, occupied) * 18e3;
|
|
1876
|
+
}
|
|
1877
|
+
const segmentLen = Math.abs(placement.lineA[0] - placement.lineB[0]) + Math.abs(placement.lineA[1] - placement.lineB[1]);
|
|
1878
|
+
const labelSpan = placement.vertical ? box.h : box.w;
|
|
1879
|
+
score += Math.max(0, Math.max(36, labelSpan * 2.4) - segmentLen) * 80;
|
|
1880
|
+
const center = placement.vertical ? box.y + box.h / 2 : box.x + box.w / 2;
|
|
1881
|
+
const start = placement.vertical ? placement.lineA[1] : placement.lineA[0];
|
|
1882
|
+
const end = placement.vertical ? placement.lineB[1] : placement.lineB[0];
|
|
1883
|
+
const endpointDistance = Math.min(Math.abs(center - start), Math.abs(center - end));
|
|
1884
|
+
score += Math.max(0, 12 - endpointDistance) * 80;
|
|
1885
|
+
return score;
|
|
1886
|
+
}
|
|
1887
|
+
function estimateTextWidth(text, fontSize) {
|
|
1888
|
+
let width = 0;
|
|
1889
|
+
for (const char of text) {
|
|
1890
|
+
if (char === " ") width += fontSize * 0.35;
|
|
1891
|
+
else if (char.charCodeAt(0) <= 127) width += fontSize * 0.58;
|
|
1892
|
+
else width += fontSize;
|
|
1893
|
+
}
|
|
1894
|
+
return Math.max(fontSize * 2, width);
|
|
1895
|
+
}
|
|
1896
|
+
function longestSegment(pts) {
|
|
1897
|
+
let best = 0;
|
|
1898
|
+
let bestLen = -1;
|
|
1899
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
1900
|
+
const dx = pts[i + 1][0] - pts[i][0];
|
|
1901
|
+
const dy = pts[i + 1][1] - pts[i][1];
|
|
1902
|
+
const len = dx * dx + dy * dy;
|
|
1903
|
+
if (len > bestLen) {
|
|
1904
|
+
bestLen = len;
|
|
1905
|
+
best = i;
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
const a = pts[best];
|
|
1909
|
+
const b = pts[best + 1];
|
|
1910
|
+
return [a, b];
|
|
1911
|
+
}
|
|
1912
|
+
function expandBox(box, pad) {
|
|
1913
|
+
return { x: box.x - pad, y: box.y - pad, w: box.w + pad * 2, h: box.h + pad * 2 };
|
|
1914
|
+
}
|
|
1915
|
+
function rectOverlapArea(a, b) {
|
|
1916
|
+
const x = Math.max(0, Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x));
|
|
1917
|
+
const y = Math.max(0, Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y));
|
|
1918
|
+
return x * y;
|
|
1919
|
+
}
|
|
1920
|
+
function rectBorderOverlapPenalty(a, b) {
|
|
1921
|
+
const nearLeft = Math.abs(a.x - b.x) < 1 || Math.abs(a.x + a.w - b.x) < 1;
|
|
1922
|
+
const nearRight = Math.abs(a.x - (b.x + b.w)) < 1 || Math.abs(a.x + a.w - (b.x + b.w)) < 1;
|
|
1923
|
+
const nearTop = Math.abs(a.y - b.y) < 1 || Math.abs(a.y + a.h - b.y) < 1;
|
|
1924
|
+
const nearBottom = Math.abs(a.y - (b.y + b.h)) < 1 || Math.abs(a.y + a.h - (b.y + b.h)) < 1;
|
|
1925
|
+
return Number(nearLeft || nearRight || nearTop || nearBottom);
|
|
1926
|
+
}
|
|
1927
|
+
function rectBorderBandOverlapArea(a, b, band) {
|
|
1928
|
+
return rectOverlapArea(a, { x: b.x - band, y: b.y - band, w: band * 2, h: b.h + band * 2 }) + rectOverlapArea(a, { x: b.x + b.w - band, y: b.y - band, w: band * 2, h: b.h + band * 2 }) + rectOverlapArea(a, { x: b.x - band, y: b.y - band, w: b.w + band * 2, h: band * 2 }) + rectOverlapArea(a, { x: b.x - band, y: b.y + b.h - band, w: b.w + band * 2, h: band * 2 });
|
|
1929
|
+
}
|
|
1930
|
+
function segmentIntersectsRect(a, b, box) {
|
|
1931
|
+
const left = box.x;
|
|
1932
|
+
const right = box.x + box.w;
|
|
1933
|
+
const top = box.y;
|
|
1934
|
+
const bottom = box.y + box.h;
|
|
1935
|
+
if (Math.abs(a[0] - b[0]) < EPS2) {
|
|
1936
|
+
const x = a[0];
|
|
1937
|
+
if (x <= left || x >= right) return false;
|
|
1938
|
+
return intervalOverlap2(a[1], b[1], top, bottom) > EPS2;
|
|
1939
|
+
}
|
|
1940
|
+
if (Math.abs(a[1] - b[1]) < EPS2) {
|
|
1941
|
+
const y = a[1];
|
|
1942
|
+
if (y <= top || y >= bottom) return false;
|
|
1943
|
+
return intervalOverlap2(a[0], b[0], left, right) > EPS2;
|
|
1944
|
+
}
|
|
1945
|
+
return false;
|
|
1946
|
+
}
|
|
1947
|
+
function intervalOverlap2(a1, a2, b1, b2) {
|
|
1948
|
+
const minA = Math.min(a1, a2);
|
|
1949
|
+
const maxA = Math.max(a1, a2);
|
|
1950
|
+
const minB = Math.min(b1, b2);
|
|
1951
|
+
const maxB = Math.max(b1, b2);
|
|
1952
|
+
return Math.max(0, Math.min(maxA, maxB) - Math.max(minA, minB));
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
// src/core/refs.ts
|
|
1956
|
+
function refsToMarkdown(doc) {
|
|
1957
|
+
const ids = sortedIds(doc);
|
|
1958
|
+
const lines = [];
|
|
1959
|
+
lines.push("## \u7B26\u53F7\u306E\u8AAC\u660E / Reference Signs");
|
|
1960
|
+
lines.push("");
|
|
1961
|
+
lines.push("| \u7B26\u53F7 | \u540D\u79F0(\u65E5\u672C\u8A9E) | Name (English) |");
|
|
1962
|
+
lines.push("|------|---------------|----------------|");
|
|
1963
|
+
for (const id of ids) {
|
|
1964
|
+
const n = doc.nodes.get(id);
|
|
1965
|
+
lines.push(`| ${id} | ${n.label.ja ?? ""} | ${n.label.en ?? ""} |`);
|
|
1966
|
+
}
|
|
1967
|
+
return lines.join("\n");
|
|
1968
|
+
}
|
|
1969
|
+
function refsToCsv(doc) {
|
|
1970
|
+
const ids = sortedIds(doc);
|
|
1971
|
+
const lines = ["id,ja,en"];
|
|
1972
|
+
for (const id of ids) {
|
|
1973
|
+
const n = doc.nodes.get(id);
|
|
1974
|
+
lines.push(`${csv(id)},${csv(n.label.ja ?? "")},${csv(n.label.en ?? "")}`);
|
|
1975
|
+
}
|
|
1976
|
+
return lines.join("\n");
|
|
1977
|
+
}
|
|
1978
|
+
function csv(s) {
|
|
1979
|
+
return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
|
|
1980
|
+
}
|
|
1981
|
+
function sortedIds(doc) {
|
|
1982
|
+
const ids = [...doc.nodes.keys()].filter((id) => id !== "*");
|
|
1983
|
+
return ids.sort((a, b) => {
|
|
1984
|
+
const na = parseInt(a, 10);
|
|
1985
|
+
const nb = parseInt(b, 10);
|
|
1986
|
+
if (!isNaN(na) && !isNaN(nb)) return na - nb;
|
|
1987
|
+
return a.localeCompare(b);
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
// src/core/samples.ts
|
|
1992
|
+
var SAMPLE_ORDER = [
|
|
1993
|
+
"block",
|
|
1994
|
+
"system",
|
|
1995
|
+
"iot",
|
|
1996
|
+
"imagePipeline",
|
|
1997
|
+
"controlLoop",
|
|
1998
|
+
"flow",
|
|
1999
|
+
"state",
|
|
2000
|
+
"seq",
|
|
2001
|
+
"handshake"
|
|
2002
|
+
];
|
|
2003
|
+
var SAMPLES = {
|
|
2004
|
+
block: {
|
|
2005
|
+
label: "\u30D6\u30ED\u30C3\u30AF\u56F3",
|
|
2006
|
+
hint: "\u300C:\u300D\u3092\u4F7F\u3046\u3068\u30D6\u30ED\u30C3\u30AF\u56F3\u306B\u306A\u308B",
|
|
2007
|
+
source: `# \u30D6\u30ED\u30C3\u30AF\u56F3(\u88C5\u7F6E\u30AF\u30EC\u30FC\u30E0\u7528)
|
|
2008
|
+
# \u30D2\u30F3\u30C8:\u300C:\u300D\u3067\u5305\u542B\u95A2\u4FC2\u3092\u66F8\u304F \u2192 \u30D6\u30ED\u30C3\u30AF\u56F3\u3068\u5224\u5B9A\u3055\u308C\u308B
|
|
2009
|
+
|
|
2010
|
+
10 = \u5236\u5FA1\u88C5\u7F6E / control device
|
|
2011
|
+
11 = CPU
|
|
2012
|
+
12 = \u30E1\u30E2\u30EA / memory
|
|
2013
|
+
13 = "I/O \u30A4\u30F3\u30BF\u30FC\u30D5\u30A7\u30FC\u30B9" / "I/O interface"
|
|
2014
|
+
20 = \u5916\u90E8\u6A5F\u5668 / external device
|
|
2015
|
+
|
|
2016
|
+
10 : 11 12 13
|
|
2017
|
+
|
|
2018
|
+
11 - 12
|
|
2019
|
+
11 - 13
|
|
2020
|
+
13 -> 20 : \u4FE1\u53F7 / signal
|
|
2021
|
+
`
|
|
2022
|
+
},
|
|
2023
|
+
system: {
|
|
2024
|
+
label: "\u30B7\u30B9\u30C6\u30E0\u5168\u4F53",
|
|
2025
|
+
hint: "\u968E\u5C64\u69CB\u6210 + \u5916\u90E8\u63A5\u7D9A",
|
|
2026
|
+
source: `# \u30B7\u30B9\u30C6\u30E0\u5168\u4F53(\u968E\u5C64+\u5916\u90E8\u63A5\u7D9A)
|
|
2027
|
+
# \u30D2\u30F3\u30C8: \u5185\u90E8\u30D6\u30ED\u30C3\u30AF\u3068\u5916\u90E8\u88C5\u7F6E\u3092\u5206\u3051\u3066\u66F8\u304F\u3068\u3001\u7279\u8A31\u56F3\u9762\u3089\u3057\u3044\u69CB\u6210\u306B\u306A\u308B
|
|
2028
|
+
|
|
2029
|
+
100 = \u30B7\u30B9\u30C6\u30E0\u672C\u4F53 / Main system
|
|
2030
|
+
10 = \u5236\u5FA1\u90E8 / Control
|
|
2031
|
+
20 = \u901A\u4FE1\u90E8 / Comm
|
|
2032
|
+
11 = CPU
|
|
2033
|
+
12 = \u30E1\u30E2\u30EA / memory
|
|
2034
|
+
21 = \u7121\u7DDA\u90E8 / wireless
|
|
2035
|
+
22 = \u6709\u7DDA\u90E8 / wired
|
|
2036
|
+
30 = \u5916\u90E8\u30B5\u30FC\u30D0 / external server
|
|
2037
|
+
40 = \u5916\u90E8\u7AEF\u672B / external terminal
|
|
2038
|
+
|
|
2039
|
+
100 : 10 20
|
|
2040
|
+
10 : 11 12
|
|
2041
|
+
20 : 21 22
|
|
2042
|
+
|
|
2043
|
+
21 .> 40 : \u7121\u7DDA / wireless
|
|
2044
|
+
22 -> 30 : \u6709\u7DDA / wired
|
|
2045
|
+
30 <-> 40 : \u901A\u4FE1 / comm
|
|
2046
|
+
`
|
|
2047
|
+
},
|
|
2048
|
+
iot: {
|
|
2049
|
+
label: "IoT/\u30AF\u30E9\u30A6\u30C9",
|
|
2050
|
+
hint: "\u30BB\u30F3\u30B5\u7AEF\u672B\u30FB\u30B2\u30FC\u30C8\u30A6\u30A7\u30A4\u30FB\u30AF\u30E9\u30A6\u30C9\u306E\u5178\u578B\u69CB\u6210",
|
|
2051
|
+
source: `# IoT/\u30AF\u30E9\u30A6\u30C9\u69CB\u6210
|
|
2052
|
+
# \u30D2\u30F3\u30C8: \u7AEF\u672B\u5185\u90E8\u306E\u69CB\u6210\u3068\u30AF\u30E9\u30A6\u30C9\u5074\u306E\u69CB\u6210\u3092\u5225\u30B3\u30F3\u30C6\u30CA\u306B\u3059\u308B
|
|
2053
|
+
|
|
2054
|
+
100 = \u30BB\u30F3\u30B5\u7AEF\u672B / sensor terminal
|
|
2055
|
+
10 = \u691C\u51FA\u90E8 / detector
|
|
2056
|
+
11 = \u6E29\u5EA6\u30BB\u30F3\u30B5 / temperature sensor
|
|
2057
|
+
12 = \u52A0\u901F\u5EA6\u30BB\u30F3\u30B5 / acceleration sensor
|
|
2058
|
+
20 = \u51E6\u7406\u90E8 / processor
|
|
2059
|
+
21 = \u53D6\u5F97\u90E8 / acquisition unit
|
|
2060
|
+
22 = \u5224\u5B9A\u90E8 / determination unit
|
|
2061
|
+
30 = \u901A\u4FE1\u90E8 / communication unit
|
|
2062
|
+
200 = \u30B2\u30FC\u30C8\u30A6\u30A7\u30A4 / gateway
|
|
2063
|
+
300 = \u30AF\u30E9\u30A6\u30C9\u30B5\u30FC\u30D0 / cloud server
|
|
2064
|
+
310 = \u53D7\u4FE1\u90E8 / receiver
|
|
2065
|
+
320 = \u89E3\u6790\u90E8 / analyzer
|
|
2066
|
+
330 = \u8A18\u61B6\u90E8 / storage
|
|
2067
|
+
|
|
2068
|
+
100 : 10 20 30
|
|
2069
|
+
10 : 11 12
|
|
2070
|
+
20 : 21 22
|
|
2071
|
+
300 : 310 320 330
|
|
2072
|
+
|
|
2073
|
+
10 -> 20 : \u30BB\u30F3\u30B5\u5024 / sensor value
|
|
2074
|
+
20 -> 30 : \u9001\u4FE1\u30C7\u30FC\u30BF / transmission data
|
|
2075
|
+
30 .> 200 : \u7121\u7DDA / wireless
|
|
2076
|
+
200 -> 310 : \u4E2D\u7D99 / relay
|
|
2077
|
+
310 -> 320 : \u30C7\u30FC\u30BF / data
|
|
2078
|
+
320 -> 330 : \u7D50\u679C / result
|
|
2079
|
+
`
|
|
2080
|
+
},
|
|
2081
|
+
imagePipeline: {
|
|
2082
|
+
label: "\u753B\u50CF\u51E6\u7406",
|
|
2083
|
+
hint: "\u753B\u50CF\u5165\u529B\u304B\u3089\u5224\u5B9A\u7D50\u679C\u307E\u3067\u306E\u51E6\u7406\u30D1\u30A4\u30D7\u30E9\u30A4\u30F3",
|
|
2084
|
+
source: `# \u753B\u50CF\u51E6\u7406\u30D1\u30A4\u30D7\u30E9\u30A4\u30F3(\u65B9\u6CD5\u30AF\u30EC\u30FC\u30E0\u7528)
|
|
2085
|
+
# \u30D2\u30F3\u30C8: \u7247\u65B9\u5411\u306E\u63A5\u7D9A\u306E\u307F\u306A\u3089\u30D5\u30ED\u30FC\u30C1\u30E3\u30FC\u30C8\u3068\u3057\u3066\u63CF\u753B\u3055\u308C\u308B
|
|
2086
|
+
|
|
2087
|
+
S100 = \u753B\u50CF\u3092\u53D6\u5F97 / Acquire image
|
|
2088
|
+
S110 = \u524D\u51E6\u7406 / Preprocess
|
|
2089
|
+
S120 = \u7279\u5FB4\u91CF\u3092\u62BD\u51FA / Extract features
|
|
2090
|
+
S130 = \u6B20\u9665\u3042\u308A? / Defect?
|
|
2091
|
+
S140 = \u30A2\u30E9\u30FC\u30C8\u3092\u51FA\u529B / Output alert
|
|
2092
|
+
S150 = \u6B63\u5E38\u7D50\u679C\u3092\u8A18\u9332 / Record normal result
|
|
2093
|
+
S160 = \u7D42\u4E86 / End
|
|
2094
|
+
|
|
2095
|
+
S100 -> S110
|
|
2096
|
+
S110 -> S120
|
|
2097
|
+
S120 -> S130
|
|
2098
|
+
S130 -> S140 : Yes
|
|
2099
|
+
S130 -> S150 : No
|
|
2100
|
+
S140 -> S160
|
|
2101
|
+
S150 -> S160
|
|
2102
|
+
`
|
|
2103
|
+
},
|
|
2104
|
+
controlLoop: {
|
|
2105
|
+
label: "\u5236\u5FA1\u30EB\u30FC\u30D7",
|
|
2106
|
+
hint: "\u30BB\u30F3\u30B5\u30FB\u5236\u5FA1\u5668\u30FB\u30A2\u30AF\u30C1\u30E5\u30A8\u30FC\u30BF\u306E\u30D5\u30A3\u30FC\u30C9\u30D0\u30C3\u30AF\u69CB\u6210",
|
|
2107
|
+
source: `# \u5236\u5FA1\u30EB\u30FC\u30D7(\u88C5\u7F6E\u30AF\u30EC\u30FC\u30E0\u7528)
|
|
2108
|
+
# \u30D2\u30F3\u30C8: \u30EB\u30FC\u30D7\u3059\u308B\u69CB\u6210\u306F\u30011\u3064\u306E\u30B7\u30B9\u30C6\u30E0\u5185\u3067\u6642\u8A08\u56DE\u308A\u306B\u4E26\u3079\u308B\u3068\u8AAD\u307F\u3084\u3059\u3044
|
|
2109
|
+
|
|
2110
|
+
100 = \u5236\u5FA1\u30B7\u30B9\u30C6\u30E0 / control system
|
|
2111
|
+
10 = \u5236\u5FA1\u90E8 / controller
|
|
2112
|
+
11 = \u76EE\u6A19\u5024\u53D6\u5F97\u90E8 / target acquisition unit
|
|
2113
|
+
12 = \u504F\u5DEE\u7B97\u51FA\u90E8 / error calculator
|
|
2114
|
+
13 = \u6307\u4EE4\u751F\u6210\u90E8 / command generator
|
|
2115
|
+
20 = \u99C6\u52D5\u90E8 / driver
|
|
2116
|
+
30 = \u30BB\u30F3\u30B5\u90E8 / sensor unit
|
|
2117
|
+
40 = \u5BFE\u8C61\u88C5\u7F6E / controlled object
|
|
2118
|
+
|
|
2119
|
+
100 : 30 10 40 20
|
|
2120
|
+
10 : 11 12 13
|
|
2121
|
+
|
|
2122
|
+
30 -> 12 : \u6E2C\u5B9A\u5024 / measured value
|
|
2123
|
+
11 -> 12 : \u76EE\u6A19\u5024 / target value
|
|
2124
|
+
12 -> 13 : \u504F\u5DEE / error
|
|
2125
|
+
13 -> 20 : \u6307\u4EE4 / command
|
|
2126
|
+
20 -> 40 : \u99C6\u52D5\u4FE1\u53F7 / drive signal
|
|
2127
|
+
40 .> 30 : \u30D5\u30A3\u30FC\u30C9\u30D0\u30C3\u30AF / feedback
|
|
2128
|
+
`
|
|
2129
|
+
},
|
|
2130
|
+
flow: {
|
|
2131
|
+
label: "\u30D5\u30ED\u30FC\u30C1\u30E3\u30FC\u30C8",
|
|
2132
|
+
hint: "\u30E9\u30D9\u30EB\u306B\u300C?\u300D\u304C\u3042\u308B \u2192 \u30D5\u30ED\u30FC\u3068\u5224\u5B9A",
|
|
2133
|
+
source: `# \u30D5\u30ED\u30FC\u30C1\u30E3\u30FC\u30C8(\u65B9\u6CD5\u30AF\u30EC\u30FC\u30E0\u7528)
|
|
2134
|
+
# \u30D2\u30F3\u30C8:\u30E9\u30D9\u30EB\u672B\u5C3E\u306B\u300C?\u300D \u2192 \u83F1\u5F62(\u6761\u4EF6\u5206\u5C90)\u306B\u81EA\u52D5\u63A8\u8AD6
|
|
2135
|
+
|
|
2136
|
+
S100 = \u958B\u59CB / Start
|
|
2137
|
+
S110 = \u6761\u4EF6A? / "Condition A?"
|
|
2138
|
+
S120 = \u51E6\u7406X / Process X
|
|
2139
|
+
S130 = \u51E6\u7406Y / Process Y
|
|
2140
|
+
S140 = \u7D42\u4E86 / End
|
|
2141
|
+
|
|
2142
|
+
S100 -> S110
|
|
2143
|
+
S110 -> S120 : Yes
|
|
2144
|
+
S110 -> S130 : No
|
|
2145
|
+
S120 -> S140
|
|
2146
|
+
S130 -> S140
|
|
2147
|
+
`
|
|
2148
|
+
},
|
|
2149
|
+
state: {
|
|
2150
|
+
label: "\u72B6\u614B\u9077\u79FB\u56F3",
|
|
2151
|
+
hint: "\u300C*\u300D\u3092\u4F7F\u3046 \u2192 \u72B6\u614B\u9077\u79FB\u3068\u5224\u5B9A",
|
|
2152
|
+
source: `# \u72B6\u614B\u9077\u79FB\u56F3
|
|
2153
|
+
# \u30D2\u30F3\u30C8:\u300C*\u300D\u304C\u521D\u671F/\u7D42\u7AEF\u306E\u7B26\u53F7(\u9ED2\u4E38\u3067\u63CF\u753B)
|
|
2154
|
+
|
|
2155
|
+
S1 = \u5F85\u6A5F / Idle
|
|
2156
|
+
S2 = \u52D5\u4F5C\u4E2D / Running
|
|
2157
|
+
S3 = \u30A8\u30E9\u30FC / Error
|
|
2158
|
+
|
|
2159
|
+
* -> S1
|
|
2160
|
+
S1 -> S2 : \u8D77\u52D5 / start
|
|
2161
|
+
S2 -> S1 : \u505C\u6B62 / stop
|
|
2162
|
+
S2 -> S3 : \u7570\u5E38 / fault
|
|
2163
|
+
S3 -> S1 : \u30EA\u30BB\u30C3\u30C8 / reset
|
|
2164
|
+
S3 -> *
|
|
2165
|
+
`
|
|
2166
|
+
},
|
|
2167
|
+
seq: {
|
|
2168
|
+
label: "\u30B7\u30FC\u30B1\u30F3\u30B9\u56F3",
|
|
2169
|
+
hint: "\u300C:\u300D\u3082\u300C?\u300D\u3082\u300C*\u300D\u3082\u306A\u3044 \u2192 \u30B7\u30FC\u30B1\u30F3\u30B9",
|
|
2170
|
+
source: `# \u30B7\u30FC\u30B1\u30F3\u30B9\u56F3(\u30D7\u30ED\u30C8\u30B3\u30EB\u7CFB)
|
|
2171
|
+
# \u30D2\u30F3\u30C8:\u5305\u542B\u3082?\u3082*\u3082\u7121\u3044 \u2192 \u30B7\u30FC\u30B1\u30F3\u30B9\u56F3\u3068\u3057\u3066\u63CF\u753B
|
|
2172
|
+
# \u30A2\u30AF\u30BF\u306F\u767B\u5834\u9806\u306B\u5DE6\u304B\u3089\u4E26\u3076
|
|
2173
|
+
|
|
2174
|
+
100 = \u30AF\u30E9\u30A4\u30A2\u30F3\u30C8 / client
|
|
2175
|
+
200 = \u30B5\u30FC\u30D0 / server
|
|
2176
|
+
|
|
2177
|
+
100 -> 200 : \u8A8D\u8A3C\u8981\u6C42 / auth request
|
|
2178
|
+
200 -> 100 : \u30C8\u30FC\u30AF\u30F3 / token
|
|
2179
|
+
100 -> 200 : \u30EA\u30BD\u30FC\u30B9\u8981\u6C42 / resource request
|
|
2180
|
+
200 -> 100 : \u30EA\u30BD\u30FC\u30B9\u5FDC\u7B54 / resource response
|
|
2181
|
+
`
|
|
2182
|
+
},
|
|
2183
|
+
handshake: {
|
|
2184
|
+
label: "\u30CF\u30F3\u30C9\u30B7\u30A7\u30A4\u30AF",
|
|
2185
|
+
hint: "\u8981\u6C42/\u5FDC\u7B54/\u78BA\u7ACB/\u7D42\u4E86\u306E\u901A\u4FE1\u30B7\u30FC\u30B1\u30F3\u30B9",
|
|
2186
|
+
source: `# \u901A\u4FE1\u30CF\u30F3\u30C9\u30B7\u30A7\u30A4\u30AF(\u30B7\u30FC\u30B1\u30F3\u30B9\u56F3)
|
|
2187
|
+
# \u30D2\u30F3\u30C8: \u5F80\u5FA9\u30E1\u30C3\u30BB\u30FC\u30B8\u304C\u3042\u308B\u3068\u30B7\u30FC\u30B1\u30F3\u30B9\u56F3\u306B\u306A\u308B
|
|
2188
|
+
|
|
2189
|
+
100 = \u7AEF\u672B / terminal
|
|
2190
|
+
200 = \u30B5\u30FC\u30D0 / server
|
|
2191
|
+
|
|
2192
|
+
100 -> 200 : \u63A5\u7D9A\u8981\u6C42 / connect request
|
|
2193
|
+
200 -> 100 : \u5FDC\u7B54 / response
|
|
2194
|
+
100 -> 200 : \u8A8D\u8A3C\u60C5\u5831 / credentials
|
|
2195
|
+
200 -> 100 : \u8A8D\u8A3C\u7D50\u679C / auth result
|
|
2196
|
+
100 <-> 200 : \u30C7\u30FC\u30BF\u901A\u4FE1 / data exchange
|
|
2197
|
+
100 -> 200 : \u5207\u65AD\u8981\u6C42 / disconnect
|
|
2198
|
+
200 -> 100 : \u5207\u65AD\u5B8C\u4E86 / disconnected
|
|
2199
|
+
`
|
|
2200
|
+
}
|
|
2201
|
+
};
|
|
2202
|
+
|
|
2203
|
+
// src/core/patterns.ts
|
|
2204
|
+
var PATTERN_LABEL = {
|
|
2205
|
+
cond: "\u6761\u4EF6\u5206\u5C90",
|
|
2206
|
+
container: "\u89AA\u5B50\u69CB\u6210",
|
|
2207
|
+
external: "\u89AA\u5B50+\u5916\u90E8",
|
|
2208
|
+
seq: "\u30B7\u30FC\u30B1\u30F3\u30B9",
|
|
2209
|
+
state: "\u72B6\u614B\u9077\u79FB",
|
|
2210
|
+
bidir: "\u53CC\u65B9\u5411\u901A\u4FE1",
|
|
2211
|
+
hierarchy: "3\u968E\u5C64\u69CB\u6210",
|
|
2212
|
+
pipeline: "N\u6BB5\u968E\u51E6\u7406",
|
|
2213
|
+
parallel: "\u4E26\u5217\u51E6\u7406",
|
|
2214
|
+
handshake: "\u901A\u4FE1\u30CF\u30F3\u30C9\u30B7\u30A7\u30A4\u30AF",
|
|
2215
|
+
state_with_cond: "\u72B6\u614B\u5224\u5B9A\u30D5\u30ED\u30FC",
|
|
2216
|
+
system: "\u30B7\u30B9\u30C6\u30E0\u5168\u4F53"
|
|
2217
|
+
};
|
|
2218
|
+
var PATTERN_SOURCE = {
|
|
2219
|
+
cond: `S100 = \u958B\u59CB / Start
|
|
2220
|
+
S110 = \u5224\u5B9A? / Decision?
|
|
2221
|
+
S120 = Yes\u51E6\u7406 / Yes Process
|
|
2222
|
+
S130 = No\u51E6\u7406 / No Process
|
|
2223
|
+
S140 = \u7D42\u4E86 / End
|
|
2224
|
+
S100 -> S110
|
|
2225
|
+
S110 -> S120 : Yes
|
|
2226
|
+
S110 -> S130 : No
|
|
2227
|
+
S120 -> S140
|
|
2228
|
+
S130 -> S140`,
|
|
2229
|
+
container: `10 = \u89AA\u88C5\u7F6E / Parent
|
|
2230
|
+
11 = \u90E8\u54C11 / Part 1
|
|
2231
|
+
12 = \u90E8\u54C12 / Part 2
|
|
2232
|
+
13 = \u90E8\u54C13 / Part 3
|
|
2233
|
+
10 : 11 12 13`,
|
|
2234
|
+
external: `10 = \u5236\u5FA1\u88C5\u7F6E / control device
|
|
2235
|
+
11 = \u5165\u529B\u90E8 / input
|
|
2236
|
+
12 = \u51E6\u7406\u90E8 / processor
|
|
2237
|
+
20 = \u5916\u90E8\u6A5F\u5668 / external device
|
|
2238
|
+
10 : 11 12
|
|
2239
|
+
12 -> 20 : \u4FE1\u53F7 / signal`,
|
|
2240
|
+
seq: `100 = \u30AF\u30E9\u30A4\u30A2\u30F3\u30C8 / client
|
|
2241
|
+
200 = \u30B5\u30FC\u30D0 / server
|
|
2242
|
+
100 -> 200 : \u8981\u6C42 / request
|
|
2243
|
+
200 -> 100 : \u5FDC\u7B54 / response
|
|
2244
|
+
100 -> 200 : \u5207\u65AD / disconnect`,
|
|
2245
|
+
state: `S1 = \u5F85\u6A5F / Idle
|
|
2246
|
+
S2 = \u52D5\u4F5C\u4E2D / Running
|
|
2247
|
+
* -> S1
|
|
2248
|
+
S1 -> S2 : \u8D77\u52D5 / start
|
|
2249
|
+
S2 -> S1 : \u505C\u6B62 / stop`,
|
|
2250
|
+
bidir: `100 = \u7AEF\u672BA / Terminal A
|
|
2251
|
+
200 = \u7AEF\u672BB / Terminal B
|
|
2252
|
+
100 <-> 200 : \u901A\u4FE1 / comm`,
|
|
2253
|
+
hierarchy: `100 = \u30B7\u30B9\u30C6\u30E0 / System
|
|
2254
|
+
10 = \u30B5\u30D6\u30B7\u30B9\u30C6\u30E0A / Subsystem A
|
|
2255
|
+
20 = \u30B5\u30D6\u30B7\u30B9\u30C6\u30E0B / Subsystem B
|
|
2256
|
+
11 = \u90E8\u54C1A1 / Part A1
|
|
2257
|
+
12 = \u90E8\u54C1A2 / Part A2
|
|
2258
|
+
21 = \u90E8\u54C1B1 / Part B1
|
|
2259
|
+
22 = \u90E8\u54C1B2 / Part B2
|
|
2260
|
+
100 : 10 20
|
|
2261
|
+
10 : 11 12
|
|
2262
|
+
20 : 21 22`,
|
|
2263
|
+
pipeline: `S100 = \u5165\u529B / Input
|
|
2264
|
+
S110 = \u524D\u51E6\u7406 / Preprocess
|
|
2265
|
+
S120 = \u4E3B\u51E6\u7406 / Main Process
|
|
2266
|
+
S130 = \u5F8C\u51E6\u7406 / Postprocess
|
|
2267
|
+
S140 = \u51FA\u529B / Output
|
|
2268
|
+
S100 -> S110
|
|
2269
|
+
S110 -> S120
|
|
2270
|
+
S120 -> S130
|
|
2271
|
+
S130 -> S140`,
|
|
2272
|
+
parallel: `S100 = \u958B\u59CB / Start
|
|
2273
|
+
S110 = \u5206\u5C90 / Branch
|
|
2274
|
+
S120 = \u7D4C\u8DEFA / Path A
|
|
2275
|
+
S130 = \u7D4C\u8DEFB / Path B
|
|
2276
|
+
S140 = \u7D4C\u8DEFC / Path C
|
|
2277
|
+
S150 = \u5408\u6D41 / Join
|
|
2278
|
+
S160 = \u7D42\u4E86 / End
|
|
2279
|
+
S100 -> S110
|
|
2280
|
+
S110 -> S120
|
|
2281
|
+
S110 -> S130
|
|
2282
|
+
S110 -> S140
|
|
2283
|
+
S120 -> S150
|
|
2284
|
+
S130 -> S150
|
|
2285
|
+
S140 -> S150
|
|
2286
|
+
S150 -> S160`,
|
|
2287
|
+
handshake: `100 = \u7AEF\u672B / Terminal
|
|
2288
|
+
200 = \u30B5\u30FC\u30D0 / Server
|
|
2289
|
+
100 -> 200 : SYN\u8981\u6C42 / SYN
|
|
2290
|
+
200 -> 100 : SYN+ACK / SYN+ACK
|
|
2291
|
+
100 -> 200 : ACK\u5FDC\u7B54 / ACK
|
|
2292
|
+
100 <-> 200 : \u30C7\u30FC\u30BF\u4EA4\u63DB / data exchange
|
|
2293
|
+
100 -> 200 : FIN / FIN
|
|
2294
|
+
200 -> 100 : ACK / ACK`,
|
|
2295
|
+
state_with_cond: `S100 = \u5F85\u6A5F\u72B6\u614B / Idle state
|
|
2296
|
+
S110 = \u691C\u8A3C\u51E6\u7406 / Verify
|
|
2297
|
+
S120 = OK? / OK?
|
|
2298
|
+
S130 = \u5B8C\u4E86\u72B6\u614B / Done state
|
|
2299
|
+
S140 = \u5931\u6557\u72B6\u614B / Failed state
|
|
2300
|
+
S150 = \u518D\u8A66\u884C / Retry
|
|
2301
|
+
S100 -> S110 : \u8981\u6C42 / request
|
|
2302
|
+
S110 -> S120 : \u5224\u5B9A / check
|
|
2303
|
+
S120 -> S130 : OK
|
|
2304
|
+
S120 -> S140 : NG
|
|
2305
|
+
S140 -> S150
|
|
2306
|
+
S150 -> S100`,
|
|
2307
|
+
system: `100 = \u30B7\u30B9\u30C6\u30E0\u672C\u4F53 / Main system
|
|
2308
|
+
10 = \u5236\u5FA1\u90E8 / Control
|
|
2309
|
+
20 = \u901A\u4FE1\u90E8 / Comm
|
|
2310
|
+
11 = CPU
|
|
2311
|
+
12 = \u30E1\u30E2\u30EA / memory
|
|
2312
|
+
21 = \u7121\u7DDA\u90E8 / wireless
|
|
2313
|
+
22 = \u6709\u7DDA\u90E8 / wired
|
|
2314
|
+
30 = \u5916\u90E8\u30B5\u30FC\u30D0 / external server
|
|
2315
|
+
40 = \u5916\u90E8\u7AEF\u672B / external terminal
|
|
2316
|
+
100 : 10 20
|
|
2317
|
+
10 : 11 12
|
|
2318
|
+
20 : 21 22
|
|
2319
|
+
21 .> 40 : \u7121\u7DDA / wireless
|
|
2320
|
+
22 -> 30 : \u6709\u7DDA / wired
|
|
2321
|
+
30 <-> 40 : \u901A\u4FE1 / comm`
|
|
2322
|
+
};
|
|
2323
|
+
export {
|
|
2324
|
+
OP_TABLE,
|
|
2325
|
+
PATTERN_LABEL,
|
|
2326
|
+
PATTERN_SOURCE,
|
|
2327
|
+
SAMPLES,
|
|
2328
|
+
SAMPLE_ORDER,
|
|
2329
|
+
chooseLabelPlacement,
|
|
2330
|
+
estimateTextWidth,
|
|
2331
|
+
layout,
|
|
2332
|
+
parse,
|
|
2333
|
+
refsToCsv,
|
|
2334
|
+
refsToMarkdown,
|
|
2335
|
+
render,
|
|
2336
|
+
splitBilingual,
|
|
2337
|
+
stripComment
|
|
2338
|
+
};
|