@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/pdgkit.js
ADDED
|
@@ -0,0 +1,3534 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// bin/pdgkit.ts
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
|
|
6
|
+
// src/core/parser.ts
|
|
7
|
+
var ID_PATTERN = /[A-Za-z0-9_*]+/.source;
|
|
8
|
+
var OP_TABLE = [
|
|
9
|
+
{ lit: "<->", kind: "bidir" },
|
|
10
|
+
{ lit: "=>", kind: "thick" },
|
|
11
|
+
{ lit: "->", kind: "arrow" },
|
|
12
|
+
{ lit: "<-", kind: "arrow", reverse: true },
|
|
13
|
+
{ lit: ".>", kind: "dashed-arrow" },
|
|
14
|
+
{ lit: "..", kind: "dashed" },
|
|
15
|
+
{ lit: "-", kind: "line" }
|
|
16
|
+
];
|
|
17
|
+
var DEF_RE = new RegExp(`^(${ID_PATTERN})\\s*=(?!>)\\s*(.*)$`);
|
|
18
|
+
var CONN_RE = new RegExp(
|
|
19
|
+
`^(${ID_PATTERN})\\s+(<->|=>|->|<-|\\.>|\\.\\.|-)\\s+(${ID_PATTERN})\\s*(?::\\s*(.*))?$`
|
|
20
|
+
);
|
|
21
|
+
var CONT_RE = new RegExp(`^(${ID_PATTERN})\\s*:\\s*(.+)$`);
|
|
22
|
+
function parse(source) {
|
|
23
|
+
const doc = {
|
|
24
|
+
nodes: /* @__PURE__ */ new Map(),
|
|
25
|
+
containments: [],
|
|
26
|
+
edges: [],
|
|
27
|
+
diagnostics: [],
|
|
28
|
+
kind: "block"
|
|
29
|
+
};
|
|
30
|
+
const lines = source.split(/\r?\n/);
|
|
31
|
+
for (let i = 0; i < lines.length; i++) {
|
|
32
|
+
const raw = stripComment(lines[i]);
|
|
33
|
+
const line = raw.trim();
|
|
34
|
+
if (!line) continue;
|
|
35
|
+
if (handleDef(line, i + 1, doc)) continue;
|
|
36
|
+
if (handleConn(line, i + 1, doc)) continue;
|
|
37
|
+
if (handleCont(line, i + 1, doc)) continue;
|
|
38
|
+
doc.diagnostics.push({
|
|
39
|
+
severity: "error",
|
|
40
|
+
line: i + 1,
|
|
41
|
+
col: 1,
|
|
42
|
+
message: `\u69CB\u6587\u4E0D\u660E: "${line}"`
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
doc.kind = inferKind(doc);
|
|
46
|
+
return doc;
|
|
47
|
+
}
|
|
48
|
+
function handleDef(line, lineNum, doc) {
|
|
49
|
+
const m = line.match(DEF_RE);
|
|
50
|
+
if (!m) return false;
|
|
51
|
+
const id = m[1];
|
|
52
|
+
const tail = m[2];
|
|
53
|
+
const label = splitBilingual(tail);
|
|
54
|
+
const existing = doc.nodes.get(id);
|
|
55
|
+
if (existing) {
|
|
56
|
+
doc.diagnostics.push({
|
|
57
|
+
severity: "warning",
|
|
58
|
+
line: lineNum,
|
|
59
|
+
col: 1,
|
|
60
|
+
message: `\u7B26\u53F7 "${id}" \u306F\u518D\u5B9A\u7FA9\u3055\u308C\u307E\u3057\u305F`
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
doc.nodes.set(id, {
|
|
64
|
+
id,
|
|
65
|
+
label,
|
|
66
|
+
implicit: false
|
|
67
|
+
});
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
function handleConn(line, lineNum, doc) {
|
|
71
|
+
const m = line.match(CONN_RE);
|
|
72
|
+
if (!m) return false;
|
|
73
|
+
const opLit = m[2];
|
|
74
|
+
const entry = OP_TABLE.find((o) => o.lit === opLit);
|
|
75
|
+
const from = entry.reverse ? m[3] : m[1];
|
|
76
|
+
const to = entry.reverse ? m[1] : m[3];
|
|
77
|
+
const labelText = m[4] ?? "";
|
|
78
|
+
const edge = {
|
|
79
|
+
from,
|
|
80
|
+
to,
|
|
81
|
+
op: entry.kind,
|
|
82
|
+
label: labelText.trim() ? splitBilingual(labelText) : void 0,
|
|
83
|
+
line: lineNum
|
|
84
|
+
};
|
|
85
|
+
doc.edges.push(edge);
|
|
86
|
+
ensureNode(doc, from);
|
|
87
|
+
ensureNode(doc, to);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
function handleCont(line, lineNum, doc) {
|
|
91
|
+
const m = line.match(CONT_RE);
|
|
92
|
+
if (!m) return false;
|
|
93
|
+
const parent = m[1];
|
|
94
|
+
const rest = m[2].trim();
|
|
95
|
+
const children = rest.split(/\s+/).filter(Boolean);
|
|
96
|
+
const idRe = new RegExp(`^${ID_PATTERN}$`);
|
|
97
|
+
for (const c of children) {
|
|
98
|
+
if (!idRe.test(c)) {
|
|
99
|
+
doc.diagnostics.push({
|
|
100
|
+
severity: "error",
|
|
101
|
+
line: lineNum,
|
|
102
|
+
col: 1,
|
|
103
|
+
message: `\u5305\u542B\u306E\u5B50\u3068\u3057\u3066\u4E0D\u6B63\u306A\u30C8\u30FC\u30AF\u30F3: "${c}"`
|
|
104
|
+
});
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
doc.containments.push({ parent, children, line: lineNum });
|
|
109
|
+
ensureNode(doc, parent);
|
|
110
|
+
for (const c of children) ensureNode(doc, c);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
function ensureNode(doc, id) {
|
|
114
|
+
let n = doc.nodes.get(id);
|
|
115
|
+
if (n) return n;
|
|
116
|
+
n = { id, label: {}, implicit: true };
|
|
117
|
+
doc.nodes.set(id, n);
|
|
118
|
+
return n;
|
|
119
|
+
}
|
|
120
|
+
function stripComment(line) {
|
|
121
|
+
let inQuote = false;
|
|
122
|
+
for (let i = 0; i < line.length; i++) {
|
|
123
|
+
const c = line[i];
|
|
124
|
+
if (c === '"') inQuote = !inQuote;
|
|
125
|
+
else if (!inQuote && c === "#") return line.slice(0, i);
|
|
126
|
+
}
|
|
127
|
+
return line;
|
|
128
|
+
}
|
|
129
|
+
function splitBilingual(text) {
|
|
130
|
+
const s = text.trim();
|
|
131
|
+
if (!s) return {};
|
|
132
|
+
const slashIdx = findBilingualSeparator(s);
|
|
133
|
+
if (slashIdx === -1) {
|
|
134
|
+
return { ja: stripQuotes(s) };
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
ja: stripQuotes(s.slice(0, slashIdx).trim()),
|
|
138
|
+
en: stripQuotes(s.slice(slashIdx + 1).trim())
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function findBilingualSeparator(s) {
|
|
142
|
+
let inQuote = false;
|
|
143
|
+
for (let i = 0; i < s.length; i++) {
|
|
144
|
+
if (s[i] === '"') {
|
|
145
|
+
inQuote = !inQuote;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (!inQuote && s[i] === "/" && isSpace(s[i - 1]) && isSpace(s[i + 1])) return i;
|
|
149
|
+
}
|
|
150
|
+
return -1;
|
|
151
|
+
}
|
|
152
|
+
function isSpace(ch) {
|
|
153
|
+
return ch === " " || ch === " ";
|
|
154
|
+
}
|
|
155
|
+
function stripQuotes(s) {
|
|
156
|
+
if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) {
|
|
157
|
+
return s.slice(1, -1);
|
|
158
|
+
}
|
|
159
|
+
return s;
|
|
160
|
+
}
|
|
161
|
+
function inferKind(doc) {
|
|
162
|
+
if (doc.containments.length > 0) return "block";
|
|
163
|
+
for (const n of doc.nodes.values()) {
|
|
164
|
+
const ja = n.label.ja ?? "";
|
|
165
|
+
const en = n.label.en ?? "";
|
|
166
|
+
if (ja.endsWith("?") || en.endsWith("?")) return "flow";
|
|
167
|
+
}
|
|
168
|
+
if (doc.nodes.has("*")) return "state";
|
|
169
|
+
const pairs = /* @__PURE__ */ new Set();
|
|
170
|
+
for (const e of doc.edges) {
|
|
171
|
+
if (e.op === "bidir") return "seq";
|
|
172
|
+
const fwd = `${e.from}|${e.to}`;
|
|
173
|
+
const rev = `${e.to}|${e.from}`;
|
|
174
|
+
if (pairs.has(rev)) return "seq";
|
|
175
|
+
pairs.add(fwd);
|
|
176
|
+
}
|
|
177
|
+
return "flow";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/core/layout.ts
|
|
181
|
+
var NODE_W = 36;
|
|
182
|
+
var NODE_H = 14;
|
|
183
|
+
var PAD = 8;
|
|
184
|
+
var GRID_GAP = 24;
|
|
185
|
+
var TITLE_H = 5;
|
|
186
|
+
var PARENT_EDGE_LANE_H = 14;
|
|
187
|
+
var MARGIN = 8;
|
|
188
|
+
var ROOT_GAP = MARGIN * 4;
|
|
189
|
+
var ROUTE_GAP = PAD * 1.5;
|
|
190
|
+
var OBSTACLE_PAD = 0.8;
|
|
191
|
+
var BORDER_CLEARANCE = 4;
|
|
192
|
+
var ARROW_TERMINAL_CLEARANCE = 4.6;
|
|
193
|
+
var THICK_ARROW_TERMINAL_CLEARANCE = 5.4;
|
|
194
|
+
var VERTICAL_PORT_RATIO = 0.25;
|
|
195
|
+
var PORT_STUB = 6;
|
|
196
|
+
var MAX_ROUTE_LANES = 18;
|
|
197
|
+
var EPS = 1e-3;
|
|
198
|
+
function layout(doc) {
|
|
199
|
+
switch (doc.kind) {
|
|
200
|
+
case "block":
|
|
201
|
+
return layoutBlock(doc);
|
|
202
|
+
case "flow":
|
|
203
|
+
return layoutFlow(doc);
|
|
204
|
+
case "state":
|
|
205
|
+
return layoutState(doc);
|
|
206
|
+
case "seq":
|
|
207
|
+
return layoutSeq(doc);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function layoutBlock(doc) {
|
|
211
|
+
const childMap = /* @__PURE__ */ new Map();
|
|
212
|
+
for (const c of doc.containments) childMap.set(c.parent, c.children);
|
|
213
|
+
const parentMap = /* @__PURE__ */ new Map();
|
|
214
|
+
for (const c of doc.containments) {
|
|
215
|
+
for (const child of c.children) parentMap.set(child, c.parent);
|
|
216
|
+
}
|
|
217
|
+
const childIds = /* @__PURE__ */ new Set();
|
|
218
|
+
for (const cs of childMap.values()) for (const c of cs) childIds.add(c);
|
|
219
|
+
const allIds = [...doc.nodes.keys()];
|
|
220
|
+
const roots = allIds.filter((id) => !childIds.has(id));
|
|
221
|
+
const placed = [];
|
|
222
|
+
const positions = /* @__PURE__ */ new Map();
|
|
223
|
+
function size(id) {
|
|
224
|
+
const children = childMap.get(id);
|
|
225
|
+
if (!children || children.length === 0) return { w: NODE_W, h: NODE_H };
|
|
226
|
+
const sizes = children.map(size);
|
|
227
|
+
if (arrangementOf(id) === "grid") {
|
|
228
|
+
const cols = gridColsOf(id, children);
|
|
229
|
+
const colW = Math.max(...sizes.map((s) => s.w));
|
|
230
|
+
const rowHeights = gridRowHeights(sizes, cols);
|
|
231
|
+
return {
|
|
232
|
+
w: cols * colW + 2 * PAD + (cols - 1) * GRID_GAP,
|
|
233
|
+
h: titleHeightOf(id) + rowHeights.reduce((sum, h) => sum + h, 0) + 2 * PAD + Math.max(0, rowHeights.length - 1) * GRID_GAP
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
const maxW = Math.max(...sizes.map((s) => s.w));
|
|
237
|
+
const totH = sizes.reduce((a, b) => a + b.h, 0);
|
|
238
|
+
return {
|
|
239
|
+
w: maxW + 2 * PAD,
|
|
240
|
+
h: titleHeightOf(id) + totH + 2 * PAD + (children.length - 1) * GRID_GAP
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function arrangementOf(id) {
|
|
244
|
+
const children = childMap.get(id);
|
|
245
|
+
if (!children || children.length <= 2) return "stack";
|
|
246
|
+
if (hasParentToChildEdges(id, children)) return "grid";
|
|
247
|
+
return hasLinearChildFlow(children, doc.edges, childMap) ? "stack" : "grid";
|
|
248
|
+
}
|
|
249
|
+
function gridColsOf(id, children) {
|
|
250
|
+
if (hasParentToChildEdges(id, children)) return Math.min(children.length, 4);
|
|
251
|
+
if (children.length >= 7) return 4;
|
|
252
|
+
if (children.length >= 5) return 3;
|
|
253
|
+
return 2;
|
|
254
|
+
}
|
|
255
|
+
function gridRowHeights(sizes, cols) {
|
|
256
|
+
const rows = Math.ceil(sizes.length / cols);
|
|
257
|
+
const heights = [];
|
|
258
|
+
for (let row = 0; row < rows; row++) {
|
|
259
|
+
const rowSizes = sizes.slice(row * cols, row * cols + cols);
|
|
260
|
+
heights.push(Math.max(...rowSizes.map((s) => s.h)));
|
|
261
|
+
}
|
|
262
|
+
return heights;
|
|
263
|
+
}
|
|
264
|
+
function titleHeightOf(id) {
|
|
265
|
+
const children = childMap.get(id);
|
|
266
|
+
return children && hasParentToChildEdges(id, children) ? TITLE_H + PARENT_EDGE_LANE_H : TITLE_H;
|
|
267
|
+
}
|
|
268
|
+
function hasParentToChildEdges(id, children) {
|
|
269
|
+
return doc.edges.some((edge) => edge.from === id && children.some((child) => child === edge.to || containsDescendant(child, edge.to, childMap)));
|
|
270
|
+
}
|
|
271
|
+
function place(id, ox, oy) {
|
|
272
|
+
const s = size(id);
|
|
273
|
+
positions.set(id, { x: ox, y: oy, w: s.w, h: s.h });
|
|
274
|
+
const node = doc.nodes.get(id);
|
|
275
|
+
const children = childMap.get(id);
|
|
276
|
+
if (!children || children.length === 0) {
|
|
277
|
+
placed.push({
|
|
278
|
+
id,
|
|
279
|
+
x: ox,
|
|
280
|
+
y: oy,
|
|
281
|
+
w: s.w,
|
|
282
|
+
h: s.h,
|
|
283
|
+
label: node?.label ?? {},
|
|
284
|
+
shape: "box",
|
|
285
|
+
isContainer: false
|
|
286
|
+
});
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const sizes = children.map(size);
|
|
290
|
+
if (arrangementOf(id) === "grid") {
|
|
291
|
+
const cols = gridColsOf(id, children);
|
|
292
|
+
const colW = Math.max(...sizes.map((s2) => s2.w));
|
|
293
|
+
const rowHeights = gridRowHeights(sizes, cols);
|
|
294
|
+
const rowTops = [];
|
|
295
|
+
let rowTop = oy + titleHeightOf(id) + PAD;
|
|
296
|
+
for (let row = 0; row < rowHeights.length; row++) {
|
|
297
|
+
rowTops[row] = rowTop;
|
|
298
|
+
rowTop += rowHeights[row] + GRID_GAP;
|
|
299
|
+
}
|
|
300
|
+
for (let i = 0; i < children.length; i++) {
|
|
301
|
+
const r = Math.floor(i / cols);
|
|
302
|
+
const cc = i % cols;
|
|
303
|
+
const cx = ox + PAD + cc * (colW + GRID_GAP) + (colW - sizes[i].w) / 2;
|
|
304
|
+
const cy = rowTops[r] + (rowHeights[r] - sizes[i].h) / 2;
|
|
305
|
+
place(children[i], cx, cy);
|
|
306
|
+
}
|
|
307
|
+
alignGridRows(children, sizes, rowHeights, rowTops, cols, childMap, doc.edges, positions, placed);
|
|
308
|
+
} else {
|
|
309
|
+
const maxW = Math.max(...sizes.map((s2) => s2.w));
|
|
310
|
+
let yy = oy + titleHeightOf(id) + PAD;
|
|
311
|
+
for (let i = 0; i < children.length; i++) {
|
|
312
|
+
const cx = ox + PAD + (maxW - sizes[i].w) / 2;
|
|
313
|
+
place(children[i], cx, yy);
|
|
314
|
+
yy += sizes[i].h + GRID_GAP;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
placed.push({
|
|
318
|
+
id,
|
|
319
|
+
x: ox,
|
|
320
|
+
y: oy,
|
|
321
|
+
w: s.w,
|
|
322
|
+
h: s.h,
|
|
323
|
+
label: node?.label ?? {},
|
|
324
|
+
shape: "box",
|
|
325
|
+
isContainer: true
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
let cur = MARGIN;
|
|
329
|
+
for (const r of roots) {
|
|
330
|
+
const previousIds = new Set(positions.keys());
|
|
331
|
+
place(r, cur, MARGIN);
|
|
332
|
+
const subtreeIds = collectSubtreeIds(r, childMap);
|
|
333
|
+
const dy = rootAlignmentDelta(r, subtreeIds, previousIds, doc.edges, positions);
|
|
334
|
+
if (Math.abs(dy) >= EPS) shiftSubtree(subtreeIds, dy, positions, placed);
|
|
335
|
+
const sz = size(r);
|
|
336
|
+
cur += sz.w + ROOT_GAP;
|
|
337
|
+
}
|
|
338
|
+
const edges = makeBlockEdges(doc.edges, positions, placed, parentMap);
|
|
339
|
+
placed.sort((a, b) => {
|
|
340
|
+
if (a.isContainer !== b.isContainer) return a.isContainer ? -1 : 1;
|
|
341
|
+
if (a.isContainer && b.isContainer) {
|
|
342
|
+
const depthDiff = depthOf(a.id, parentMap) - depthOf(b.id, parentMap);
|
|
343
|
+
if (depthDiff !== 0) return depthDiff;
|
|
344
|
+
return b.w * b.h - a.w * a.h;
|
|
345
|
+
}
|
|
346
|
+
return a.y - b.y || a.x - b.x;
|
|
347
|
+
});
|
|
348
|
+
const boxes = [...positions.values()];
|
|
349
|
+
const edgePoints = edges.flatMap((edge) => edge.points);
|
|
350
|
+
const maxBoxX = boxes.length ? Math.max(...boxes.map((b) => b.x + b.w)) : MARGIN;
|
|
351
|
+
const maxBoxY = boxes.length ? Math.max(...boxes.map((b) => b.y + b.h)) : MARGIN;
|
|
352
|
+
const maxEdgeX = edgePoints.length ? Math.max(...edgePoints.map(([x]) => x)) : MARGIN;
|
|
353
|
+
const maxEdgeY = edgePoints.length ? Math.max(...edgePoints.map(([, y]) => y)) : MARGIN;
|
|
354
|
+
const width = Math.max(maxBoxX, maxEdgeX) + MARGIN;
|
|
355
|
+
const height = Math.max(maxBoxY, maxEdgeY) + MARGIN;
|
|
356
|
+
return { nodes: placed, edges, width, height, kind: "block" };
|
|
357
|
+
}
|
|
358
|
+
function layoutFlow(doc) {
|
|
359
|
+
const ids = [...doc.nodes.keys()];
|
|
360
|
+
const { byRank } = computeRanks(doc);
|
|
361
|
+
function shapeOf(id) {
|
|
362
|
+
const n = doc.nodes.get(id);
|
|
363
|
+
const ja = n?.label.ja ?? "";
|
|
364
|
+
const en = n?.label.en ?? "";
|
|
365
|
+
if (ja.endsWith("?") || en.endsWith("?")) return "diamond";
|
|
366
|
+
const inc = doc.edges.filter((e) => e.to === id).length;
|
|
367
|
+
const out = doc.edges.filter((e) => e.from === id).length;
|
|
368
|
+
if (inc === 0 || out === 0) return "round";
|
|
369
|
+
return "box";
|
|
370
|
+
}
|
|
371
|
+
const V_GAP = 14;
|
|
372
|
+
const H_GAP = 10;
|
|
373
|
+
const positions = /* @__PURE__ */ new Map();
|
|
374
|
+
const sortedRanks = [...byRank.keys()].sort((a, b) => a - b);
|
|
375
|
+
let y = MARGIN;
|
|
376
|
+
let maxX = 0;
|
|
377
|
+
for (const r of sortedRanks) {
|
|
378
|
+
const lane = byRank.get(r);
|
|
379
|
+
const widths = lane.map((id) => shapeOf(id) === "diamond" ? NODE_W * 1.2 : NODE_W);
|
|
380
|
+
const totalW = widths.reduce((a, b) => a + b, 0) + (lane.length - 1) * H_GAP;
|
|
381
|
+
let x = MARGIN;
|
|
382
|
+
const canvasW = Math.max(totalW + 2 * MARGIN, 200);
|
|
383
|
+
x = (canvasW - totalW) / 2;
|
|
384
|
+
for (let i = 0; i < lane.length; i++) {
|
|
385
|
+
positions.set(lane[i], { x, y, w: widths[i], h: NODE_H });
|
|
386
|
+
x += widths[i] + H_GAP;
|
|
387
|
+
}
|
|
388
|
+
if (x > maxX) maxX = x;
|
|
389
|
+
y += NODE_H + V_GAP;
|
|
390
|
+
}
|
|
391
|
+
for (const id of ids) {
|
|
392
|
+
if (!positions.has(id)) {
|
|
393
|
+
positions.set(id, { x: MARGIN, y, w: NODE_W, h: NODE_H });
|
|
394
|
+
y += NODE_H + V_GAP;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const placed = [];
|
|
398
|
+
for (const [id, b] of positions) {
|
|
399
|
+
placed.push({
|
|
400
|
+
id,
|
|
401
|
+
...b,
|
|
402
|
+
label: doc.nodes.get(id)?.label ?? {},
|
|
403
|
+
shape: shapeOf(id),
|
|
404
|
+
isContainer: false
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
const edges = makeEdges(doc.edges, positions);
|
|
408
|
+
return { nodes: placed, edges, width: maxX + MARGIN, height: y + MARGIN, kind: "flow" };
|
|
409
|
+
}
|
|
410
|
+
function layoutState(doc) {
|
|
411
|
+
const { byRank } = computeRanks(doc);
|
|
412
|
+
function shapeOf(id) {
|
|
413
|
+
if (id === "*") return "circle";
|
|
414
|
+
return "round";
|
|
415
|
+
}
|
|
416
|
+
const V_GAP = 14, H_GAP = 10;
|
|
417
|
+
const positions = /* @__PURE__ */ new Map();
|
|
418
|
+
const sortedRanks = [...byRank.keys()].sort((a, b) => a - b);
|
|
419
|
+
let y = MARGIN;
|
|
420
|
+
let maxX = 0;
|
|
421
|
+
for (const r of sortedRanks) {
|
|
422
|
+
const lane = byRank.get(r);
|
|
423
|
+
const widths = lane.map((id) => shapeOf(id) === "circle" ? 6 : NODE_W);
|
|
424
|
+
const heights = lane.map((id) => shapeOf(id) === "circle" ? 6 : NODE_H);
|
|
425
|
+
const totalW = widths.reduce((a, b) => a + b, 0) + (lane.length - 1) * H_GAP;
|
|
426
|
+
const canvasW = Math.max(totalW + 2 * MARGIN, 200);
|
|
427
|
+
let x = (canvasW - totalW) / 2;
|
|
428
|
+
for (let i = 0; i < lane.length; i++) {
|
|
429
|
+
positions.set(lane[i], { x, y: y + (NODE_H - heights[i]) / 2, w: widths[i], h: heights[i] });
|
|
430
|
+
x += widths[i] + H_GAP;
|
|
431
|
+
}
|
|
432
|
+
if (x > maxX) maxX = x;
|
|
433
|
+
y += NODE_H + V_GAP;
|
|
434
|
+
}
|
|
435
|
+
for (const id of doc.nodes.keys()) {
|
|
436
|
+
if (!positions.has(id)) {
|
|
437
|
+
positions.set(id, { x: MARGIN, y, w: NODE_W, h: NODE_H });
|
|
438
|
+
y += NODE_H + V_GAP;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const placed = [];
|
|
442
|
+
for (const [id, b] of positions) {
|
|
443
|
+
placed.push({
|
|
444
|
+
id,
|
|
445
|
+
...b,
|
|
446
|
+
label: doc.nodes.get(id)?.label ?? {},
|
|
447
|
+
shape: shapeOf(id),
|
|
448
|
+
isContainer: false
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
const edges = makeEdges(doc.edges, positions);
|
|
452
|
+
return { nodes: placed, edges, width: maxX + MARGIN, height: y + MARGIN, kind: "state" };
|
|
453
|
+
}
|
|
454
|
+
function layoutSeq(doc) {
|
|
455
|
+
const seen = /* @__PURE__ */ new Set();
|
|
456
|
+
const actors = [];
|
|
457
|
+
for (const e of doc.edges) {
|
|
458
|
+
for (const id of [e.from, e.to]) {
|
|
459
|
+
if (!seen.has(id)) {
|
|
460
|
+
seen.add(id);
|
|
461
|
+
actors.push(id);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
for (const id of doc.nodes.keys()) {
|
|
466
|
+
if (!seen.has(id)) {
|
|
467
|
+
seen.add(id);
|
|
468
|
+
actors.push(id);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
const ACTOR_W = 40, ACTOR_H = 12, COL_GAP = 28, MSG_GAP = 12;
|
|
472
|
+
const xOf = /* @__PURE__ */ new Map();
|
|
473
|
+
const placed = [];
|
|
474
|
+
let x = MARGIN;
|
|
475
|
+
for (const id of actors) {
|
|
476
|
+
xOf.set(id, x + ACTOR_W / 2);
|
|
477
|
+
placed.push({
|
|
478
|
+
id,
|
|
479
|
+
x,
|
|
480
|
+
y: MARGIN,
|
|
481
|
+
w: ACTOR_W,
|
|
482
|
+
h: ACTOR_H,
|
|
483
|
+
label: doc.nodes.get(id)?.label ?? {},
|
|
484
|
+
shape: "actor",
|
|
485
|
+
isContainer: false
|
|
486
|
+
});
|
|
487
|
+
x += ACTOR_W + COL_GAP;
|
|
488
|
+
}
|
|
489
|
+
let y = MARGIN + ACTOR_H + MSG_GAP;
|
|
490
|
+
const msgEdges = [];
|
|
491
|
+
for (const e of doc.edges) {
|
|
492
|
+
const xa = xOf.get(e.from);
|
|
493
|
+
const xb = xOf.get(e.to);
|
|
494
|
+
if (xa === void 0 || xb === void 0) continue;
|
|
495
|
+
msgEdges.push({
|
|
496
|
+
from: e.from,
|
|
497
|
+
to: e.to,
|
|
498
|
+
points: [[xa, y], [xb, y]],
|
|
499
|
+
label: e.label,
|
|
500
|
+
op: e.op
|
|
501
|
+
});
|
|
502
|
+
y += MSG_GAP;
|
|
503
|
+
}
|
|
504
|
+
const lifelines = actors.map((id) => ({
|
|
505
|
+
from: id,
|
|
506
|
+
to: id,
|
|
507
|
+
points: [[xOf.get(id), MARGIN + ACTOR_H], [xOf.get(id), y + MSG_GAP]],
|
|
508
|
+
op: "dashed",
|
|
509
|
+
isLifeline: true
|
|
510
|
+
}));
|
|
511
|
+
return {
|
|
512
|
+
nodes: placed,
|
|
513
|
+
edges: [...lifelines, ...msgEdges],
|
|
514
|
+
width: x,
|
|
515
|
+
height: y + MARGIN * 2,
|
|
516
|
+
kind: "seq"
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
function computeRanks(doc) {
|
|
520
|
+
const ids = [...doc.nodes.keys()];
|
|
521
|
+
const incoming = /* @__PURE__ */ new Map();
|
|
522
|
+
for (const id of ids) incoming.set(id, 0);
|
|
523
|
+
for (const e of doc.edges) incoming.set(e.to, (incoming.get(e.to) ?? 0) + 1);
|
|
524
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
525
|
+
for (const e of doc.edges) {
|
|
526
|
+
if (!outgoing.has(e.from)) outgoing.set(e.from, []);
|
|
527
|
+
outgoing.get(e.from).push(e.to);
|
|
528
|
+
}
|
|
529
|
+
let sources = ids.filter((id) => incoming.get(id) === 0);
|
|
530
|
+
if (sources.length === 0) {
|
|
531
|
+
sources = ids.includes("*") ? ["*"] : ids.length ? [ids[0]] : [];
|
|
532
|
+
}
|
|
533
|
+
const rank = /* @__PURE__ */ new Map();
|
|
534
|
+
const visited = /* @__PURE__ */ new Set();
|
|
535
|
+
for (const s of sources) {
|
|
536
|
+
rank.set(s, 0);
|
|
537
|
+
visited.add(s);
|
|
538
|
+
}
|
|
539
|
+
let frontier = [...sources];
|
|
540
|
+
while (frontier.length) {
|
|
541
|
+
const next = [];
|
|
542
|
+
for (const n of frontier) {
|
|
543
|
+
const r = rank.get(n);
|
|
544
|
+
for (const m of outgoing.get(n) ?? []) {
|
|
545
|
+
if (!visited.has(m)) {
|
|
546
|
+
rank.set(m, r + 1);
|
|
547
|
+
visited.add(m);
|
|
548
|
+
next.push(m);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
frontier = next;
|
|
553
|
+
}
|
|
554
|
+
let maxRank = 0;
|
|
555
|
+
for (const r of rank.values()) if (r > maxRank) maxRank = r;
|
|
556
|
+
for (const id of ids) {
|
|
557
|
+
if (!visited.has(id)) {
|
|
558
|
+
maxRank++;
|
|
559
|
+
rank.set(id, maxRank);
|
|
560
|
+
visited.add(id);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const byRank = /* @__PURE__ */ new Map();
|
|
564
|
+
for (const [id, r] of rank) {
|
|
565
|
+
if (!byRank.has(r)) byRank.set(r, []);
|
|
566
|
+
byRank.get(r).push(id);
|
|
567
|
+
}
|
|
568
|
+
return { byRank };
|
|
569
|
+
}
|
|
570
|
+
function makeEdges(srcEdges, positions) {
|
|
571
|
+
return srcEdges.map((e) => {
|
|
572
|
+
const a = positions.get(e.from);
|
|
573
|
+
const b = positions.get(e.to);
|
|
574
|
+
if (!a || !b) {
|
|
575
|
+
return { from: e.from, to: e.to, points: [], label: e.label, op: e.op };
|
|
576
|
+
}
|
|
577
|
+
return {
|
|
578
|
+
from: e.from,
|
|
579
|
+
to: e.to,
|
|
580
|
+
points: orthogonalRoute(a, b),
|
|
581
|
+
label: e.label,
|
|
582
|
+
op: e.op
|
|
583
|
+
};
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
function hasLinearChildFlow(children, edges, childMap) {
|
|
587
|
+
const childSet = new Set(children);
|
|
588
|
+
const pairs = /* @__PURE__ */ new Set();
|
|
589
|
+
const incoming = /* @__PURE__ */ new Map();
|
|
590
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
591
|
+
for (const child of children) {
|
|
592
|
+
incoming.set(child, 0);
|
|
593
|
+
outgoing.set(child, 0);
|
|
594
|
+
}
|
|
595
|
+
for (const edge of edges) {
|
|
596
|
+
const from = topChildFor(edge.from, childSet, childMap);
|
|
597
|
+
const to = topChildFor(edge.to, childSet, childMap);
|
|
598
|
+
if (!from || !to || from === to) continue;
|
|
599
|
+
const key = `${from}|${to}`;
|
|
600
|
+
if (pairs.has(key)) continue;
|
|
601
|
+
pairs.add(key);
|
|
602
|
+
outgoing.set(from, (outgoing.get(from) ?? 0) + 1);
|
|
603
|
+
incoming.set(to, (incoming.get(to) ?? 0) + 1);
|
|
604
|
+
}
|
|
605
|
+
if (pairs.size < children.length - 1) return false;
|
|
606
|
+
let starts = 0;
|
|
607
|
+
let ends = 0;
|
|
608
|
+
for (const child of children) {
|
|
609
|
+
const inc = incoming.get(child) ?? 0;
|
|
610
|
+
const out = outgoing.get(child) ?? 0;
|
|
611
|
+
if (inc > 1 || out > 1) return false;
|
|
612
|
+
if (inc === 0 && out === 1) starts++;
|
|
613
|
+
if (inc === 1 && out === 0) ends++;
|
|
614
|
+
}
|
|
615
|
+
return starts === 1 && ends === 1;
|
|
616
|
+
}
|
|
617
|
+
function topChildFor(id, childSet, childMap) {
|
|
618
|
+
if (childSet.has(id)) return id;
|
|
619
|
+
for (const child of childSet) {
|
|
620
|
+
if (containsDescendant(child, id, childMap)) return child;
|
|
621
|
+
}
|
|
622
|
+
return void 0;
|
|
623
|
+
}
|
|
624
|
+
function containsDescendant(ancestor, id, childMap) {
|
|
625
|
+
const children = childMap.get(ancestor);
|
|
626
|
+
if (!children) return false;
|
|
627
|
+
for (const child of children) {
|
|
628
|
+
if (child === id || containsDescendant(child, id, childMap)) return true;
|
|
629
|
+
}
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
function collectSubtreeIds(id, childMap) {
|
|
633
|
+
const ids = /* @__PURE__ */ new Set([id]);
|
|
634
|
+
for (const child of childMap.get(id) ?? []) {
|
|
635
|
+
for (const descendant of collectSubtreeIds(child, childMap)) ids.add(descendant);
|
|
636
|
+
}
|
|
637
|
+
return ids;
|
|
638
|
+
}
|
|
639
|
+
function alignGridRows(children, sizes, rowHeights, rowTops, cols, childMap, edges, positions, placed) {
|
|
640
|
+
const rows = Math.ceil(children.length / cols);
|
|
641
|
+
for (let row = 0; row < rows; row++) {
|
|
642
|
+
const rowChildren = children.slice(row * cols, row * cols + cols);
|
|
643
|
+
const rowTop = rowTops[row];
|
|
644
|
+
const rowH = rowHeights[row];
|
|
645
|
+
if (rowTop === void 0 || rowH === void 0) continue;
|
|
646
|
+
for (const child of rowChildren) {
|
|
647
|
+
const childIndex = children.indexOf(child);
|
|
648
|
+
const childSize = sizes[childIndex];
|
|
649
|
+
if (!childSize || childSize.h >= rowH - EPS) continue;
|
|
650
|
+
const subtreeIds = collectSubtreeIds(child, childMap);
|
|
651
|
+
const siblingIds = /* @__PURE__ */ new Set();
|
|
652
|
+
for (const sibling of rowChildren) {
|
|
653
|
+
if (sibling === child) continue;
|
|
654
|
+
for (const id of collectSubtreeIds(sibling, childMap)) siblingIds.add(id);
|
|
655
|
+
}
|
|
656
|
+
const deltas = [];
|
|
657
|
+
for (const edge of edges) {
|
|
658
|
+
const fromChild = subtreeIds.has(edge.from);
|
|
659
|
+
const toChild = subtreeIds.has(edge.to);
|
|
660
|
+
if (fromChild && siblingIds.has(edge.to)) {
|
|
661
|
+
addWeightedDelta(
|
|
662
|
+
deltas,
|
|
663
|
+
centerY(positions.get(edge.to)) - centerY(positions.get(edge.from)),
|
|
664
|
+
alignmentWeight(edge, false)
|
|
665
|
+
);
|
|
666
|
+
} else if (toChild && siblingIds.has(edge.from)) {
|
|
667
|
+
addWeightedDelta(
|
|
668
|
+
deltas,
|
|
669
|
+
centerY(positions.get(edge.from)) - centerY(positions.get(edge.to)),
|
|
670
|
+
alignmentWeight(edge, true)
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (deltas.length === 0) continue;
|
|
675
|
+
deltas.sort((a, b) => a - b);
|
|
676
|
+
const desired = deltas[Math.floor(deltas.length / 2)];
|
|
677
|
+
const box = positions.get(child);
|
|
678
|
+
if (!box) continue;
|
|
679
|
+
const minDy = rowTop - box.y;
|
|
680
|
+
const maxDy = rowTop + rowH - childSize.h - box.y;
|
|
681
|
+
const dy = Math.min(maxDy, Math.max(minDy, desired));
|
|
682
|
+
if (Math.abs(dy) >= EPS) shiftSubtree(subtreeIds, dy, positions, placed);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
function rootAlignmentDelta(rootId, subtreeIds, previousIds, edges, positions) {
|
|
687
|
+
const deltas = [];
|
|
688
|
+
for (const edge of edges) {
|
|
689
|
+
const fromCurrent = subtreeIds.has(edge.from);
|
|
690
|
+
const toCurrent = subtreeIds.has(edge.to);
|
|
691
|
+
const fromPrevious = previousIds.has(edge.from);
|
|
692
|
+
const toPrevious = previousIds.has(edge.to);
|
|
693
|
+
if (fromCurrent && toPrevious) {
|
|
694
|
+
addWeightedDelta(
|
|
695
|
+
deltas,
|
|
696
|
+
centerY(positions.get(edge.to)) - centerY(positions.get(edge.from)),
|
|
697
|
+
alignmentWeight(edge, false)
|
|
698
|
+
);
|
|
699
|
+
} else if (toCurrent && fromPrevious) {
|
|
700
|
+
addWeightedDelta(
|
|
701
|
+
deltas,
|
|
702
|
+
centerY(positions.get(edge.from)) - centerY(positions.get(edge.to)),
|
|
703
|
+
alignmentWeight(edge, true)
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if (deltas.length === 0) return 0;
|
|
708
|
+
deltas.sort((a, b) => a - b);
|
|
709
|
+
const desired = deltas[Math.floor(deltas.length / 2)];
|
|
710
|
+
const root = positions.get(rootId);
|
|
711
|
+
if (!root) return desired;
|
|
712
|
+
return Math.max(MARGIN - root.y, desired);
|
|
713
|
+
}
|
|
714
|
+
function addWeightedDelta(deltas, delta, weight) {
|
|
715
|
+
if (!Number.isFinite(delta)) return;
|
|
716
|
+
for (let i = 0; i < weight; i++) deltas.push(delta);
|
|
717
|
+
}
|
|
718
|
+
function alignmentWeight(edge, currentIsTarget) {
|
|
719
|
+
const feedback = edge.op === "dashed" || edge.op === "dashed-arrow";
|
|
720
|
+
return (feedback ? 1 : 3) + (currentIsTarget ? 1 : 0);
|
|
721
|
+
}
|
|
722
|
+
function centerY(box) {
|
|
723
|
+
return box ? box.y + box.h / 2 : Number.NaN;
|
|
724
|
+
}
|
|
725
|
+
function shiftSubtree(ids, dy, positions, placed) {
|
|
726
|
+
for (const id of ids) {
|
|
727
|
+
const box = positions.get(id);
|
|
728
|
+
if (box) box.y += dy;
|
|
729
|
+
}
|
|
730
|
+
for (const node of placed) {
|
|
731
|
+
if (ids.has(node.id)) node.y += dy;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
function makeBlockEdges(srcEdges, positions, obstacles, parentMap) {
|
|
735
|
+
const containerIds = new Set(obstacles.filter((o) => o.isContainer).map((o) => o.id));
|
|
736
|
+
const plans = srcEdges.map((edge, index) => {
|
|
737
|
+
const a = positions.get(edge.from);
|
|
738
|
+
const b = positions.get(edge.to);
|
|
739
|
+
if (!a || !b) {
|
|
740
|
+
return { edge, index, endpointBoundaries: [], endpointInteriorBarriers: [] };
|
|
741
|
+
}
|
|
742
|
+
const routeA = routeEndpointBox(edge.from, edge.to, positions, parentMap, containerIds) ?? a;
|
|
743
|
+
const routeB = routeEndpointBox(edge.to, edge.from, positions, parentMap, containerIds) ?? b;
|
|
744
|
+
const endpointBoundaries = [
|
|
745
|
+
...routeA === a ? [] : [a],
|
|
746
|
+
...routeB === b ? [] : [b]
|
|
747
|
+
];
|
|
748
|
+
const endpointInteriorBarriers = [
|
|
749
|
+
...isExternalContainerEndpoint(edge.from, edge.to, parentMap, containerIds) ? [a] : [],
|
|
750
|
+
...isExternalContainerEndpoint(edge.to, edge.from, parentMap, containerIds) ? [b] : []
|
|
751
|
+
];
|
|
752
|
+
return {
|
|
753
|
+
edge,
|
|
754
|
+
index,
|
|
755
|
+
routeA,
|
|
756
|
+
routeB,
|
|
757
|
+
endpointBoundaries,
|
|
758
|
+
endpointInteriorBarriers,
|
|
759
|
+
bounds: commonRoutingBounds(edge.from, edge.to, parentMap, positions)
|
|
760
|
+
};
|
|
761
|
+
});
|
|
762
|
+
return routePlansSequentially(plans, obstacles, parentMap);
|
|
763
|
+
}
|
|
764
|
+
function routePlansSequentially(plans, baseObstacles, parentMap) {
|
|
765
|
+
const routed = [];
|
|
766
|
+
for (const plan of plans) {
|
|
767
|
+
const searchBox = routeSearchBox(plan);
|
|
768
|
+
const routedArrowGuards = routed.flatMap((edge, index) => arrowGuardObstaclesFor(edge, index));
|
|
769
|
+
const routedEdgeGuards = routed.flatMap((edge, index) => edgeGuardObstaclesFor(edge, index));
|
|
770
|
+
const extraArrowGuards = routedArrowGuards.filter((guard) => guard.edgeIndex !== plan.index && !sharesRouteEndpoint(guard, plan.edge) && (!searchBox || boxesOverlap(searchBox, guard)));
|
|
771
|
+
const extraEdgeGuards = routedEdgeGuards.filter((guard) => shouldUseEdgeGuard(guard, plan, searchBox));
|
|
772
|
+
routed.push(routeBlockPlan(plan, [...baseObstacles, ...extraArrowGuards, ...extraEdgeGuards], parentMap));
|
|
773
|
+
}
|
|
774
|
+
return routed;
|
|
775
|
+
}
|
|
776
|
+
function routeBlockPlan(plan, obstacles, parentMap) {
|
|
777
|
+
const { edge, routeA, routeB } = plan;
|
|
778
|
+
if (!routeA || !routeB) {
|
|
779
|
+
return { from: edge.from, to: edge.to, points: [], label: edge.label, op: edge.op };
|
|
780
|
+
}
|
|
781
|
+
return {
|
|
782
|
+
from: edge.from,
|
|
783
|
+
to: edge.to,
|
|
784
|
+
points: avoidObstaclesRoute(
|
|
785
|
+
routeA,
|
|
786
|
+
routeB,
|
|
787
|
+
edge.from,
|
|
788
|
+
edge.to,
|
|
789
|
+
obstacles,
|
|
790
|
+
parentMap,
|
|
791
|
+
plan.bounds,
|
|
792
|
+
plan.endpointBoundaries,
|
|
793
|
+
plan.endpointInteriorBarriers,
|
|
794
|
+
edge.op
|
|
795
|
+
),
|
|
796
|
+
label: edge.label,
|
|
797
|
+
op: edge.op
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
function arrowGuardObstaclesFor(edge, edgeIndex) {
|
|
801
|
+
if (edge.isLifeline || edge.points.length < 2) return [];
|
|
802
|
+
const guards = [];
|
|
803
|
+
if (hasEndArrow(edge.op)) {
|
|
804
|
+
guards.push(makeArrowGuard(edge, edgeIndex, edge.points[edge.points.length - 1], "end"));
|
|
805
|
+
}
|
|
806
|
+
if (edge.op === "bidir") {
|
|
807
|
+
guards.push(makeArrowGuard(edge, edgeIndex, edge.points[0], "start"));
|
|
808
|
+
}
|
|
809
|
+
return guards;
|
|
810
|
+
}
|
|
811
|
+
function hasEndArrow(op) {
|
|
812
|
+
return op !== "line" && op !== "dashed";
|
|
813
|
+
}
|
|
814
|
+
function makeArrowGuard(edge, edgeIndex, tip, side) {
|
|
815
|
+
const half = edge.op === "thick" ? 5 : 4.2;
|
|
816
|
+
return {
|
|
817
|
+
id: `__arrow_guard_${edgeIndex}_${side}`,
|
|
818
|
+
x: tip[0] - half,
|
|
819
|
+
y: tip[1] - half,
|
|
820
|
+
w: half * 2,
|
|
821
|
+
h: half * 2,
|
|
822
|
+
isContainer: false,
|
|
823
|
+
edgeIndex,
|
|
824
|
+
edgeFrom: edge.from,
|
|
825
|
+
edgeTo: edge.to
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
function sharesRouteEndpoint(guard, edge) {
|
|
829
|
+
return guard.edgeFrom === edge.from || guard.edgeFrom === edge.to || guard.edgeTo === edge.from || guard.edgeTo === edge.to;
|
|
830
|
+
}
|
|
831
|
+
function edgeGuardObstaclesFor(edge, edgeIndex) {
|
|
832
|
+
if (edge.points.length < 2) return [];
|
|
833
|
+
const guards = [];
|
|
834
|
+
const clear = 2.2;
|
|
835
|
+
const trim = 1.2;
|
|
836
|
+
for (let i = 0; i < edge.points.length - 1; i++) {
|
|
837
|
+
const a = edge.points[i];
|
|
838
|
+
const b = edge.points[i + 1];
|
|
839
|
+
const len = segmentLength(a, b);
|
|
840
|
+
if (len <= trim * 2) continue;
|
|
841
|
+
if (Math.abs(a[0] - b[0]) < EPS) {
|
|
842
|
+
const y1 = Math.min(a[1], b[1]) + trim;
|
|
843
|
+
const y2 = Math.max(a[1], b[1]) - trim;
|
|
844
|
+
guards.push({
|
|
845
|
+
id: `__edge_guard_${edgeIndex}_${i}`,
|
|
846
|
+
x: a[0] - clear,
|
|
847
|
+
y: y1,
|
|
848
|
+
w: clear * 2,
|
|
849
|
+
h: y2 - y1,
|
|
850
|
+
isContainer: false,
|
|
851
|
+
edgeIndex,
|
|
852
|
+
edgeOp: edge.op,
|
|
853
|
+
orientation: "vertical"
|
|
854
|
+
});
|
|
855
|
+
} else if (Math.abs(a[1] - b[1]) < EPS) {
|
|
856
|
+
const x1 = Math.min(a[0], b[0]) + trim;
|
|
857
|
+
const x2 = Math.max(a[0], b[0]) - trim;
|
|
858
|
+
guards.push({
|
|
859
|
+
id: `__edge_guard_${edgeIndex}_${i}`,
|
|
860
|
+
x: x1,
|
|
861
|
+
y: a[1] - clear,
|
|
862
|
+
w: x2 - x1,
|
|
863
|
+
h: clear * 2,
|
|
864
|
+
isContainer: false,
|
|
865
|
+
edgeIndex,
|
|
866
|
+
edgeOp: edge.op,
|
|
867
|
+
orientation: "horizontal"
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
return guards;
|
|
872
|
+
}
|
|
873
|
+
function shouldUseEdgeGuard(guard, plan, searchBox) {
|
|
874
|
+
if (guard.edgeIndex === plan.index) return false;
|
|
875
|
+
return !searchBox || boxesOverlap(searchBox, guard);
|
|
876
|
+
}
|
|
877
|
+
function routeSearchBox(plan) {
|
|
878
|
+
if (!plan.routeA || !plan.routeB) return void 0;
|
|
879
|
+
const left = Math.min(plan.routeA.x, plan.routeB.x);
|
|
880
|
+
const top = Math.min(plan.routeA.y, plan.routeB.y);
|
|
881
|
+
const right = Math.max(plan.routeA.x + plan.routeA.w, plan.routeB.x + plan.routeB.w);
|
|
882
|
+
const bottom = Math.max(plan.routeA.y + plan.routeA.h, plan.routeB.y + plan.routeB.h);
|
|
883
|
+
const pad = ROUTE_GAP * 4;
|
|
884
|
+
const candidate = {
|
|
885
|
+
x: left - pad,
|
|
886
|
+
y: top - pad,
|
|
887
|
+
w: right - left + pad * 2,
|
|
888
|
+
h: bottom - top + pad * 2
|
|
889
|
+
};
|
|
890
|
+
if (!plan.bounds) return candidate;
|
|
891
|
+
return {
|
|
892
|
+
x: Math.max(candidate.x, plan.bounds.x),
|
|
893
|
+
y: Math.max(candidate.y, plan.bounds.y),
|
|
894
|
+
w: Math.min(candidate.x + candidate.w, plan.bounds.x + plan.bounds.w) - Math.max(candidate.x, plan.bounds.x),
|
|
895
|
+
h: Math.min(candidate.y + candidate.h, plan.bounds.y + plan.bounds.h) - Math.max(candidate.y, plan.bounds.y)
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
function boxesOverlap(a, b) {
|
|
899
|
+
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;
|
|
900
|
+
}
|
|
901
|
+
function routeEndpointBox(id, otherId, positions, parentMap, containerIds) {
|
|
902
|
+
const container = positions.get(id);
|
|
903
|
+
const other = positions.get(otherId);
|
|
904
|
+
if (!container || !other) return void 0;
|
|
905
|
+
if (!isAncestorOf(id, otherId, parentMap)) {
|
|
906
|
+
if (!containerIds.has(id)) return void 0;
|
|
907
|
+
return container;
|
|
908
|
+
}
|
|
909
|
+
const left = container.x + PAD;
|
|
910
|
+
const right = container.x + container.w - PAD;
|
|
911
|
+
const top = container.y + TITLE_H + PAD / 2;
|
|
912
|
+
const x = Math.min(right, Math.max(left, other.x + other.w / 2));
|
|
913
|
+
return { x: x - 0.1, y: top - 0.1, w: 0.2, h: 0.2 };
|
|
914
|
+
}
|
|
915
|
+
function isExternalContainerEndpoint(id, otherId, parentMap, containerIds) {
|
|
916
|
+
return containerIds.has(id) && !isAncestorOf(id, otherId, parentMap);
|
|
917
|
+
}
|
|
918
|
+
function isAncestorOf(ancestor, id, parentMap) {
|
|
919
|
+
let cur = id;
|
|
920
|
+
const seen = /* @__PURE__ */ new Set();
|
|
921
|
+
while (parentMap.has(cur) && !seen.has(cur)) {
|
|
922
|
+
seen.add(cur);
|
|
923
|
+
cur = parentMap.get(cur);
|
|
924
|
+
if (cur === ancestor) return true;
|
|
925
|
+
}
|
|
926
|
+
return false;
|
|
927
|
+
}
|
|
928
|
+
function depthOf(id, parentMap) {
|
|
929
|
+
let depth = 0;
|
|
930
|
+
let cur = id;
|
|
931
|
+
const seen = /* @__PURE__ */ new Set();
|
|
932
|
+
while (parentMap.has(cur) && !seen.has(cur)) {
|
|
933
|
+
seen.add(cur);
|
|
934
|
+
cur = parentMap.get(cur);
|
|
935
|
+
depth++;
|
|
936
|
+
}
|
|
937
|
+
return depth;
|
|
938
|
+
}
|
|
939
|
+
function avoidObstaclesRoute(a, b, from, to, obstacles, parentMap, bounds, endpointBoundaries = [], endpointInteriorBarriers = [], op = "line", strictEdgeGuards = true) {
|
|
940
|
+
const routedObstacles = obstacles.filter((o) => o.id !== from && o.id !== to);
|
|
941
|
+
const leafObstacles = routedObstacles.filter((o) => !o.isContainer);
|
|
942
|
+
const hardLeafObstacles = leafObstacles.filter((o) => o.id.startsWith("__arrow_guard_"));
|
|
943
|
+
const allowedContainers = allowedContainerIds(from, to, parentMap);
|
|
944
|
+
const passableContainerObstacles = routedObstacles.filter((o) => o.isContainer && allowedContainers.has(o.id));
|
|
945
|
+
const blockedContainerObstacles = routedObstacles.filter((o) => o.isContainer && !allowedContainers.has(o.id));
|
|
946
|
+
const externalContainerObstacles = passableContainerObstacles.filter((o) => isAncestorOf(o.id, from, parentMap) !== isAncestorOf(o.id, to, parentMap));
|
|
947
|
+
const boundaryObstacles = endpointBoundaries.map((box, index) => ({
|
|
948
|
+
...box,
|
|
949
|
+
id: `__endpoint_boundary_${index}`,
|
|
950
|
+
isContainer: true
|
|
951
|
+
}));
|
|
952
|
+
const realLeafObstacles = leafObstacles.filter((o) => !o.id.startsWith("__"));
|
|
953
|
+
const edgeGuardObstacles = leafObstacles.filter(isEdgeGuardObstacle);
|
|
954
|
+
const straight = preferredStraightRoute(a, b);
|
|
955
|
+
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, [
|
|
956
|
+
...endpointBoundaries,
|
|
957
|
+
...passableContainerObstacles,
|
|
958
|
+
...blockedContainerObstacles
|
|
959
|
+
], BORDER_CLEARANCE)) {
|
|
960
|
+
return straight;
|
|
961
|
+
}
|
|
962
|
+
const lanes = routeLanes(a, b, [...routedObstacles, ...boundaryObstacles], bounds);
|
|
963
|
+
let best = orthogonalRoute(a, b);
|
|
964
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
965
|
+
for (const start of portsOf(a)) {
|
|
966
|
+
for (const end of portsOf(b)) {
|
|
967
|
+
const sourcePort = start.point;
|
|
968
|
+
const targetPort = end.point;
|
|
969
|
+
const sp = portLeadPoint(start);
|
|
970
|
+
const ep = portLeadPoint(end);
|
|
971
|
+
const candidates = [
|
|
972
|
+
[sourcePort, sp, [ep[0], sp[1]], ep, targetPort],
|
|
973
|
+
[sourcePort, sp, [sp[0], ep[1]], ep, targetPort]
|
|
974
|
+
];
|
|
975
|
+
if (Math.abs(sp[0] - ep[0]) < EPS || Math.abs(sp[1] - ep[1]) < EPS) {
|
|
976
|
+
candidates.push([sourcePort, sp, ep, targetPort]);
|
|
977
|
+
}
|
|
978
|
+
for (const x of lanes.xs) {
|
|
979
|
+
candidates.push([sourcePort, sp, [x, sp[1]], [x, ep[1]], ep, targetPort]);
|
|
980
|
+
}
|
|
981
|
+
for (const y of lanes.ys) {
|
|
982
|
+
candidates.push([sourcePort, sp, [sp[0], y], [ep[0], y], ep, targetPort]);
|
|
983
|
+
}
|
|
984
|
+
for (const x of lanes.xs) {
|
|
985
|
+
for (const y of lanes.ys) {
|
|
986
|
+
candidates.push([sourcePort, sp, [x, sp[1]], [x, y], [ep[0], y], ep, targetPort]);
|
|
987
|
+
candidates.push([sourcePort, sp, [sp[0], y], [x, y], [x, ep[1]], ep, targetPort]);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
for (const candidate of candidates) {
|
|
991
|
+
const normalized = normalizeRoute(candidate);
|
|
992
|
+
if (!isOrthogonalRoute(normalized)) continue;
|
|
993
|
+
if (!hasArrowTerminalClearance(normalized, op)) continue;
|
|
994
|
+
if (bounds && !routeFitsBounds(normalized, bounds)) continue;
|
|
995
|
+
if (routeOverlapsAnyBorder(normalized, [a, b], 0)) continue;
|
|
996
|
+
if (routeIntersectsAnyInterior(normalized, endpointInteriorBarriers, 0)) continue;
|
|
997
|
+
if (routeIntersectsAnyInterior(normalized, realLeafObstacles, OBSTACLE_PAD)) continue;
|
|
998
|
+
if (routeIntersectsAnyInterior(normalized, hardLeafObstacles, OBSTACLE_PAD)) continue;
|
|
999
|
+
if (strictEdgeGuards && routeSharesAnyEdgeGuardLane(normalized, edgeGuardObstacles)) continue;
|
|
1000
|
+
if (routeIntersectsAnyInterior(normalized, blockedContainerObstacles, OBSTACLE_PAD)) continue;
|
|
1001
|
+
if (routeOverlapsAnyBorder(normalized, [
|
|
1002
|
+
...endpointBoundaries,
|
|
1003
|
+
...passableContainerObstacles,
|
|
1004
|
+
...blockedContainerObstacles
|
|
1005
|
+
], BORDER_CLEARANCE)) continue;
|
|
1006
|
+
const score = scoreRoute(
|
|
1007
|
+
normalized,
|
|
1008
|
+
a,
|
|
1009
|
+
b,
|
|
1010
|
+
leafObstacles,
|
|
1011
|
+
blockedContainerObstacles,
|
|
1012
|
+
passableContainerObstacles,
|
|
1013
|
+
externalContainerObstacles,
|
|
1014
|
+
endpointBoundaries
|
|
1015
|
+
) + portPairPenalty(a, b, start.side, end.side, op) + start.offsetPenalty + end.offsetPenalty;
|
|
1016
|
+
if (score < bestScore) {
|
|
1017
|
+
best = normalized;
|
|
1018
|
+
bestScore = score;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
if (bestScore === Number.POSITIVE_INFINITY) {
|
|
1024
|
+
if (strictEdgeGuards && edgeGuardObstacles.length > 0) {
|
|
1025
|
+
return avoidObstaclesRoute(
|
|
1026
|
+
a,
|
|
1027
|
+
b,
|
|
1028
|
+
from,
|
|
1029
|
+
to,
|
|
1030
|
+
obstacles,
|
|
1031
|
+
parentMap,
|
|
1032
|
+
bounds,
|
|
1033
|
+
endpointBoundaries,
|
|
1034
|
+
endpointInteriorBarriers,
|
|
1035
|
+
op,
|
|
1036
|
+
false
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
return orthogonalRoute(a, b);
|
|
1040
|
+
}
|
|
1041
|
+
return best;
|
|
1042
|
+
}
|
|
1043
|
+
function preferredStraightRoute(a, b) {
|
|
1044
|
+
const acx = a.x + a.w / 2;
|
|
1045
|
+
const acy = a.y + a.h / 2;
|
|
1046
|
+
const bcx = b.x + b.w / 2;
|
|
1047
|
+
const bcy = b.y + b.h / 2;
|
|
1048
|
+
if (Math.abs(acy - bcy) < EPS) {
|
|
1049
|
+
if (a.x + a.w <= b.x) return [[a.x + a.w, acy], [b.x, bcy]];
|
|
1050
|
+
if (b.x + b.w <= a.x) return [[a.x, acy], [b.x + b.w, bcy]];
|
|
1051
|
+
}
|
|
1052
|
+
if (Math.abs(acx - bcx) < EPS) {
|
|
1053
|
+
if (a.y + a.h <= b.y) return [[acx, a.y + a.h], [bcx, b.y]];
|
|
1054
|
+
if (b.y + b.h <= a.y) return [[acx, a.y], [bcx, b.y + b.h]];
|
|
1055
|
+
}
|
|
1056
|
+
return void 0;
|
|
1057
|
+
}
|
|
1058
|
+
function hasArrowTerminalClearance(points, op) {
|
|
1059
|
+
if (points.length < 2) return false;
|
|
1060
|
+
if (op !== "line" && op !== "dashed") {
|
|
1061
|
+
if (terminalSegmentLength(points, false) < arrowTerminalClearance(op)) return false;
|
|
1062
|
+
}
|
|
1063
|
+
if (op === "bidir") {
|
|
1064
|
+
if (terminalSegmentLength(points, true) < arrowTerminalClearance(op)) return false;
|
|
1065
|
+
}
|
|
1066
|
+
return true;
|
|
1067
|
+
}
|
|
1068
|
+
function terminalSegmentLength(points, atStart) {
|
|
1069
|
+
if (points.length < 2) return 0;
|
|
1070
|
+
const a = atStart ? points[0] : points[points.length - 1];
|
|
1071
|
+
const b = atStart ? points[1] : points[points.length - 2];
|
|
1072
|
+
return segmentLength(a, b);
|
|
1073
|
+
}
|
|
1074
|
+
function arrowTerminalClearance(op) {
|
|
1075
|
+
return op === "thick" ? THICK_ARROW_TERMINAL_CLEARANCE : ARROW_TERMINAL_CLEARANCE;
|
|
1076
|
+
}
|
|
1077
|
+
function routeIntersectsAnyInterior(points, boxes, pad) {
|
|
1078
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
1079
|
+
for (const box of boxes) {
|
|
1080
|
+
if (boxInteriorOverlap(points[i], points[i + 1], box, pad) > EPS) return true;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
return false;
|
|
1084
|
+
}
|
|
1085
|
+
function routeOverlapsAnyBorder(points, boxes, tolerance) {
|
|
1086
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
1087
|
+
for (const box of boxes) {
|
|
1088
|
+
if (boxBorderProximityOverlap(points[i], points[i + 1], box, tolerance) > EPS) return true;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
return false;
|
|
1092
|
+
}
|
|
1093
|
+
function routeSharesAnyEdgeGuardLane(points, guards) {
|
|
1094
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
1095
|
+
for (const guard of guards) {
|
|
1096
|
+
if (segmentSharesEdgeGuardLane(points[i], points[i + 1], guard)) return true;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
return false;
|
|
1100
|
+
}
|
|
1101
|
+
function segmentSharesEdgeGuardLane(a, b, guard) {
|
|
1102
|
+
const segmentVertical = Math.abs(a[0] - b[0]) < EPS;
|
|
1103
|
+
const segmentHorizontal = Math.abs(a[1] - b[1]) < EPS;
|
|
1104
|
+
if (segmentVertical && guard.orientation === "vertical") {
|
|
1105
|
+
const guardX = guard.x + guard.w / 2;
|
|
1106
|
+
if (Math.abs(a[0] - guardX) > guard.w / 2 + EPS) return false;
|
|
1107
|
+
return intervalOverlap(a[1], b[1], guard.y, guard.y + guard.h) > EPS;
|
|
1108
|
+
}
|
|
1109
|
+
if (segmentHorizontal && guard.orientation === "horizontal") {
|
|
1110
|
+
const guardY = guard.y + guard.h / 2;
|
|
1111
|
+
if (Math.abs(a[1] - guardY) > guard.h / 2 + EPS) return false;
|
|
1112
|
+
return intervalOverlap(a[0], b[0], guard.x, guard.x + guard.w) > EPS;
|
|
1113
|
+
}
|
|
1114
|
+
return false;
|
|
1115
|
+
}
|
|
1116
|
+
function allowedContainerIds(from, to, parentMap) {
|
|
1117
|
+
return /* @__PURE__ */ new Set([from, to, ...ancestorsOf(from, parentMap), ...ancestorsOf(to, parentMap)]);
|
|
1118
|
+
}
|
|
1119
|
+
function portsOf(box) {
|
|
1120
|
+
const cx = box.x + box.w / 2;
|
|
1121
|
+
const cy = box.y + box.h / 2;
|
|
1122
|
+
const xOffset = Math.min(box.w * 0.25, Math.max(3, box.w / 2 - 4));
|
|
1123
|
+
const yOffset = Math.min(box.h * 0.25, Math.max(2, box.h / 2 - 3));
|
|
1124
|
+
const sidePenalty = 9;
|
|
1125
|
+
return [
|
|
1126
|
+
{ point: [box.x + box.w, cy], side: "right", offsetPenalty: 0 },
|
|
1127
|
+
{ point: [box.x, cy], side: "left", offsetPenalty: 0 },
|
|
1128
|
+
{ point: [cx, box.y + box.h], side: "bottom", offsetPenalty: 0 },
|
|
1129
|
+
{ point: [cx, box.y], side: "top", offsetPenalty: 0 },
|
|
1130
|
+
{ point: [box.x + box.w, cy - yOffset], side: "right", offsetPenalty: sidePenalty },
|
|
1131
|
+
{ point: [box.x + box.w, cy + yOffset], side: "right", offsetPenalty: sidePenalty },
|
|
1132
|
+
{ point: [box.x, cy - yOffset], side: "left", offsetPenalty: sidePenalty },
|
|
1133
|
+
{ point: [box.x, cy + yOffset], side: "left", offsetPenalty: sidePenalty },
|
|
1134
|
+
{ point: [cx - xOffset, box.y + box.h], side: "bottom", offsetPenalty: sidePenalty },
|
|
1135
|
+
{ point: [cx + xOffset, box.y + box.h], side: "bottom", offsetPenalty: sidePenalty },
|
|
1136
|
+
{ point: [cx - xOffset, box.y], side: "top", offsetPenalty: sidePenalty },
|
|
1137
|
+
{ point: [cx + xOffset, box.y], side: "top", offsetPenalty: sidePenalty }
|
|
1138
|
+
];
|
|
1139
|
+
}
|
|
1140
|
+
function portLeadPoint(port) {
|
|
1141
|
+
const [x, y] = port.point;
|
|
1142
|
+
switch (port.side) {
|
|
1143
|
+
case "right":
|
|
1144
|
+
return [x + PORT_STUB, y];
|
|
1145
|
+
case "left":
|
|
1146
|
+
return [x - PORT_STUB, y];
|
|
1147
|
+
case "bottom":
|
|
1148
|
+
return [x, y + PORT_STUB];
|
|
1149
|
+
case "top":
|
|
1150
|
+
return [x, y - PORT_STUB];
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
function portPairPenalty(source, target, start, end, op) {
|
|
1154
|
+
const sx = source.x + source.w / 2;
|
|
1155
|
+
const sy = source.y + source.h / 2;
|
|
1156
|
+
const tx = target.x + target.w / 2;
|
|
1157
|
+
const ty = target.y + target.h / 2;
|
|
1158
|
+
const dx = tx - sx;
|
|
1159
|
+
const dy = ty - sy;
|
|
1160
|
+
if (op === "dashed") return dashedPortPairPenalty(start, end, dx, dy, source, target);
|
|
1161
|
+
return sourcePortPenalty(start, dx, dy, source) + targetPortPenalty(end, dx, dy, target);
|
|
1162
|
+
}
|
|
1163
|
+
function dashedPortPairPenalty(start, end, dx, dy, source, target) {
|
|
1164
|
+
if (Math.abs(dy) > Math.min(source.h, target.h) * 0.8) {
|
|
1165
|
+
const startDesired = dy > 0 ? "bottom" : "top";
|
|
1166
|
+
const endDesired = dy > 0 ? "top" : "bottom";
|
|
1167
|
+
return dashedSidePenalty(start, startDesired) + dashedSidePenalty(end, endDesired);
|
|
1168
|
+
}
|
|
1169
|
+
if (Math.abs(dx) > Math.min(source.w, target.w) * 0.8) {
|
|
1170
|
+
const startDesired = dx > 0 ? "right" : "left";
|
|
1171
|
+
const endDesired = dx > 0 ? "left" : "right";
|
|
1172
|
+
return dashedSidePenalty(start, startDesired) + dashedSidePenalty(end, endDesired);
|
|
1173
|
+
}
|
|
1174
|
+
return 0;
|
|
1175
|
+
}
|
|
1176
|
+
function dashedSidePenalty(side, desired) {
|
|
1177
|
+
if (side === desired) return 0;
|
|
1178
|
+
if (side === oppositeSide(desired)) return 1400;
|
|
1179
|
+
return 450;
|
|
1180
|
+
}
|
|
1181
|
+
function sourcePortPenalty(side, dx, dy, box) {
|
|
1182
|
+
if (Math.abs(dy) > box.h * 0.8 && Math.abs(dy) >= Math.abs(dx) * VERTICAL_PORT_RATIO) {
|
|
1183
|
+
const desired = dy > 0 ? "bottom" : "top";
|
|
1184
|
+
if (side === desired) return 0;
|
|
1185
|
+
return side === oppositeSide(desired) ? 800 : 180;
|
|
1186
|
+
}
|
|
1187
|
+
if (Math.abs(dx) > box.w * 0.5) {
|
|
1188
|
+
const desired = dx > 0 ? "right" : "left";
|
|
1189
|
+
if (side === desired) return 0;
|
|
1190
|
+
return side === oppositeSide(desired) ? 900 : 90;
|
|
1191
|
+
}
|
|
1192
|
+
if (Math.abs(dy) > box.h * 0.8) {
|
|
1193
|
+
const desired = dy > 0 ? "bottom" : "top";
|
|
1194
|
+
if (side === desired) return 0;
|
|
1195
|
+
return side === oppositeSide(desired) ? 600 : 120;
|
|
1196
|
+
}
|
|
1197
|
+
return 0;
|
|
1198
|
+
}
|
|
1199
|
+
function targetPortPenalty(side, dx, dy, box) {
|
|
1200
|
+
if (Math.abs(dy) > box.h * 0.8 && Math.abs(dy) >= Math.abs(dx) * VERTICAL_PORT_RATIO) {
|
|
1201
|
+
const desired = dy > 0 ? "top" : "bottom";
|
|
1202
|
+
if (side === desired) return 0;
|
|
1203
|
+
return side === oppositeSide(desired) ? 1400 : 360;
|
|
1204
|
+
}
|
|
1205
|
+
if (Math.abs(dx) > box.w * 0.5 && Math.abs(dx) >= Math.abs(dy) * 1.2) {
|
|
1206
|
+
const desired = dx > 0 ? "left" : "right";
|
|
1207
|
+
if (side === desired) return 0;
|
|
1208
|
+
return side === oppositeSide(desired) ? 1200 : 220;
|
|
1209
|
+
}
|
|
1210
|
+
if (Math.abs(dy) > box.h * 0.35) {
|
|
1211
|
+
const desired = dy > 0 ? "top" : "bottom";
|
|
1212
|
+
if (side === desired) return 0;
|
|
1213
|
+
return side === "left" || side === "right" ? 9e3 : 1200;
|
|
1214
|
+
}
|
|
1215
|
+
if (Math.abs(dx) > box.w * 0.5) {
|
|
1216
|
+
const desired = dx > 0 ? "left" : "right";
|
|
1217
|
+
if (side === desired) return 0;
|
|
1218
|
+
return side === oppositeSide(desired) ? 1e3 : 160;
|
|
1219
|
+
}
|
|
1220
|
+
return 0;
|
|
1221
|
+
}
|
|
1222
|
+
function oppositeSide(side) {
|
|
1223
|
+
switch (side) {
|
|
1224
|
+
case "right":
|
|
1225
|
+
return "left";
|
|
1226
|
+
case "left":
|
|
1227
|
+
return "right";
|
|
1228
|
+
case "bottom":
|
|
1229
|
+
return "top";
|
|
1230
|
+
case "top":
|
|
1231
|
+
return "bottom";
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
function routeLanes(a, b, obstacles, bounds) {
|
|
1235
|
+
const boxes = [a, b, ...obstacles];
|
|
1236
|
+
const laneMargin = ROUTE_GAP * 2;
|
|
1237
|
+
const minX = bounds ? bounds.x : Math.max(MARGIN, Math.min(...boxes.map((o) => o.x)) - laneMargin);
|
|
1238
|
+
const maxX = bounds ? bounds.x + bounds.w : Math.max(...boxes.map((o) => o.x + o.w)) + laneMargin;
|
|
1239
|
+
const minY = bounds ? bounds.y : Math.max(MARGIN, Math.min(...boxes.map((o) => o.y)) - laneMargin);
|
|
1240
|
+
const maxY = bounds ? bounds.y + bounds.h : Math.max(...boxes.map((o) => o.y + o.h)) + laneMargin;
|
|
1241
|
+
const xs = [(a.x + a.w / 2 + b.x + b.w / 2) / 2];
|
|
1242
|
+
const ys = [(a.y + a.h / 2 + b.y + b.h / 2) / 2];
|
|
1243
|
+
for (const box of boxes) {
|
|
1244
|
+
for (const gap of [ROUTE_GAP, ROUTE_GAP * 1.5, ROUTE_GAP * 2]) {
|
|
1245
|
+
xs.push(box.x - gap, box.x + box.w + gap);
|
|
1246
|
+
ys.push(box.y - gap, box.y + box.h + gap);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
if (bounds) {
|
|
1250
|
+
xs.push(bounds.x, bounds.x + bounds.w);
|
|
1251
|
+
ys.push(bounds.y, bounds.y + bounds.h);
|
|
1252
|
+
}
|
|
1253
|
+
return {
|
|
1254
|
+
xs: limitRouteLanes(
|
|
1255
|
+
uniqueSorted(xs.filter((x) => x >= minX && x <= maxX)),
|
|
1256
|
+
(a.x + a.w / 2 + b.x + b.w / 2) / 2
|
|
1257
|
+
),
|
|
1258
|
+
ys: limitRouteLanes(
|
|
1259
|
+
uniqueSorted(ys.filter((y) => y >= minY && y <= maxY)),
|
|
1260
|
+
(a.y + a.h / 2 + b.y + b.h / 2) / 2
|
|
1261
|
+
)
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
function limitRouteLanes(values, anchor) {
|
|
1265
|
+
if (values.length <= MAX_ROUTE_LANES) return values;
|
|
1266
|
+
const keep = /* @__PURE__ */ new Set([values[0], values[values.length - 1]]);
|
|
1267
|
+
for (const value of [...values].sort((a, b) => Math.abs(a - anchor) - Math.abs(b - anchor))) {
|
|
1268
|
+
keep.add(value);
|
|
1269
|
+
if (keep.size >= MAX_ROUTE_LANES) break;
|
|
1270
|
+
}
|
|
1271
|
+
return [...keep].sort((a, b) => a - b);
|
|
1272
|
+
}
|
|
1273
|
+
function commonRoutingBounds(from, to, parentMap, positions) {
|
|
1274
|
+
const common = nearestCommonAncestor(from, to, parentMap);
|
|
1275
|
+
if (!common) return void 0;
|
|
1276
|
+
const box = positions.get(common);
|
|
1277
|
+
if (!box) return void 0;
|
|
1278
|
+
const inset = ROUTE_GAP / 2;
|
|
1279
|
+
const top = box.y + TITLE_H + PAD / 2;
|
|
1280
|
+
const bottom = box.y + box.h - inset;
|
|
1281
|
+
return {
|
|
1282
|
+
x: box.x + inset,
|
|
1283
|
+
y: top,
|
|
1284
|
+
w: Math.max(0, box.w - inset * 2),
|
|
1285
|
+
h: Math.max(0, bottom - top)
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
function nearestCommonAncestor(from, to, parentMap) {
|
|
1289
|
+
const toAncestors = new Set(ancestorsOf(to, parentMap));
|
|
1290
|
+
return ancestorsOf(from, parentMap).find((id) => toAncestors.has(id));
|
|
1291
|
+
}
|
|
1292
|
+
function ancestorsOf(id, parentMap) {
|
|
1293
|
+
const out = [];
|
|
1294
|
+
let cur = id;
|
|
1295
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1296
|
+
while (parentMap.has(cur) && !seen.has(cur)) {
|
|
1297
|
+
seen.add(cur);
|
|
1298
|
+
cur = parentMap.get(cur);
|
|
1299
|
+
out.push(cur);
|
|
1300
|
+
}
|
|
1301
|
+
return out;
|
|
1302
|
+
}
|
|
1303
|
+
function routeFitsBounds(points, bounds) {
|
|
1304
|
+
const right = bounds.x + bounds.w;
|
|
1305
|
+
const bottom = bounds.y + bounds.h;
|
|
1306
|
+
return points.every(([x, y]) => x >= bounds.x - EPS && x <= right + EPS && y >= bounds.y - EPS && y <= bottom + EPS);
|
|
1307
|
+
}
|
|
1308
|
+
function uniqueSorted(values) {
|
|
1309
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1310
|
+
const out = [];
|
|
1311
|
+
for (const value of values) {
|
|
1312
|
+
const rounded = Math.round(value * 1e3) / 1e3;
|
|
1313
|
+
const key = rounded.toFixed(3);
|
|
1314
|
+
if (seen.has(key)) continue;
|
|
1315
|
+
seen.add(key);
|
|
1316
|
+
out.push(rounded);
|
|
1317
|
+
}
|
|
1318
|
+
return out.sort((a, b) => a - b);
|
|
1319
|
+
}
|
|
1320
|
+
function normalizeRoute(points) {
|
|
1321
|
+
const deduped = [];
|
|
1322
|
+
for (const point of points) {
|
|
1323
|
+
const prev = deduped[deduped.length - 1];
|
|
1324
|
+
if (!prev || Math.abs(prev[0] - point[0]) >= EPS || Math.abs(prev[1] - point[1]) >= EPS) {
|
|
1325
|
+
deduped.push(point);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
const out = [];
|
|
1329
|
+
for (const point of deduped) {
|
|
1330
|
+
out.push(point);
|
|
1331
|
+
while (out.length >= 3) {
|
|
1332
|
+
const a = out[out.length - 3];
|
|
1333
|
+
const b = out[out.length - 2];
|
|
1334
|
+
const c = out[out.length - 1];
|
|
1335
|
+
const sameX = Math.abs(a[0] - b[0]) < EPS && Math.abs(b[0] - c[0]) < EPS;
|
|
1336
|
+
const sameY = Math.abs(a[1] - b[1]) < EPS && Math.abs(b[1] - c[1]) < EPS;
|
|
1337
|
+
if (!sameX && !sameY) break;
|
|
1338
|
+
out.splice(out.length - 2, 1);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
return out;
|
|
1342
|
+
}
|
|
1343
|
+
function isOrthogonalRoute(points) {
|
|
1344
|
+
if (points.length < 2) return false;
|
|
1345
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
1346
|
+
const a = points[i];
|
|
1347
|
+
const b = points[i + 1];
|
|
1348
|
+
if (Math.abs(a[0] - b[0]) >= EPS && Math.abs(a[1] - b[1]) >= EPS) return false;
|
|
1349
|
+
}
|
|
1350
|
+
return true;
|
|
1351
|
+
}
|
|
1352
|
+
function scoreRoute(points, source, target, leafObstacles, blockedContainerObstacles, passableContainerObstacles, externalContainerObstacles, endpointBoundaries) {
|
|
1353
|
+
let score = 0;
|
|
1354
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
1355
|
+
const a = points[i];
|
|
1356
|
+
const b = points[i + 1];
|
|
1357
|
+
score += segmentLength(a, b);
|
|
1358
|
+
const sourceInterior = boxInteriorOverlap(a, b, source, 0);
|
|
1359
|
+
if (sourceInterior > 0) score += 22e4 + sourceInterior * 1500;
|
|
1360
|
+
const targetInterior = boxInteriorOverlap(a, b, target, 0);
|
|
1361
|
+
if (targetInterior > 0) score += 22e4 + targetInterior * 1500;
|
|
1362
|
+
const sourceBorder = boxBorderOverlap(a, b, source);
|
|
1363
|
+
if (sourceBorder > 0) score += 8e3 + sourceBorder * 120;
|
|
1364
|
+
const targetBorder = boxBorderOverlap(a, b, target);
|
|
1365
|
+
if (targetBorder > 0) score += 8e3 + targetBorder * 120;
|
|
1366
|
+
for (const boundary of endpointBoundaries) {
|
|
1367
|
+
const overlap = boxBorderOverlap(a, b, boundary);
|
|
1368
|
+
if (overlap > 0) score += 12e3 + overlap * 160;
|
|
1369
|
+
}
|
|
1370
|
+
for (const obstacle of leafObstacles) {
|
|
1371
|
+
const overlap = boxInteriorOverlap(a, b, obstacle, OBSTACLE_PAD);
|
|
1372
|
+
if (overlap > 0) score += leafObstaclePenalty(obstacle, a, b, overlap);
|
|
1373
|
+
}
|
|
1374
|
+
for (const obstacle of blockedContainerObstacles) {
|
|
1375
|
+
const overlap = boxInteriorOverlap(a, b, obstacle, OBSTACLE_PAD);
|
|
1376
|
+
if (overlap > 0) score += 18e4 + overlap * 1200;
|
|
1377
|
+
const border = boxBorderOverlap(a, b, obstacle);
|
|
1378
|
+
if (border > 0) score += 18e3 + border * 300;
|
|
1379
|
+
}
|
|
1380
|
+
for (const obstacle of passableContainerObstacles) {
|
|
1381
|
+
const titleOverlap = boxInteriorOverlap(a, b, containerTitleRoutingBand(obstacle), OBSTACLE_PAD);
|
|
1382
|
+
if (titleOverlap > 0) score += 14e3 + titleOverlap * 120;
|
|
1383
|
+
const overlap = boxBorderOverlap(a, b, obstacle);
|
|
1384
|
+
if (overlap > 0) score += 6e3 + overlap * 120;
|
|
1385
|
+
}
|
|
1386
|
+
for (const obstacle of externalContainerObstacles) {
|
|
1387
|
+
const overlap = boxInteriorOverlap(a, b, obstacle, 0);
|
|
1388
|
+
if (overlap > 0) score += overlap * 40;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
score += Math.max(0, points.length - 2) * 3;
|
|
1392
|
+
score += routeExcursionPenalty(points, source, target);
|
|
1393
|
+
return score;
|
|
1394
|
+
}
|
|
1395
|
+
function routeExcursionPenalty(points, source, target) {
|
|
1396
|
+
const pad = ROUTE_GAP * 5;
|
|
1397
|
+
const left = Math.min(source.x, target.x) - pad;
|
|
1398
|
+
const top = Math.min(source.y, target.y) - pad;
|
|
1399
|
+
const right = Math.max(source.x + source.w, target.x + target.w) + pad;
|
|
1400
|
+
const bottom = Math.max(source.y + source.h, target.y + target.h) + pad;
|
|
1401
|
+
let penalty = 0;
|
|
1402
|
+
for (const [x, y] of points) {
|
|
1403
|
+
if (x < left) penalty += (left - x) * 90;
|
|
1404
|
+
if (x > right) penalty += (x - right) * 90;
|
|
1405
|
+
if (y < top) penalty += (top - y) * 90;
|
|
1406
|
+
if (y > bottom) penalty += (y - bottom) * 90;
|
|
1407
|
+
}
|
|
1408
|
+
return penalty;
|
|
1409
|
+
}
|
|
1410
|
+
function leafObstaclePenalty(obstacle, a, b, overlap) {
|
|
1411
|
+
if (obstacle.id.startsWith("__arrow_guard_")) return 7e4 + overlap * 700;
|
|
1412
|
+
if (isEdgeGuardObstacle(obstacle)) return edgeGuardPenalty(obstacle, a, b, overlap);
|
|
1413
|
+
return 16e4 + overlap * 1200;
|
|
1414
|
+
}
|
|
1415
|
+
function edgeGuardPenalty(obstacle, a, b, overlap) {
|
|
1416
|
+
const segmentVertical = Math.abs(a[0] - b[0]) < EPS;
|
|
1417
|
+
const guardVertical = obstacle.orientation === "vertical";
|
|
1418
|
+
if (segmentVertical === guardVertical) return 85e3 + overlap * 1200;
|
|
1419
|
+
return 500 + overlap * 160;
|
|
1420
|
+
}
|
|
1421
|
+
function isEdgeGuardObstacle(obstacle) {
|
|
1422
|
+
return obstacle.id.startsWith("__edge_guard_") && "orientation" in obstacle && (obstacle.orientation === "vertical" || obstacle.orientation === "horizontal");
|
|
1423
|
+
}
|
|
1424
|
+
function containerTitleRoutingBand(container) {
|
|
1425
|
+
return {
|
|
1426
|
+
x: container.x,
|
|
1427
|
+
y: container.y,
|
|
1428
|
+
w: container.w,
|
|
1429
|
+
h: TITLE_H + PAD
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
function segmentLength(a, b) {
|
|
1433
|
+
return Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]);
|
|
1434
|
+
}
|
|
1435
|
+
function boxInteriorOverlap(a, b, box, pad) {
|
|
1436
|
+
const left = box.x - pad;
|
|
1437
|
+
const right = box.x + box.w + pad;
|
|
1438
|
+
const top = box.y - pad;
|
|
1439
|
+
const bottom = box.y + box.h + pad;
|
|
1440
|
+
if (Math.abs(a[0] - b[0]) < EPS) {
|
|
1441
|
+
const x = a[0];
|
|
1442
|
+
if (x <= left || x >= right) return 0;
|
|
1443
|
+
return intervalOverlap(a[1], b[1], top, bottom);
|
|
1444
|
+
}
|
|
1445
|
+
if (Math.abs(a[1] - b[1]) < EPS) {
|
|
1446
|
+
const y = a[1];
|
|
1447
|
+
if (y <= top || y >= bottom) return 0;
|
|
1448
|
+
return intervalOverlap(a[0], b[0], left, right);
|
|
1449
|
+
}
|
|
1450
|
+
return 0;
|
|
1451
|
+
}
|
|
1452
|
+
function boxBorderOverlap(a, b, box) {
|
|
1453
|
+
return boxBorderProximityOverlap(a, b, box, 0);
|
|
1454
|
+
}
|
|
1455
|
+
function boxBorderProximityOverlap(a, b, box, tolerance) {
|
|
1456
|
+
if (Math.abs(a[0] - b[0]) < EPS) {
|
|
1457
|
+
const x = a[0];
|
|
1458
|
+
if (Math.abs(x - box.x) > tolerance + EPS && Math.abs(x - (box.x + box.w)) > tolerance + EPS) return 0;
|
|
1459
|
+
return intervalOverlap(a[1], b[1], box.y, box.y + box.h);
|
|
1460
|
+
}
|
|
1461
|
+
if (Math.abs(a[1] - b[1]) < EPS) {
|
|
1462
|
+
const y = a[1];
|
|
1463
|
+
if (Math.abs(y - box.y) > tolerance + EPS && Math.abs(y - (box.y + box.h)) > tolerance + EPS) return 0;
|
|
1464
|
+
return intervalOverlap(a[0], b[0], box.x, box.x + box.w);
|
|
1465
|
+
}
|
|
1466
|
+
return 0;
|
|
1467
|
+
}
|
|
1468
|
+
function intervalOverlap(a1, a2, b1, b2) {
|
|
1469
|
+
const minA = Math.min(a1, a2);
|
|
1470
|
+
const maxA = Math.max(a1, a2);
|
|
1471
|
+
const minB = Math.min(b1, b2);
|
|
1472
|
+
const maxB = Math.max(b1, b2);
|
|
1473
|
+
return Math.max(0, Math.min(maxA, maxB) - Math.max(minA, minB));
|
|
1474
|
+
}
|
|
1475
|
+
function orthogonalRoute(a, b) {
|
|
1476
|
+
const ca = { x: a.x + a.w / 2, y: a.y + a.h / 2 };
|
|
1477
|
+
const cb = { x: b.x + b.w / 2, y: b.y + b.h / 2 };
|
|
1478
|
+
const dx = cb.x - ca.x;
|
|
1479
|
+
const dy = cb.y - ca.y;
|
|
1480
|
+
let pa, pb;
|
|
1481
|
+
const separatedHorizontally = a.x + a.w <= b.x || b.x + b.w <= a.x;
|
|
1482
|
+
const separatedVertically = a.y + a.h <= b.y || b.y + b.h <= a.y;
|
|
1483
|
+
const horizontal = separatedHorizontally ? true : separatedVertically ? false : Math.abs(dx) > Math.abs(dy);
|
|
1484
|
+
if (horizontal) {
|
|
1485
|
+
pa = [dx > 0 ? a.x + a.w : a.x, ca.y];
|
|
1486
|
+
pb = [dx > 0 ? b.x : b.x + b.w, cb.y];
|
|
1487
|
+
if (Math.abs(pa[1] - pb[1]) < 0.5) return [pa, pb];
|
|
1488
|
+
const midX = (pa[0] + pb[0]) / 2;
|
|
1489
|
+
return [pa, [midX, pa[1]], [midX, pb[1]], pb];
|
|
1490
|
+
} else {
|
|
1491
|
+
pa = [ca.x, dy > 0 ? a.y + a.h : a.y];
|
|
1492
|
+
pb = [cb.x, dy > 0 ? b.y : b.y + b.h];
|
|
1493
|
+
if (Math.abs(pa[0] - pb[0]) < 0.5) return [pa, pb];
|
|
1494
|
+
const midY = (pa[1] + pb[1]) / 2;
|
|
1495
|
+
return [pa, [pa[0], midY], [pb[0], midY], pb];
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// src/core/render.ts
|
|
1500
|
+
var NS = "http://www.w3.org/2000/svg";
|
|
1501
|
+
var FONT_FAMILY = '"Hiragino Sans", "Yu Gothic", "Noto Sans CJK JP", sans-serif';
|
|
1502
|
+
var LABEL_OFFSET = 4;
|
|
1503
|
+
var LABEL_FONT_JA = 2.4;
|
|
1504
|
+
var LABEL_FONT_EN = 2.1;
|
|
1505
|
+
var LABEL_GAP = 0.6;
|
|
1506
|
+
var LABEL_OFFSET_STEPS = [0, 3.2, 6.4];
|
|
1507
|
+
var EPS2 = 1e-3;
|
|
1508
|
+
function render(laid, opts = {}) {
|
|
1509
|
+
const lang = opts.lang ?? "ja";
|
|
1510
|
+
const svg = el("svg", {
|
|
1511
|
+
xmlns: NS,
|
|
1512
|
+
viewBox: `0 0 ${laid.width} ${laid.height}`,
|
|
1513
|
+
"font-family": FONT_FAMILY,
|
|
1514
|
+
"shape-rendering": "geometricPrecision"
|
|
1515
|
+
});
|
|
1516
|
+
const containers = laid.nodes.filter((n) => n.isContainer);
|
|
1517
|
+
const leaves = laid.nodes.filter((n) => !n.isContainer);
|
|
1518
|
+
const labelBoxes = [];
|
|
1519
|
+
for (const c of containers) svg.appendChild(renderContainer(c, lang));
|
|
1520
|
+
for (const e of laid.edges) svg.appendChild(renderEdge(e, lang, laid, labelBoxes));
|
|
1521
|
+
for (const e of laid.edges) svg.appendChild(renderEdgeHeads(e));
|
|
1522
|
+
for (const n of leaves) svg.appendChild(renderNode(n, lang));
|
|
1523
|
+
return svg;
|
|
1524
|
+
}
|
|
1525
|
+
function renderContainer(n, lang) {
|
|
1526
|
+
const g = el("g");
|
|
1527
|
+
g.appendChild(el("rect", {
|
|
1528
|
+
x: n.x,
|
|
1529
|
+
y: n.y,
|
|
1530
|
+
width: n.w,
|
|
1531
|
+
height: n.h,
|
|
1532
|
+
fill: "white",
|
|
1533
|
+
stroke: "#000",
|
|
1534
|
+
"stroke-width": 0.3
|
|
1535
|
+
}));
|
|
1536
|
+
const labels = pickLabel(n.label, lang);
|
|
1537
|
+
if (labels.length || n.id) {
|
|
1538
|
+
const t = el("text", {
|
|
1539
|
+
x: n.x + 2,
|
|
1540
|
+
y: n.y + 3.5,
|
|
1541
|
+
"font-size": 2.6,
|
|
1542
|
+
fill: "#000"
|
|
1543
|
+
});
|
|
1544
|
+
t.textContent = (n.id ? n.id + " " : "") + (labels[0] ?? "");
|
|
1545
|
+
g.appendChild(t);
|
|
1546
|
+
}
|
|
1547
|
+
return g;
|
|
1548
|
+
}
|
|
1549
|
+
function renderNode(n, lang) {
|
|
1550
|
+
const g = el("g");
|
|
1551
|
+
g.appendChild(renderShape(n));
|
|
1552
|
+
const lines = [];
|
|
1553
|
+
if (n.id && n.id !== "*") lines.push(n.id);
|
|
1554
|
+
const labels = pickLabel(n.label, lang);
|
|
1555
|
+
for (const l of labels) lines.push(l);
|
|
1556
|
+
if (lines.length === 0) return g;
|
|
1557
|
+
const fontSize = 2.8;
|
|
1558
|
+
const lineH = fontSize * 1.2;
|
|
1559
|
+
const totalH = lines.length * lineH;
|
|
1560
|
+
const startY = n.y + n.h / 2 - totalH / 2 + lineH * 0.8;
|
|
1561
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1562
|
+
const t = el("text", {
|
|
1563
|
+
x: n.x + n.w / 2,
|
|
1564
|
+
y: startY + i * lineH,
|
|
1565
|
+
"font-size": fontSize,
|
|
1566
|
+
fill: "#000",
|
|
1567
|
+
"text-anchor": "middle"
|
|
1568
|
+
});
|
|
1569
|
+
t.textContent = lines[i];
|
|
1570
|
+
g.appendChild(t);
|
|
1571
|
+
}
|
|
1572
|
+
return g;
|
|
1573
|
+
}
|
|
1574
|
+
function renderShape(n) {
|
|
1575
|
+
const stroke = "#000";
|
|
1576
|
+
const fill = "white";
|
|
1577
|
+
const sw = 0.4;
|
|
1578
|
+
switch (n.shape) {
|
|
1579
|
+
case "round":
|
|
1580
|
+
return el("rect", {
|
|
1581
|
+
x: n.x,
|
|
1582
|
+
y: n.y,
|
|
1583
|
+
width: n.w,
|
|
1584
|
+
height: n.h,
|
|
1585
|
+
rx: Math.min(n.w, n.h) / 2,
|
|
1586
|
+
ry: Math.min(n.w, n.h) / 2,
|
|
1587
|
+
fill,
|
|
1588
|
+
stroke,
|
|
1589
|
+
"stroke-width": sw
|
|
1590
|
+
});
|
|
1591
|
+
case "circle": {
|
|
1592
|
+
const r = Math.min(n.w, n.h) / 2;
|
|
1593
|
+
return el("circle", {
|
|
1594
|
+
cx: n.x + n.w / 2,
|
|
1595
|
+
cy: n.y + n.h / 2,
|
|
1596
|
+
r,
|
|
1597
|
+
fill: "#000",
|
|
1598
|
+
stroke
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
case "diamond": {
|
|
1602
|
+
const cx = n.x + n.w / 2;
|
|
1603
|
+
const cy = n.y + n.h / 2;
|
|
1604
|
+
const pts = [[cx, n.y], [n.x + n.w, cy], [cx, n.y + n.h], [n.x, cy]].map((p) => p.join(",")).join(" ");
|
|
1605
|
+
return el("polygon", { points: pts, fill, stroke, "stroke-width": sw });
|
|
1606
|
+
}
|
|
1607
|
+
case "actor":
|
|
1608
|
+
case "box":
|
|
1609
|
+
default:
|
|
1610
|
+
return el("rect", {
|
|
1611
|
+
x: n.x,
|
|
1612
|
+
y: n.y,
|
|
1613
|
+
width: n.w,
|
|
1614
|
+
height: n.h,
|
|
1615
|
+
fill,
|
|
1616
|
+
stroke,
|
|
1617
|
+
"stroke-width": sw
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
function renderEdge(e, lang, laid, labelBoxes) {
|
|
1622
|
+
const g = el("g");
|
|
1623
|
+
if (e.points.length < 2) return g;
|
|
1624
|
+
const bodyPoints = edgeBodyPoints(e);
|
|
1625
|
+
if (bodyPoints.length < 2) return g;
|
|
1626
|
+
const d = bodyPoints.map((p, i) => i === 0 ? `M ${p[0]} ${p[1]}` : `L ${p[0]} ${p[1]}`).join(" ");
|
|
1627
|
+
const path = el("path", {
|
|
1628
|
+
d,
|
|
1629
|
+
fill: "none",
|
|
1630
|
+
stroke: "#000",
|
|
1631
|
+
"stroke-width": strokeWidth(e.op),
|
|
1632
|
+
"stroke-linecap": "butt",
|
|
1633
|
+
"stroke-linejoin": "miter"
|
|
1634
|
+
});
|
|
1635
|
+
const dash = strokeDash(e.op);
|
|
1636
|
+
if (dash) path.setAttribute("stroke-dasharray", dash);
|
|
1637
|
+
g.appendChild(path);
|
|
1638
|
+
const labels = pickLabel(e.label, lang);
|
|
1639
|
+
if (labels.length) {
|
|
1640
|
+
const ja = labels[0];
|
|
1641
|
+
const en = labels[1];
|
|
1642
|
+
const placement = chooseLabelPlacement(e, labels, laid, labelBoxes);
|
|
1643
|
+
labelBoxes.push(expandBox(placement.box, 0.8));
|
|
1644
|
+
const drawText = (text, x, y, fontSize, anchor, baseline) => {
|
|
1645
|
+
const t = el("text", {
|
|
1646
|
+
x,
|
|
1647
|
+
y,
|
|
1648
|
+
"font-size": fontSize,
|
|
1649
|
+
fill: "#000",
|
|
1650
|
+
"text-anchor": anchor,
|
|
1651
|
+
"dominant-baseline": baseline
|
|
1652
|
+
});
|
|
1653
|
+
t.textContent = text;
|
|
1654
|
+
g.appendChild(t);
|
|
1655
|
+
};
|
|
1656
|
+
drawText(ja, placement.ja.x, placement.ja.y, LABEL_FONT_JA, placement.anchor, placement.ja.baseline);
|
|
1657
|
+
if (en && placement.en) {
|
|
1658
|
+
drawText(en, placement.en.x, placement.en.y, LABEL_FONT_EN, placement.anchor, placement.en.baseline);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
return g;
|
|
1662
|
+
}
|
|
1663
|
+
function renderEdgeHeads(e) {
|
|
1664
|
+
const g = el("g");
|
|
1665
|
+
if (e.isLifeline) return g;
|
|
1666
|
+
const endHead = arrowHeadFor(e, false);
|
|
1667
|
+
const startHead = arrowHeadFor(e, true);
|
|
1668
|
+
if (endHead) g.appendChild(endHead);
|
|
1669
|
+
if (startHead) g.appendChild(startHead);
|
|
1670
|
+
return g;
|
|
1671
|
+
}
|
|
1672
|
+
function strokeWidth(op) {
|
|
1673
|
+
return op === "thick" ? 0.7 : 0.4;
|
|
1674
|
+
}
|
|
1675
|
+
function strokeDash(op) {
|
|
1676
|
+
return op === "dashed" || op === "dashed-arrow" ? "1.4 1.2" : null;
|
|
1677
|
+
}
|
|
1678
|
+
function edgeBodyPoints(e) {
|
|
1679
|
+
let points = e.points.map((p) => [p[0], p[1]]);
|
|
1680
|
+
if (e.op !== "line" && e.op !== "dashed") {
|
|
1681
|
+
points = trimPolyline(points, false, arrowLength(e.op));
|
|
1682
|
+
}
|
|
1683
|
+
if (e.op === "bidir") {
|
|
1684
|
+
points = trimPolyline(points, true, arrowLength(e.op));
|
|
1685
|
+
}
|
|
1686
|
+
return points;
|
|
1687
|
+
}
|
|
1688
|
+
function trimPolyline(points, atStart, amount) {
|
|
1689
|
+
if (points.length < 2 || amount <= 0) return points;
|
|
1690
|
+
const out = atStart ? [...points].reverse() : [...points];
|
|
1691
|
+
let remaining = amount;
|
|
1692
|
+
while (out.length >= 2 && remaining > 0) {
|
|
1693
|
+
const tip = out[out.length - 1];
|
|
1694
|
+
const prev = out[out.length - 2];
|
|
1695
|
+
const dx = prev[0] - tip[0];
|
|
1696
|
+
const dy = prev[1] - tip[1];
|
|
1697
|
+
const len = Math.hypot(dx, dy);
|
|
1698
|
+
if (len < EPS2) {
|
|
1699
|
+
out.pop();
|
|
1700
|
+
continue;
|
|
1701
|
+
}
|
|
1702
|
+
if (len > remaining) {
|
|
1703
|
+
out[out.length - 1] = [
|
|
1704
|
+
tip[0] + dx / len * remaining,
|
|
1705
|
+
tip[1] + dy / len * remaining
|
|
1706
|
+
];
|
|
1707
|
+
remaining = 0;
|
|
1708
|
+
} else {
|
|
1709
|
+
out.pop();
|
|
1710
|
+
remaining -= len;
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
return atStart ? out.reverse() : out;
|
|
1714
|
+
}
|
|
1715
|
+
function arrowLength(op) {
|
|
1716
|
+
return op === "thick" ? 3.4 : 2.8;
|
|
1717
|
+
}
|
|
1718
|
+
function arrowHeadFor(e, atStart) {
|
|
1719
|
+
if (e.points.length < 2) return null;
|
|
1720
|
+
if (atStart && e.op !== "bidir") return null;
|
|
1721
|
+
if (!atStart && (e.op === "line" || e.op === "dashed")) return null;
|
|
1722
|
+
const points = atStart ? e.points : [...e.points].reverse();
|
|
1723
|
+
return renderArrowHead(points[0], points[1], e.op === "thick");
|
|
1724
|
+
}
|
|
1725
|
+
function renderArrowHead(tip, next, bold) {
|
|
1726
|
+
const dx = next[0] - tip[0];
|
|
1727
|
+
const dy = next[1] - tip[1];
|
|
1728
|
+
const len = Math.hypot(dx, dy);
|
|
1729
|
+
if (len < 1e-3) return null;
|
|
1730
|
+
const ux = dx / len;
|
|
1731
|
+
const uy = dy / len;
|
|
1732
|
+
const arrowLen = bold ? 3.4 : 2.8;
|
|
1733
|
+
const halfW = bold ? 1.6 : 1.25;
|
|
1734
|
+
const bx = tip[0] + ux * arrowLen;
|
|
1735
|
+
const by = tip[1] + uy * arrowLen;
|
|
1736
|
+
const px = -uy;
|
|
1737
|
+
const py = ux;
|
|
1738
|
+
const pts = [
|
|
1739
|
+
tip,
|
|
1740
|
+
[bx + px * halfW, by + py * halfW],
|
|
1741
|
+
[bx - px * halfW, by - py * halfW]
|
|
1742
|
+
].map((p) => p.join(",")).join(" ");
|
|
1743
|
+
return el("polygon", {
|
|
1744
|
+
points: pts,
|
|
1745
|
+
fill: "#000",
|
|
1746
|
+
stroke: "#000",
|
|
1747
|
+
"stroke-width": 0
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
function el(tag, attrs = {}) {
|
|
1751
|
+
const node = document.createElementNS(NS, tag);
|
|
1752
|
+
for (const [k, v] of Object.entries(attrs)) node.setAttribute(k, String(v));
|
|
1753
|
+
return node;
|
|
1754
|
+
}
|
|
1755
|
+
function pickLabel(b, lang) {
|
|
1756
|
+
if (!b) return [];
|
|
1757
|
+
if (lang === "ja") return b.ja ? [b.ja] : b.en ? [b.en] : [];
|
|
1758
|
+
if (lang === "en") return b.en ? [b.en] : b.ja ? [b.ja] : [];
|
|
1759
|
+
const out = [];
|
|
1760
|
+
if (b.ja) out.push(b.ja);
|
|
1761
|
+
if (b.en) out.push(b.en);
|
|
1762
|
+
return out;
|
|
1763
|
+
}
|
|
1764
|
+
function chooseLabelPlacement(edge, labels, laid, occupiedLabels = []) {
|
|
1765
|
+
const candidates = [];
|
|
1766
|
+
const textW = Math.max(...labels.map((label, index) => estimateTextWidth(
|
|
1767
|
+
label,
|
|
1768
|
+
index === 0 ? LABEL_FONT_JA : LABEL_FONT_EN
|
|
1769
|
+
)));
|
|
1770
|
+
const textH = labels.length > 1 ? LABEL_FONT_JA + LABEL_FONT_EN + LABEL_GAP : LABEL_FONT_JA;
|
|
1771
|
+
for (let i = 0; i < edge.points.length - 1; i++) {
|
|
1772
|
+
const a = edge.points[i];
|
|
1773
|
+
const b = edge.points[i + 1];
|
|
1774
|
+
const dx = b[0] - a[0];
|
|
1775
|
+
const dy = b[1] - a[1];
|
|
1776
|
+
const len = Math.abs(dx) + Math.abs(dy);
|
|
1777
|
+
if (len < 8) continue;
|
|
1778
|
+
for (const t of [0.5, 0.35, 0.65, 0.22, 0.78]) {
|
|
1779
|
+
const x = a[0] + dx * t;
|
|
1780
|
+
const y = a[1] + dy * t;
|
|
1781
|
+
for (const offsetStep of LABEL_OFFSET_STEPS) {
|
|
1782
|
+
const offset = LABEL_OFFSET + offsetStep;
|
|
1783
|
+
const offsetPenalty = offsetStep * 9;
|
|
1784
|
+
if (Math.abs(dy) < EPS2) {
|
|
1785
|
+
candidates.push(makeHorizontalLabel(edge, laid, occupiedLabels, labels, textW, textH, a, b, x, y, "above", offset, offsetPenalty));
|
|
1786
|
+
candidates.push(makeHorizontalLabel(edge, laid, occupiedLabels, labels, textW, textH, a, b, x, y, "below", offset, offsetPenalty));
|
|
1787
|
+
} else if (Math.abs(dx) < EPS2) {
|
|
1788
|
+
candidates.push(makeVerticalLabel(edge, laid, occupiedLabels, labels, textW, textH, a, b, x, y, "right", offset, offsetPenalty));
|
|
1789
|
+
candidates.push(makeVerticalLabel(edge, laid, occupiedLabels, labels, textW, textH, a, b, x, y, "left", offset, offsetPenalty));
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
if (candidates.length === 0) {
|
|
1795
|
+
const [a, b] = longestSegment(edge.points);
|
|
1796
|
+
const x = (a[0] + b[0]) / 2;
|
|
1797
|
+
const y = (a[1] + b[1]) / 2;
|
|
1798
|
+
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);
|
|
1799
|
+
}
|
|
1800
|
+
candidates.sort((a, b) => a.score - b.score);
|
|
1801
|
+
return candidates[0];
|
|
1802
|
+
}
|
|
1803
|
+
function makeHorizontalLabel(edge, laid, occupiedLabels, labels, textW, textH, lineA, lineB, x, lineY, side, offset, offsetPenalty) {
|
|
1804
|
+
const top = side === "above" ? lineY - offset - textH : lineY + offset;
|
|
1805
|
+
const box = { x: x - textW / 2, y: top, w: textW, h: textH };
|
|
1806
|
+
let jaY;
|
|
1807
|
+
let enY;
|
|
1808
|
+
if (labels.length > 1) {
|
|
1809
|
+
jaY = top + LABEL_FONT_JA;
|
|
1810
|
+
enY = jaY + LABEL_GAP + LABEL_FONT_EN;
|
|
1811
|
+
} else {
|
|
1812
|
+
jaY = top + LABEL_FONT_JA;
|
|
1813
|
+
}
|
|
1814
|
+
const placement = {
|
|
1815
|
+
box,
|
|
1816
|
+
lineA,
|
|
1817
|
+
lineB,
|
|
1818
|
+
score: 0,
|
|
1819
|
+
vertical: false,
|
|
1820
|
+
anchor: "middle",
|
|
1821
|
+
ja: { x, y: jaY, baseline: "alphabetic" },
|
|
1822
|
+
en: enY === void 0 ? void 0 : { x, y: enY, baseline: "alphabetic" }
|
|
1823
|
+
};
|
|
1824
|
+
placement.score = labelPlacementScore(placement, edge, laid, occupiedLabels) + (side === "above" ? 0 : 8) + offsetPenalty;
|
|
1825
|
+
return placement;
|
|
1826
|
+
}
|
|
1827
|
+
function makeVerticalLabel(edge, laid, occupiedLabels, labels, textW, textH, lineA, lineB, lineX, y, side, offset, offsetPenalty) {
|
|
1828
|
+
const x = side === "right" ? lineX + offset : lineX - offset;
|
|
1829
|
+
const box = {
|
|
1830
|
+
x: side === "right" ? x : x - textW,
|
|
1831
|
+
y: y - textH / 2,
|
|
1832
|
+
w: textW,
|
|
1833
|
+
h: textH
|
|
1834
|
+
};
|
|
1835
|
+
const anchor = side === "right" ? "start" : "end";
|
|
1836
|
+
const jaY = labels.length > 1 ? y - LABEL_FONT_JA / 2 - LABEL_GAP / 2 : y;
|
|
1837
|
+
const enY = labels.length > 1 ? y + LABEL_FONT_EN / 2 + LABEL_GAP / 2 : void 0;
|
|
1838
|
+
const placement = {
|
|
1839
|
+
box,
|
|
1840
|
+
lineA,
|
|
1841
|
+
lineB,
|
|
1842
|
+
score: 0,
|
|
1843
|
+
vertical: true,
|
|
1844
|
+
anchor,
|
|
1845
|
+
ja: { x, y: jaY, baseline: "middle" },
|
|
1846
|
+
en: enY === void 0 ? void 0 : { x, y: enY, baseline: "middle" }
|
|
1847
|
+
};
|
|
1848
|
+
placement.score = labelPlacementScore(placement, edge, laid, occupiedLabels) + (side === "right" ? 0 : 8) + offsetPenalty;
|
|
1849
|
+
return placement;
|
|
1850
|
+
}
|
|
1851
|
+
function labelPlacementScore(placement, edge, laid, occupiedLabels) {
|
|
1852
|
+
let score = placement.vertical ? 5 : 0;
|
|
1853
|
+
const box = placement.box;
|
|
1854
|
+
if (box.x < 0) score += Math.abs(box.x) * 300;
|
|
1855
|
+
if (box.y < 0) score += Math.abs(box.y) * 300;
|
|
1856
|
+
if (box.x + box.w > laid.width) score += (box.x + box.w - laid.width) * 300;
|
|
1857
|
+
if (box.y + box.h > laid.height) score += (box.y + box.h - laid.height) * 300;
|
|
1858
|
+
for (const node of laid.nodes) {
|
|
1859
|
+
if (node.isContainer) {
|
|
1860
|
+
const title = { x: node.x, y: node.y, w: node.w, h: 6 };
|
|
1861
|
+
score += rectOverlapArea(box, title) * 900;
|
|
1862
|
+
score += rectBorderOverlapPenalty(box, node) * 1800;
|
|
1863
|
+
score += rectBorderBandOverlapArea(box, node, 1.8) * 9e3;
|
|
1864
|
+
} else {
|
|
1865
|
+
const bodyOverlap = rectOverlapArea(box, node);
|
|
1866
|
+
if (bodyOverlap > 0) score += 1e6 + bodyOverlap * 5e4;
|
|
1867
|
+
score += rectOverlapArea(box, expandBox(node, 1.8)) * 5200;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
for (const other of laid.edges) {
|
|
1871
|
+
for (let i = 0; i < other.points.length - 1; i++) {
|
|
1872
|
+
const a = other.points[i];
|
|
1873
|
+
const b = other.points[i + 1];
|
|
1874
|
+
const overlap = segmentIntersectsRect(a, b, expandBox(box, 0.8));
|
|
1875
|
+
if (!overlap) continue;
|
|
1876
|
+
score += other === edge ? 35 : 380;
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
for (const occupied of occupiedLabels) {
|
|
1880
|
+
score += rectOverlapArea(box, occupied) * 18e3;
|
|
1881
|
+
}
|
|
1882
|
+
const segmentLen = Math.abs(placement.lineA[0] - placement.lineB[0]) + Math.abs(placement.lineA[1] - placement.lineB[1]);
|
|
1883
|
+
const labelSpan = placement.vertical ? box.h : box.w;
|
|
1884
|
+
score += Math.max(0, Math.max(36, labelSpan * 2.4) - segmentLen) * 80;
|
|
1885
|
+
const center = placement.vertical ? box.y + box.h / 2 : box.x + box.w / 2;
|
|
1886
|
+
const start = placement.vertical ? placement.lineA[1] : placement.lineA[0];
|
|
1887
|
+
const end = placement.vertical ? placement.lineB[1] : placement.lineB[0];
|
|
1888
|
+
const endpointDistance = Math.min(Math.abs(center - start), Math.abs(center - end));
|
|
1889
|
+
score += Math.max(0, 12 - endpointDistance) * 80;
|
|
1890
|
+
return score;
|
|
1891
|
+
}
|
|
1892
|
+
function estimateTextWidth(text, fontSize) {
|
|
1893
|
+
let width = 0;
|
|
1894
|
+
for (const char of text) {
|
|
1895
|
+
if (char === " ") width += fontSize * 0.35;
|
|
1896
|
+
else if (char.charCodeAt(0) <= 127) width += fontSize * 0.58;
|
|
1897
|
+
else width += fontSize;
|
|
1898
|
+
}
|
|
1899
|
+
return Math.max(fontSize * 2, width);
|
|
1900
|
+
}
|
|
1901
|
+
function longestSegment(pts) {
|
|
1902
|
+
let best = 0;
|
|
1903
|
+
let bestLen = -1;
|
|
1904
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
1905
|
+
const dx = pts[i + 1][0] - pts[i][0];
|
|
1906
|
+
const dy = pts[i + 1][1] - pts[i][1];
|
|
1907
|
+
const len = dx * dx + dy * dy;
|
|
1908
|
+
if (len > bestLen) {
|
|
1909
|
+
bestLen = len;
|
|
1910
|
+
best = i;
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
const a = pts[best];
|
|
1914
|
+
const b = pts[best + 1];
|
|
1915
|
+
return [a, b];
|
|
1916
|
+
}
|
|
1917
|
+
function expandBox(box, pad) {
|
|
1918
|
+
return { x: box.x - pad, y: box.y - pad, w: box.w + pad * 2, h: box.h + pad * 2 };
|
|
1919
|
+
}
|
|
1920
|
+
function rectOverlapArea(a, b) {
|
|
1921
|
+
const x = Math.max(0, Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x));
|
|
1922
|
+
const y = Math.max(0, Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y));
|
|
1923
|
+
return x * y;
|
|
1924
|
+
}
|
|
1925
|
+
function rectBorderOverlapPenalty(a, b) {
|
|
1926
|
+
const nearLeft = Math.abs(a.x - b.x) < 1 || Math.abs(a.x + a.w - b.x) < 1;
|
|
1927
|
+
const nearRight = Math.abs(a.x - (b.x + b.w)) < 1 || Math.abs(a.x + a.w - (b.x + b.w)) < 1;
|
|
1928
|
+
const nearTop = Math.abs(a.y - b.y) < 1 || Math.abs(a.y + a.h - b.y) < 1;
|
|
1929
|
+
const nearBottom = Math.abs(a.y - (b.y + b.h)) < 1 || Math.abs(a.y + a.h - (b.y + b.h)) < 1;
|
|
1930
|
+
return Number(nearLeft || nearRight || nearTop || nearBottom);
|
|
1931
|
+
}
|
|
1932
|
+
function rectBorderBandOverlapArea(a, b, band) {
|
|
1933
|
+
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 });
|
|
1934
|
+
}
|
|
1935
|
+
function segmentIntersectsRect(a, b, box) {
|
|
1936
|
+
const left = box.x;
|
|
1937
|
+
const right = box.x + box.w;
|
|
1938
|
+
const top = box.y;
|
|
1939
|
+
const bottom = box.y + box.h;
|
|
1940
|
+
if (Math.abs(a[0] - b[0]) < EPS2) {
|
|
1941
|
+
const x = a[0];
|
|
1942
|
+
if (x <= left || x >= right) return false;
|
|
1943
|
+
return intervalOverlap2(a[1], b[1], top, bottom) > EPS2;
|
|
1944
|
+
}
|
|
1945
|
+
if (Math.abs(a[1] - b[1]) < EPS2) {
|
|
1946
|
+
const y = a[1];
|
|
1947
|
+
if (y <= top || y >= bottom) return false;
|
|
1948
|
+
return intervalOverlap2(a[0], b[0], left, right) > EPS2;
|
|
1949
|
+
}
|
|
1950
|
+
return false;
|
|
1951
|
+
}
|
|
1952
|
+
function intervalOverlap2(a1, a2, b1, b2) {
|
|
1953
|
+
const minA = Math.min(a1, a2);
|
|
1954
|
+
const maxA = Math.max(a1, a2);
|
|
1955
|
+
const minB = Math.min(b1, b2);
|
|
1956
|
+
const maxB = Math.max(b1, b2);
|
|
1957
|
+
return Math.max(0, Math.min(maxA, maxB) - Math.max(minA, minB));
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
// src/core/refs.ts
|
|
1961
|
+
function refsToMarkdown(doc) {
|
|
1962
|
+
const ids = sortedIds(doc);
|
|
1963
|
+
const lines = [];
|
|
1964
|
+
lines.push("## \u7B26\u53F7\u306E\u8AAC\u660E / Reference Signs");
|
|
1965
|
+
lines.push("");
|
|
1966
|
+
lines.push("| \u7B26\u53F7 | \u540D\u79F0(\u65E5\u672C\u8A9E) | Name (English) |");
|
|
1967
|
+
lines.push("|------|---------------|----------------|");
|
|
1968
|
+
for (const id of ids) {
|
|
1969
|
+
const n = doc.nodes.get(id);
|
|
1970
|
+
lines.push(`| ${id} | ${n.label.ja ?? ""} | ${n.label.en ?? ""} |`);
|
|
1971
|
+
}
|
|
1972
|
+
return lines.join("\n");
|
|
1973
|
+
}
|
|
1974
|
+
function refsToCsv(doc) {
|
|
1975
|
+
const ids = sortedIds(doc);
|
|
1976
|
+
const lines = ["id,ja,en"];
|
|
1977
|
+
for (const id of ids) {
|
|
1978
|
+
const n = doc.nodes.get(id);
|
|
1979
|
+
lines.push(`${csv(id)},${csv(n.label.ja ?? "")},${csv(n.label.en ?? "")}`);
|
|
1980
|
+
}
|
|
1981
|
+
return lines.join("\n");
|
|
1982
|
+
}
|
|
1983
|
+
function csv(s) {
|
|
1984
|
+
return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
|
|
1985
|
+
}
|
|
1986
|
+
function sortedIds(doc) {
|
|
1987
|
+
const ids = [...doc.nodes.keys()].filter((id) => id !== "*");
|
|
1988
|
+
return ids.sort((a, b) => {
|
|
1989
|
+
const na = parseInt(a, 10);
|
|
1990
|
+
const nb = parseInt(b, 10);
|
|
1991
|
+
if (!isNaN(na) && !isNaN(nb)) return na - nb;
|
|
1992
|
+
return a.localeCompare(b);
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
// src/core/samples.ts
|
|
1997
|
+
var SAMPLE_ORDER = [
|
|
1998
|
+
"block",
|
|
1999
|
+
"system",
|
|
2000
|
+
"iot",
|
|
2001
|
+
"imagePipeline",
|
|
2002
|
+
"controlLoop",
|
|
2003
|
+
"flow",
|
|
2004
|
+
"state",
|
|
2005
|
+
"seq",
|
|
2006
|
+
"handshake"
|
|
2007
|
+
];
|
|
2008
|
+
var SAMPLES = {
|
|
2009
|
+
block: {
|
|
2010
|
+
label: "\u30D6\u30ED\u30C3\u30AF\u56F3",
|
|
2011
|
+
hint: "\u300C:\u300D\u3092\u4F7F\u3046\u3068\u30D6\u30ED\u30C3\u30AF\u56F3\u306B\u306A\u308B",
|
|
2012
|
+
source: `# \u30D6\u30ED\u30C3\u30AF\u56F3(\u88C5\u7F6E\u30AF\u30EC\u30FC\u30E0\u7528)
|
|
2013
|
+
# \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
|
|
2014
|
+
|
|
2015
|
+
10 = \u5236\u5FA1\u88C5\u7F6E / control device
|
|
2016
|
+
11 = CPU
|
|
2017
|
+
12 = \u30E1\u30E2\u30EA / memory
|
|
2018
|
+
13 = "I/O \u30A4\u30F3\u30BF\u30FC\u30D5\u30A7\u30FC\u30B9" / "I/O interface"
|
|
2019
|
+
20 = \u5916\u90E8\u6A5F\u5668 / external device
|
|
2020
|
+
|
|
2021
|
+
10 : 11 12 13
|
|
2022
|
+
|
|
2023
|
+
11 - 12
|
|
2024
|
+
11 - 13
|
|
2025
|
+
13 -> 20 : \u4FE1\u53F7 / signal
|
|
2026
|
+
`
|
|
2027
|
+
},
|
|
2028
|
+
system: {
|
|
2029
|
+
label: "\u30B7\u30B9\u30C6\u30E0\u5168\u4F53",
|
|
2030
|
+
hint: "\u968E\u5C64\u69CB\u6210 + \u5916\u90E8\u63A5\u7D9A",
|
|
2031
|
+
source: `# \u30B7\u30B9\u30C6\u30E0\u5168\u4F53(\u968E\u5C64+\u5916\u90E8\u63A5\u7D9A)
|
|
2032
|
+
# \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
|
|
2033
|
+
|
|
2034
|
+
100 = \u30B7\u30B9\u30C6\u30E0\u672C\u4F53 / Main system
|
|
2035
|
+
10 = \u5236\u5FA1\u90E8 / Control
|
|
2036
|
+
20 = \u901A\u4FE1\u90E8 / Comm
|
|
2037
|
+
11 = CPU
|
|
2038
|
+
12 = \u30E1\u30E2\u30EA / memory
|
|
2039
|
+
21 = \u7121\u7DDA\u90E8 / wireless
|
|
2040
|
+
22 = \u6709\u7DDA\u90E8 / wired
|
|
2041
|
+
30 = \u5916\u90E8\u30B5\u30FC\u30D0 / external server
|
|
2042
|
+
40 = \u5916\u90E8\u7AEF\u672B / external terminal
|
|
2043
|
+
|
|
2044
|
+
100 : 10 20
|
|
2045
|
+
10 : 11 12
|
|
2046
|
+
20 : 21 22
|
|
2047
|
+
|
|
2048
|
+
21 .> 40 : \u7121\u7DDA / wireless
|
|
2049
|
+
22 -> 30 : \u6709\u7DDA / wired
|
|
2050
|
+
30 <-> 40 : \u901A\u4FE1 / comm
|
|
2051
|
+
`
|
|
2052
|
+
},
|
|
2053
|
+
iot: {
|
|
2054
|
+
label: "IoT/\u30AF\u30E9\u30A6\u30C9",
|
|
2055
|
+
hint: "\u30BB\u30F3\u30B5\u7AEF\u672B\u30FB\u30B2\u30FC\u30C8\u30A6\u30A7\u30A4\u30FB\u30AF\u30E9\u30A6\u30C9\u306E\u5178\u578B\u69CB\u6210",
|
|
2056
|
+
source: `# IoT/\u30AF\u30E9\u30A6\u30C9\u69CB\u6210
|
|
2057
|
+
# \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
|
|
2058
|
+
|
|
2059
|
+
100 = \u30BB\u30F3\u30B5\u7AEF\u672B / sensor terminal
|
|
2060
|
+
10 = \u691C\u51FA\u90E8 / detector
|
|
2061
|
+
11 = \u6E29\u5EA6\u30BB\u30F3\u30B5 / temperature sensor
|
|
2062
|
+
12 = \u52A0\u901F\u5EA6\u30BB\u30F3\u30B5 / acceleration sensor
|
|
2063
|
+
20 = \u51E6\u7406\u90E8 / processor
|
|
2064
|
+
21 = \u53D6\u5F97\u90E8 / acquisition unit
|
|
2065
|
+
22 = \u5224\u5B9A\u90E8 / determination unit
|
|
2066
|
+
30 = \u901A\u4FE1\u90E8 / communication unit
|
|
2067
|
+
200 = \u30B2\u30FC\u30C8\u30A6\u30A7\u30A4 / gateway
|
|
2068
|
+
300 = \u30AF\u30E9\u30A6\u30C9\u30B5\u30FC\u30D0 / cloud server
|
|
2069
|
+
310 = \u53D7\u4FE1\u90E8 / receiver
|
|
2070
|
+
320 = \u89E3\u6790\u90E8 / analyzer
|
|
2071
|
+
330 = \u8A18\u61B6\u90E8 / storage
|
|
2072
|
+
|
|
2073
|
+
100 : 10 20 30
|
|
2074
|
+
10 : 11 12
|
|
2075
|
+
20 : 21 22
|
|
2076
|
+
300 : 310 320 330
|
|
2077
|
+
|
|
2078
|
+
10 -> 20 : \u30BB\u30F3\u30B5\u5024 / sensor value
|
|
2079
|
+
20 -> 30 : \u9001\u4FE1\u30C7\u30FC\u30BF / transmission data
|
|
2080
|
+
30 .> 200 : \u7121\u7DDA / wireless
|
|
2081
|
+
200 -> 310 : \u4E2D\u7D99 / relay
|
|
2082
|
+
310 -> 320 : \u30C7\u30FC\u30BF / data
|
|
2083
|
+
320 -> 330 : \u7D50\u679C / result
|
|
2084
|
+
`
|
|
2085
|
+
},
|
|
2086
|
+
imagePipeline: {
|
|
2087
|
+
label: "\u753B\u50CF\u51E6\u7406",
|
|
2088
|
+
hint: "\u753B\u50CF\u5165\u529B\u304B\u3089\u5224\u5B9A\u7D50\u679C\u307E\u3067\u306E\u51E6\u7406\u30D1\u30A4\u30D7\u30E9\u30A4\u30F3",
|
|
2089
|
+
source: `# \u753B\u50CF\u51E6\u7406\u30D1\u30A4\u30D7\u30E9\u30A4\u30F3(\u65B9\u6CD5\u30AF\u30EC\u30FC\u30E0\u7528)
|
|
2090
|
+
# \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
|
|
2091
|
+
|
|
2092
|
+
S100 = \u753B\u50CF\u3092\u53D6\u5F97 / Acquire image
|
|
2093
|
+
S110 = \u524D\u51E6\u7406 / Preprocess
|
|
2094
|
+
S120 = \u7279\u5FB4\u91CF\u3092\u62BD\u51FA / Extract features
|
|
2095
|
+
S130 = \u6B20\u9665\u3042\u308A? / Defect?
|
|
2096
|
+
S140 = \u30A2\u30E9\u30FC\u30C8\u3092\u51FA\u529B / Output alert
|
|
2097
|
+
S150 = \u6B63\u5E38\u7D50\u679C\u3092\u8A18\u9332 / Record normal result
|
|
2098
|
+
S160 = \u7D42\u4E86 / End
|
|
2099
|
+
|
|
2100
|
+
S100 -> S110
|
|
2101
|
+
S110 -> S120
|
|
2102
|
+
S120 -> S130
|
|
2103
|
+
S130 -> S140 : Yes
|
|
2104
|
+
S130 -> S150 : No
|
|
2105
|
+
S140 -> S160
|
|
2106
|
+
S150 -> S160
|
|
2107
|
+
`
|
|
2108
|
+
},
|
|
2109
|
+
controlLoop: {
|
|
2110
|
+
label: "\u5236\u5FA1\u30EB\u30FC\u30D7",
|
|
2111
|
+
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",
|
|
2112
|
+
source: `# \u5236\u5FA1\u30EB\u30FC\u30D7(\u88C5\u7F6E\u30AF\u30EC\u30FC\u30E0\u7528)
|
|
2113
|
+
# \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
|
|
2114
|
+
|
|
2115
|
+
100 = \u5236\u5FA1\u30B7\u30B9\u30C6\u30E0 / control system
|
|
2116
|
+
10 = \u5236\u5FA1\u90E8 / controller
|
|
2117
|
+
11 = \u76EE\u6A19\u5024\u53D6\u5F97\u90E8 / target acquisition unit
|
|
2118
|
+
12 = \u504F\u5DEE\u7B97\u51FA\u90E8 / error calculator
|
|
2119
|
+
13 = \u6307\u4EE4\u751F\u6210\u90E8 / command generator
|
|
2120
|
+
20 = \u99C6\u52D5\u90E8 / driver
|
|
2121
|
+
30 = \u30BB\u30F3\u30B5\u90E8 / sensor unit
|
|
2122
|
+
40 = \u5BFE\u8C61\u88C5\u7F6E / controlled object
|
|
2123
|
+
|
|
2124
|
+
100 : 30 10 40 20
|
|
2125
|
+
10 : 11 12 13
|
|
2126
|
+
|
|
2127
|
+
30 -> 12 : \u6E2C\u5B9A\u5024 / measured value
|
|
2128
|
+
11 -> 12 : \u76EE\u6A19\u5024 / target value
|
|
2129
|
+
12 -> 13 : \u504F\u5DEE / error
|
|
2130
|
+
13 -> 20 : \u6307\u4EE4 / command
|
|
2131
|
+
20 -> 40 : \u99C6\u52D5\u4FE1\u53F7 / drive signal
|
|
2132
|
+
40 .> 30 : \u30D5\u30A3\u30FC\u30C9\u30D0\u30C3\u30AF / feedback
|
|
2133
|
+
`
|
|
2134
|
+
},
|
|
2135
|
+
flow: {
|
|
2136
|
+
label: "\u30D5\u30ED\u30FC\u30C1\u30E3\u30FC\u30C8",
|
|
2137
|
+
hint: "\u30E9\u30D9\u30EB\u306B\u300C?\u300D\u304C\u3042\u308B \u2192 \u30D5\u30ED\u30FC\u3068\u5224\u5B9A",
|
|
2138
|
+
source: `# \u30D5\u30ED\u30FC\u30C1\u30E3\u30FC\u30C8(\u65B9\u6CD5\u30AF\u30EC\u30FC\u30E0\u7528)
|
|
2139
|
+
# \u30D2\u30F3\u30C8:\u30E9\u30D9\u30EB\u672B\u5C3E\u306B\u300C?\u300D \u2192 \u83F1\u5F62(\u6761\u4EF6\u5206\u5C90)\u306B\u81EA\u52D5\u63A8\u8AD6
|
|
2140
|
+
|
|
2141
|
+
S100 = \u958B\u59CB / Start
|
|
2142
|
+
S110 = \u6761\u4EF6A? / "Condition A?"
|
|
2143
|
+
S120 = \u51E6\u7406X / Process X
|
|
2144
|
+
S130 = \u51E6\u7406Y / Process Y
|
|
2145
|
+
S140 = \u7D42\u4E86 / End
|
|
2146
|
+
|
|
2147
|
+
S100 -> S110
|
|
2148
|
+
S110 -> S120 : Yes
|
|
2149
|
+
S110 -> S130 : No
|
|
2150
|
+
S120 -> S140
|
|
2151
|
+
S130 -> S140
|
|
2152
|
+
`
|
|
2153
|
+
},
|
|
2154
|
+
state: {
|
|
2155
|
+
label: "\u72B6\u614B\u9077\u79FB\u56F3",
|
|
2156
|
+
hint: "\u300C*\u300D\u3092\u4F7F\u3046 \u2192 \u72B6\u614B\u9077\u79FB\u3068\u5224\u5B9A",
|
|
2157
|
+
source: `# \u72B6\u614B\u9077\u79FB\u56F3
|
|
2158
|
+
# \u30D2\u30F3\u30C8:\u300C*\u300D\u304C\u521D\u671F/\u7D42\u7AEF\u306E\u7B26\u53F7(\u9ED2\u4E38\u3067\u63CF\u753B)
|
|
2159
|
+
|
|
2160
|
+
S1 = \u5F85\u6A5F / Idle
|
|
2161
|
+
S2 = \u52D5\u4F5C\u4E2D / Running
|
|
2162
|
+
S3 = \u30A8\u30E9\u30FC / Error
|
|
2163
|
+
|
|
2164
|
+
* -> S1
|
|
2165
|
+
S1 -> S2 : \u8D77\u52D5 / start
|
|
2166
|
+
S2 -> S1 : \u505C\u6B62 / stop
|
|
2167
|
+
S2 -> S3 : \u7570\u5E38 / fault
|
|
2168
|
+
S3 -> S1 : \u30EA\u30BB\u30C3\u30C8 / reset
|
|
2169
|
+
S3 -> *
|
|
2170
|
+
`
|
|
2171
|
+
},
|
|
2172
|
+
seq: {
|
|
2173
|
+
label: "\u30B7\u30FC\u30B1\u30F3\u30B9\u56F3",
|
|
2174
|
+
hint: "\u300C:\u300D\u3082\u300C?\u300D\u3082\u300C*\u300D\u3082\u306A\u3044 \u2192 \u30B7\u30FC\u30B1\u30F3\u30B9",
|
|
2175
|
+
source: `# \u30B7\u30FC\u30B1\u30F3\u30B9\u56F3(\u30D7\u30ED\u30C8\u30B3\u30EB\u7CFB)
|
|
2176
|
+
# \u30D2\u30F3\u30C8:\u5305\u542B\u3082?\u3082*\u3082\u7121\u3044 \u2192 \u30B7\u30FC\u30B1\u30F3\u30B9\u56F3\u3068\u3057\u3066\u63CF\u753B
|
|
2177
|
+
# \u30A2\u30AF\u30BF\u306F\u767B\u5834\u9806\u306B\u5DE6\u304B\u3089\u4E26\u3076
|
|
2178
|
+
|
|
2179
|
+
100 = \u30AF\u30E9\u30A4\u30A2\u30F3\u30C8 / client
|
|
2180
|
+
200 = \u30B5\u30FC\u30D0 / server
|
|
2181
|
+
|
|
2182
|
+
100 -> 200 : \u8A8D\u8A3C\u8981\u6C42 / auth request
|
|
2183
|
+
200 -> 100 : \u30C8\u30FC\u30AF\u30F3 / token
|
|
2184
|
+
100 -> 200 : \u30EA\u30BD\u30FC\u30B9\u8981\u6C42 / resource request
|
|
2185
|
+
200 -> 100 : \u30EA\u30BD\u30FC\u30B9\u5FDC\u7B54 / resource response
|
|
2186
|
+
`
|
|
2187
|
+
},
|
|
2188
|
+
handshake: {
|
|
2189
|
+
label: "\u30CF\u30F3\u30C9\u30B7\u30A7\u30A4\u30AF",
|
|
2190
|
+
hint: "\u8981\u6C42/\u5FDC\u7B54/\u78BA\u7ACB/\u7D42\u4E86\u306E\u901A\u4FE1\u30B7\u30FC\u30B1\u30F3\u30B9",
|
|
2191
|
+
source: `# \u901A\u4FE1\u30CF\u30F3\u30C9\u30B7\u30A7\u30A4\u30AF(\u30B7\u30FC\u30B1\u30F3\u30B9\u56F3)
|
|
2192
|
+
# \u30D2\u30F3\u30C8: \u5F80\u5FA9\u30E1\u30C3\u30BB\u30FC\u30B8\u304C\u3042\u308B\u3068\u30B7\u30FC\u30B1\u30F3\u30B9\u56F3\u306B\u306A\u308B
|
|
2193
|
+
|
|
2194
|
+
100 = \u7AEF\u672B / terminal
|
|
2195
|
+
200 = \u30B5\u30FC\u30D0 / server
|
|
2196
|
+
|
|
2197
|
+
100 -> 200 : \u63A5\u7D9A\u8981\u6C42 / connect request
|
|
2198
|
+
200 -> 100 : \u5FDC\u7B54 / response
|
|
2199
|
+
100 -> 200 : \u8A8D\u8A3C\u60C5\u5831 / credentials
|
|
2200
|
+
200 -> 100 : \u8A8D\u8A3C\u7D50\u679C / auth result
|
|
2201
|
+
100 <-> 200 : \u30C7\u30FC\u30BF\u901A\u4FE1 / data exchange
|
|
2202
|
+
100 -> 200 : \u5207\u65AD\u8981\u6C42 / disconnect
|
|
2203
|
+
200 -> 100 : \u5207\u65AD\u5B8C\u4E86 / disconnected
|
|
2204
|
+
`
|
|
2205
|
+
}
|
|
2206
|
+
};
|
|
2207
|
+
|
|
2208
|
+
// src/node/index.ts
|
|
2209
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
2210
|
+
|
|
2211
|
+
// src/node/dom.ts
|
|
2212
|
+
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
2213
|
+
var ShimElement = class {
|
|
2214
|
+
constructor(namespaceURI, tagName) {
|
|
2215
|
+
this.namespaceURI = namespaceURI;
|
|
2216
|
+
this.tagName = tagName;
|
|
2217
|
+
}
|
|
2218
|
+
namespaceURI;
|
|
2219
|
+
tagName;
|
|
2220
|
+
nodeType = "element";
|
|
2221
|
+
attrs = /* @__PURE__ */ new Map();
|
|
2222
|
+
children = [];
|
|
2223
|
+
text = null;
|
|
2224
|
+
setAttribute(name, value) {
|
|
2225
|
+
this.attrs.set(name, String(value));
|
|
2226
|
+
}
|
|
2227
|
+
getAttribute(name) {
|
|
2228
|
+
return this.attrs.has(name) ? this.attrs.get(name) : null;
|
|
2229
|
+
}
|
|
2230
|
+
removeAttribute(name) {
|
|
2231
|
+
this.attrs.delete(name);
|
|
2232
|
+
}
|
|
2233
|
+
appendChild(child) {
|
|
2234
|
+
this.text = null;
|
|
2235
|
+
this.children.push(child);
|
|
2236
|
+
return child;
|
|
2237
|
+
}
|
|
2238
|
+
set textContent(value) {
|
|
2239
|
+
this.text = value == null ? "" : String(value);
|
|
2240
|
+
this.children = [];
|
|
2241
|
+
}
|
|
2242
|
+
get textContent() {
|
|
2243
|
+
if (this.text != null) return this.text;
|
|
2244
|
+
return this.children.map((c) => c.textContent).join("");
|
|
2245
|
+
}
|
|
2246
|
+
};
|
|
2247
|
+
var shimDocument = {
|
|
2248
|
+
createElementNS(ns, qualifiedName) {
|
|
2249
|
+
return new ShimElement(ns, qualifiedName);
|
|
2250
|
+
},
|
|
2251
|
+
createElement(tagName) {
|
|
2252
|
+
return new ShimElement(SVG_NS, tagName);
|
|
2253
|
+
}
|
|
2254
|
+
};
|
|
2255
|
+
function withShimDocument(fn) {
|
|
2256
|
+
const g = globalThis;
|
|
2257
|
+
const prev = g.document;
|
|
2258
|
+
g.document = shimDocument;
|
|
2259
|
+
try {
|
|
2260
|
+
return fn();
|
|
2261
|
+
} finally {
|
|
2262
|
+
g.document = prev;
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
function escapeAttr(value) {
|
|
2266
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2267
|
+
}
|
|
2268
|
+
function escapeText(value) {
|
|
2269
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2270
|
+
}
|
|
2271
|
+
function serializeSvg(node) {
|
|
2272
|
+
const attrs = [...node.attrs].map(([name, value]) => ` ${name}="${escapeAttr(value)}"`).join("");
|
|
2273
|
+
if (node.text != null) {
|
|
2274
|
+
return `<${node.tagName}${attrs}>${escapeText(node.text)}</${node.tagName}>`;
|
|
2275
|
+
}
|
|
2276
|
+
if (node.children.length === 0) {
|
|
2277
|
+
return `<${node.tagName}${attrs}/>`;
|
|
2278
|
+
}
|
|
2279
|
+
const inner = node.children.map(serializeSvg).join("");
|
|
2280
|
+
return `<${node.tagName}${attrs}>${inner}</${node.tagName}>`;
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
// src/node/content-box.ts
|
|
2284
|
+
var DEFAULT_BLEED = 3;
|
|
2285
|
+
function num(node, name, fallback = 0) {
|
|
2286
|
+
const raw = node.getAttribute(name);
|
|
2287
|
+
if (raw == null) return fallback;
|
|
2288
|
+
const n = Number.parseFloat(raw);
|
|
2289
|
+
return Number.isFinite(n) ? n : fallback;
|
|
2290
|
+
}
|
|
2291
|
+
function parseNumbers(text) {
|
|
2292
|
+
const out = [];
|
|
2293
|
+
const re = /-?\d*\.?\d+(?:e[-+]?\d+)?/gi;
|
|
2294
|
+
let m;
|
|
2295
|
+
while ((m = re.exec(text)) !== null) out.push(Number.parseFloat(m[0]));
|
|
2296
|
+
return out;
|
|
2297
|
+
}
|
|
2298
|
+
function primitiveBounds(node) {
|
|
2299
|
+
switch (node.tagName) {
|
|
2300
|
+
case "rect": {
|
|
2301
|
+
const x = num(node, "x");
|
|
2302
|
+
const y = num(node, "y");
|
|
2303
|
+
const w = num(node, "width");
|
|
2304
|
+
const h = num(node, "height");
|
|
2305
|
+
if (w <= 0 && h <= 0) return null;
|
|
2306
|
+
return { minX: x, minY: y, maxX: x + w, maxY: y + h };
|
|
2307
|
+
}
|
|
2308
|
+
case "circle": {
|
|
2309
|
+
const cx = num(node, "cx");
|
|
2310
|
+
const cy = num(node, "cy");
|
|
2311
|
+
const r = num(node, "r");
|
|
2312
|
+
if (r <= 0) return null;
|
|
2313
|
+
return { minX: cx - r, minY: cy - r, maxX: cx + r, maxY: cy + r };
|
|
2314
|
+
}
|
|
2315
|
+
case "line": {
|
|
2316
|
+
const x1 = num(node, "x1");
|
|
2317
|
+
const y1 = num(node, "y1");
|
|
2318
|
+
const x2 = num(node, "x2");
|
|
2319
|
+
const y2 = num(node, "y2");
|
|
2320
|
+
return {
|
|
2321
|
+
minX: Math.min(x1, x2),
|
|
2322
|
+
minY: Math.min(y1, y2),
|
|
2323
|
+
maxX: Math.max(x1, x2),
|
|
2324
|
+
maxY: Math.max(y1, y2)
|
|
2325
|
+
};
|
|
2326
|
+
}
|
|
2327
|
+
case "polygon":
|
|
2328
|
+
case "polyline": {
|
|
2329
|
+
const nums = parseNumbers(node.getAttribute("points") ?? "");
|
|
2330
|
+
return boundsFromPairs(nums);
|
|
2331
|
+
}
|
|
2332
|
+
case "path": {
|
|
2333
|
+
const nums = parseNumbers(node.getAttribute("d") ?? "");
|
|
2334
|
+
return boundsFromPairs(nums);
|
|
2335
|
+
}
|
|
2336
|
+
case "text": {
|
|
2337
|
+
return textBounds(node);
|
|
2338
|
+
}
|
|
2339
|
+
default:
|
|
2340
|
+
return null;
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
function boundsFromPairs(nums) {
|
|
2344
|
+
if (nums.length < 2) return null;
|
|
2345
|
+
let minX = Infinity;
|
|
2346
|
+
let minY = Infinity;
|
|
2347
|
+
let maxX = -Infinity;
|
|
2348
|
+
let maxY = -Infinity;
|
|
2349
|
+
for (let i = 0; i + 1 < nums.length; i += 2) {
|
|
2350
|
+
const x = nums[i];
|
|
2351
|
+
const y = nums[i + 1];
|
|
2352
|
+
if (x < minX) minX = x;
|
|
2353
|
+
if (y < minY) minY = y;
|
|
2354
|
+
if (x > maxX) maxX = x;
|
|
2355
|
+
if (y > maxY) maxY = y;
|
|
2356
|
+
}
|
|
2357
|
+
if (!Number.isFinite(minX)) return null;
|
|
2358
|
+
return { minX, minY, maxX, maxY };
|
|
2359
|
+
}
|
|
2360
|
+
function textBounds(node) {
|
|
2361
|
+
const text = node.textContent;
|
|
2362
|
+
if (!text) return null;
|
|
2363
|
+
const x = num(node, "x");
|
|
2364
|
+
const y = num(node, "y");
|
|
2365
|
+
const fontSize = num(node, "font-size", 2.8);
|
|
2366
|
+
const width = estimateTextWidth(text, fontSize);
|
|
2367
|
+
const anchor = node.getAttribute("text-anchor") ?? "start";
|
|
2368
|
+
let minX;
|
|
2369
|
+
if (anchor === "middle") minX = x - width / 2;
|
|
2370
|
+
else if (anchor === "end") minX = x - width;
|
|
2371
|
+
else minX = x;
|
|
2372
|
+
const maxX = minX + width;
|
|
2373
|
+
const baseline = node.getAttribute("dominant-baseline") ?? "alphabetic";
|
|
2374
|
+
let minY;
|
|
2375
|
+
let maxY;
|
|
2376
|
+
if (baseline === "middle" || baseline === "central") {
|
|
2377
|
+
minY = y - fontSize * 0.6;
|
|
2378
|
+
maxY = y + fontSize * 0.6;
|
|
2379
|
+
} else {
|
|
2380
|
+
minY = y - fontSize * 0.8;
|
|
2381
|
+
maxY = y + fontSize * 0.25;
|
|
2382
|
+
}
|
|
2383
|
+
return { minX, minY, maxX, maxY };
|
|
2384
|
+
}
|
|
2385
|
+
function collectBounds(node, acc) {
|
|
2386
|
+
const b = primitiveBounds(node);
|
|
2387
|
+
if (b) acc.push(b);
|
|
2388
|
+
for (const child of node.children) collectBounds(child, acc);
|
|
2389
|
+
}
|
|
2390
|
+
function unionAll(bounds) {
|
|
2391
|
+
if (bounds.length === 0) return null;
|
|
2392
|
+
let minX = Infinity;
|
|
2393
|
+
let minY = Infinity;
|
|
2394
|
+
let maxX = -Infinity;
|
|
2395
|
+
let maxY = -Infinity;
|
|
2396
|
+
for (const b of bounds) {
|
|
2397
|
+
if (b.minX < minX) minX = b.minX;
|
|
2398
|
+
if (b.minY < minY) minY = b.minY;
|
|
2399
|
+
if (b.maxX > maxX) maxX = b.maxX;
|
|
2400
|
+
if (b.maxY > maxY) maxY = b.maxY;
|
|
2401
|
+
}
|
|
2402
|
+
return { minX, minY, maxX, maxY };
|
|
2403
|
+
}
|
|
2404
|
+
function fallbackViewBox(root) {
|
|
2405
|
+
const nums = parseNumbers(root.getAttribute("viewBox") ?? "");
|
|
2406
|
+
if (nums.length === 4 && nums[2] > 0 && nums[3] > 0) {
|
|
2407
|
+
return { minX: nums[0], minY: nums[1], width: nums[2], height: nums[3] };
|
|
2408
|
+
}
|
|
2409
|
+
return { minX: 0, minY: 0, width: 210, height: 297 };
|
|
2410
|
+
}
|
|
2411
|
+
function computeContentBox(root, bleed = DEFAULT_BLEED) {
|
|
2412
|
+
const all = [];
|
|
2413
|
+
collectBounds(root, all);
|
|
2414
|
+
const merged = unionAll(all);
|
|
2415
|
+
if (!merged) return fallbackViewBox(root);
|
|
2416
|
+
const pad = Math.max(0, bleed);
|
|
2417
|
+
return {
|
|
2418
|
+
minX: merged.minX - pad,
|
|
2419
|
+
minY: merged.minY - pad,
|
|
2420
|
+
width: Math.max(1, merged.maxX - merged.minX + pad * 2),
|
|
2421
|
+
height: Math.max(1, merged.maxY - merged.minY + pad * 2)
|
|
2422
|
+
};
|
|
2423
|
+
}
|
|
2424
|
+
function svgDisplayDimensions(viewBox, targetSide = 1600) {
|
|
2425
|
+
const longest = Math.max(viewBox.width, viewBox.height, 1);
|
|
2426
|
+
const scale = Math.max(1, targetSide / longest);
|
|
2427
|
+
return {
|
|
2428
|
+
width: Math.max(1, Math.ceil(viewBox.width * scale)),
|
|
2429
|
+
height: Math.max(1, Math.ceil(viewBox.height * scale)),
|
|
2430
|
+
scale
|
|
2431
|
+
};
|
|
2432
|
+
}
|
|
2433
|
+
var RASTER_SCALE = 8;
|
|
2434
|
+
var RASTER_MAX_SIDE = 24e3;
|
|
2435
|
+
var RASTER_MAX_PIXELS = 1e8;
|
|
2436
|
+
function rasterDimensions(viewBox, scale = RASTER_SCALE) {
|
|
2437
|
+
const requested = Number.isFinite(scale) && scale > 0 ? scale : RASTER_SCALE;
|
|
2438
|
+
const sideScale = RASTER_MAX_SIDE / Math.max(viewBox.width, viewBox.height, 1);
|
|
2439
|
+
const pixelScale = Math.sqrt(RASTER_MAX_PIXELS / Math.max(viewBox.width * viewBox.height, 1));
|
|
2440
|
+
const capped = Math.min(requested, sideScale, pixelScale);
|
|
2441
|
+
return {
|
|
2442
|
+
width: Math.max(1, Math.ceil(viewBox.width * capped)),
|
|
2443
|
+
height: Math.max(1, Math.ceil(viewBox.height * capped)),
|
|
2444
|
+
scale: capped
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
// src/node/svg.ts
|
|
2449
|
+
var SVG_NS2 = "http://www.w3.org/2000/svg";
|
|
2450
|
+
var DEFAULT_BLEED2 = 3;
|
|
2451
|
+
var DEFAULT_TARGET_SIDE = 1600;
|
|
2452
|
+
function buildSvgModel(source, opts = {}) {
|
|
2453
|
+
const {
|
|
2454
|
+
lang = "ja",
|
|
2455
|
+
crop = true,
|
|
2456
|
+
bleed = DEFAULT_BLEED2,
|
|
2457
|
+
targetSide = DEFAULT_TARGET_SIDE
|
|
2458
|
+
} = opts;
|
|
2459
|
+
const doc = parse(source);
|
|
2460
|
+
const laid = layout(doc);
|
|
2461
|
+
const svgEl = withShimDocument(() => render(laid, { lang }));
|
|
2462
|
+
const viewBox = crop ? computeContentBox(svgEl, bleed) : { minX: 0, minY: 0, width: laid.width, height: laid.height };
|
|
2463
|
+
const display = svgDisplayDimensions(viewBox, targetSide);
|
|
2464
|
+
svgEl.setAttribute("xmlns", SVG_NS2);
|
|
2465
|
+
svgEl.setAttribute("version", "1.1");
|
|
2466
|
+
svgEl.setAttribute(
|
|
2467
|
+
"viewBox",
|
|
2468
|
+
`${viewBox.minX} ${viewBox.minY} ${viewBox.width} ${viewBox.height}`
|
|
2469
|
+
);
|
|
2470
|
+
svgEl.setAttribute("width", String(display.width));
|
|
2471
|
+
svgEl.setAttribute("height", String(display.height));
|
|
2472
|
+
svgEl.setAttribute("preserveAspectRatio", "xMidYMid meet");
|
|
2473
|
+
return { el: svgEl, kind: doc.kind, viewBox, width: display.width, height: display.height };
|
|
2474
|
+
}
|
|
2475
|
+
function renderToSvg(source, opts = {}) {
|
|
2476
|
+
const model = buildSvgModel(source, opts);
|
|
2477
|
+
const body = serializeSvg(model.el);
|
|
2478
|
+
const svg = opts.xmlDeclaration ?? true ? `<?xml version="1.0" encoding="UTF-8"?>
|
|
2479
|
+
${body}` : body;
|
|
2480
|
+
return {
|
|
2481
|
+
svg,
|
|
2482
|
+
kind: model.kind,
|
|
2483
|
+
viewBox: model.viewBox,
|
|
2484
|
+
width: model.width,
|
|
2485
|
+
height: model.height
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
// src/node/assets.ts
|
|
2490
|
+
import { existsSync, readFileSync } from "fs";
|
|
2491
|
+
import { dirname, join } from "path";
|
|
2492
|
+
import { fileURLToPath } from "url";
|
|
2493
|
+
var FONT_FAMILY_NAME = "IPAexGothic";
|
|
2494
|
+
var cachedPath = null;
|
|
2495
|
+
var cachedBuffer = null;
|
|
2496
|
+
function moduleDir() {
|
|
2497
|
+
let url = "";
|
|
2498
|
+
try {
|
|
2499
|
+
url = import.meta.url;
|
|
2500
|
+
} catch {
|
|
2501
|
+
}
|
|
2502
|
+
if (url) {
|
|
2503
|
+
try {
|
|
2504
|
+
return dirname(fileURLToPath(url));
|
|
2505
|
+
} catch {
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
if (typeof __dirname !== "undefined") return __dirname;
|
|
2509
|
+
return process.cwd();
|
|
2510
|
+
}
|
|
2511
|
+
function resolvePackageFile(...segments) {
|
|
2512
|
+
let dir = moduleDir();
|
|
2513
|
+
for (let i = 0; i < 8; i++) {
|
|
2514
|
+
const candidate = join(dir, ...segments);
|
|
2515
|
+
if (existsSync(candidate)) return candidate;
|
|
2516
|
+
const parent = dirname(dir);
|
|
2517
|
+
if (parent === dir) break;
|
|
2518
|
+
dir = parent;
|
|
2519
|
+
}
|
|
2520
|
+
throw new Error(
|
|
2521
|
+
`pdgkit: bundled file ${segments.join("/")} not found. Ensure the package was installed with its data files intact.`
|
|
2522
|
+
);
|
|
2523
|
+
}
|
|
2524
|
+
function resolveFontPath() {
|
|
2525
|
+
if (cachedPath) return cachedPath;
|
|
2526
|
+
cachedPath = resolvePackageFile("assets", "ipaexg.ttf");
|
|
2527
|
+
return cachedPath;
|
|
2528
|
+
}
|
|
2529
|
+
function loadFontBuffer() {
|
|
2530
|
+
if (cachedBuffer) return cachedBuffer;
|
|
2531
|
+
cachedBuffer = readFileSync(resolveFontPath());
|
|
2532
|
+
return cachedBuffer;
|
|
2533
|
+
}
|
|
2534
|
+
function loadFontBase64() {
|
|
2535
|
+
return loadFontBuffer().toString("base64");
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
// src/node/validate.ts
|
|
2539
|
+
var KIND_LABEL_JA = {
|
|
2540
|
+
block: "\u30D6\u30ED\u30C3\u30AF\u56F3",
|
|
2541
|
+
flow: "\u30D5\u30ED\u30FC\u30C1\u30E3\u30FC\u30C8",
|
|
2542
|
+
state: "\u72B6\u614B\u9077\u79FB\u56F3",
|
|
2543
|
+
seq: "\u30B7\u30FC\u30B1\u30F3\u30B9\u56F3"
|
|
2544
|
+
};
|
|
2545
|
+
function commentOf(line) {
|
|
2546
|
+
let inQuote = false;
|
|
2547
|
+
for (let i = 0; i < line.length; i++) {
|
|
2548
|
+
const c = line[i];
|
|
2549
|
+
if (c === '"') inQuote = !inQuote;
|
|
2550
|
+
else if (!inQuote && c === "#") return line.slice(i + 1);
|
|
2551
|
+
}
|
|
2552
|
+
return null;
|
|
2553
|
+
}
|
|
2554
|
+
function uncommented(line) {
|
|
2555
|
+
let inQuote = false;
|
|
2556
|
+
for (let i = 0; i < line.length; i++) {
|
|
2557
|
+
const c = line[i];
|
|
2558
|
+
if (c === '"') inQuote = !inQuote;
|
|
2559
|
+
else if (!inQuote && c === "#") return line.slice(0, i);
|
|
2560
|
+
}
|
|
2561
|
+
return line;
|
|
2562
|
+
}
|
|
2563
|
+
var KIND_DIRECTIVE_RE = /^\s*!?\s*(?:pdgkit\s*:)?\s*kind\s*[:=]?\s*(block|flow|state|seq)\b/i;
|
|
2564
|
+
function findKindDirective(lines) {
|
|
2565
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2566
|
+
const comment = commentOf(lines[i]);
|
|
2567
|
+
if (comment == null) continue;
|
|
2568
|
+
const m = comment.match(KIND_DIRECTIVE_RE);
|
|
2569
|
+
if (m) return { kind: m[1].toLowerCase(), line: i + 1 };
|
|
2570
|
+
}
|
|
2571
|
+
return null;
|
|
2572
|
+
}
|
|
2573
|
+
var SPACED_OP_RE = /(?:^|\s)(?:<->|=>|->|<-|\.>|\.\.|-)(?:\s|$)/g;
|
|
2574
|
+
var GLUED_OP_RE = /[A-Za-z0-9_*](?:<->|<-|->|=>|\.>|\.\.|-)[A-Za-z0-9_*]/;
|
|
2575
|
+
function countSpacedOps(text) {
|
|
2576
|
+
const matches = text.match(SPACED_OP_RE);
|
|
2577
|
+
return matches ? matches.length : 0;
|
|
2578
|
+
}
|
|
2579
|
+
function validate(source) {
|
|
2580
|
+
const doc = parse(source);
|
|
2581
|
+
const diagnostics = [...doc.diagnostics];
|
|
2582
|
+
const lines = source.split(/\r?\n/);
|
|
2583
|
+
const directive = findKindDirective(lines);
|
|
2584
|
+
const declaredKind = directive ? directive.kind : null;
|
|
2585
|
+
let kindMatches = null;
|
|
2586
|
+
if (directive) {
|
|
2587
|
+
kindMatches = directive.kind === doc.kind;
|
|
2588
|
+
if (!kindMatches) {
|
|
2589
|
+
diagnostics.push({
|
|
2590
|
+
severity: "error",
|
|
2591
|
+
line: directive.line,
|
|
2592
|
+
col: 1,
|
|
2593
|
+
message: `\u56F3\u7A2E\u30A2\u30B5\u30FC\u30B7\u30E7\u30F3\u4E0D\u4E00\u81F4: \u5BA3\u8A00=${KIND_LABEL_JA[directive.kind]}(${directive.kind}) \u3060\u304C\u63A8\u8AD6=${KIND_LABEL_JA[doc.kind]}(${doc.kind})\u3002\u69CB\u9020(\u5305\u542B\u300C:\u300D/ \u672B\u5C3E\u300C?\u300D/ \u7B26\u53F7\u300C*\u300D/ \u5F80\u5FA9\u30FB\u300C<->\u300D)\u3092\u898B\u76F4\u3059\u304B\u3001\u5BA3\u8A00\u3092\u4FEE\u6B63\u3057\u3066\u304F\u3060\u3055\u3044\u3002`
|
|
2594
|
+
});
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
const unknownLines = new Set(
|
|
2598
|
+
doc.diagnostics.filter((d) => d.severity === "error" && d.message.startsWith("\u69CB\u6587\u4E0D\u660E")).map((d) => d.line)
|
|
2599
|
+
);
|
|
2600
|
+
for (const lineNum of unknownLines) {
|
|
2601
|
+
const code = uncommented(lines[lineNum - 1] ?? "").trim();
|
|
2602
|
+
if (!code) continue;
|
|
2603
|
+
if (countSpacedOps(code) >= 2) {
|
|
2604
|
+
diagnostics.push({
|
|
2605
|
+
severity: "info",
|
|
2606
|
+
line: lineNum,
|
|
2607
|
+
col: 1,
|
|
2608
|
+
message: "\u9023\u9396\u8A18\u6CD5(A -> B -> C)\u306F\u672A\u5BFE\u5FDC\u3067\u3059\u30021\u884C\u306B1\u63A5\u7D9A\u3067\u5206\u5272\u3057\u3066\u304F\u3060\u3055\u3044(\u4F8B: A -> B / B -> C)\u3002"
|
|
2609
|
+
});
|
|
2610
|
+
} else if (GLUED_OP_RE.test(code)) {
|
|
2611
|
+
diagnostics.push({
|
|
2612
|
+
severity: "info",
|
|
2613
|
+
line: lineNum,
|
|
2614
|
+
col: 1,
|
|
2615
|
+
message: '\u63A5\u7D9A\u6F14\u7B97\u5B50\u306E\u524D\u5F8C\u306B\u306F\u534A\u89D2\u30B9\u30DA\u30FC\u30B9\u304C\u5FC5\u8981\u3067\u3059(\u4F8B: "11-12" \u3067\u306F\u306A\u304F "11 - 12")\u3002'
|
|
2616
|
+
});
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
diagnostics.sort((a, b) => a.line - b.line || a.col - b.col);
|
|
2620
|
+
const counts = {
|
|
2621
|
+
errors: diagnostics.filter((d) => d.severity === "error").length,
|
|
2622
|
+
warnings: diagnostics.filter((d) => d.severity === "warning").length,
|
|
2623
|
+
infos: diagnostics.filter((d) => d.severity === "info").length
|
|
2624
|
+
};
|
|
2625
|
+
return {
|
|
2626
|
+
ok: counts.errors === 0,
|
|
2627
|
+
kind: doc.kind,
|
|
2628
|
+
declaredKind,
|
|
2629
|
+
kindMatches,
|
|
2630
|
+
diagnostics,
|
|
2631
|
+
counts
|
|
2632
|
+
};
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
// src/node/raster.ts
|
|
2636
|
+
var JPEG_QUALITY = 100;
|
|
2637
|
+
async function renderPixels(source, opts) {
|
|
2638
|
+
const { lang = "ja", scale = 8, bleed = 3 } = opts;
|
|
2639
|
+
const { svg, viewBox } = renderToSvg(source, { lang, crop: true, bleed });
|
|
2640
|
+
const dims = rasterDimensions(viewBox, scale);
|
|
2641
|
+
const { Resvg } = await import("@resvg/resvg-js");
|
|
2642
|
+
const resvg = new Resvg(svg, {
|
|
2643
|
+
background: "rgba(255,255,255,1)",
|
|
2644
|
+
fitTo: { mode: "width", value: dims.width },
|
|
2645
|
+
font: {
|
|
2646
|
+
fontFiles: [resolveFontPath()],
|
|
2647
|
+
loadSystemFonts: false,
|
|
2648
|
+
defaultFontFamily: FONT_FAMILY_NAME
|
|
2649
|
+
}
|
|
2650
|
+
});
|
|
2651
|
+
const rendered = resvg.render();
|
|
2652
|
+
const raw = {
|
|
2653
|
+
pixels: rendered.pixels,
|
|
2654
|
+
width: rendered.width,
|
|
2655
|
+
height: rendered.height
|
|
2656
|
+
};
|
|
2657
|
+
return { raw, png: rendered.asPng() };
|
|
2658
|
+
}
|
|
2659
|
+
async function renderToPng(source, opts = {}) {
|
|
2660
|
+
const { png } = await renderPixels(source, opts);
|
|
2661
|
+
return png;
|
|
2662
|
+
}
|
|
2663
|
+
async function renderToJpeg(source, opts = {}) {
|
|
2664
|
+
const { raw } = await renderPixels(source, opts);
|
|
2665
|
+
const jpeg = (await import("jpeg-js")).default;
|
|
2666
|
+
const encoded = jpeg.encode(
|
|
2667
|
+
{ data: Buffer.from(raw.pixels), width: raw.width, height: raw.height },
|
|
2668
|
+
JPEG_QUALITY
|
|
2669
|
+
);
|
|
2670
|
+
return encoded.data;
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
// src/node/ooxml.ts
|
|
2674
|
+
var PPTX_SLIDE_W = 12192e3;
|
|
2675
|
+
var PPTX_SLIDE_H = 6858e3;
|
|
2676
|
+
var PPTX_MARGIN = 457200;
|
|
2677
|
+
function fitRectIntoPage(sourceW, sourceH, pageW, pageH, margin = 10) {
|
|
2678
|
+
const usableW = Math.max(1, pageW - margin * 2);
|
|
2679
|
+
const usableH = Math.max(1, pageH - margin * 2);
|
|
2680
|
+
const scale = Math.min(usableW / sourceW, usableH / sourceH);
|
|
2681
|
+
const width = sourceW * scale;
|
|
2682
|
+
const height = sourceH * scale;
|
|
2683
|
+
return { x: (pageW - width) / 2, y: (pageH - height) / 2, width, height };
|
|
2684
|
+
}
|
|
2685
|
+
function fitRectIntoSlide(sourceW, sourceH, slideW = PPTX_SLIDE_W, slideH = PPTX_SLIDE_H, margin = PPTX_MARGIN) {
|
|
2686
|
+
return fitRectIntoPage(sourceW, sourceH, slideW, slideH, margin);
|
|
2687
|
+
}
|
|
2688
|
+
function buildPptxPackage(imagePng, imageW, imageH) {
|
|
2689
|
+
const fit = fitRectIntoSlide(imageW, imageH);
|
|
2690
|
+
const iso = (/* @__PURE__ */ new Date()).toISOString();
|
|
2691
|
+
return createStoredZip([
|
|
2692
|
+
["[Content_Types].xml", contentTypesXml()],
|
|
2693
|
+
["_rels/.rels", rootRelsXml()],
|
|
2694
|
+
["docProps/app.xml", appPropsXml()],
|
|
2695
|
+
["docProps/core.xml", corePropsXml(iso)],
|
|
2696
|
+
["ppt/presentation.xml", presentationXml()],
|
|
2697
|
+
["ppt/_rels/presentation.xml.rels", presentationRelsXml()],
|
|
2698
|
+
["ppt/slides/slide1.xml", slideXml(fit)],
|
|
2699
|
+
["ppt/slides/_rels/slide1.xml.rels", slideRelsXml()],
|
|
2700
|
+
["ppt/slideMasters/slideMaster1.xml", slideMasterXml()],
|
|
2701
|
+
["ppt/slideMasters/_rels/slideMaster1.xml.rels", slideMasterRelsXml()],
|
|
2702
|
+
["ppt/slideLayouts/slideLayout1.xml", slideLayoutXml()],
|
|
2703
|
+
["ppt/slideLayouts/_rels/slideLayout1.xml.rels", slideLayoutRelsXml()],
|
|
2704
|
+
["ppt/theme/theme1.xml", themeXml()],
|
|
2705
|
+
["ppt/media/image1.png", imagePng]
|
|
2706
|
+
]);
|
|
2707
|
+
}
|
|
2708
|
+
function buildEditablePptxPackage(svgEl) {
|
|
2709
|
+
const viewBox = computeContentBox(svgEl);
|
|
2710
|
+
const fit = fitRectIntoSlide(viewBox.width, viewBox.height);
|
|
2711
|
+
const shapes = editablePptxShapes(svgEl, viewBox, fit);
|
|
2712
|
+
const iso = (/* @__PURE__ */ new Date()).toISOString();
|
|
2713
|
+
return createStoredZip([
|
|
2714
|
+
["[Content_Types].xml", contentTypesXmlEditable()],
|
|
2715
|
+
["_rels/.rels", rootRelsXml()],
|
|
2716
|
+
["docProps/app.xml", appPropsXml()],
|
|
2717
|
+
["docProps/core.xml", corePropsXml(iso)],
|
|
2718
|
+
["ppt/presentation.xml", presentationXml()],
|
|
2719
|
+
["ppt/_rels/presentation.xml.rels", presentationRelsXml()],
|
|
2720
|
+
["ppt/slides/slide1.xml", editableSlideXml(shapes)],
|
|
2721
|
+
["ppt/slides/_rels/slide1.xml.rels", editableSlideRelsXml()],
|
|
2722
|
+
["ppt/slideMasters/slideMaster1.xml", slideMasterXml()],
|
|
2723
|
+
["ppt/slideMasters/_rels/slideMaster1.xml.rels", slideMasterRelsXml()],
|
|
2724
|
+
["ppt/slideLayouts/slideLayout1.xml", slideLayoutXml()],
|
|
2725
|
+
["ppt/slideLayouts/_rels/slideLayout1.xml.rels", slideLayoutRelsXml()],
|
|
2726
|
+
["ppt/theme/theme1.xml", themeXml()]
|
|
2727
|
+
]);
|
|
2728
|
+
}
|
|
2729
|
+
function collectElements(root, tags) {
|
|
2730
|
+
const out = [];
|
|
2731
|
+
const walk = (node) => {
|
|
2732
|
+
if (tags.has(node.tagName.toLowerCase())) out.push(node);
|
|
2733
|
+
for (const child of node.children) walk(child);
|
|
2734
|
+
};
|
|
2735
|
+
walk(root);
|
|
2736
|
+
return out;
|
|
2737
|
+
}
|
|
2738
|
+
function editablePptxShapes(svgEl, viewBox, fit) {
|
|
2739
|
+
const ctx = {
|
|
2740
|
+
nextId: 2,
|
|
2741
|
+
tx: {
|
|
2742
|
+
minX: viewBox.minX,
|
|
2743
|
+
minY: viewBox.minY,
|
|
2744
|
+
scaleX: fit.width / viewBox.width,
|
|
2745
|
+
scaleY: fit.height / viewBox.height,
|
|
2746
|
+
fitX: fit.x,
|
|
2747
|
+
fitY: fit.y
|
|
2748
|
+
}
|
|
2749
|
+
};
|
|
2750
|
+
const out = [];
|
|
2751
|
+
const elements = collectElements(svgEl, /* @__PURE__ */ new Set(["rect", "circle", "polygon", "path", "text", "line", "polyline"]));
|
|
2752
|
+
for (const el2 of elements) {
|
|
2753
|
+
const tag = el2.tagName.toLowerCase();
|
|
2754
|
+
if (tag === "rect") out.push(...pptxRect(el2, ctx));
|
|
2755
|
+
else if (tag === "circle") out.push(...pptxCircle(el2, ctx));
|
|
2756
|
+
else if (tag === "polygon") out.push(...pptxPolygon(el2, ctx));
|
|
2757
|
+
else if (tag === "path") out.push(...pptxPath(el2, ctx));
|
|
2758
|
+
else if (tag === "line") out.push(...pptxLine(el2, ctx));
|
|
2759
|
+
else if (tag === "polyline") out.push(...pptxPolyline(el2, ctx));
|
|
2760
|
+
else if (tag === "text") out.push(...pptxText(el2, ctx));
|
|
2761
|
+
}
|
|
2762
|
+
return out.filter(Boolean).join("\n");
|
|
2763
|
+
}
|
|
2764
|
+
function pptxRect(el2, ctx) {
|
|
2765
|
+
const x = numAttr(el2, "x");
|
|
2766
|
+
const y = numAttr(el2, "y");
|
|
2767
|
+
const w = numAttr(el2, "width");
|
|
2768
|
+
const h = numAttr(el2, "height");
|
|
2769
|
+
if (w <= 0 || h <= 0) return [];
|
|
2770
|
+
const p = mapBox(ctx.tx, x, y, w, h);
|
|
2771
|
+
const rx = Math.max(numAttr(el2, "rx"), numAttr(el2, "ry"));
|
|
2772
|
+
return [shapeXml(
|
|
2773
|
+
ctx.nextId++,
|
|
2774
|
+
"rect",
|
|
2775
|
+
p.x,
|
|
2776
|
+
p.y,
|
|
2777
|
+
p.w,
|
|
2778
|
+
p.h,
|
|
2779
|
+
rx > 0 ? "roundRect" : "rect",
|
|
2780
|
+
fillXml(colorAttr(el2, "fill", "none")),
|
|
2781
|
+
lineXml(lineStyleFromElement(el2, ctx.tx))
|
|
2782
|
+
)];
|
|
2783
|
+
}
|
|
2784
|
+
function pptxCircle(el2, ctx) {
|
|
2785
|
+
const cx = numAttr(el2, "cx");
|
|
2786
|
+
const cy = numAttr(el2, "cy");
|
|
2787
|
+
const r = numAttr(el2, "r");
|
|
2788
|
+
if (r <= 0) return [];
|
|
2789
|
+
const p = mapBox(ctx.tx, cx - r, cy - r, r * 2, r * 2);
|
|
2790
|
+
return [shapeXml(
|
|
2791
|
+
ctx.nextId++,
|
|
2792
|
+
"circle",
|
|
2793
|
+
p.x,
|
|
2794
|
+
p.y,
|
|
2795
|
+
p.w,
|
|
2796
|
+
p.h,
|
|
2797
|
+
"ellipse",
|
|
2798
|
+
fillXml(colorAttr(el2, "fill", "#000")),
|
|
2799
|
+
lineXml(lineStyleFromElement(el2, ctx.tx))
|
|
2800
|
+
)];
|
|
2801
|
+
}
|
|
2802
|
+
function pptxPolygon(el2, ctx) {
|
|
2803
|
+
const points = parsePoints(el2.getAttribute("points"));
|
|
2804
|
+
if (points.length < 3) return [];
|
|
2805
|
+
return [freeformXml(
|
|
2806
|
+
ctx.nextId++,
|
|
2807
|
+
"polygon",
|
|
2808
|
+
points.map((point) => mapPoint(ctx.tx, point[0], point[1])),
|
|
2809
|
+
fillXml(colorAttr(el2, "fill", "none")),
|
|
2810
|
+
lineXml(lineStyleFromElement(el2, ctx.tx))
|
|
2811
|
+
)];
|
|
2812
|
+
}
|
|
2813
|
+
function pptxPath(el2, ctx) {
|
|
2814
|
+
const points = parsePathPoints(el2.getAttribute("d"));
|
|
2815
|
+
if (points.length < 2) return [];
|
|
2816
|
+
const line = lineStyleFromElement(el2, ctx.tx);
|
|
2817
|
+
const out = [];
|
|
2818
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
2819
|
+
const a = mapPoint(ctx.tx, points[i][0], points[i][1]);
|
|
2820
|
+
const b = mapPoint(ctx.tx, points[i + 1][0], points[i + 1][1]);
|
|
2821
|
+
out.push(connectorXml(ctx.nextId++, "line", a.x, a.y, b.x, b.y, line));
|
|
2822
|
+
}
|
|
2823
|
+
return out;
|
|
2824
|
+
}
|
|
2825
|
+
function pptxLine(el2, ctx) {
|
|
2826
|
+
const a = mapPoint(ctx.tx, numAttr(el2, "x1"), numAttr(el2, "y1"));
|
|
2827
|
+
const b = mapPoint(ctx.tx, numAttr(el2, "x2"), numAttr(el2, "y2"));
|
|
2828
|
+
return [connectorXml(ctx.nextId++, "line", a.x, a.y, b.x, b.y, lineStyleFromElement(el2, ctx.tx))];
|
|
2829
|
+
}
|
|
2830
|
+
function pptxPolyline(el2, ctx) {
|
|
2831
|
+
const points = parsePoints(el2.getAttribute("points"));
|
|
2832
|
+
if (points.length < 2) return [];
|
|
2833
|
+
const line = lineStyleFromElement(el2, ctx.tx);
|
|
2834
|
+
const out = [];
|
|
2835
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
2836
|
+
const a = mapPoint(ctx.tx, points[i][0], points[i][1]);
|
|
2837
|
+
const b = mapPoint(ctx.tx, points[i + 1][0], points[i + 1][1]);
|
|
2838
|
+
out.push(connectorXml(ctx.nextId++, "line", a.x, a.y, b.x, b.y, line));
|
|
2839
|
+
}
|
|
2840
|
+
return out;
|
|
2841
|
+
}
|
|
2842
|
+
function pptxText(el2, ctx) {
|
|
2843
|
+
const text = (el2.textContent ?? "").trim();
|
|
2844
|
+
if (!text) return [];
|
|
2845
|
+
const x = numAttr(el2, "x");
|
|
2846
|
+
const y = numAttr(el2, "y");
|
|
2847
|
+
const fontSize = numAttr(el2, "font-size", 2.8);
|
|
2848
|
+
const widthSvg = estimateSvgTextWidth(text, fontSize);
|
|
2849
|
+
const heightSvg = fontSize * 1.35;
|
|
2850
|
+
const anchor = el2.getAttribute("text-anchor") ?? "start";
|
|
2851
|
+
const baseline = el2.getAttribute("dominant-baseline") ?? "alphabetic";
|
|
2852
|
+
let left = x;
|
|
2853
|
+
if (anchor === "middle") left -= widthSvg / 2;
|
|
2854
|
+
else if (anchor === "end") left -= widthSvg;
|
|
2855
|
+
let top = y - fontSize * 0.95;
|
|
2856
|
+
if (baseline === "middle" || baseline === "central") top = y - heightSvg / 2;
|
|
2857
|
+
const p = mapBox(ctx.tx, left, top, widthSvg, heightSvg);
|
|
2858
|
+
const fontPt = Math.max(1, Math.round(fontSize * ctx.tx.scaleY / 12700 * 100) / 100);
|
|
2859
|
+
const align = anchor === "middle" ? "ctr" : anchor === "end" ? "r" : "l";
|
|
2860
|
+
return [textBoxXml(ctx.nextId++, p.x, p.y, p.w, p.h, text, fontPt, align, colorAttr(el2, "fill", "#000"))];
|
|
2861
|
+
}
|
|
2862
|
+
function mapPoint(tx, x, y) {
|
|
2863
|
+
return {
|
|
2864
|
+
x: Math.round(tx.fitX + (x - tx.minX) * tx.scaleX),
|
|
2865
|
+
y: Math.round(tx.fitY + (y - tx.minY) * tx.scaleY)
|
|
2866
|
+
};
|
|
2867
|
+
}
|
|
2868
|
+
function mapBox(tx, x, y, w, h) {
|
|
2869
|
+
const a = mapPoint(tx, x, y);
|
|
2870
|
+
return { x: a.x, y: a.y, w: Math.max(1, Math.round(w * tx.scaleX)), h: Math.max(1, Math.round(h * tx.scaleY)) };
|
|
2871
|
+
}
|
|
2872
|
+
function numAttr(el2, name, fallback = 0) {
|
|
2873
|
+
const n = Number.parseFloat(el2.getAttribute(name) ?? "");
|
|
2874
|
+
return Number.isFinite(n) ? n : fallback;
|
|
2875
|
+
}
|
|
2876
|
+
function colorAttr(el2, name, fallback) {
|
|
2877
|
+
const value = (el2.getAttribute(name) ?? fallback).trim();
|
|
2878
|
+
if (!value || value === "none" || value === "transparent") return "none";
|
|
2879
|
+
if (value.startsWith("#")) {
|
|
2880
|
+
const hex = value.slice(1);
|
|
2881
|
+
if (/^[0-9a-f]{3}$/i.test(hex)) return hex.split("").map((c) => c + c).join("").toUpperCase();
|
|
2882
|
+
if (/^[0-9a-f]{6}$/i.test(hex)) return hex.toUpperCase();
|
|
2883
|
+
}
|
|
2884
|
+
if (value.toLowerCase() === "white") return "FFFFFF";
|
|
2885
|
+
if (value.toLowerCase() === "black") return "000000";
|
|
2886
|
+
return fallback === "none" ? "none" : "000000";
|
|
2887
|
+
}
|
|
2888
|
+
function lineStyleFromElement(el2, tx) {
|
|
2889
|
+
const stroke = colorAttr(el2, "stroke", "none");
|
|
2890
|
+
if (stroke === "none") return null;
|
|
2891
|
+
const svgWidth = numAttr(el2, "stroke-width", 0.4);
|
|
2892
|
+
const width = Math.max(1270, Math.round(svgWidth * (tx.scaleX + tx.scaleY) / 2));
|
|
2893
|
+
return { color: stroke, width, dash: !!el2.getAttribute("stroke-dasharray") };
|
|
2894
|
+
}
|
|
2895
|
+
function parsePoints(raw) {
|
|
2896
|
+
if (!raw) return [];
|
|
2897
|
+
const nums = raw.trim().split(/[\s,]+/).map(Number).filter(Number.isFinite);
|
|
2898
|
+
const out = [];
|
|
2899
|
+
for (let i = 0; i + 1 < nums.length; i += 2) out.push([nums[i], nums[i + 1]]);
|
|
2900
|
+
return out;
|
|
2901
|
+
}
|
|
2902
|
+
function parsePathPoints(raw) {
|
|
2903
|
+
if (!raw) return [];
|
|
2904
|
+
const nums = raw.replace(/[MLZ]/gi, " ").trim().split(/[\s,]+/).map(Number).filter(Number.isFinite);
|
|
2905
|
+
const out = [];
|
|
2906
|
+
for (let i = 0; i + 1 < nums.length; i += 2) out.push([nums[i], nums[i + 1]]);
|
|
2907
|
+
return out;
|
|
2908
|
+
}
|
|
2909
|
+
function estimateSvgTextWidth(text, fontSize) {
|
|
2910
|
+
let width = 0;
|
|
2911
|
+
for (const char of text) {
|
|
2912
|
+
if (char === " ") width += fontSize * 0.35;
|
|
2913
|
+
else if (char.charCodeAt(0) <= 127) width += fontSize * 0.58;
|
|
2914
|
+
else width += fontSize;
|
|
2915
|
+
}
|
|
2916
|
+
return Math.max(fontSize * 2.2, width + fontSize * 0.8);
|
|
2917
|
+
}
|
|
2918
|
+
function shapeXml(id, name, x, y, w, h, preset, fill, line) {
|
|
2919
|
+
return `<p:sp>
|
|
2920
|
+
<p:nvSpPr><p:cNvPr id="${id}" name="${xmlAttr(name)}"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>
|
|
2921
|
+
<p:spPr><a:xfrm><a:off x="${x}" y="${y}"/><a:ext cx="${w}" cy="${h}"/></a:xfrm><a:prstGeom prst="${preset}"><a:avLst/></a:prstGeom>${fill}${line}</p:spPr>
|
|
2922
|
+
</p:sp>`;
|
|
2923
|
+
}
|
|
2924
|
+
function connectorXml(id, name, x1, y1, x2, y2, line) {
|
|
2925
|
+
const x = Math.min(x1, x2);
|
|
2926
|
+
const y = Math.min(y1, y2);
|
|
2927
|
+
const w = Math.max(1, Math.abs(x2 - x1));
|
|
2928
|
+
const h = Math.max(1, Math.abs(y2 - y1));
|
|
2929
|
+
const flipH = x2 < x1 ? ' flipH="1"' : "";
|
|
2930
|
+
const flipV = y2 < y1 ? ' flipV="1"' : "";
|
|
2931
|
+
return `<p:cxnSp>
|
|
2932
|
+
<p:nvCxnSpPr><p:cNvPr id="${id}" name="${xmlAttr(name)}"/><p:cNvCxnSpPr/><p:nvPr/></p:nvCxnSpPr>
|
|
2933
|
+
<p:spPr><a:xfrm${flipH}${flipV}><a:off x="${x}" y="${y}"/><a:ext cx="${w}" cy="${h}"/></a:xfrm><a:prstGeom prst="line"><a:avLst/></a:prstGeom>${lineXml(line)}</p:spPr>
|
|
2934
|
+
</p:cxnSp>`;
|
|
2935
|
+
}
|
|
2936
|
+
function freeformXml(id, name, points, fill, line) {
|
|
2937
|
+
const left = Math.min(...points.map((p) => p.x));
|
|
2938
|
+
const top = Math.min(...points.map((p) => p.y));
|
|
2939
|
+
const right = Math.max(...points.map((p) => p.x));
|
|
2940
|
+
const bottom = Math.max(...points.map((p) => p.y));
|
|
2941
|
+
const w = Math.max(1, right - left);
|
|
2942
|
+
const h = Math.max(1, bottom - top);
|
|
2943
|
+
const local = points.map((p) => ({ x: p.x - left, y: p.y - top }));
|
|
2944
|
+
const first = local[0];
|
|
2945
|
+
const rest = local.slice(1).map((p) => `<a:lnTo><a:pt x="${p.x}" y="${p.y}"/></a:lnTo>`).join("");
|
|
2946
|
+
return `<p:sp>
|
|
2947
|
+
<p:nvSpPr><p:cNvPr id="${id}" name="${xmlAttr(name)}"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>
|
|
2948
|
+
<p:spPr><a:xfrm><a:off x="${left}" y="${top}"/><a:ext cx="${w}" cy="${h}"/></a:xfrm><a:custGeom><a:avLst/><a:gdLst/><a:ahLst/><a:cxnLst/><a:rect l="l" t="t" r="r" b="b"/><a:pathLst><a:path w="${w}" h="${h}"><a:moveTo><a:pt x="${first.x}" y="${first.y}"/></a:moveTo>${rest}<a:close/></a:path></a:pathLst></a:custGeom>${fill}${line}</p:spPr>
|
|
2949
|
+
</p:sp>`;
|
|
2950
|
+
}
|
|
2951
|
+
function textBoxXml(id, x, y, w, h, text, fontPt, align, color) {
|
|
2952
|
+
const sz = Math.max(100, Math.round(fontPt * 100));
|
|
2953
|
+
return `<p:sp>
|
|
2954
|
+
<p:nvSpPr><p:cNvPr id="${id}" name="text"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr>
|
|
2955
|
+
<p:spPr><a:xfrm><a:off x="${x}" y="${y}"/><a:ext cx="${w}" cy="${h}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:noFill/><a:ln><a:noFill/></a:ln></p:spPr>
|
|
2956
|
+
<p:txBody><a:bodyPr wrap="none" anchor="ctr" lIns="0" tIns="0" rIns="0" bIns="0"><a:spAutoFit/></a:bodyPr><a:lstStyle/><a:p><a:pPr algn="${align}"/><a:r><a:rPr lang="ja-JP" sz="${sz}" dirty="0">${fillXml(color)}</a:rPr><a:t>${xmlText(text)}</a:t></a:r></a:p></p:txBody>
|
|
2957
|
+
</p:sp>`;
|
|
2958
|
+
}
|
|
2959
|
+
function fillXml(color) {
|
|
2960
|
+
return color === "none" ? "<a:noFill/>" : `<a:solidFill><a:srgbClr val="${xmlAttr(color)}"/></a:solidFill>`;
|
|
2961
|
+
}
|
|
2962
|
+
function lineXml(line) {
|
|
2963
|
+
if (!line) return "<a:ln><a:noFill/></a:ln>";
|
|
2964
|
+
const dash = line.dash ? '<a:prstDash val="dash"/>' : '<a:prstDash val="solid"/>';
|
|
2965
|
+
return `<a:ln w="${line.width}" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:srgbClr val="${xmlAttr(line.color)}"/></a:solidFill>${dash}</a:ln>`;
|
|
2966
|
+
}
|
|
2967
|
+
function xmlText(text) {
|
|
2968
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
2969
|
+
}
|
|
2970
|
+
function xmlAttr(text) {
|
|
2971
|
+
return xmlText(text).replace(/"/g, """);
|
|
2972
|
+
}
|
|
2973
|
+
function createStoredZip(entries) {
|
|
2974
|
+
const encoder = new TextEncoder();
|
|
2975
|
+
const files = entries.map(([name, data]) => {
|
|
2976
|
+
const bytes = typeof data === "string" ? encoder.encode(data) : data;
|
|
2977
|
+
return { name, nameBytes: encoder.encode(name), bytes, crc: crc32(bytes) };
|
|
2978
|
+
});
|
|
2979
|
+
const chunks = [];
|
|
2980
|
+
const central = [];
|
|
2981
|
+
let offset = 0;
|
|
2982
|
+
for (const file of files) {
|
|
2983
|
+
const local = zipLocalHeader(file.nameBytes, file.bytes, file.crc);
|
|
2984
|
+
chunks.push(local, file.nameBytes, file.bytes);
|
|
2985
|
+
central.push(zipCentralHeader(file.nameBytes, file.bytes, file.crc, offset), file.nameBytes);
|
|
2986
|
+
offset += local.length + file.nameBytes.length + file.bytes.length;
|
|
2987
|
+
}
|
|
2988
|
+
const centralOffset = offset;
|
|
2989
|
+
const centralSize = central.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
2990
|
+
return concatBytes([...chunks, ...central, zipEndRecord(files.length, centralSize, centralOffset)]);
|
|
2991
|
+
}
|
|
2992
|
+
function zipLocalHeader(name, data, crc) {
|
|
2993
|
+
const out = new Uint8Array(30);
|
|
2994
|
+
const view = new DataView(out.buffer);
|
|
2995
|
+
view.setUint32(0, 67324752, true);
|
|
2996
|
+
view.setUint16(4, 20, true);
|
|
2997
|
+
view.setUint16(6, 0, true);
|
|
2998
|
+
view.setUint16(8, 0, true);
|
|
2999
|
+
view.setUint16(10, 0, true);
|
|
3000
|
+
view.setUint16(12, 0, true);
|
|
3001
|
+
view.setUint32(14, crc, true);
|
|
3002
|
+
view.setUint32(18, data.length, true);
|
|
3003
|
+
view.setUint32(22, data.length, true);
|
|
3004
|
+
view.setUint16(26, name.length, true);
|
|
3005
|
+
view.setUint16(28, 0, true);
|
|
3006
|
+
return out;
|
|
3007
|
+
}
|
|
3008
|
+
function zipCentralHeader(name, data, crc, offset) {
|
|
3009
|
+
const out = new Uint8Array(46);
|
|
3010
|
+
const view = new DataView(out.buffer);
|
|
3011
|
+
view.setUint32(0, 33639248, true);
|
|
3012
|
+
view.setUint16(4, 20, true);
|
|
3013
|
+
view.setUint16(6, 20, true);
|
|
3014
|
+
view.setUint16(8, 0, true);
|
|
3015
|
+
view.setUint16(10, 0, true);
|
|
3016
|
+
view.setUint16(12, 0, true);
|
|
3017
|
+
view.setUint16(14, 0, true);
|
|
3018
|
+
view.setUint32(16, crc, true);
|
|
3019
|
+
view.setUint32(20, data.length, true);
|
|
3020
|
+
view.setUint32(24, data.length, true);
|
|
3021
|
+
view.setUint16(28, name.length, true);
|
|
3022
|
+
view.setUint16(30, 0, true);
|
|
3023
|
+
view.setUint16(32, 0, true);
|
|
3024
|
+
view.setUint16(34, 0, true);
|
|
3025
|
+
view.setUint16(36, 0, true);
|
|
3026
|
+
view.setUint32(38, 0, true);
|
|
3027
|
+
view.setUint32(42, offset, true);
|
|
3028
|
+
return out;
|
|
3029
|
+
}
|
|
3030
|
+
function zipEndRecord(entries, centralSize, centralOffset) {
|
|
3031
|
+
const out = new Uint8Array(22);
|
|
3032
|
+
const view = new DataView(out.buffer);
|
|
3033
|
+
view.setUint32(0, 101010256, true);
|
|
3034
|
+
view.setUint16(4, 0, true);
|
|
3035
|
+
view.setUint16(6, 0, true);
|
|
3036
|
+
view.setUint16(8, entries, true);
|
|
3037
|
+
view.setUint16(10, entries, true);
|
|
3038
|
+
view.setUint32(12, centralSize, true);
|
|
3039
|
+
view.setUint32(16, centralOffset, true);
|
|
3040
|
+
view.setUint16(20, 0, true);
|
|
3041
|
+
return out;
|
|
3042
|
+
}
|
|
3043
|
+
function concatBytes(chunks) {
|
|
3044
|
+
const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
3045
|
+
const out = new Uint8Array(total);
|
|
3046
|
+
let offset = 0;
|
|
3047
|
+
for (const chunk of chunks) {
|
|
3048
|
+
out.set(chunk, offset);
|
|
3049
|
+
offset += chunk.length;
|
|
3050
|
+
}
|
|
3051
|
+
return out;
|
|
3052
|
+
}
|
|
3053
|
+
function crc32(bytes) {
|
|
3054
|
+
let crc = 4294967295;
|
|
3055
|
+
for (const byte of bytes) {
|
|
3056
|
+
crc ^= byte;
|
|
3057
|
+
for (let i = 0; i < 8; i++) {
|
|
3058
|
+
crc = crc >>> 1 ^ 3988292384 & -(crc & 1);
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
return (crc ^ 4294967295) >>> 0;
|
|
3062
|
+
}
|
|
3063
|
+
function contentTypesXml() {
|
|
3064
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
3065
|
+
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
|
3066
|
+
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
|
3067
|
+
<Default Extension="xml" ContentType="application/xml"/>
|
|
3068
|
+
<Default Extension="png" ContentType="image/png"/>
|
|
3069
|
+
<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>
|
|
3070
|
+
<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>
|
|
3071
|
+
<Override PartName="/ppt/presentation.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"/>
|
|
3072
|
+
<Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>
|
|
3073
|
+
<Override PartName="/ppt/slideMasters/slideMaster1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"/>
|
|
3074
|
+
<Override PartName="/ppt/slideLayouts/slideLayout1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"/>
|
|
3075
|
+
<Override PartName="/ppt/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>
|
|
3076
|
+
</Types>`;
|
|
3077
|
+
}
|
|
3078
|
+
function contentTypesXmlEditable() {
|
|
3079
|
+
return contentTypesXml().replace('<Default Extension="png" ContentType="image/png"/>\n', "");
|
|
3080
|
+
}
|
|
3081
|
+
function rootRelsXml() {
|
|
3082
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
3083
|
+
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
3084
|
+
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="ppt/presentation.xml"/>
|
|
3085
|
+
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>
|
|
3086
|
+
<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>
|
|
3087
|
+
</Relationships>`;
|
|
3088
|
+
}
|
|
3089
|
+
function appPropsXml() {
|
|
3090
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
3091
|
+
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
|
|
3092
|
+
<Application>pdgkit</Application>
|
|
3093
|
+
<PresentationFormat>Widescreen</PresentationFormat>
|
|
3094
|
+
<Slides>1</Slides>
|
|
3095
|
+
<Notes>0</Notes>
|
|
3096
|
+
<HiddenSlides>0</HiddenSlides>
|
|
3097
|
+
</Properties>`;
|
|
3098
|
+
}
|
|
3099
|
+
function corePropsXml(iso) {
|
|
3100
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
3101
|
+
<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
|
3102
|
+
<dc:title>pdgkit figure</dc:title>
|
|
3103
|
+
<dc:creator>pdgkit</dc:creator>
|
|
3104
|
+
<cp:lastModifiedBy>pdgkit</cp:lastModifiedBy>
|
|
3105
|
+
<dcterms:created xsi:type="dcterms:W3CDTF">${iso}</dcterms:created>
|
|
3106
|
+
<dcterms:modified xsi:type="dcterms:W3CDTF">${iso}</dcterms:modified>
|
|
3107
|
+
</cp:coreProperties>`;
|
|
3108
|
+
}
|
|
3109
|
+
function presentationXml() {
|
|
3110
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
3111
|
+
<p:presentation xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
|
|
3112
|
+
<p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId1"/></p:sldMasterIdLst>
|
|
3113
|
+
<p:sldIdLst><p:sldId id="256" r:id="rId2"/></p:sldIdLst>
|
|
3114
|
+
<p:sldSz cx="${PPTX_SLIDE_W}" cy="${PPTX_SLIDE_H}" type="screen16x9"/>
|
|
3115
|
+
<p:notesSz cx="6858000" cy="9144000"/>
|
|
3116
|
+
<p:defaultTextStyle/>
|
|
3117
|
+
</p:presentation>`;
|
|
3118
|
+
}
|
|
3119
|
+
function presentationRelsXml() {
|
|
3120
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
3121
|
+
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
3122
|
+
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMasters/slideMaster1.xml"/>
|
|
3123
|
+
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/>
|
|
3124
|
+
</Relationships>`;
|
|
3125
|
+
}
|
|
3126
|
+
function slideXml(fit) {
|
|
3127
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
3128
|
+
<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
|
|
3129
|
+
<p:cSld name="pdgkit">
|
|
3130
|
+
<p:bg><p:bgPr><a:solidFill><a:srgbClr val="FFFFFF"/></a:solidFill><a:effectLst/></p:bgPr></p:bg>
|
|
3131
|
+
<p:spTree>
|
|
3132
|
+
<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
|
|
3133
|
+
<p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${PPTX_SLIDE_W}" cy="${PPTX_SLIDE_H}"/><a:chOff x="0" y="0"/><a:chExt cx="${PPTX_SLIDE_W}" cy="${PPTX_SLIDE_H}"/></a:xfrm></p:grpSpPr>
|
|
3134
|
+
<p:pic>
|
|
3135
|
+
<p:nvPicPr><p:cNvPr id="2" name="figure.png"/><p:cNvPicPr><a:picLocks noChangeAspect="1"/></p:cNvPicPr><p:nvPr/></p:nvPicPr>
|
|
3136
|
+
<p:blipFill><a:blip r:embed="rId1"/><a:stretch><a:fillRect/></a:stretch></p:blipFill>
|
|
3137
|
+
<p:spPr><a:xfrm><a:off x="${Math.round(fit.x)}" y="${Math.round(fit.y)}"/><a:ext cx="${Math.round(fit.width)}" cy="${Math.round(fit.height)}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr>
|
|
3138
|
+
</p:pic>
|
|
3139
|
+
</p:spTree>
|
|
3140
|
+
</p:cSld>
|
|
3141
|
+
<p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr>
|
|
3142
|
+
</p:sld>`;
|
|
3143
|
+
}
|
|
3144
|
+
function slideRelsXml() {
|
|
3145
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
3146
|
+
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
3147
|
+
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/image1.png"/>
|
|
3148
|
+
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>
|
|
3149
|
+
</Relationships>`;
|
|
3150
|
+
}
|
|
3151
|
+
function editableSlideXml(shapes) {
|
|
3152
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
3153
|
+
<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
|
|
3154
|
+
<p:cSld name="pdgkit">
|
|
3155
|
+
<p:bg><p:bgPr><a:solidFill><a:srgbClr val="FFFFFF"/></a:solidFill><a:effectLst/></p:bgPr></p:bg>
|
|
3156
|
+
<p:spTree>
|
|
3157
|
+
<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr>
|
|
3158
|
+
<p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${PPTX_SLIDE_W}" cy="${PPTX_SLIDE_H}"/><a:chOff x="0" y="0"/><a:chExt cx="${PPTX_SLIDE_W}" cy="${PPTX_SLIDE_H}"/></a:xfrm></p:grpSpPr>
|
|
3159
|
+
${shapes}
|
|
3160
|
+
</p:spTree>
|
|
3161
|
+
</p:cSld>
|
|
3162
|
+
<p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr>
|
|
3163
|
+
</p:sld>`;
|
|
3164
|
+
}
|
|
3165
|
+
function editableSlideRelsXml() {
|
|
3166
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
3167
|
+
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
3168
|
+
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>
|
|
3169
|
+
</Relationships>`;
|
|
3170
|
+
}
|
|
3171
|
+
function slideMasterXml() {
|
|
3172
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
3173
|
+
<p:sldMaster xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
|
|
3174
|
+
<p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${PPTX_SLIDE_W}" cy="${PPTX_SLIDE_H}"/><a:chOff x="0" y="0"/><a:chExt cx="${PPTX_SLIDE_W}" cy="${PPTX_SLIDE_H}"/></a:xfrm></p:grpSpPr></p:spTree></p:cSld>
|
|
3175
|
+
<p:clrMap bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/>
|
|
3176
|
+
<p:sldLayoutIdLst><p:sldLayoutId id="2147483649" r:id="rId1"/></p:sldLayoutIdLst>
|
|
3177
|
+
<p:txStyles><p:titleStyle/><p:bodyStyle/><p:otherStyle/></p:txStyles>
|
|
3178
|
+
</p:sldMaster>`;
|
|
3179
|
+
}
|
|
3180
|
+
function slideMasterRelsXml() {
|
|
3181
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
3182
|
+
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
3183
|
+
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>
|
|
3184
|
+
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="../theme/theme1.xml"/>
|
|
3185
|
+
</Relationships>`;
|
|
3186
|
+
}
|
|
3187
|
+
function slideLayoutXml() {
|
|
3188
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
3189
|
+
<p:sldLayout xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" type="blank" preserve="1">
|
|
3190
|
+
<p:cSld name="Blank"><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${PPTX_SLIDE_W}" cy="${PPTX_SLIDE_H}"/><a:chOff x="0" y="0"/><a:chExt cx="${PPTX_SLIDE_W}" cy="${PPTX_SLIDE_H}"/></a:xfrm></p:grpSpPr></p:spTree></p:cSld>
|
|
3191
|
+
<p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr>
|
|
3192
|
+
</p:sldLayout>`;
|
|
3193
|
+
}
|
|
3194
|
+
function slideLayoutRelsXml() {
|
|
3195
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
3196
|
+
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
3197
|
+
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="../slideMasters/slideMaster1.xml"/>
|
|
3198
|
+
</Relationships>`;
|
|
3199
|
+
}
|
|
3200
|
+
function themeXml() {
|
|
3201
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
3202
|
+
<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="pdgkit">
|
|
3203
|
+
<a:themeElements>
|
|
3204
|
+
<a:clrScheme name="pdgkit"><a:dk1><a:srgbClr val="000000"/></a:dk1><a:lt1><a:srgbClr val="FFFFFF"/></a:lt1><a:dk2><a:srgbClr val="000000"/></a:dk2><a:lt2><a:srgbClr val="FFFFFF"/></a:lt2><a:accent1><a:srgbClr val="0B5FFF"/></a:accent1><a:accent2><a:srgbClr val="666666"/></a:accent2><a:accent3><a:srgbClr val="999999"/></a:accent3><a:accent4><a:srgbClr val="CCCCCC"/></a:accent4><a:accent5><a:srgbClr val="333333"/></a:accent5><a:accent6><a:srgbClr val="111111"/></a:accent6><a:hlink><a:srgbClr val="0B5FFF"/></a:hlink><a:folHlink><a:srgbClr val="0B5FFF"/></a:folHlink></a:clrScheme>
|
|
3205
|
+
<a:fontScheme name="pdgkit"><a:majorFont><a:latin typeface="Arial"/><a:ea typeface="Yu Gothic"/><a:cs typeface="Arial"/></a:majorFont><a:minorFont><a:latin typeface="Arial"/><a:ea typeface="Yu Gothic"/><a:cs typeface="Arial"/></a:minorFont></a:fontScheme>
|
|
3206
|
+
<a:fmtScheme name="pdgkit"><a:fillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:fillStyleLst><a:lnStyleLst><a:ln w="9525" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="solid"/></a:ln></a:lnStyleLst><a:effectStyleLst><a:effectStyle><a:effectLst/></a:effectStyle></a:effectStyleLst><a:bgFillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:bgFillStyleLst></a:fmtScheme>
|
|
3207
|
+
</a:themeElements>
|
|
3208
|
+
<a:objectDefaults/>
|
|
3209
|
+
<a:extraClrSchemeLst/>
|
|
3210
|
+
</a:theme>`;
|
|
3211
|
+
}
|
|
3212
|
+
|
|
3213
|
+
// src/node/pdf.ts
|
|
3214
|
+
async function renderToPdf(source, opts = {}) {
|
|
3215
|
+
const { lang = "ja", bleed = 3, vector = true, scale = 8 } = opts;
|
|
3216
|
+
const { svg, viewBox } = renderToSvg(source, { lang, crop: true, bleed });
|
|
3217
|
+
if (vector) {
|
|
3218
|
+
try {
|
|
3219
|
+
return await renderVectorPdf(svg, viewBox);
|
|
3220
|
+
} catch {
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
return await renderRasterPdf(source, viewBox, { lang, scale, bleed });
|
|
3224
|
+
}
|
|
3225
|
+
async function renderVectorPdf(svg, viewBox) {
|
|
3226
|
+
const { JSDOM } = await import("jsdom");
|
|
3227
|
+
const { jsPDF } = await import("jspdf");
|
|
3228
|
+
const { svg2pdf } = await import("svg2pdf.js");
|
|
3229
|
+
const dom = new JSDOM(svg, { contentType: "image/svg+xml" });
|
|
3230
|
+
patchSvgGeometry(dom.window);
|
|
3231
|
+
const svgEl = dom.window.document.documentElement;
|
|
3232
|
+
svgEl.setAttribute("font-family", FONT_FAMILY_NAME);
|
|
3233
|
+
for (const t of Array.from(dom.window.document.getElementsByTagName("text"))) {
|
|
3234
|
+
t.setAttribute("font-family", FONT_FAMILY_NAME);
|
|
3235
|
+
}
|
|
3236
|
+
const orientation = viewBox.width > viewBox.height ? "l" : "p";
|
|
3237
|
+
const pdf = new jsPDF({ unit: "mm", format: "a4", orientation });
|
|
3238
|
+
try {
|
|
3239
|
+
pdf.addFileToVFS(`${FONT_FAMILY_NAME}.ttf`, loadFontBase64());
|
|
3240
|
+
pdf.addFont(`${FONT_FAMILY_NAME}.ttf`, FONT_FAMILY_NAME, "normal");
|
|
3241
|
+
} catch {
|
|
3242
|
+
}
|
|
3243
|
+
const pageW = pdf.internal.pageSize.getWidth();
|
|
3244
|
+
const pageH = pdf.internal.pageSize.getHeight();
|
|
3245
|
+
const fit = fitRectIntoPage(viewBox.width, viewBox.height, pageW, pageH, 10);
|
|
3246
|
+
await svg2pdf(svgEl, pdf, {
|
|
3247
|
+
x: fit.x,
|
|
3248
|
+
y: fit.y,
|
|
3249
|
+
width: fit.width,
|
|
3250
|
+
height: fit.height
|
|
3251
|
+
});
|
|
3252
|
+
return new Uint8Array(pdf.output("arraybuffer"));
|
|
3253
|
+
}
|
|
3254
|
+
async function renderRasterPdf(source, viewBox, opts) {
|
|
3255
|
+
const { jsPDF } = await import("jspdf");
|
|
3256
|
+
const png = await renderToPng(source, opts);
|
|
3257
|
+
const orientation = viewBox.width > viewBox.height ? "l" : "p";
|
|
3258
|
+
const pdf = new jsPDF({ unit: "mm", format: "a4", orientation });
|
|
3259
|
+
const pageW = pdf.internal.pageSize.getWidth();
|
|
3260
|
+
const pageH = pdf.internal.pageSize.getHeight();
|
|
3261
|
+
const fit = fitRectIntoPage(viewBox.width, viewBox.height, pageW, pageH, 10);
|
|
3262
|
+
const dataUrl = "data:image/png;base64," + Buffer.from(png).toString("base64");
|
|
3263
|
+
pdf.addImage(dataUrl, "PNG", fit.x, fit.y, fit.width, fit.height);
|
|
3264
|
+
return new Uint8Array(pdf.output("arraybuffer"));
|
|
3265
|
+
}
|
|
3266
|
+
function patchSvgGeometry(win) {
|
|
3267
|
+
const proto = win.SVGElement?.prototype;
|
|
3268
|
+
if (!proto) return;
|
|
3269
|
+
const numOf = (el2, name, d = 0) => {
|
|
3270
|
+
const v = Number.parseFloat(el2.getAttribute?.(name) ?? "");
|
|
3271
|
+
return Number.isFinite(v) ? v : d;
|
|
3272
|
+
};
|
|
3273
|
+
proto.getBBox = function() {
|
|
3274
|
+
const tag = (this.tagName ?? "").toLowerCase();
|
|
3275
|
+
if (tag === "rect") {
|
|
3276
|
+
return { x: numOf(this, "x"), y: numOf(this, "y"), width: numOf(this, "width"), height: numOf(this, "height") };
|
|
3277
|
+
}
|
|
3278
|
+
if (tag === "circle") {
|
|
3279
|
+
const r = numOf(this, "r");
|
|
3280
|
+
return { x: numOf(this, "cx") - r, y: numOf(this, "cy") - r, width: 2 * r, height: 2 * r };
|
|
3281
|
+
}
|
|
3282
|
+
if (tag === "text") {
|
|
3283
|
+
const fs2 = numOf(this, "font-size", 2.8);
|
|
3284
|
+
return { x: numOf(this, "x"), y: numOf(this, "y") - fs2, width: estimateTextWidth(this.textContent ?? "", fs2), height: fs2 * 1.2 };
|
|
3285
|
+
}
|
|
3286
|
+
return { x: 0, y: 0, width: 0, height: 0 };
|
|
3287
|
+
};
|
|
3288
|
+
proto.getComputedTextLength = function() {
|
|
3289
|
+
const fs2 = numOf(this, "font-size", 2.8);
|
|
3290
|
+
return estimateTextWidth(this.textContent ?? "", fs2);
|
|
3291
|
+
};
|
|
3292
|
+
for (const m of ["getCTM", "getScreenCTM"]) {
|
|
3293
|
+
if (typeof proto[m] !== "function") {
|
|
3294
|
+
proto[m] = function() {
|
|
3295
|
+
return null;
|
|
3296
|
+
};
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
// src/node/pptx.ts
|
|
3302
|
+
async function renderToPptx(source, opts = {}) {
|
|
3303
|
+
const { lang = "ja", editable = false, scale = 8, bleed = 3 } = opts;
|
|
3304
|
+
if (editable) {
|
|
3305
|
+
const model = buildSvgModel(source, { lang, crop: true, bleed });
|
|
3306
|
+
return buildEditablePptxPackage(model.el);
|
|
3307
|
+
}
|
|
3308
|
+
const { viewBox } = buildSvgModel(source, { lang, crop: true, bleed });
|
|
3309
|
+
const dims = rasterDimensions(viewBox, scale);
|
|
3310
|
+
const png = await renderToPng(source, { lang, scale, bleed });
|
|
3311
|
+
return buildPptxPackage(png, dims.width, dims.height);
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3314
|
+
// src/node/index.ts
|
|
3315
|
+
var VERSION = "0.1.0";
|
|
3316
|
+
function loadAuthoringGuide() {
|
|
3317
|
+
return readFileSync2(resolvePackageFile("docs", "ai-authoring-guide.md"), "utf8");
|
|
3318
|
+
}
|
|
3319
|
+
|
|
3320
|
+
// bin/pdgkit.ts
|
|
3321
|
+
function parseArgs(args) {
|
|
3322
|
+
const positionals = [];
|
|
3323
|
+
const opts = /* @__PURE__ */ new Map();
|
|
3324
|
+
for (let i = 0; i < args.length; i++) {
|
|
3325
|
+
const a = args[i];
|
|
3326
|
+
if (a === "-o" || a === "--out") {
|
|
3327
|
+
opts.set("out", args[++i] ?? "");
|
|
3328
|
+
} else if (a.startsWith("--no-")) {
|
|
3329
|
+
opts.set(a.slice(5), "false");
|
|
3330
|
+
} else if (a.startsWith("--")) {
|
|
3331
|
+
const eq = a.indexOf("=");
|
|
3332
|
+
if (eq !== -1) {
|
|
3333
|
+
opts.set(a.slice(2, eq), a.slice(eq + 1));
|
|
3334
|
+
} else {
|
|
3335
|
+
const next = args[i + 1];
|
|
3336
|
+
if (next !== void 0 && !next.startsWith("-")) {
|
|
3337
|
+
opts.set(a.slice(2), next);
|
|
3338
|
+
i++;
|
|
3339
|
+
} else {
|
|
3340
|
+
opts.set(a.slice(2), true);
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
} else {
|
|
3344
|
+
positionals.push(a);
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
return { positionals, opts };
|
|
3348
|
+
}
|
|
3349
|
+
function fail(message, code = 1) {
|
|
3350
|
+
process.stderr.write(`pdgkit: ${message}
|
|
3351
|
+
`);
|
|
3352
|
+
process.exit(code);
|
|
3353
|
+
}
|
|
3354
|
+
function readStdin() {
|
|
3355
|
+
try {
|
|
3356
|
+
return fs.readFileSync(0, "utf8");
|
|
3357
|
+
} catch {
|
|
3358
|
+
return "";
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
function resolveSource(positional, opts) {
|
|
3362
|
+
const sample = opts.get("sample");
|
|
3363
|
+
if (typeof sample === "string") {
|
|
3364
|
+
if (!SAMPLE_ORDER.includes(sample)) {
|
|
3365
|
+
fail(`unknown sample "${sample}". Run "pdgkit samples" to list them.`, 2);
|
|
3366
|
+
}
|
|
3367
|
+
return SAMPLES[sample].source;
|
|
3368
|
+
}
|
|
3369
|
+
if (positional && positional !== "-") {
|
|
3370
|
+
try {
|
|
3371
|
+
return fs.readFileSync(positional, "utf8");
|
|
3372
|
+
} catch {
|
|
3373
|
+
fail(`cannot read file: ${positional}`, 2);
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
return readStdin();
|
|
3377
|
+
}
|
|
3378
|
+
function getLang(opts) {
|
|
3379
|
+
const lang = opts.get("lang");
|
|
3380
|
+
if (lang === "ja" || lang === "en" || lang === "both") return lang;
|
|
3381
|
+
if (lang === void 0 || lang === true) return "ja";
|
|
3382
|
+
fail(`invalid --lang "${String(lang)}" (expected ja, en, or both)`, 2);
|
|
3383
|
+
}
|
|
3384
|
+
function writeOut(opts, text) {
|
|
3385
|
+
const out = opts.get("out");
|
|
3386
|
+
if (typeof out === "string" && out) {
|
|
3387
|
+
fs.writeFileSync(out, text, "utf8");
|
|
3388
|
+
process.stderr.write(`pdgkit: wrote ${out}
|
|
3389
|
+
`);
|
|
3390
|
+
} else {
|
|
3391
|
+
process.stdout.write(text.endsWith("\n") ? text : text + "\n");
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
function writeBinary(opts, bytes) {
|
|
3395
|
+
const out = opts.get("out");
|
|
3396
|
+
if (typeof out !== "string" || !out) {
|
|
3397
|
+
fail("binary output requires -o <file>", 2);
|
|
3398
|
+
}
|
|
3399
|
+
fs.writeFileSync(out, bytes);
|
|
3400
|
+
process.stderr.write(`pdgkit: wrote ${out} (${bytes.length} bytes)
|
|
3401
|
+
`);
|
|
3402
|
+
}
|
|
3403
|
+
function formatDiagnostic(d) {
|
|
3404
|
+
const tag = d.severity === "error" ? "ERROR" : d.severity === "warning" ? "WARN " : "INFO ";
|
|
3405
|
+
return ` ${tag} line ${d.line}:${d.col} ${d.message}`;
|
|
3406
|
+
}
|
|
3407
|
+
async function cmdRender(args) {
|
|
3408
|
+
const source = resolveSource(args.positionals[0], args.opts);
|
|
3409
|
+
const lang = getLang(args.opts);
|
|
3410
|
+
const to = args.opts.get("to") ?? "svg";
|
|
3411
|
+
const crop = args.opts.get("crop") !== "false";
|
|
3412
|
+
switch (to) {
|
|
3413
|
+
case "svg": {
|
|
3414
|
+
const { svg } = renderToSvg(source, { lang, crop });
|
|
3415
|
+
writeOut(args.opts, svg);
|
|
3416
|
+
return;
|
|
3417
|
+
}
|
|
3418
|
+
case "png":
|
|
3419
|
+
writeBinary(args.opts, await renderToPng(source, { lang }));
|
|
3420
|
+
return;
|
|
3421
|
+
case "jpeg":
|
|
3422
|
+
case "jpg":
|
|
3423
|
+
writeBinary(args.opts, await renderToJpeg(source, { lang }));
|
|
3424
|
+
return;
|
|
3425
|
+
case "pdf":
|
|
3426
|
+
writeBinary(args.opts, await renderToPdf(source, { lang }));
|
|
3427
|
+
return;
|
|
3428
|
+
case "pptx":
|
|
3429
|
+
writeBinary(args.opts, await renderToPptx(source, { lang, editable: args.opts.get("editable") === true }));
|
|
3430
|
+
return;
|
|
3431
|
+
default:
|
|
3432
|
+
fail(`unknown --to "${to}" (expected svg, png, jpeg, pdf, or pptx)`, 2);
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
function cmdValidate(args) {
|
|
3436
|
+
const source = resolveSource(args.positionals[0], args.opts);
|
|
3437
|
+
const result = validate(source);
|
|
3438
|
+
const lines = [];
|
|
3439
|
+
lines.push(`kind: ${result.kind}${result.declaredKind ? ` (declared: ${result.declaredKind})` : ""}`);
|
|
3440
|
+
if (result.diagnostics.length === 0) {
|
|
3441
|
+
lines.push("no diagnostics");
|
|
3442
|
+
} else {
|
|
3443
|
+
for (const d of result.diagnostics) lines.push(formatDiagnostic(d));
|
|
3444
|
+
}
|
|
3445
|
+
lines.push(
|
|
3446
|
+
`summary: ${result.counts.errors} error(s), ${result.counts.warnings} warning(s), ${result.counts.infos} info`
|
|
3447
|
+
);
|
|
3448
|
+
process.stderr.write(lines.join("\n") + "\n");
|
|
3449
|
+
process.exit(result.ok ? 0 : 1);
|
|
3450
|
+
}
|
|
3451
|
+
function cmdRefs(args) {
|
|
3452
|
+
const source = resolveSource(args.positionals[0], args.opts);
|
|
3453
|
+
const doc = parse(source);
|
|
3454
|
+
const format = args.opts.get("format") ?? "md";
|
|
3455
|
+
if (format === "md" || format === "markdown") {
|
|
3456
|
+
writeOut(args.opts, refsToMarkdown(doc));
|
|
3457
|
+
} else if (format === "csv") {
|
|
3458
|
+
writeOut(args.opts, refsToCsv(doc));
|
|
3459
|
+
} else {
|
|
3460
|
+
fail(`unknown --format "${format}" (expected md or csv)`, 2);
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
function cmdGuide() {
|
|
3464
|
+
process.stdout.write(loadAuthoringGuide().replace(/\n*$/, "\n"));
|
|
3465
|
+
}
|
|
3466
|
+
function cmdSamples() {
|
|
3467
|
+
const rows = SAMPLE_ORDER.map((id) => ` ${id.padEnd(16)} ${SAMPLES[id].label} \u2014 ${SAMPLES[id].hint}`);
|
|
3468
|
+
process.stdout.write(`built-in samples:
|
|
3469
|
+
${rows.join("\n")}
|
|
3470
|
+
`);
|
|
3471
|
+
}
|
|
3472
|
+
var HELP = `pdgkit ${VERSION} \u2014 headless engine for the PatentDSL (.pdg) language
|
|
3473
|
+
|
|
3474
|
+
usage:
|
|
3475
|
+
pdgkit render <input> [--to svg|png|jpeg|pdf|pptx] [--lang ja|en|both] [-o file] [--no-crop]
|
|
3476
|
+
pdgkit validate <input> [--lang ja|en|both]
|
|
3477
|
+
pdgkit refs <input> [--format md|csv] [-o file]
|
|
3478
|
+
pdgkit guide
|
|
3479
|
+
pdgkit samples
|
|
3480
|
+
pdgkit version
|
|
3481
|
+
|
|
3482
|
+
<input> is a file path, "-" for stdin, or omitted with --sample <id>.
|
|
3483
|
+
"pdgkit guide" prints the .pdg authoring guide, so an AI can read it instead of
|
|
3484
|
+
having it pasted in.
|
|
3485
|
+
|
|
3486
|
+
examples:
|
|
3487
|
+
pdgkit render fig1.pdg -o fig1.svg
|
|
3488
|
+
pdgkit render --sample block --lang both -o block.svg
|
|
3489
|
+
cat fig1.pdg | pdgkit validate -
|
|
3490
|
+
pdgkit refs fig1.pdg --format csv -o signs.csv
|
|
3491
|
+
pdgkit render fig1.pdg --to pdf -o fig1.pdf
|
|
3492
|
+
pdgkit render fig1.pdg --to pptx --editable -o fig1.pptx
|
|
3493
|
+
|
|
3494
|
+
formats: svg, png, jpeg, pdf, pptx (add --editable for editable PowerPoint shapes).
|
|
3495
|
+
an MCP server is also available: run \`pdgkit-mcp\`.`;
|
|
3496
|
+
async function main() {
|
|
3497
|
+
const argv = process.argv.slice(2);
|
|
3498
|
+
const cmd = argv[0];
|
|
3499
|
+
const rest = parseArgs(argv.slice(1));
|
|
3500
|
+
switch (cmd) {
|
|
3501
|
+
case "render":
|
|
3502
|
+
await cmdRender(rest);
|
|
3503
|
+
break;
|
|
3504
|
+
case "validate":
|
|
3505
|
+
cmdValidate(rest);
|
|
3506
|
+
break;
|
|
3507
|
+
case "refs":
|
|
3508
|
+
cmdRefs(rest);
|
|
3509
|
+
break;
|
|
3510
|
+
case "samples":
|
|
3511
|
+
cmdSamples();
|
|
3512
|
+
break;
|
|
3513
|
+
case "guide":
|
|
3514
|
+
cmdGuide();
|
|
3515
|
+
break;
|
|
3516
|
+
case "version":
|
|
3517
|
+
case "--version":
|
|
3518
|
+
case "-v":
|
|
3519
|
+
process.stdout.write(`${VERSION}
|
|
3520
|
+
`);
|
|
3521
|
+
break;
|
|
3522
|
+
case void 0:
|
|
3523
|
+
case "help":
|
|
3524
|
+
case "--help":
|
|
3525
|
+
case "-h":
|
|
3526
|
+
process.stdout.write(HELP + "\n");
|
|
3527
|
+
break;
|
|
3528
|
+
default:
|
|
3529
|
+
fail(`unknown command "${cmd}". Run "pdgkit help".`, 2);
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
3532
|
+
main().catch((err) => {
|
|
3533
|
+
fail(err instanceof Error ? err.message : String(err), 1);
|
|
3534
|
+
});
|