@theodo-group/epure 0.1.0 → 0.2.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/README.md +20 -0
- package/dist-lib/Canvas-BcrF3N4l.d.ts +96 -0
- package/dist-lib/chunk-7YSWFGM7.js +1125 -0
- package/dist-lib/chunk-KBK6AYWD.js +70 -0
- package/dist-lib/icons.d.ts +37 -0
- package/dist-lib/icons.js +18 -0
- package/dist-lib/libavoid.wasm +0 -0
- package/dist-lib/react.d.ts +95 -0
- package/dist-lib/react.js +50 -0
- package/dist-lib/render.d.ts +100 -0
- package/dist-lib/render.js +2717 -0
- package/dist-server/epure.mjs +1 -1
- package/package.json +27 -4
|
@@ -0,0 +1,2717 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Area,
|
|
3
|
+
AreaLabel,
|
|
4
|
+
Edge,
|
|
5
|
+
EdgeDefs,
|
|
6
|
+
Node,
|
|
7
|
+
STROKE_WIDTH,
|
|
8
|
+
labelPillSize
|
|
9
|
+
} from "./chunk-7YSWFGM7.js";
|
|
10
|
+
import {
|
|
11
|
+
iconById
|
|
12
|
+
} from "./chunk-KBK6AYWD.js";
|
|
13
|
+
|
|
14
|
+
// lib/render.ts
|
|
15
|
+
import { existsSync } from "fs";
|
|
16
|
+
import { dirname, join as join2 } from "path";
|
|
17
|
+
import { fileURLToPath } from "url";
|
|
18
|
+
|
|
19
|
+
// src/layout/elk.ts
|
|
20
|
+
import {
|
|
21
|
+
init,
|
|
22
|
+
routeEdges
|
|
23
|
+
} from "@mr_mint/elkjs-libavoid";
|
|
24
|
+
|
|
25
|
+
// src/layout/areaTree.ts
|
|
26
|
+
var buildAreaTree = (areas) => {
|
|
27
|
+
const byId = new Map(areas.map((a) => [a.id, a]));
|
|
28
|
+
const parentsOf = /* @__PURE__ */ new Map();
|
|
29
|
+
for (const a of areas) {
|
|
30
|
+
for (const mid of a.members) {
|
|
31
|
+
if (!byId.has(mid) || mid === a.id) continue;
|
|
32
|
+
const arr = parentsOf.get(mid) ?? [];
|
|
33
|
+
arr.push(a.id);
|
|
34
|
+
parentsOf.set(mid, arr);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const depthOf = /* @__PURE__ */ new Map();
|
|
38
|
+
const depth = (id, stack) => {
|
|
39
|
+
const hit = depthOf.get(id);
|
|
40
|
+
if (hit !== void 0) return hit;
|
|
41
|
+
if (stack.has(id)) return 0;
|
|
42
|
+
stack.add(id);
|
|
43
|
+
let d = 0;
|
|
44
|
+
for (const pid of parentsOf.get(id) ?? []) {
|
|
45
|
+
d = Math.max(d, depth(pid, stack) + 1);
|
|
46
|
+
}
|
|
47
|
+
stack.delete(id);
|
|
48
|
+
depthOf.set(id, d);
|
|
49
|
+
return d;
|
|
50
|
+
};
|
|
51
|
+
for (const a of areas) depth(a.id, /* @__PURE__ */ new Set());
|
|
52
|
+
const leafNodesOf = /* @__PURE__ */ new Map();
|
|
53
|
+
const leaves = (id, stack) => {
|
|
54
|
+
const hit = leafNodesOf.get(id);
|
|
55
|
+
if (hit) return hit;
|
|
56
|
+
const out = /* @__PURE__ */ new Set();
|
|
57
|
+
if (stack.has(id)) return out;
|
|
58
|
+
stack.add(id);
|
|
59
|
+
for (const mid of byId.get(id)?.members ?? []) {
|
|
60
|
+
if (byId.has(mid)) {
|
|
61
|
+
for (const n of leaves(mid, stack)) out.add(n);
|
|
62
|
+
} else {
|
|
63
|
+
out.add(mid);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
stack.delete(id);
|
|
67
|
+
leafNodesOf.set(id, out);
|
|
68
|
+
return out;
|
|
69
|
+
};
|
|
70
|
+
for (const a of areas) leaves(a.id, /* @__PURE__ */ new Set());
|
|
71
|
+
const ancestorsOf = /* @__PURE__ */ new Map();
|
|
72
|
+
const ancestors = (id, stack) => {
|
|
73
|
+
const hit = ancestorsOf.get(id);
|
|
74
|
+
if (hit) return hit;
|
|
75
|
+
const out = /* @__PURE__ */ new Set();
|
|
76
|
+
if (stack.has(id)) return out;
|
|
77
|
+
stack.add(id);
|
|
78
|
+
for (const pid of parentsOf.get(id) ?? []) {
|
|
79
|
+
out.add(pid);
|
|
80
|
+
for (const anc of ancestors(pid, stack)) out.add(anc);
|
|
81
|
+
}
|
|
82
|
+
stack.delete(id);
|
|
83
|
+
ancestorsOf.set(id, out);
|
|
84
|
+
return out;
|
|
85
|
+
};
|
|
86
|
+
for (const a of areas) ancestors(a.id, /* @__PURE__ */ new Set());
|
|
87
|
+
return { byId, depthOf, leafNodesOf, ancestorsOf };
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// src/layout/elk.ts
|
|
91
|
+
var wasmLocator = "/libavoid.wasm";
|
|
92
|
+
var setLibavoidWasmPath = (path) => {
|
|
93
|
+
wasmLocator = path;
|
|
94
|
+
};
|
|
95
|
+
var AREA_PAD = 24;
|
|
96
|
+
var NESTED_AREA_PAD = 48;
|
|
97
|
+
var TITLE_INSET_X = 14;
|
|
98
|
+
var TITLE_HEIGHT = 22;
|
|
99
|
+
var TITLE_CHAR_PX = 7;
|
|
100
|
+
var TITLE_PAD_X = 10;
|
|
101
|
+
var TITLE_MIN_W = 40;
|
|
102
|
+
var areaTitleRect = (areaRect, label) => ({
|
|
103
|
+
x: areaRect.x + TITLE_INSET_X,
|
|
104
|
+
y: areaRect.y - TITLE_HEIGHT / 2,
|
|
105
|
+
w: Math.max(TITLE_MIN_W, label.length * TITLE_CHAR_PX + TITLE_PAD_X * 2),
|
|
106
|
+
h: TITLE_HEIGHT
|
|
107
|
+
});
|
|
108
|
+
var areaBlocksEdge = (area, srcId, tgtId) => !area.members.has(srcId) && !area.members.has(tgtId);
|
|
109
|
+
var rectContains = (outer, inner) => inner.x >= outer.x && inner.y >= outer.y && inner.x + inner.w <= outer.x + outer.w && inner.y + inner.h <= outer.y + outer.h;
|
|
110
|
+
var routeOptions = (gridSize) => ({
|
|
111
|
+
routingType: "orthogonal",
|
|
112
|
+
shapeBufferDistance: Math.max(8, gridSize / 2),
|
|
113
|
+
idealNudgingDistance: gridSize,
|
|
114
|
+
segmentPenalty: 10,
|
|
115
|
+
// A crossing costs ~10 extra segments (segmentPenalty 10) — enough to detour
|
|
116
|
+
// around an avoidable crossing without over-bending simple diagrams.
|
|
117
|
+
crossingPenalty: 100,
|
|
118
|
+
nudgeOrthogonalSegmentsConnectedToShapes: true,
|
|
119
|
+
nudgeSharedPathsWithCommonEndPoint: true
|
|
120
|
+
});
|
|
121
|
+
var edgeKey = (sourceId, targetId) => `${sourceId}->${targetId}`;
|
|
122
|
+
var snap = (v, gridSize) => Math.round(v / gridSize) * gridSize;
|
|
123
|
+
var polylineLength = (points) => {
|
|
124
|
+
let total = 0;
|
|
125
|
+
for (let i = 1; i < points.length; i += 1) {
|
|
126
|
+
const a = points[i - 1];
|
|
127
|
+
const b = points[i];
|
|
128
|
+
total += Math.hypot(b.x - a.x, b.y - a.y);
|
|
129
|
+
}
|
|
130
|
+
return total;
|
|
131
|
+
};
|
|
132
|
+
var longestHorizontalMidpoint = (points) => {
|
|
133
|
+
let bestLen = 0;
|
|
134
|
+
let best;
|
|
135
|
+
for (let i = 1; i < points.length; i += 1) {
|
|
136
|
+
const a = points[i - 1];
|
|
137
|
+
const b = points[i];
|
|
138
|
+
if (a.y === b.y) {
|
|
139
|
+
const len = Math.abs(b.x - a.x);
|
|
140
|
+
if (len > bestLen) {
|
|
141
|
+
bestLen = len;
|
|
142
|
+
best = { x: (a.x + b.x) / 2, y: a.y };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return best;
|
|
147
|
+
};
|
|
148
|
+
var pathMidpoint = (points) => {
|
|
149
|
+
if (points.length === 0) return { x: 0, y: 0 };
|
|
150
|
+
const total = polylineLength(points);
|
|
151
|
+
const half = total / 2;
|
|
152
|
+
let acc = 0;
|
|
153
|
+
for (let i = 1; i < points.length; i += 1) {
|
|
154
|
+
const a = points[i - 1];
|
|
155
|
+
const b = points[i];
|
|
156
|
+
const seg = Math.hypot(b.x - a.x, b.y - a.y);
|
|
157
|
+
if (acc + seg >= half) {
|
|
158
|
+
const t = seg === 0 ? 0 : (half - acc) / seg;
|
|
159
|
+
return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
|
|
160
|
+
}
|
|
161
|
+
acc += seg;
|
|
162
|
+
}
|
|
163
|
+
return points[points.length - 1];
|
|
164
|
+
};
|
|
165
|
+
var distToRect = (p, r) => {
|
|
166
|
+
const dx = Math.max(r.x - p.x, 0, p.x - (r.x + r.w));
|
|
167
|
+
const dy = Math.max(r.y - p.y, 0, p.y - (r.y + r.h));
|
|
168
|
+
return Math.hypot(dx, dy);
|
|
169
|
+
};
|
|
170
|
+
var clearanceAt = (p, nodes) => {
|
|
171
|
+
let min = Infinity;
|
|
172
|
+
for (const id in nodes) min = Math.min(min, distToRect(p, nodes[id]));
|
|
173
|
+
return min;
|
|
174
|
+
};
|
|
175
|
+
var LABEL_MIN_CLEARANCE = 12;
|
|
176
|
+
var chooseLabelAnchor = (points, nodes) => {
|
|
177
|
+
const preferred = longestHorizontalMidpoint(points) ?? pathMidpoint(points);
|
|
178
|
+
if (clearanceAt(preferred, nodes) >= LABEL_MIN_CLEARANCE) return preferred;
|
|
179
|
+
let best = preferred;
|
|
180
|
+
let bestClear = clearanceAt(preferred, nodes);
|
|
181
|
+
const STEP = 16;
|
|
182
|
+
for (let i = 1; i < points.length; i += 1) {
|
|
183
|
+
const a = points[i - 1];
|
|
184
|
+
const b = points[i];
|
|
185
|
+
const len = Math.hypot(b.x - a.x, b.y - a.y);
|
|
186
|
+
const steps = Math.max(1, Math.round(len / STEP));
|
|
187
|
+
for (let s = 1; s < steps; s += 1) {
|
|
188
|
+
const t = s / steps;
|
|
189
|
+
const p = { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
|
|
190
|
+
const c = clearanceAt(p, nodes);
|
|
191
|
+
if (c > bestClear) {
|
|
192
|
+
bestClear = c;
|
|
193
|
+
best = p;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return best;
|
|
198
|
+
};
|
|
199
|
+
var makeEdgeId = (source, target, index) => `${source}->${target}#${index}`;
|
|
200
|
+
var toPixelRect = (node, gridSize) => ({
|
|
201
|
+
x: (node.cx - node.w / 2) * gridSize,
|
|
202
|
+
y: (node.cy - node.h / 2) * gridSize,
|
|
203
|
+
w: node.w * gridSize,
|
|
204
|
+
h: node.h * gridSize
|
|
205
|
+
});
|
|
206
|
+
var faceCache = null;
|
|
207
|
+
var topologySig = (diagram) => diagram.nodes.map((n) => n.id).join(",") + "|" + diagram.edges.map((e) => `${e.source}>${e.target}`).join(",") + "|" + diagram.areas.map((a) => `${a.id}:${[...a.members].sort().join("+")}`).join(",");
|
|
208
|
+
var route = async (diagram, layout, opts = {}) => {
|
|
209
|
+
const { gridSize } = layout;
|
|
210
|
+
const quick = opts.quick === true;
|
|
211
|
+
const pixelNodes = {};
|
|
212
|
+
for (const [id, node] of Object.entries(layout.nodes)) {
|
|
213
|
+
pixelNodes[id] = toPixelRect(node, gridSize);
|
|
214
|
+
}
|
|
215
|
+
const ensureRect = (id) => {
|
|
216
|
+
if (!pixelNodes[id]) {
|
|
217
|
+
pixelNodes[id] = toPixelRect({ cx: 0, cy: 0, w: 4, h: 2 }, gridSize);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
for (const n of diagram.nodes) ensureRect(n.id);
|
|
221
|
+
for (const e of diagram.edges) {
|
|
222
|
+
ensureRect(e.source);
|
|
223
|
+
ensureRect(e.target);
|
|
224
|
+
}
|
|
225
|
+
const areaTree = buildAreaTree(diagram.areas);
|
|
226
|
+
const areaRectById = /* @__PURE__ */ new Map();
|
|
227
|
+
const NEST_EXTRA = NESTED_AREA_PAD - AREA_PAD;
|
|
228
|
+
const computeAreaRect = (aid, stack) => {
|
|
229
|
+
const hit = areaRectById.get(aid);
|
|
230
|
+
if (hit) return hit;
|
|
231
|
+
if (stack.has(aid)) return null;
|
|
232
|
+
stack.add(aid);
|
|
233
|
+
const area = areaTree.byId.get(aid);
|
|
234
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
235
|
+
for (const mid of area.members) {
|
|
236
|
+
let r;
|
|
237
|
+
if (areaTree.byId.has(mid)) {
|
|
238
|
+
const child = computeAreaRect(mid, stack);
|
|
239
|
+
r = child && {
|
|
240
|
+
x: child.x - NEST_EXTRA,
|
|
241
|
+
y: child.y - NEST_EXTRA,
|
|
242
|
+
w: child.w + NEST_EXTRA * 2,
|
|
243
|
+
h: child.h + NEST_EXTRA * 2
|
|
244
|
+
};
|
|
245
|
+
} else {
|
|
246
|
+
r = pixelNodes[mid] ?? null;
|
|
247
|
+
}
|
|
248
|
+
if (!r) continue;
|
|
249
|
+
minX = Math.min(minX, r.x);
|
|
250
|
+
minY = Math.min(minY, r.y);
|
|
251
|
+
maxX = Math.max(maxX, r.x + r.w);
|
|
252
|
+
maxY = Math.max(maxY, r.y + r.h);
|
|
253
|
+
}
|
|
254
|
+
stack.delete(aid);
|
|
255
|
+
if (!isFinite(minX)) return null;
|
|
256
|
+
const rect = {
|
|
257
|
+
x: minX - AREA_PAD,
|
|
258
|
+
y: minY - AREA_PAD,
|
|
259
|
+
w: maxX - minX + AREA_PAD * 2,
|
|
260
|
+
h: maxY - minY + AREA_PAD * 2
|
|
261
|
+
};
|
|
262
|
+
areaRectById.set(aid, rect);
|
|
263
|
+
return rect;
|
|
264
|
+
};
|
|
265
|
+
const areaObstacles = [];
|
|
266
|
+
for (const a of diagram.areas) {
|
|
267
|
+
const rect = computeAreaRect(a.id, /* @__PURE__ */ new Set());
|
|
268
|
+
if (!rect) continue;
|
|
269
|
+
areaObstacles.push({
|
|
270
|
+
id: a.id,
|
|
271
|
+
rect,
|
|
272
|
+
members: areaTree.leafNodesOf.get(a.id) ?? /* @__PURE__ */ new Set()
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
const titleObstacles = [];
|
|
276
|
+
for (const a of diagram.areas) {
|
|
277
|
+
if (!a.label) continue;
|
|
278
|
+
const rect = areaRectById.get(a.id);
|
|
279
|
+
if (!rect) continue;
|
|
280
|
+
titleObstacles.push({
|
|
281
|
+
id: a.id,
|
|
282
|
+
rect: areaTitleRect(rect, a.label),
|
|
283
|
+
members: /* @__PURE__ */ new Set()
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
const titleChildren = titleObstacles.map((t) => ({
|
|
287
|
+
id: `__title__${t.id}`,
|
|
288
|
+
x: t.rect.x,
|
|
289
|
+
y: t.rect.y,
|
|
290
|
+
width: t.rect.w,
|
|
291
|
+
height: t.rect.h
|
|
292
|
+
}));
|
|
293
|
+
const edgeMeta = /* @__PURE__ */ new Map();
|
|
294
|
+
const edgeIds = [];
|
|
295
|
+
diagram.edges.forEach((e, i) => {
|
|
296
|
+
const src = pixelNodes[e.source];
|
|
297
|
+
const tgt = pixelNodes[e.target];
|
|
298
|
+
const srcCx = src.x + src.w / 2;
|
|
299
|
+
const srcCy = src.y + src.h / 2;
|
|
300
|
+
const tgtCx = tgt.x + tgt.w / 2;
|
|
301
|
+
const tgtCy = tgt.y + tgt.h / 2;
|
|
302
|
+
const dx = tgtCx - srcCx;
|
|
303
|
+
const dy = tgtCy - srcCy;
|
|
304
|
+
let sourceSide;
|
|
305
|
+
let targetSide;
|
|
306
|
+
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
307
|
+
sourceSide = dx >= 0 ? "E" : "W";
|
|
308
|
+
targetSide = dx >= 0 ? "W" : "E";
|
|
309
|
+
} else {
|
|
310
|
+
sourceSide = dy >= 0 ? "S" : "N";
|
|
311
|
+
targetSide = dy >= 0 ? "N" : "S";
|
|
312
|
+
}
|
|
313
|
+
const stored = layout.edges[edgeKey(e.source, e.target)];
|
|
314
|
+
const explicitSource = stored?.sourceSide !== void 0;
|
|
315
|
+
const explicitTarget = stored?.targetSide !== void 0;
|
|
316
|
+
if (explicitSource) sourceSide = stored.sourceSide;
|
|
317
|
+
if (explicitTarget) targetSide = stored.targetSide;
|
|
318
|
+
const id = makeEdgeId(e.source, e.target, i);
|
|
319
|
+
edgeMeta.set(id, {
|
|
320
|
+
source: e.source,
|
|
321
|
+
target: e.target,
|
|
322
|
+
sourceSide,
|
|
323
|
+
targetSide,
|
|
324
|
+
explicitSource,
|
|
325
|
+
explicitTarget
|
|
326
|
+
});
|
|
327
|
+
edgeIds.push(id);
|
|
328
|
+
});
|
|
329
|
+
const options = routeOptions(gridSize);
|
|
330
|
+
const sig = topologySig(diagram);
|
|
331
|
+
const cacheHit = faceCache?.sig === sig;
|
|
332
|
+
if (quick && cacheHit) {
|
|
333
|
+
for (const id of edgeIds) {
|
|
334
|
+
const m = edgeMeta.get(id);
|
|
335
|
+
const f = faceCache.faces.get(id);
|
|
336
|
+
if (!f) continue;
|
|
337
|
+
if (!m.explicitSource) m.sourceSide = f.s;
|
|
338
|
+
if (!m.explicitTarget) m.targetSide = f.t;
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
try {
|
|
342
|
+
await init(wasmLocator);
|
|
343
|
+
const plainNodes = diagram.nodes.map((n) => {
|
|
344
|
+
const pos = pixelNodes[n.id];
|
|
345
|
+
return { id: n.id, x: pos.x, y: pos.y, width: pos.w, height: pos.h };
|
|
346
|
+
});
|
|
347
|
+
const centerEdges = edgeIds.map((id) => {
|
|
348
|
+
const m = edgeMeta.get(id);
|
|
349
|
+
return { id, source: m.source, target: m.target };
|
|
350
|
+
});
|
|
351
|
+
const learned = await routeEdges(
|
|
352
|
+
{ id: "root", children: plainNodes, edges: centerEdges },
|
|
353
|
+
options
|
|
354
|
+
);
|
|
355
|
+
for (const id of edgeIds) {
|
|
356
|
+
const m = edgeMeta.get(id);
|
|
357
|
+
if (m.explicitSource && m.explicitTarget) continue;
|
|
358
|
+
const res = learned.get(id);
|
|
359
|
+
if (!res) continue;
|
|
360
|
+
const poly = cleanPolyline([res.sourcePoint, ...res.bendPoints, res.targetPoint]);
|
|
361
|
+
if (poly.length < 2) continue;
|
|
362
|
+
if (!m.explicitSource) m.sourceSide = sideFromSegment(poly[0], poly[1]);
|
|
363
|
+
if (!m.explicitTarget) {
|
|
364
|
+
m.targetSide = sideFromSegment(poly[poly.length - 1], poly[poly.length - 2]);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const faces = /* @__PURE__ */ new Map();
|
|
368
|
+
for (const id of edgeIds) {
|
|
369
|
+
const m = edgeMeta.get(id);
|
|
370
|
+
faces.set(id, { s: m.sourceSide, t: m.targetSide });
|
|
371
|
+
}
|
|
372
|
+
faceCache = { sig, faces };
|
|
373
|
+
} catch {
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const edgeAnchors = computeEdgeAnchors(edgeIds, edgeMeta, pixelNodes, areaObstacles);
|
|
377
|
+
const srcPortId = (id) => `${id}::s`;
|
|
378
|
+
const tgtPortId = (id) => `${id}::t`;
|
|
379
|
+
const buildGraph = (anchors) => {
|
|
380
|
+
const portsByNode = /* @__PURE__ */ new Map();
|
|
381
|
+
const addPort = (nodeId, pid, abs) => {
|
|
382
|
+
const node = pixelNodes[nodeId];
|
|
383
|
+
const arr = portsByNode.get(nodeId) ?? [];
|
|
384
|
+
arr.push({ id: pid, x: abs.x - node.x, y: abs.y - node.y, width: 0, height: 0 });
|
|
385
|
+
portsByNode.set(nodeId, arr);
|
|
386
|
+
};
|
|
387
|
+
for (const id of edgeIds) {
|
|
388
|
+
const m = edgeMeta.get(id);
|
|
389
|
+
const h = anchors.get(id);
|
|
390
|
+
addPort(m.source, srcPortId(id), h.sourceAnchor);
|
|
391
|
+
addPort(m.target, tgtPortId(id), h.targetAnchor);
|
|
392
|
+
}
|
|
393
|
+
const elkNodes = diagram.nodes.map((n) => {
|
|
394
|
+
const pos = pixelNodes[n.id];
|
|
395
|
+
return { id: n.id, x: pos.x, y: pos.y, width: pos.w, height: pos.h, ports: portsByNode.get(n.id) ?? [] };
|
|
396
|
+
});
|
|
397
|
+
const elkEdges = edgeIds.map((id) => {
|
|
398
|
+
const m = edgeMeta.get(id);
|
|
399
|
+
return { id, source: m.source, target: m.target, sourcePort: srcPortId(id), targetPort: tgtPortId(id) };
|
|
400
|
+
});
|
|
401
|
+
return { elkNodes, elkEdges };
|
|
402
|
+
};
|
|
403
|
+
const runLibavoid = async (elkNodes, elkEdges, skipAreas = false) => {
|
|
404
|
+
await init(wasmLocator);
|
|
405
|
+
if (skipAreas || areaObstacles.length === 0) {
|
|
406
|
+
return routeEdges(
|
|
407
|
+
{ id: "root", children: [...elkNodes, ...titleChildren], edges: elkEdges },
|
|
408
|
+
options
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
const groups = /* @__PURE__ */ new Map();
|
|
412
|
+
for (const e of elkEdges) {
|
|
413
|
+
const m = edgeMeta.get(e.id);
|
|
414
|
+
const blocking = areaObstacles.filter((a) => areaBlocksEdge(a, m.source, m.target));
|
|
415
|
+
const blockingIds = new Set(blocking.map((a) => a.id));
|
|
416
|
+
const topmost = blocking.filter((a) => {
|
|
417
|
+
const ancs = areaTree.ancestorsOf.get(a.id);
|
|
418
|
+
if (!ancs) return true;
|
|
419
|
+
for (const anc of ancs) if (blockingIds.has(anc)) return false;
|
|
420
|
+
return true;
|
|
421
|
+
});
|
|
422
|
+
const sig2 = topmost.map((a) => a.id).sort().join("|");
|
|
423
|
+
let g = groups.get(sig2);
|
|
424
|
+
if (!g) {
|
|
425
|
+
g = { areas: topmost, edges: [] };
|
|
426
|
+
groups.set(sig2, g);
|
|
427
|
+
}
|
|
428
|
+
g.edges.push(e);
|
|
429
|
+
}
|
|
430
|
+
const merged = /* @__PURE__ */ new Map();
|
|
431
|
+
for (const g of groups.values()) {
|
|
432
|
+
const covered = /* @__PURE__ */ new Set();
|
|
433
|
+
for (const a of g.areas) for (const nid of a.members) covered.add(nid);
|
|
434
|
+
const areaChildren = g.areas.map((a) => ({
|
|
435
|
+
id: `__area__${a.id}`,
|
|
436
|
+
x: a.rect.x,
|
|
437
|
+
y: a.rect.y,
|
|
438
|
+
width: a.rect.w,
|
|
439
|
+
height: a.rect.h
|
|
440
|
+
}));
|
|
441
|
+
const groupTitles = [];
|
|
442
|
+
for (const t of titleObstacles) {
|
|
443
|
+
if (g.areas.some((a) => rectContains(a.rect, t.rect))) continue;
|
|
444
|
+
groupTitles.push({
|
|
445
|
+
id: `__title__${t.id}`,
|
|
446
|
+
x: t.rect.x,
|
|
447
|
+
y: t.rect.y,
|
|
448
|
+
width: t.rect.w,
|
|
449
|
+
height: t.rect.h
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
const r = await routeEdges(
|
|
453
|
+
{
|
|
454
|
+
id: "root",
|
|
455
|
+
children: [
|
|
456
|
+
...elkNodes.filter((n) => !covered.has(n.id)),
|
|
457
|
+
...areaChildren,
|
|
458
|
+
...groupTitles
|
|
459
|
+
],
|
|
460
|
+
edges: g.edges
|
|
461
|
+
},
|
|
462
|
+
options
|
|
463
|
+
);
|
|
464
|
+
for (const [k, v] of r) merged.set(k, v);
|
|
465
|
+
}
|
|
466
|
+
return merged;
|
|
467
|
+
};
|
|
468
|
+
const routeOnce = async (anchors, skipAreas = false) => {
|
|
469
|
+
const { elkNodes, elkEdges } = buildGraph(anchors);
|
|
470
|
+
return runLibavoid(elkNodes, elkEdges, skipAreas);
|
|
471
|
+
};
|
|
472
|
+
let routes;
|
|
473
|
+
let libavoidOk = false;
|
|
474
|
+
try {
|
|
475
|
+
routes = await routeOnce(edgeAnchors, quick);
|
|
476
|
+
libavoidOk = true;
|
|
477
|
+
} catch {
|
|
478
|
+
routes = /* @__PURE__ */ new Map();
|
|
479
|
+
for (const id of edgeIds) {
|
|
480
|
+
const meta = edgeMeta.get(id);
|
|
481
|
+
const h = edgeAnchors.get(id);
|
|
482
|
+
routes.set(id, {
|
|
483
|
+
sourcePoint: h.sourceAnchor,
|
|
484
|
+
targetPoint: h.targetAnchor,
|
|
485
|
+
bendPoints: [],
|
|
486
|
+
sourceSide: connectionSideFromSide(meta.sourceSide),
|
|
487
|
+
targetSide: connectionSideFromSide(meta.targetSide)
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (libavoidOk && !quick) {
|
|
492
|
+
const MAX_ROUNDS = 4;
|
|
493
|
+
const polysOf = (rs) => {
|
|
494
|
+
const m = /* @__PURE__ */ new Map();
|
|
495
|
+
for (const id of edgeIds) {
|
|
496
|
+
const r = rs.get(id);
|
|
497
|
+
if (r) m.set(id, polyOf(r));
|
|
498
|
+
}
|
|
499
|
+
return m;
|
|
500
|
+
};
|
|
501
|
+
let bestPolys = polysOf(routes);
|
|
502
|
+
let bestCount = crossingPairs(bestPolys, edgeIds).length;
|
|
503
|
+
for (let round = 0; round < MAX_ROUNDS && bestCount > 0; round += 1) {
|
|
504
|
+
const used = /* @__PURE__ */ new Set();
|
|
505
|
+
const swaps = [];
|
|
506
|
+
for (const [a, b] of crossingPairs(bestPolys, edgeIds)) {
|
|
507
|
+
if (used.has(a) || used.has(b)) continue;
|
|
508
|
+
const f = sharedFace(a, b, edgeMeta);
|
|
509
|
+
if (!f) continue;
|
|
510
|
+
used.add(a);
|
|
511
|
+
used.add(b);
|
|
512
|
+
swaps.push({ a, b, endA: f.endA, endB: f.endB });
|
|
513
|
+
}
|
|
514
|
+
if (swaps.length === 0) break;
|
|
515
|
+
const trial = new Map(edgeAnchors);
|
|
516
|
+
for (const s of swaps) {
|
|
517
|
+
const pa = anchorEnd(trial.get(s.a), s.endA);
|
|
518
|
+
const pb = anchorEnd(trial.get(s.b), s.endB);
|
|
519
|
+
trial.set(s.a, withAnchorEnd(trial.get(s.a), s.endA, pb));
|
|
520
|
+
trial.set(s.b, withAnchorEnd(trial.get(s.b), s.endB, pa));
|
|
521
|
+
}
|
|
522
|
+
const trialRoutes = await routeOnce(trial);
|
|
523
|
+
const trialPolys = polysOf(trialRoutes);
|
|
524
|
+
const trialCount = crossingPairs(trialPolys, edgeIds).length;
|
|
525
|
+
if (trialCount < bestCount) {
|
|
526
|
+
for (const s of swaps) {
|
|
527
|
+
const pa = anchorEnd(edgeAnchors.get(s.a), s.endA);
|
|
528
|
+
const pb = anchorEnd(edgeAnchors.get(s.b), s.endB);
|
|
529
|
+
edgeAnchors.set(s.a, withAnchorEnd(edgeAnchors.get(s.a), s.endA, pb));
|
|
530
|
+
edgeAnchors.set(s.b, withAnchorEnd(edgeAnchors.get(s.b), s.endB, pa));
|
|
531
|
+
}
|
|
532
|
+
routes = trialRoutes;
|
|
533
|
+
bestPolys = trialPolys;
|
|
534
|
+
bestCount = trialCount;
|
|
535
|
+
} else {
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
const libPolys = /* @__PURE__ */ new Map();
|
|
541
|
+
const snapObstacles = [...areaObstacles, ...titleObstacles];
|
|
542
|
+
if (libavoidOk) {
|
|
543
|
+
for (const id of edgeIds) {
|
|
544
|
+
const meta = edgeMeta.get(id);
|
|
545
|
+
const result = routes.get(id);
|
|
546
|
+
if (!result) continue;
|
|
547
|
+
const poly = cleanPolyline([
|
|
548
|
+
result.sourcePoint,
|
|
549
|
+
...result.bendPoints,
|
|
550
|
+
result.targetPoint
|
|
551
|
+
]);
|
|
552
|
+
if (poly.length < 2) continue;
|
|
553
|
+
if (!isOrthogonalPath(poly)) continue;
|
|
554
|
+
const libSourceSide = sideFromSegment(poly[0], poly[1]);
|
|
555
|
+
const libTargetSide = sideFromSegment(
|
|
556
|
+
poly[poly.length - 1],
|
|
557
|
+
poly[poly.length - 2]
|
|
558
|
+
);
|
|
559
|
+
if (meta.explicitSource && libSourceSide !== meta.sourceSide || meta.explicitTarget && libTargetSide !== meta.targetSide) {
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
libPolys.set(
|
|
563
|
+
id,
|
|
564
|
+
snapPolylineToGrid(poly, gridSize, pixelNodes, snapObstacles, meta.source, meta.target)
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
resolveSegmentOverlaps(edgeAnchors, edgeMeta, pixelNodes, gridSize, libPolys);
|
|
569
|
+
const routedEdges = [];
|
|
570
|
+
for (const id of edgeIds) {
|
|
571
|
+
const meta = edgeMeta.get(id);
|
|
572
|
+
const lib = libPolys.get(id);
|
|
573
|
+
let points;
|
|
574
|
+
if (lib) {
|
|
575
|
+
points = lib;
|
|
576
|
+
} else {
|
|
577
|
+
const { sourceAnchor, targetAnchor, bendCoord } = edgeAnchors.get(id);
|
|
578
|
+
points = buildOrthogonalPath(
|
|
579
|
+
sourceAnchor,
|
|
580
|
+
targetAnchor,
|
|
581
|
+
meta.sourceSide,
|
|
582
|
+
meta.targetSide,
|
|
583
|
+
gridSize,
|
|
584
|
+
bendCoord
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
const styleSpec = layout.edges[edgeKey(meta.source, meta.target)];
|
|
588
|
+
const baseAnchor = chooseLabelAnchor(points, pixelNodes);
|
|
589
|
+
const labelDx = styleSpec?.labelDx ?? 0;
|
|
590
|
+
const labelDy = styleSpec?.labelDy ?? 0;
|
|
591
|
+
const labelAnchor = baseAnchor ? { x: baseAnchor.x + labelDx * gridSize, y: baseAnchor.y + labelDy * gridSize } : void 0;
|
|
592
|
+
routedEdges.push({
|
|
593
|
+
id,
|
|
594
|
+
source: { nodeId: meta.source, side: meta.sourceSide },
|
|
595
|
+
target: { nodeId: meta.target, side: meta.targetSide },
|
|
596
|
+
points,
|
|
597
|
+
labelAnchor,
|
|
598
|
+
color: styleSpec?.color,
|
|
599
|
+
lineStyle: styleSpec?.lineStyle,
|
|
600
|
+
width: styleSpec?.width,
|
|
601
|
+
startCap: styleSpec?.startCap,
|
|
602
|
+
endCap: styleSpec?.endCap,
|
|
603
|
+
// Surfaced so the label drag handle can read the committed offset as its
|
|
604
|
+
// starting point (the anchor above already bakes it in).
|
|
605
|
+
labelDx: styleSpec?.labelDx,
|
|
606
|
+
labelDy: styleSpec?.labelDy
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
const nodes = diagram.nodes.map((n) => {
|
|
610
|
+
const pos = pixelNodes[n.id];
|
|
611
|
+
const layoutNode = layout.nodes[n.id];
|
|
612
|
+
return {
|
|
613
|
+
id: n.id,
|
|
614
|
+
x: pos.x,
|
|
615
|
+
y: pos.y,
|
|
616
|
+
w: pos.w,
|
|
617
|
+
h: pos.h,
|
|
618
|
+
textSize: layoutNode?.textSize,
|
|
619
|
+
textColor: layoutNode?.textColor,
|
|
620
|
+
borderColor: layoutNode?.borderColor,
|
|
621
|
+
borderStyle: layoutNode?.borderStyle,
|
|
622
|
+
fillColor: layoutNode?.fillColor,
|
|
623
|
+
shape: layoutNode?.shape,
|
|
624
|
+
icon: layoutNode?.icon,
|
|
625
|
+
iconPosition: layoutNode?.iconPosition
|
|
626
|
+
};
|
|
627
|
+
});
|
|
628
|
+
const areas = diagram.areas.map((a, declIndex) => ({ a, declIndex })).sort(
|
|
629
|
+
(x, y) => (areaTree.depthOf.get(x.a.id) ?? 0) - (areaTree.depthOf.get(y.a.id) ?? 0) || x.declIndex - y.declIndex
|
|
630
|
+
).map(({ a }) => {
|
|
631
|
+
const style = layout.areas?.[a.id];
|
|
632
|
+
const rect = areaRectById.get(a.id);
|
|
633
|
+
if (!rect) {
|
|
634
|
+
return { id: a.id, label: a.label, members: a.members, x: 0, y: 0, w: 0, h: 0, ...style };
|
|
635
|
+
}
|
|
636
|
+
return {
|
|
637
|
+
id: a.id,
|
|
638
|
+
label: a.label,
|
|
639
|
+
members: a.members,
|
|
640
|
+
x: rect.x,
|
|
641
|
+
y: rect.y,
|
|
642
|
+
w: rect.w,
|
|
643
|
+
h: rect.h,
|
|
644
|
+
borderColor: style?.borderColor,
|
|
645
|
+
borderStyle: style?.borderStyle,
|
|
646
|
+
fillColor: style?.fillColor
|
|
647
|
+
};
|
|
648
|
+
});
|
|
649
|
+
return {
|
|
650
|
+
gridSize,
|
|
651
|
+
nodes,
|
|
652
|
+
areas,
|
|
653
|
+
edges: routedEdges
|
|
654
|
+
};
|
|
655
|
+
};
|
|
656
|
+
var isHorizontalSide = (side) => side === "E" || side === "W";
|
|
657
|
+
var sideFromSegment = (from, to) => {
|
|
658
|
+
const dx = to.x - from.x;
|
|
659
|
+
const dy = to.y - from.y;
|
|
660
|
+
if (Math.abs(dx) >= Math.abs(dy)) return dx >= 0 ? "E" : "W";
|
|
661
|
+
return dy >= 0 ? "S" : "N";
|
|
662
|
+
};
|
|
663
|
+
var FACE_MARGIN = 12;
|
|
664
|
+
var faceCoord = (node, side) => side === "E" ? node.x + node.w : side === "W" ? node.x : side === "S" ? node.y + node.h : node.y;
|
|
665
|
+
var faceRange = (node, side) => {
|
|
666
|
+
const horiz = isHorizontalSide(side);
|
|
667
|
+
const origin = horiz ? node.y : node.x;
|
|
668
|
+
const size = horiz ? node.h : node.w;
|
|
669
|
+
const margin = Math.min(FACE_MARGIN, size / 4);
|
|
670
|
+
return [origin + margin, origin + size - margin];
|
|
671
|
+
};
|
|
672
|
+
var FACE_FRAME = {
|
|
673
|
+
N: { n: { x: 0, y: -1 }, t: { x: 1, y: 0 } },
|
|
674
|
+
S: { n: { x: 0, y: 1 }, t: { x: 1, y: 0 } },
|
|
675
|
+
E: { n: { x: 1, y: 0 }, t: { x: 0, y: 1 } },
|
|
676
|
+
W: { n: { x: -1, y: 0 }, t: { x: 0, y: 1 } }
|
|
677
|
+
};
|
|
678
|
+
var fanOrderKey = (fanCenter, connectedCenter, side) => {
|
|
679
|
+
const dx = connectedCenter.x - fanCenter.x;
|
|
680
|
+
const dy = connectedCenter.y - fanCenter.y;
|
|
681
|
+
const { n, t } = FACE_FRAME[side];
|
|
682
|
+
return Math.atan2(dx * t.x + dy * t.y, dx * n.x + dy * n.y);
|
|
683
|
+
};
|
|
684
|
+
var computeEdgeAnchors = (edgeIds, edgeMeta, nodes, areaObstacles = []) => {
|
|
685
|
+
const faceGroups = /* @__PURE__ */ new Map();
|
|
686
|
+
for (const eid of edgeIds) {
|
|
687
|
+
const m = edgeMeta.get(eid);
|
|
688
|
+
for (const [nodeId, side, connId] of [
|
|
689
|
+
[m.source, m.sourceSide, m.target],
|
|
690
|
+
[m.target, m.targetSide, m.source]
|
|
691
|
+
]) {
|
|
692
|
+
const key = `${nodeId}:${side}`;
|
|
693
|
+
let arr = faceGroups.get(key);
|
|
694
|
+
if (!arr) {
|
|
695
|
+
arr = [];
|
|
696
|
+
faceGroups.set(key, arr);
|
|
697
|
+
}
|
|
698
|
+
arr.push({ edgeId: eid, connectedId: connId });
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
const distributed = /* @__PURE__ */ new Map();
|
|
702
|
+
const multiFaces = /* @__PURE__ */ new Set();
|
|
703
|
+
const bendCoords = /* @__PURE__ */ new Map();
|
|
704
|
+
for (const [key, group] of faceGroups) {
|
|
705
|
+
if (group.length < 2) continue;
|
|
706
|
+
const [nodeId, sideStr] = key.split(":");
|
|
707
|
+
const fanSide = sideStr;
|
|
708
|
+
const fanNode = nodes[nodeId];
|
|
709
|
+
const horiz = isHorizontalSide(fanSide);
|
|
710
|
+
const [lo, hi] = faceRange(fanNode, fanSide);
|
|
711
|
+
const n = group.length;
|
|
712
|
+
const fanCenter = {
|
|
713
|
+
x: fanNode.x + fanNode.w / 2,
|
|
714
|
+
y: fanNode.y + fanNode.h / 2
|
|
715
|
+
};
|
|
716
|
+
const connectedCenter = (id) => {
|
|
717
|
+
const c = nodes[id];
|
|
718
|
+
return { x: c.x + c.w / 2, y: c.y + c.h / 2 };
|
|
719
|
+
};
|
|
720
|
+
const sorted = [...group].sort(
|
|
721
|
+
(a, b) => fanOrderKey(fanCenter, connectedCenter(a.connectedId), fanSide) - fanOrderKey(fanCenter, connectedCenter(b.connectedId), fanSide)
|
|
722
|
+
);
|
|
723
|
+
const step = (hi - lo) / n;
|
|
724
|
+
for (let i = 0; i < n; i++) {
|
|
725
|
+
distributed.set(`${sorted[i].edgeId}:${nodeId}`, lo + step * (i + 0.5));
|
|
726
|
+
}
|
|
727
|
+
for (const g of group) multiFaces.add(`${g.edgeId}:${nodeId}`);
|
|
728
|
+
const fanFC = faceCoord(fanNode, fanSide);
|
|
729
|
+
const fanCenterPerp = horiz ? fanNode.y + fanNode.h / 2 : fanNode.x + fanNode.w / 2;
|
|
730
|
+
const distOf = /* @__PURE__ */ new Map();
|
|
731
|
+
for (const item of sorted) {
|
|
732
|
+
const otherNode = nodes[item.connectedId];
|
|
733
|
+
const otherPerp = horiz ? otherNode.y + otherNode.h / 2 : otherNode.x + otherNode.w / 2;
|
|
734
|
+
distOf.set(item.edgeId, Math.abs(otherPerp - fanCenterPerp));
|
|
735
|
+
}
|
|
736
|
+
const uniqueDists = [...new Set(distOf.values())].sort((a, b) => b - a);
|
|
737
|
+
const numDepths = uniqueDists.length;
|
|
738
|
+
for (const item of sorted) {
|
|
739
|
+
const m = edgeMeta.get(item.edgeId);
|
|
740
|
+
const isSource = m.source === nodeId;
|
|
741
|
+
const otherSide = isSource ? m.targetSide : m.sourceSide;
|
|
742
|
+
const otherNode = nodes[item.connectedId];
|
|
743
|
+
if (!otherNode) continue;
|
|
744
|
+
const otherFC = faceCoord(otherNode, otherSide);
|
|
745
|
+
const depth = uniqueDists.indexOf(distOf.get(item.edgeId));
|
|
746
|
+
const offset = (otherFC - fanFC) * (depth + 1) / (numDepths + 1);
|
|
747
|
+
if (!bendCoords.has(item.edgeId)) {
|
|
748
|
+
bendCoords.set(item.edgeId, fanFC + offset);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const result = /* @__PURE__ */ new Map();
|
|
753
|
+
for (const eid of edgeIds) {
|
|
754
|
+
const m = edgeMeta.get(eid);
|
|
755
|
+
const src = nodes[m.source];
|
|
756
|
+
const tgt = nodes[m.target];
|
|
757
|
+
const srcMulti = multiFaces.has(`${eid}:${m.source}`);
|
|
758
|
+
const tgtMulti = multiFaces.has(`${eid}:${m.target}`);
|
|
759
|
+
const srcHoriz = isHorizontalSide(m.sourceSide);
|
|
760
|
+
const tgtHoriz = isHorizontalSide(m.targetSide);
|
|
761
|
+
const srcFC = faceCoord(src, m.sourceSide);
|
|
762
|
+
const tgtFC = faceCoord(tgt, m.targetSide);
|
|
763
|
+
let srcPerp;
|
|
764
|
+
let tgtPerp;
|
|
765
|
+
if (srcMulti && tgtMulti) {
|
|
766
|
+
srcPerp = distributed.get(`${eid}:${m.source}`);
|
|
767
|
+
tgtPerp = distributed.get(`${eid}:${m.target}`);
|
|
768
|
+
} else if (srcMulti) {
|
|
769
|
+
srcPerp = distributed.get(`${eid}:${m.source}`);
|
|
770
|
+
tgtPerp = tgtHoriz ? tgt.y + tgt.h / 2 : tgt.x + tgt.w / 2;
|
|
771
|
+
} else if (tgtMulti) {
|
|
772
|
+
tgtPerp = distributed.get(`${eid}:${m.target}`);
|
|
773
|
+
srcPerp = srcHoriz ? src.y + src.h / 2 : src.x + src.w / 2;
|
|
774
|
+
} else {
|
|
775
|
+
srcPerp = srcHoriz ? src.y + src.h / 2 : src.x + src.w / 2;
|
|
776
|
+
tgtPerp = tgtHoriz ? tgt.y + tgt.h / 2 : tgt.x + tgt.w / 2;
|
|
777
|
+
}
|
|
778
|
+
const sourceAnchor = srcHoriz ? { x: srcFC, y: srcPerp } : { x: srcPerp, y: srcFC };
|
|
779
|
+
const targetAnchor = tgtHoriz ? { x: tgtFC, y: tgtPerp } : { x: tgtPerp, y: tgtFC };
|
|
780
|
+
let bendCoord = bendCoords.get(eid);
|
|
781
|
+
if (srcHoriz === tgtHoriz && sourceAnchor.x !== targetAnchor.x && sourceAnchor.y !== targetAnchor.y) {
|
|
782
|
+
const candidate = bendCoord ?? (srcHoriz ? (sourceAnchor.x + targetAnchor.x) / 2 : (sourceAnchor.y + targetAnchor.y) / 2);
|
|
783
|
+
const adjusted = avoidObstacles(
|
|
784
|
+
candidate,
|
|
785
|
+
srcHoriz,
|
|
786
|
+
sourceAnchor,
|
|
787
|
+
targetAnchor,
|
|
788
|
+
nodes,
|
|
789
|
+
m.source,
|
|
790
|
+
m.target,
|
|
791
|
+
areaObstacles
|
|
792
|
+
);
|
|
793
|
+
if (adjusted !== candidate) bendCoord = adjusted;
|
|
794
|
+
}
|
|
795
|
+
result.set(eid, { sourceAnchor, targetAnchor, bendCoord });
|
|
796
|
+
}
|
|
797
|
+
return result;
|
|
798
|
+
};
|
|
799
|
+
var OBSTACLE_PAD = 8;
|
|
800
|
+
var avoidObstacles = (candidate, horizontalSides, src, tgt, nodes, srcId, tgtId, areaObstacles = []) => {
|
|
801
|
+
const aLo = horizontalSides ? Math.min(src.y, tgt.y) : Math.min(src.x, tgt.x);
|
|
802
|
+
const aHi = horizontalSides ? Math.max(src.y, tgt.y) : Math.max(src.x, tgt.x);
|
|
803
|
+
const pLo = horizontalSides ? Math.min(src.x, tgt.x) : Math.min(src.y, tgt.y);
|
|
804
|
+
const pHi = horizontalSides ? Math.max(src.x, tgt.x) : Math.max(src.y, tgt.y);
|
|
805
|
+
if (pHi - pLo <= 0) return candidate;
|
|
806
|
+
const rects = [];
|
|
807
|
+
for (const [id, n] of Object.entries(nodes)) {
|
|
808
|
+
if (id === srcId || id === tgtId) continue;
|
|
809
|
+
rects.push(n);
|
|
810
|
+
}
|
|
811
|
+
for (const a of areaObstacles) {
|
|
812
|
+
if (areaBlocksEdge(a, srcId, tgtId)) rects.push(a.rect);
|
|
813
|
+
}
|
|
814
|
+
const obstacles = [];
|
|
815
|
+
for (const n of rects) {
|
|
816
|
+
const nALo = horizontalSides ? n.y : n.x;
|
|
817
|
+
const nAHi = horizontalSides ? n.y + n.h : n.x + n.w;
|
|
818
|
+
const nPLo = horizontalSides ? n.x : n.y;
|
|
819
|
+
const nPHi = horizontalSides ? n.x + n.w : n.y + n.h;
|
|
820
|
+
if (nAHi <= aLo || nALo >= aHi) continue;
|
|
821
|
+
if (nPHi <= pLo || nPLo >= pHi) continue;
|
|
822
|
+
obstacles.push([nPLo, nPHi]);
|
|
823
|
+
}
|
|
824
|
+
if (obstacles.length === 0) return candidate;
|
|
825
|
+
const blocking = obstacles.find(([lo, hi]) => candidate > lo && candidate < hi);
|
|
826
|
+
if (!blocking) return candidate;
|
|
827
|
+
const leftCandidate = blocking[0] - OBSTACLE_PAD;
|
|
828
|
+
const rightCandidate = blocking[1] + OBSTACLE_PAD;
|
|
829
|
+
const leftOk = leftCandidate > pLo && !obstacles.some(([lo, hi]) => leftCandidate > lo && leftCandidate < hi);
|
|
830
|
+
const rightOk = rightCandidate < pHi && !obstacles.some(([lo, hi]) => rightCandidate > lo && rightCandidate < hi);
|
|
831
|
+
if (leftOk && rightOk) {
|
|
832
|
+
return Math.abs(leftCandidate - candidate) <= Math.abs(rightCandidate - candidate) ? leftCandidate : rightCandidate;
|
|
833
|
+
}
|
|
834
|
+
if (leftOk) return leftCandidate;
|
|
835
|
+
if (rightOk) return rightCandidate;
|
|
836
|
+
return candidate;
|
|
837
|
+
};
|
|
838
|
+
var zJogCoord = (a, c, gridSize) => {
|
|
839
|
+
const snapped = snap((a + c) / 2, gridSize);
|
|
840
|
+
if (snapped !== a && snapped !== c) return snapped;
|
|
841
|
+
return (a + c) / 2;
|
|
842
|
+
};
|
|
843
|
+
var CROSS_INSET = 1.5;
|
|
844
|
+
var segCrossesRect = (a, b, r) => {
|
|
845
|
+
const x0 = r.x + CROSS_INSET;
|
|
846
|
+
const x1 = r.x + r.w - CROSS_INSET;
|
|
847
|
+
const y0 = r.y + CROSS_INSET;
|
|
848
|
+
const y1 = r.y + r.h - CROSS_INSET;
|
|
849
|
+
if (x1 <= x0 || y1 <= y0) return false;
|
|
850
|
+
if (a.y === b.y) {
|
|
851
|
+
if (a.y <= y0 || a.y >= y1) return false;
|
|
852
|
+
return Math.min(a.x, b.x) < x1 && Math.max(a.x, b.x) > x0;
|
|
853
|
+
}
|
|
854
|
+
if (a.x === b.x) {
|
|
855
|
+
if (a.x <= x0 || a.x >= x1) return false;
|
|
856
|
+
return Math.min(a.y, b.y) < y1 && Math.max(a.y, b.y) > y0;
|
|
857
|
+
}
|
|
858
|
+
return false;
|
|
859
|
+
};
|
|
860
|
+
var pathCrossesForeignNode = (pts, nodes, srcId, tgtId, areaObstacles = []) => {
|
|
861
|
+
const blocking = areaObstacles.filter((a) => areaBlocksEdge(a, srcId, tgtId));
|
|
862
|
+
for (let i = 1; i < pts.length; i += 1) {
|
|
863
|
+
for (const [id, r] of Object.entries(nodes)) {
|
|
864
|
+
if (id === srcId || id === tgtId) continue;
|
|
865
|
+
if (segCrossesRect(pts[i - 1], pts[i], r)) return true;
|
|
866
|
+
}
|
|
867
|
+
for (const a of blocking) {
|
|
868
|
+
if (segCrossesRect(pts[i - 1], pts[i], a.rect)) return true;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
return false;
|
|
872
|
+
};
|
|
873
|
+
var cleanPolyline = (pts) => {
|
|
874
|
+
const dedup = [];
|
|
875
|
+
for (const p of pts) {
|
|
876
|
+
const tail = dedup[dedup.length - 1];
|
|
877
|
+
if (tail && tail.x === p.x && tail.y === p.y) continue;
|
|
878
|
+
dedup.push({ x: p.x, y: p.y });
|
|
879
|
+
}
|
|
880
|
+
const out = [];
|
|
881
|
+
for (let i = 0; i < dedup.length; i += 1) {
|
|
882
|
+
const prev = out[out.length - 1];
|
|
883
|
+
const cur = dedup[i];
|
|
884
|
+
const next = dedup[i + 1];
|
|
885
|
+
if (prev && next && (prev.x === cur.x && cur.x === next.x || prev.y === cur.y && cur.y === next.y)) {
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
out.push(cur);
|
|
889
|
+
}
|
|
890
|
+
return out;
|
|
891
|
+
};
|
|
892
|
+
var polyOf = (r) => cleanPolyline([r.sourcePoint, ...r.bendPoints, r.targetPoint]);
|
|
893
|
+
var isOrthogonalPath = (pts) => {
|
|
894
|
+
for (let i = 1; i < pts.length; i += 1) {
|
|
895
|
+
const a = pts[i - 1], b = pts[i];
|
|
896
|
+
if (Math.abs(a.x - b.x) > 0.5 && Math.abs(a.y - b.y) > 0.5) return false;
|
|
897
|
+
}
|
|
898
|
+
return true;
|
|
899
|
+
};
|
|
900
|
+
var polysCross = (a, b) => {
|
|
901
|
+
for (let i = 1; i < a.length; i += 1) {
|
|
902
|
+
for (let j = 1; j < b.length; j += 1) {
|
|
903
|
+
const a1 = a[i - 1], a2 = a[i], b1 = b[j - 1], b2 = b[j];
|
|
904
|
+
const aH = Math.abs(a1.y - a2.y) < 0.5 && Math.abs(a1.x - a2.x) > 0.5;
|
|
905
|
+
const aV = Math.abs(a1.x - a2.x) < 0.5 && Math.abs(a1.y - a2.y) > 0.5;
|
|
906
|
+
const bH = Math.abs(b1.y - b2.y) < 0.5 && Math.abs(b1.x - b2.x) > 0.5;
|
|
907
|
+
const bV = Math.abs(b1.x - b2.x) < 0.5 && Math.abs(b1.y - b2.y) > 0.5;
|
|
908
|
+
let h = null;
|
|
909
|
+
let v = null;
|
|
910
|
+
if (aH && bV) {
|
|
911
|
+
h = [a1, a2];
|
|
912
|
+
v = [b1, b2];
|
|
913
|
+
} else if (aV && bH) {
|
|
914
|
+
h = [b1, b2];
|
|
915
|
+
v = [a1, a2];
|
|
916
|
+
}
|
|
917
|
+
if (!h || !v) continue;
|
|
918
|
+
const hy = h[0].y, vx = v[0].x, e = 1;
|
|
919
|
+
if (vx > Math.min(h[0].x, h[1].x) + e && vx < Math.max(h[0].x, h[1].x) - e && hy > Math.min(v[0].y, v[1].y) + e && hy < Math.max(v[0].y, v[1].y) - e) return true;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
return false;
|
|
923
|
+
};
|
|
924
|
+
var crossingPairs = (polys, ids) => {
|
|
925
|
+
const out = [];
|
|
926
|
+
for (let i = 0; i < ids.length; i += 1) {
|
|
927
|
+
for (let j = i + 1; j < ids.length; j += 1) {
|
|
928
|
+
const A = polys.get(ids[i]), B = polys.get(ids[j]);
|
|
929
|
+
if (A && B && polysCross(A, B)) out.push([ids[i], ids[j]]);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return out;
|
|
933
|
+
};
|
|
934
|
+
var sharedFace = (a, b, meta) => {
|
|
935
|
+
const ma = meta.get(a), mb = meta.get(b);
|
|
936
|
+
const endsA = [
|
|
937
|
+
["source", ma.source, ma.sourceSide],
|
|
938
|
+
["target", ma.target, ma.targetSide]
|
|
939
|
+
];
|
|
940
|
+
const endsB = [
|
|
941
|
+
["source", mb.source, mb.sourceSide],
|
|
942
|
+
["target", mb.target, mb.targetSide]
|
|
943
|
+
];
|
|
944
|
+
for (const [ea, na, sa] of endsA) {
|
|
945
|
+
for (const [eb, nb, sb] of endsB) {
|
|
946
|
+
if (na === nb && sa === sb) return { endA: ea, endB: eb };
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
return null;
|
|
950
|
+
};
|
|
951
|
+
var anchorEnd = (h, end) => end === "source" ? h.sourceAnchor : h.targetAnchor;
|
|
952
|
+
var withAnchorEnd = (h, end, pt) => end === "source" ? { ...h, sourceAnchor: pt } : { ...h, targetAnchor: pt };
|
|
953
|
+
var snapPolylineToGrid = (poly, gridSize, nodes, areaObstacles, srcId, tgtId) => {
|
|
954
|
+
if (poly.length < 4) return cleanPolyline(poly);
|
|
955
|
+
const out = poly.map((p) => ({ x: p.x, y: p.y }));
|
|
956
|
+
for (let i = 1; i < out.length - 2; i += 1) {
|
|
957
|
+
const a = out[i];
|
|
958
|
+
const b = out[i + 1];
|
|
959
|
+
const before = out[i - 1];
|
|
960
|
+
const after = out[i + 2];
|
|
961
|
+
if (Math.abs(a.x - b.x) < 0.5) {
|
|
962
|
+
const sx = snap(a.x, gridSize);
|
|
963
|
+
const lo = Math.min(before.x, after.x);
|
|
964
|
+
const hi = Math.max(before.x, after.x);
|
|
965
|
+
if (sx > lo && sx < hi) {
|
|
966
|
+
a.x = sx;
|
|
967
|
+
b.x = sx;
|
|
968
|
+
}
|
|
969
|
+
} else if (Math.abs(a.y - b.y) < 0.5) {
|
|
970
|
+
const sy = snap(a.y, gridSize);
|
|
971
|
+
const lo = Math.min(before.y, after.y);
|
|
972
|
+
const hi = Math.max(before.y, after.y);
|
|
973
|
+
if (sy > lo && sy < hi) {
|
|
974
|
+
a.y = sy;
|
|
975
|
+
b.y = sy;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
const snapped = cleanPolyline(out);
|
|
980
|
+
if (pathCrossesForeignNode(snapped, nodes, srcId, tgtId, areaObstacles)) {
|
|
981
|
+
return cleanPolyline(poly);
|
|
982
|
+
}
|
|
983
|
+
return snapped;
|
|
984
|
+
};
|
|
985
|
+
var buildOrthogonalPath = (A, C, sourceSide, targetSide, gridSize, bendCoord) => {
|
|
986
|
+
const srcHoriz = isHorizontalSide(sourceSide);
|
|
987
|
+
const tgtHoriz = isHorizontalSide(targetSide);
|
|
988
|
+
const out = [{ x: A.x, y: A.y }];
|
|
989
|
+
if (srcHoriz === tgtHoriz) {
|
|
990
|
+
if (srcHoriz) {
|
|
991
|
+
if (A.y !== C.y) {
|
|
992
|
+
const midX = bendCoord ?? zJogCoord(A.x, C.x, gridSize);
|
|
993
|
+
out.push({ x: midX, y: A.y }, { x: midX, y: C.y });
|
|
994
|
+
}
|
|
995
|
+
} else if (A.x !== C.x) {
|
|
996
|
+
const midY = bendCoord ?? zJogCoord(A.y, C.y, gridSize);
|
|
997
|
+
out.push({ x: A.x, y: midY }, { x: C.x, y: midY });
|
|
998
|
+
}
|
|
999
|
+
} else {
|
|
1000
|
+
const corner = srcHoriz ? { x: C.x, y: A.y } : { x: A.x, y: C.y };
|
|
1001
|
+
out.push(corner);
|
|
1002
|
+
}
|
|
1003
|
+
out.push({ x: C.x, y: C.y });
|
|
1004
|
+
const deduped = [];
|
|
1005
|
+
for (const p of out) {
|
|
1006
|
+
const tail = deduped[deduped.length - 1];
|
|
1007
|
+
if (tail && tail.x === p.x && tail.y === p.y) continue;
|
|
1008
|
+
deduped.push(p);
|
|
1009
|
+
}
|
|
1010
|
+
return deduped;
|
|
1011
|
+
};
|
|
1012
|
+
var segmentsFor = (hints, m, gridSize) => {
|
|
1013
|
+
const path = buildOrthogonalPath(
|
|
1014
|
+
hints.sourceAnchor,
|
|
1015
|
+
hints.targetAnchor,
|
|
1016
|
+
m.sourceSide,
|
|
1017
|
+
m.targetSide,
|
|
1018
|
+
gridSize,
|
|
1019
|
+
hints.bendCoord
|
|
1020
|
+
);
|
|
1021
|
+
const segs = [];
|
|
1022
|
+
for (let i = 1; i < path.length; i += 1) {
|
|
1023
|
+
const a = path[i - 1];
|
|
1024
|
+
const b = path[i];
|
|
1025
|
+
const isFirst = i === 1;
|
|
1026
|
+
const isLast = i === path.length - 1;
|
|
1027
|
+
const endpoint = isFirst ? "source" : isLast ? "target" : null;
|
|
1028
|
+
if (a.x === b.x && a.y !== b.y) {
|
|
1029
|
+
segs.push({
|
|
1030
|
+
axis: "V",
|
|
1031
|
+
coord: a.x,
|
|
1032
|
+
lo: Math.min(a.y, b.y),
|
|
1033
|
+
hi: Math.max(a.y, b.y),
|
|
1034
|
+
endpoint
|
|
1035
|
+
});
|
|
1036
|
+
} else if (a.y === b.y && a.x !== b.x) {
|
|
1037
|
+
segs.push({
|
|
1038
|
+
axis: "H",
|
|
1039
|
+
coord: a.y,
|
|
1040
|
+
lo: Math.min(a.x, b.x),
|
|
1041
|
+
hi: Math.max(a.x, b.x),
|
|
1042
|
+
endpoint
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
return segs;
|
|
1047
|
+
};
|
|
1048
|
+
var segmentsFromPolyline = (poly) => {
|
|
1049
|
+
const segs = [];
|
|
1050
|
+
for (let i = 1; i < poly.length; i += 1) {
|
|
1051
|
+
const a = poly[i - 1];
|
|
1052
|
+
const b = poly[i];
|
|
1053
|
+
if (Math.abs(a.x - b.x) < 0.5 && Math.abs(a.y - b.y) >= 0.5) {
|
|
1054
|
+
segs.push({ axis: "V", coord: a.x, lo: Math.min(a.y, b.y), hi: Math.max(a.y, b.y), endpoint: null });
|
|
1055
|
+
} else if (Math.abs(a.y - b.y) < 0.5 && Math.abs(a.x - b.x) >= 0.5) {
|
|
1056
|
+
segs.push({ axis: "H", coord: a.y, lo: Math.min(a.x, b.x), hi: Math.max(a.x, b.x), endpoint: null });
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
return segs;
|
|
1060
|
+
};
|
|
1061
|
+
var nudgeAnchorAlongFace = (node, side, current, attempts) => {
|
|
1062
|
+
const horiz = isHorizontalSide(side);
|
|
1063
|
+
const [lo, hi] = faceRange(node, side);
|
|
1064
|
+
const cur = horiz ? current.y : current.x;
|
|
1065
|
+
for (const delta of attempts) {
|
|
1066
|
+
const next = cur + delta;
|
|
1067
|
+
if (next >= lo && next <= hi) {
|
|
1068
|
+
return horiz ? { x: current.x, y: next } : { x: next, y: current.y };
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return current;
|
|
1072
|
+
};
|
|
1073
|
+
var resolveSegmentOverlaps = (edgeAnchors, edgeMeta, nodes, gridSize, skip) => {
|
|
1074
|
+
const OVERLAP_TOLERANCE = 4;
|
|
1075
|
+
const nudgeAmount = Math.max(gridSize / 2, 16);
|
|
1076
|
+
const NUDGE_ATTEMPTS = [nudgeAmount, -nudgeAmount, nudgeAmount * 2, -nudgeAmount * 2];
|
|
1077
|
+
const BEND_MIN_GAP = gridSize;
|
|
1078
|
+
const MAX_ITERATIONS = 16;
|
|
1079
|
+
const MAX_ENDPOINT_NUDGES = 3;
|
|
1080
|
+
const endpointNudges = /* @__PURE__ */ new Map();
|
|
1081
|
+
const bendCoordOf = (h, axis) => h.bendCoord ?? (axis === "V" ? (h.sourceAnchor.x + h.targetAnchor.x) / 2 : (h.sourceAnchor.y + h.targetAnchor.y) / 2);
|
|
1082
|
+
const fixedSegments = [];
|
|
1083
|
+
for (const poly of skip.values()) fixedSegments.push(...segmentsFromPolyline(poly));
|
|
1084
|
+
const spanOverlap = (a, b) => Math.min(a.hi, b.hi) - Math.max(a.lo, b.lo);
|
|
1085
|
+
const segmentsCross = (a, b) => {
|
|
1086
|
+
if (a.axis === b.axis) return false;
|
|
1087
|
+
const v = a.axis === "V" ? a : b;
|
|
1088
|
+
const h = a.axis === "V" ? b : a;
|
|
1089
|
+
return v.coord > h.lo && v.coord < h.hi && h.coord > v.lo && h.coord < v.hi;
|
|
1090
|
+
};
|
|
1091
|
+
const relocateBendLeg = (edgeId, leg, segs) => {
|
|
1092
|
+
const hints = edgeAnchors.get(edgeId);
|
|
1093
|
+
const meta = edgeMeta.get(edgeId);
|
|
1094
|
+
if (!hints || !meta) return false;
|
|
1095
|
+
const axis = leg.axis;
|
|
1096
|
+
const span = axis === "V" ? [hints.sourceAnchor.x, hints.targetAnchor.x] : [hints.sourceAnchor.y, hints.targetAnchor.y];
|
|
1097
|
+
const margin = Math.min(nudgeAmount, (Math.max(...span) - Math.min(...span)) / 3);
|
|
1098
|
+
const lo = Math.min(...span) + margin;
|
|
1099
|
+
const hi = Math.max(...span) - margin;
|
|
1100
|
+
if (hi <= lo) return false;
|
|
1101
|
+
const otherSegs = [...fixedSegments];
|
|
1102
|
+
for (const [oid, osegs] of segs) if (oid !== edgeId) otherSegs.push(...osegs);
|
|
1103
|
+
const blockers = otherSegs.filter((os) => os.axis === axis && spanOverlap(os, leg) > OVERLAP_TOLERANCE).map((os) => os.coord);
|
|
1104
|
+
const clear = (c) => c >= lo && c <= hi && blockers.every((b) => Math.abs(c - b) >= BEND_MIN_GAP - 1);
|
|
1105
|
+
const crossingsAt = (c) => {
|
|
1106
|
+
let n = 0;
|
|
1107
|
+
for (const ms of segmentsFor({ ...hints, bendCoord: c }, meta, gridSize)) {
|
|
1108
|
+
for (const os of otherSegs) if (segmentsCross(ms, os)) n += 1;
|
|
1109
|
+
}
|
|
1110
|
+
return n;
|
|
1111
|
+
};
|
|
1112
|
+
const cur = bendCoordOf(hints, axis);
|
|
1113
|
+
const step = Math.max(8, gridSize / 4);
|
|
1114
|
+
let best = null;
|
|
1115
|
+
for (let d = step; d <= hi - lo; d += step) {
|
|
1116
|
+
for (const c of [cur + d, cur - d]) {
|
|
1117
|
+
if (!clear(c)) continue;
|
|
1118
|
+
const cross = crossingsAt(c);
|
|
1119
|
+
if (cross === 0) {
|
|
1120
|
+
edgeAnchors.set(edgeId, { ...hints, bendCoord: c });
|
|
1121
|
+
return true;
|
|
1122
|
+
}
|
|
1123
|
+
if (!best || cross < best.cross) best = { c, cross };
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
if (best) {
|
|
1127
|
+
edgeAnchors.set(edgeId, { ...hints, bendCoord: best.c });
|
|
1128
|
+
return true;
|
|
1129
|
+
}
|
|
1130
|
+
return false;
|
|
1131
|
+
};
|
|
1132
|
+
for (let iter = 0; iter < MAX_ITERATIONS; iter += 1) {
|
|
1133
|
+
const segs = /* @__PURE__ */ new Map();
|
|
1134
|
+
for (const [edgeId, hints] of edgeAnchors) {
|
|
1135
|
+
if (skip.has(edgeId)) continue;
|
|
1136
|
+
const m = edgeMeta.get(edgeId);
|
|
1137
|
+
segs.set(edgeId, segmentsFor(hints, m, gridSize));
|
|
1138
|
+
}
|
|
1139
|
+
const others = (selfId) => {
|
|
1140
|
+
const out = [...fixedSegments];
|
|
1141
|
+
for (const [oid, osegs] of segs) if (oid !== selfId) out.push(...osegs);
|
|
1142
|
+
return out;
|
|
1143
|
+
};
|
|
1144
|
+
const endpointConflicts = [];
|
|
1145
|
+
const bendConflicts = [];
|
|
1146
|
+
for (const [e1, segs1] of segs) {
|
|
1147
|
+
const rest = others(e1);
|
|
1148
|
+
for (const s1 of segs1) {
|
|
1149
|
+
for (const s2 of rest) {
|
|
1150
|
+
if (s1.axis !== s2.axis) continue;
|
|
1151
|
+
if (spanOverlap(s1, s2) <= OVERLAP_TOLERANCE) continue;
|
|
1152
|
+
const coordGap = Math.abs(s1.coord - s2.coord);
|
|
1153
|
+
if (coordGap > OVERLAP_TOLERANCE) continue;
|
|
1154
|
+
if (s1.endpoint) {
|
|
1155
|
+
endpointConflicts.push({ edgeId: e1, endpoint: s1.endpoint });
|
|
1156
|
+
} else {
|
|
1157
|
+
bendConflicts.push({ edgeId: e1, leg: s1 });
|
|
1158
|
+
}
|
|
1159
|
+
break;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
let progressed = false;
|
|
1164
|
+
for (const c of endpointConflicts) {
|
|
1165
|
+
const key = `${c.edgeId}:${c.endpoint}`;
|
|
1166
|
+
if ((endpointNudges.get(key) ?? 0) >= MAX_ENDPOINT_NUDGES) continue;
|
|
1167
|
+
const m = edgeMeta.get(c.edgeId);
|
|
1168
|
+
const hints = edgeAnchors.get(c.edgeId);
|
|
1169
|
+
const isSource = c.endpoint === "source";
|
|
1170
|
+
const node = nodes[isSource ? m.source : m.target];
|
|
1171
|
+
if (!node) continue;
|
|
1172
|
+
const side = isSource ? m.sourceSide : m.targetSide;
|
|
1173
|
+
const currentAnchor = isSource ? hints.sourceAnchor : hints.targetAnchor;
|
|
1174
|
+
const nudged = nudgeAnchorAlongFace(node, side, currentAnchor, NUDGE_ATTEMPTS);
|
|
1175
|
+
if (nudged === currentAnchor) continue;
|
|
1176
|
+
endpointNudges.set(key, (endpointNudges.get(key) ?? 0) + 1);
|
|
1177
|
+
edgeAnchors.set(c.edgeId, {
|
|
1178
|
+
...hints,
|
|
1179
|
+
sourceAnchor: isSource ? nudged : hints.sourceAnchor,
|
|
1180
|
+
targetAnchor: isSource ? hints.targetAnchor : nudged
|
|
1181
|
+
});
|
|
1182
|
+
progressed = true;
|
|
1183
|
+
break;
|
|
1184
|
+
}
|
|
1185
|
+
for (const c of bendConflicts) {
|
|
1186
|
+
if (relocateBendLeg(c.edgeId, c.leg, segs)) {
|
|
1187
|
+
progressed = true;
|
|
1188
|
+
break;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
if (!progressed) return;
|
|
1192
|
+
}
|
|
1193
|
+
};
|
|
1194
|
+
var connectionSideFromSide = (side) => {
|
|
1195
|
+
switch (side) {
|
|
1196
|
+
case "N":
|
|
1197
|
+
return "north";
|
|
1198
|
+
case "S":
|
|
1199
|
+
return "south";
|
|
1200
|
+
case "E":
|
|
1201
|
+
return "east";
|
|
1202
|
+
case "W":
|
|
1203
|
+
return "west";
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
|
|
1207
|
+
// src/parser/lexer.ts
|
|
1208
|
+
import { createToken, Lexer } from "chevrotain";
|
|
1209
|
+
var WhiteSpace = createToken({
|
|
1210
|
+
name: "WhiteSpace",
|
|
1211
|
+
pattern: /[ \t\r]+/,
|
|
1212
|
+
group: Lexer.SKIPPED
|
|
1213
|
+
});
|
|
1214
|
+
var Comment = createToken({
|
|
1215
|
+
name: "Comment",
|
|
1216
|
+
// `#` to end of line; the trailing newline stays so it can still act as a
|
|
1217
|
+
// statement terminator.
|
|
1218
|
+
pattern: /#[^\n]*/,
|
|
1219
|
+
group: Lexer.SKIPPED
|
|
1220
|
+
});
|
|
1221
|
+
var Newline = createToken({
|
|
1222
|
+
name: "Newline",
|
|
1223
|
+
// Collapse consecutive newlines into one logical terminator.
|
|
1224
|
+
pattern: /\n+/,
|
|
1225
|
+
line_breaks: true
|
|
1226
|
+
});
|
|
1227
|
+
var ArrowBoth = createToken({ name: "ArrowBoth", pattern: /<->/ });
|
|
1228
|
+
var ArrowRight = createToken({ name: "ArrowRight", pattern: /->/ });
|
|
1229
|
+
var ArrowLeft = createToken({ name: "ArrowLeft", pattern: /<-/ });
|
|
1230
|
+
var DashDash = createToken({ name: "DashDash", pattern: /--/ });
|
|
1231
|
+
var LCurly = createToken({ name: "LCurly", pattern: /\{/ });
|
|
1232
|
+
var RCurly = createToken({ name: "RCurly", pattern: /\}/ });
|
|
1233
|
+
var Colon = createToken({ name: "Colon", pattern: /:/ });
|
|
1234
|
+
var Semicolon = createToken({ name: "Semicolon", pattern: /;/ });
|
|
1235
|
+
var Dot = createToken({ name: "Dot", pattern: /\./ });
|
|
1236
|
+
var StringLit = createToken({
|
|
1237
|
+
name: "StringLit",
|
|
1238
|
+
pattern: /"(?:\\.|[^"\\\n])*"|'(?:\\.|[^'\\\n])*'/
|
|
1239
|
+
});
|
|
1240
|
+
var NumberLit = createToken({
|
|
1241
|
+
name: "NumberLit",
|
|
1242
|
+
pattern: /[0-9]+(?:\.[0-9]+)?/
|
|
1243
|
+
});
|
|
1244
|
+
var LETTER = "A-Za-z\xC0-\xFF\u0100-\u024F";
|
|
1245
|
+
var DIGIT = "0-9";
|
|
1246
|
+
var Identifier = createToken({
|
|
1247
|
+
name: "Identifier",
|
|
1248
|
+
pattern: new RegExp(
|
|
1249
|
+
`[${LETTER}_][${LETTER}${DIGIT}_]*(?:-[${LETTER}${DIGIT}_]+)*`
|
|
1250
|
+
)
|
|
1251
|
+
});
|
|
1252
|
+
var allTokens = [
|
|
1253
|
+
WhiteSpace,
|
|
1254
|
+
Comment,
|
|
1255
|
+
Newline,
|
|
1256
|
+
ArrowBoth,
|
|
1257
|
+
ArrowRight,
|
|
1258
|
+
ArrowLeft,
|
|
1259
|
+
DashDash,
|
|
1260
|
+
LCurly,
|
|
1261
|
+
RCurly,
|
|
1262
|
+
Colon,
|
|
1263
|
+
Semicolon,
|
|
1264
|
+
Dot,
|
|
1265
|
+
StringLit,
|
|
1266
|
+
NumberLit,
|
|
1267
|
+
Identifier
|
|
1268
|
+
];
|
|
1269
|
+
var lexer = new Lexer(allTokens, {
|
|
1270
|
+
positionTracking: "full",
|
|
1271
|
+
ensureOptimizations: false
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
// src/parser/parser.ts
|
|
1275
|
+
import { CstParser } from "chevrotain";
|
|
1276
|
+
var D2Parser = class extends CstParser {
|
|
1277
|
+
constructor() {
|
|
1278
|
+
super(allTokens, {
|
|
1279
|
+
recoveryEnabled: false,
|
|
1280
|
+
// We only need 2-token lookahead to distinguish edges (id arrow ...)
|
|
1281
|
+
// from node/area declarations (id colon ... | id { ... } | id alone).
|
|
1282
|
+
maxLookahead: 3
|
|
1283
|
+
});
|
|
1284
|
+
this.performSelfAnalysis();
|
|
1285
|
+
}
|
|
1286
|
+
// program := (statement Newline?)*
|
|
1287
|
+
program = this.RULE("program", () => {
|
|
1288
|
+
this.MANY1(() => this.CONSUME1(Newline));
|
|
1289
|
+
this.MANY2(() => {
|
|
1290
|
+
this.SUBRULE(this.statement);
|
|
1291
|
+
this.MANY3(() => this.CONSUME2(Newline));
|
|
1292
|
+
});
|
|
1293
|
+
});
|
|
1294
|
+
// statement := edgeDecl | nodeOrAreaDecl
|
|
1295
|
+
//
|
|
1296
|
+
// Disambiguated by looking at the token after the first Identifier: an
|
|
1297
|
+
// arrow operator means we're in an edge declaration.
|
|
1298
|
+
statement = this.RULE("statement", () => {
|
|
1299
|
+
this.OR([
|
|
1300
|
+
{
|
|
1301
|
+
GATE: () => this.isArrowAhead(),
|
|
1302
|
+
ALT: () => this.SUBRULE(this.edgeDecl)
|
|
1303
|
+
},
|
|
1304
|
+
{ ALT: () => this.SUBRULE(this.nodeOrAreaDecl) }
|
|
1305
|
+
]);
|
|
1306
|
+
});
|
|
1307
|
+
// nodeOrAreaDecl := Identifier (Colon labelOrValue)? (LCurly blockItem* RCurly)?
|
|
1308
|
+
nodeOrAreaDecl = this.RULE("nodeOrAreaDecl", () => {
|
|
1309
|
+
this.CONSUME(Identifier);
|
|
1310
|
+
this.OPTION1(() => {
|
|
1311
|
+
this.CONSUME(Colon);
|
|
1312
|
+
this.SUBRULE(this.labelOrValue);
|
|
1313
|
+
});
|
|
1314
|
+
this.OPTION2(() => {
|
|
1315
|
+
this.CONSUME(LCurly);
|
|
1316
|
+
this.MANY1(() => this.CONSUME1(Newline));
|
|
1317
|
+
this.MANY2(() => {
|
|
1318
|
+
this.SUBRULE(this.blockItem);
|
|
1319
|
+
this.MANY3(() => {
|
|
1320
|
+
this.OR2([
|
|
1321
|
+
{ ALT: () => this.CONSUME(Semicolon) },
|
|
1322
|
+
{ ALT: () => this.CONSUME2(Newline) }
|
|
1323
|
+
]);
|
|
1324
|
+
});
|
|
1325
|
+
});
|
|
1326
|
+
this.CONSUME(RCurly);
|
|
1327
|
+
});
|
|
1328
|
+
});
|
|
1329
|
+
// blockItem := attr | memberStmt
|
|
1330
|
+
//
|
|
1331
|
+
// We distinguish on the second token: `Identifier Colon` or `Identifier Dot`
|
|
1332
|
+
// is an attr; `Identifier` followed by anything else (`;`, newline, `}`) is
|
|
1333
|
+
// a member declaration.
|
|
1334
|
+
blockItem = this.RULE("blockItem", () => {
|
|
1335
|
+
this.OR([
|
|
1336
|
+
{
|
|
1337
|
+
GATE: () => this.isAttrAhead(),
|
|
1338
|
+
ALT: () => this.SUBRULE(this.attr)
|
|
1339
|
+
},
|
|
1340
|
+
{ ALT: () => this.SUBRULE(this.memberStmt) }
|
|
1341
|
+
]);
|
|
1342
|
+
});
|
|
1343
|
+
// attr := Identifier (Dot Identifier)* Colon attrValue
|
|
1344
|
+
attr = this.RULE("attr", () => {
|
|
1345
|
+
this.CONSUME(Identifier);
|
|
1346
|
+
this.MANY(() => {
|
|
1347
|
+
this.CONSUME(Dot);
|
|
1348
|
+
this.CONSUME2(Identifier);
|
|
1349
|
+
});
|
|
1350
|
+
this.CONSUME(Colon);
|
|
1351
|
+
this.SUBRULE(this.attrValue);
|
|
1352
|
+
});
|
|
1353
|
+
// memberStmt := Identifier
|
|
1354
|
+
// Terminators (`;`, newline) are consumed by the enclosing block rule.
|
|
1355
|
+
memberStmt = this.RULE("memberStmt", () => {
|
|
1356
|
+
this.CONSUME(Identifier);
|
|
1357
|
+
});
|
|
1358
|
+
// edgeDecl := Identifier arrowOp Identifier (Colon labelOrValue)? (LCurly attr* RCurly)?
|
|
1359
|
+
edgeDecl = this.RULE("edgeDecl", () => {
|
|
1360
|
+
this.CONSUME(Identifier);
|
|
1361
|
+
this.SUBRULE(this.arrowOp);
|
|
1362
|
+
this.CONSUME2(Identifier);
|
|
1363
|
+
this.OPTION1(() => {
|
|
1364
|
+
this.CONSUME(Colon);
|
|
1365
|
+
this.SUBRULE(this.labelOrValue);
|
|
1366
|
+
});
|
|
1367
|
+
this.OPTION2(() => {
|
|
1368
|
+
this.CONSUME(LCurly);
|
|
1369
|
+
this.MANY1(() => this.CONSUME1(Newline));
|
|
1370
|
+
this.MANY2(() => {
|
|
1371
|
+
this.SUBRULE(this.attr);
|
|
1372
|
+
this.MANY3(() => {
|
|
1373
|
+
this.OR([
|
|
1374
|
+
{ ALT: () => this.CONSUME(Semicolon) },
|
|
1375
|
+
{ ALT: () => this.CONSUME2(Newline) }
|
|
1376
|
+
]);
|
|
1377
|
+
});
|
|
1378
|
+
});
|
|
1379
|
+
this.CONSUME(RCurly);
|
|
1380
|
+
});
|
|
1381
|
+
});
|
|
1382
|
+
// arrowOp := -> | <- | <-> | --
|
|
1383
|
+
arrowOp = this.RULE("arrowOp", () => {
|
|
1384
|
+
this.OR([
|
|
1385
|
+
{ ALT: () => this.CONSUME(ArrowRight) },
|
|
1386
|
+
{ ALT: () => this.CONSUME(ArrowLeft) },
|
|
1387
|
+
{ ALT: () => this.CONSUME(ArrowBoth) },
|
|
1388
|
+
{ ALT: () => this.CONSUME(DashDash) }
|
|
1389
|
+
]);
|
|
1390
|
+
});
|
|
1391
|
+
// labelOrValue := StringLit | Identifier+
|
|
1392
|
+
//
|
|
1393
|
+
// Unquoted labels may contain spaces (e.g. `api: Web App`), so we greedily
|
|
1394
|
+
// consume consecutive identifiers up to the next structural token.
|
|
1395
|
+
labelOrValue = this.RULE("labelOrValue", () => {
|
|
1396
|
+
this.OR([
|
|
1397
|
+
{ ALT: () => this.CONSUME(StringLit) },
|
|
1398
|
+
{
|
|
1399
|
+
ALT: () => {
|
|
1400
|
+
this.CONSUME(Identifier);
|
|
1401
|
+
this.MANY(() => this.CONSUME2(Identifier));
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
]);
|
|
1405
|
+
});
|
|
1406
|
+
// attrValue := StringLit | Identifier | NumberLit
|
|
1407
|
+
attrValue = this.RULE("attrValue", () => {
|
|
1408
|
+
this.OR([
|
|
1409
|
+
{ ALT: () => this.CONSUME(StringLit) },
|
|
1410
|
+
{ ALT: () => this.CONSUME(Identifier) },
|
|
1411
|
+
{ ALT: () => this.CONSUME(NumberLit) }
|
|
1412
|
+
]);
|
|
1413
|
+
});
|
|
1414
|
+
// --- Lookahead helpers --------------------------------------------------
|
|
1415
|
+
isArrowAhead() {
|
|
1416
|
+
const t1 = this.LA(1);
|
|
1417
|
+
const t2 = this.LA(2);
|
|
1418
|
+
if (t1.tokenType !== Identifier) return false;
|
|
1419
|
+
return t2.tokenType === ArrowRight || t2.tokenType === ArrowLeft || t2.tokenType === ArrowBoth || t2.tokenType === DashDash;
|
|
1420
|
+
}
|
|
1421
|
+
isAttrAhead() {
|
|
1422
|
+
const t1 = this.LA(1);
|
|
1423
|
+
const t2 = this.LA(2);
|
|
1424
|
+
if (t1.tokenType !== Identifier) return false;
|
|
1425
|
+
return t2.tokenType === Colon || t2.tokenType === Dot;
|
|
1426
|
+
}
|
|
1427
|
+
};
|
|
1428
|
+
var parserInstance = new D2Parser();
|
|
1429
|
+
|
|
1430
|
+
// src/parser/ast.ts
|
|
1431
|
+
var SHAPE_NAMES = [
|
|
1432
|
+
"rectangle",
|
|
1433
|
+
"cylinder",
|
|
1434
|
+
"person"
|
|
1435
|
+
];
|
|
1436
|
+
|
|
1437
|
+
// src/parser/visitor.ts
|
|
1438
|
+
var BaseVisitor = parserInstance.getBaseCstVisitorConstructor();
|
|
1439
|
+
var D2Visitor = class extends BaseVisitor {
|
|
1440
|
+
errors = [];
|
|
1441
|
+
constructor() {
|
|
1442
|
+
super();
|
|
1443
|
+
this.validateVisitor();
|
|
1444
|
+
}
|
|
1445
|
+
// -- root ---------------------------------------------------------------
|
|
1446
|
+
program(ctx) {
|
|
1447
|
+
const diagram = { nodes: [], edges: [], areas: [] };
|
|
1448
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
1449
|
+
for (const stmt of ctx.statement ?? []) {
|
|
1450
|
+
const result = this.visit(stmt);
|
|
1451
|
+
if (!result) continue;
|
|
1452
|
+
if (result.kind === "edge") {
|
|
1453
|
+
diagram.edges.push(result);
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
if (seenIds.has(result.id)) {
|
|
1457
|
+
this.errors.push({
|
|
1458
|
+
message: `Duplicate id "${result.id}".`,
|
|
1459
|
+
range: result.range
|
|
1460
|
+
});
|
|
1461
|
+
continue;
|
|
1462
|
+
}
|
|
1463
|
+
seenIds.add(result.id);
|
|
1464
|
+
if (result.kind === "node") {
|
|
1465
|
+
diagram.nodes.push(result);
|
|
1466
|
+
} else {
|
|
1467
|
+
diagram.areas.push(result);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
this.validateAreaNesting(diagram.areas);
|
|
1471
|
+
return diagram;
|
|
1472
|
+
}
|
|
1473
|
+
// Nested containers are by-reference (an area listing another area's id as a
|
|
1474
|
+
// member). The member graph must stay a DAG: an area containing itself or a
|
|
1475
|
+
// cycle between areas would make the derived boxes undefined, so both are
|
|
1476
|
+
// hard errors, anchored to the member token that closes the loop.
|
|
1477
|
+
validateAreaNesting(areas) {
|
|
1478
|
+
const areaById = new Map(areas.map((a) => [a.id, a]));
|
|
1479
|
+
const state = /* @__PURE__ */ new Map();
|
|
1480
|
+
const visit = (area) => {
|
|
1481
|
+
state.set(area.id, 1);
|
|
1482
|
+
area.members.forEach((mid, i) => {
|
|
1483
|
+
const range = area.memberRanges[i] ?? area.range;
|
|
1484
|
+
if (mid === area.id) {
|
|
1485
|
+
this.errors.push({
|
|
1486
|
+
message: `Area "${area.id}" cannot contain itself.`,
|
|
1487
|
+
range
|
|
1488
|
+
});
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
const child = areaById.get(mid);
|
|
1492
|
+
if (!child) return;
|
|
1493
|
+
if (state.get(mid) === 1) {
|
|
1494
|
+
this.errors.push({
|
|
1495
|
+
message: `Membership cycle: area "${area.id}" contains "${mid}", which already contains "${area.id}".`,
|
|
1496
|
+
range
|
|
1497
|
+
});
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
if (!state.has(mid)) visit(child);
|
|
1501
|
+
});
|
|
1502
|
+
state.set(area.id, 2);
|
|
1503
|
+
};
|
|
1504
|
+
for (const a of areas) {
|
|
1505
|
+
if (!state.has(a.id)) visit(a);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
statement(ctx) {
|
|
1509
|
+
if (ctx.edgeDecl) return this.visit(ctx.edgeDecl);
|
|
1510
|
+
if (ctx.nodeOrAreaDecl) {
|
|
1511
|
+
return this.visit(ctx.nodeOrAreaDecl);
|
|
1512
|
+
}
|
|
1513
|
+
return void 0;
|
|
1514
|
+
}
|
|
1515
|
+
// -- node / area --------------------------------------------------------
|
|
1516
|
+
nodeOrAreaDecl(ctx) {
|
|
1517
|
+
const idTok = ctx.Identifier[0];
|
|
1518
|
+
const id = idTok.image;
|
|
1519
|
+
const idRange = makeRange(idTok, idTok);
|
|
1520
|
+
const label = ctx.labelOrValue ? this.visit(ctx.labelOrValue).text : void 0;
|
|
1521
|
+
const labelRange = ctx.labelOrValue?.[0] ? this.labelValueRange(ctx.labelOrValue[0]) : void 0;
|
|
1522
|
+
const block = this.collectBlock(ctx);
|
|
1523
|
+
const endTok = ctx.RCurly?.[0] ?? (ctx.labelOrValue?.[0] ? this.lastTokenOfLabel(ctx.labelOrValue[0]) : void 0) ?? idTok;
|
|
1524
|
+
const range = makeRange(idTok, endTok);
|
|
1525
|
+
if (!ctx.LCurly) {
|
|
1526
|
+
return makeNode(id, label, "rectangle", range, idRange, labelRange);
|
|
1527
|
+
}
|
|
1528
|
+
const hasMembers = block.items.some((i) => i.kind === "member");
|
|
1529
|
+
const hasAttrs = block.items.some((i) => i.kind === "attr");
|
|
1530
|
+
if (hasMembers) {
|
|
1531
|
+
if (hasAttrs) {
|
|
1532
|
+
this.errors.push({
|
|
1533
|
+
message: `Container "${id}" mixes attributes and members; only one is allowed in an area.`,
|
|
1534
|
+
range
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
return this.buildArea(id, label, block, range);
|
|
1538
|
+
}
|
|
1539
|
+
return this.buildNode(id, label, block, range, idRange, labelRange);
|
|
1540
|
+
}
|
|
1541
|
+
buildNode(id, label, block, range, idRange, labelRange) {
|
|
1542
|
+
let shape = "rectangle";
|
|
1543
|
+
for (const item of block.items) {
|
|
1544
|
+
if (item.kind !== "attr") continue;
|
|
1545
|
+
if (item.path.length === 1 && item.path[0] === "shape") {
|
|
1546
|
+
const value = item.value?.text;
|
|
1547
|
+
if (value && SHAPE_NAMES.includes(value)) {
|
|
1548
|
+
shape = value;
|
|
1549
|
+
} else {
|
|
1550
|
+
this.errors.push({
|
|
1551
|
+
message: `Unknown shape "${value ?? ""}" on node "${id}".`,
|
|
1552
|
+
range: item.range
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
continue;
|
|
1556
|
+
}
|
|
1557
|
+
if (item.path.length > 2) {
|
|
1558
|
+
this.errors.push({
|
|
1559
|
+
message: `Unsupported dotted attribute "${item.path.join(
|
|
1560
|
+
"."
|
|
1561
|
+
)}" on node "${id}".`,
|
|
1562
|
+
range: item.range
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
return makeNode(id, label, shape, range, idRange, labelRange);
|
|
1567
|
+
}
|
|
1568
|
+
buildArea(id, label, block, range) {
|
|
1569
|
+
const members = [];
|
|
1570
|
+
const memberRanges = [];
|
|
1571
|
+
for (const item of block.items) {
|
|
1572
|
+
if (item.kind !== "member") continue;
|
|
1573
|
+
const memberId = item.path[0];
|
|
1574
|
+
if (members.includes(memberId)) {
|
|
1575
|
+
this.errors.push({
|
|
1576
|
+
message: `Area "${id}" lists member "${memberId}" more than once.`,
|
|
1577
|
+
range: item.range
|
|
1578
|
+
});
|
|
1579
|
+
continue;
|
|
1580
|
+
}
|
|
1581
|
+
members.push(memberId);
|
|
1582
|
+
memberRanges.push(item.range);
|
|
1583
|
+
}
|
|
1584
|
+
return { kind: "area", id, label, members, memberRanges, range };
|
|
1585
|
+
}
|
|
1586
|
+
// -- edge ---------------------------------------------------------------
|
|
1587
|
+
edgeDecl(ctx) {
|
|
1588
|
+
const sourceTok = ctx.Identifier[0];
|
|
1589
|
+
const targetTok = ctx.Identifier[1];
|
|
1590
|
+
const direction = this.visit(ctx.arrowOp);
|
|
1591
|
+
const label = ctx.labelOrValue ? this.visit(ctx.labelOrValue).text : void 0;
|
|
1592
|
+
let style = "solid";
|
|
1593
|
+
for (const a of ctx.attr ?? []) {
|
|
1594
|
+
const item = this.visit(a);
|
|
1595
|
+
if (item.kind !== "attr") continue;
|
|
1596
|
+
if (item.path.length === 2 && item.path[0] === "style" && item.path[1] === "stroke-dash") {
|
|
1597
|
+
style = strokeDashToStyle(item.value);
|
|
1598
|
+
continue;
|
|
1599
|
+
}
|
|
1600
|
+
if (item.path.length > 2) {
|
|
1601
|
+
this.errors.push({
|
|
1602
|
+
message: `Unsupported dotted attribute "${item.path.join(
|
|
1603
|
+
"."
|
|
1604
|
+
)}" on edge.`,
|
|
1605
|
+
range: item.range
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
const endTok = ctx.RCurly?.[0] ?? (ctx.labelOrValue?.[0] ? this.lastTokenOfLabel(ctx.labelOrValue[0]) : void 0) ?? targetTok;
|
|
1610
|
+
const range = makeRange(sourceTok, endTok);
|
|
1611
|
+
return {
|
|
1612
|
+
kind: "edge",
|
|
1613
|
+
source: sourceTok.image,
|
|
1614
|
+
target: targetTok.image,
|
|
1615
|
+
direction,
|
|
1616
|
+
label,
|
|
1617
|
+
style,
|
|
1618
|
+
range
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
arrowOp(ctx) {
|
|
1622
|
+
if (ctx.ArrowRight) return "forward";
|
|
1623
|
+
if (ctx.ArrowLeft) return "backward";
|
|
1624
|
+
if (ctx.ArrowBoth) return "bidirectional";
|
|
1625
|
+
return "none";
|
|
1626
|
+
}
|
|
1627
|
+
// -- block bodies -------------------------------------------------------
|
|
1628
|
+
blockItem(ctx) {
|
|
1629
|
+
if (ctx.attr) return this.visit(ctx.attr);
|
|
1630
|
+
return this.visit(ctx.memberStmt);
|
|
1631
|
+
}
|
|
1632
|
+
attr(ctx) {
|
|
1633
|
+
const path = ctx.Identifier.map((t) => t.image);
|
|
1634
|
+
const value = this.visit(ctx.attrValue);
|
|
1635
|
+
const range = makeRange(
|
|
1636
|
+
ctx.Identifier[0],
|
|
1637
|
+
this.lastTokenOfAttrValue(ctx.attrValue[0])
|
|
1638
|
+
);
|
|
1639
|
+
return { kind: "attr", path, value, range };
|
|
1640
|
+
}
|
|
1641
|
+
memberStmt(ctx) {
|
|
1642
|
+
const tok = ctx.Identifier[0];
|
|
1643
|
+
return {
|
|
1644
|
+
kind: "member",
|
|
1645
|
+
path: [tok.image],
|
|
1646
|
+
range: makeRange(tok, tok)
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
labelOrValue(ctx) {
|
|
1650
|
+
if (ctx.StringLit) {
|
|
1651
|
+
const tok = ctx.StringLit[0];
|
|
1652
|
+
return { kind: "string", text: unquote(tok.image) };
|
|
1653
|
+
}
|
|
1654
|
+
const text = ctx.Identifier.map((t) => t.image).join(" ");
|
|
1655
|
+
return { kind: "identifier", text };
|
|
1656
|
+
}
|
|
1657
|
+
attrValue(ctx) {
|
|
1658
|
+
if (ctx.StringLit) {
|
|
1659
|
+
const tok2 = ctx.StringLit[0];
|
|
1660
|
+
return { kind: "string", text: unquote(tok2.image), raw: tok2.image };
|
|
1661
|
+
}
|
|
1662
|
+
if (ctx.NumberLit) {
|
|
1663
|
+
const tok2 = ctx.NumberLit[0];
|
|
1664
|
+
return { kind: "number", text: tok2.image, raw: tok2.image };
|
|
1665
|
+
}
|
|
1666
|
+
const tok = ctx.Identifier[0];
|
|
1667
|
+
return { kind: "identifier", text: tok.image, raw: tok.image };
|
|
1668
|
+
}
|
|
1669
|
+
// -- helpers ------------------------------------------------------------
|
|
1670
|
+
collectBlock(ctx) {
|
|
1671
|
+
const items = [];
|
|
1672
|
+
for (const item of ctx.blockItem ?? []) {
|
|
1673
|
+
items.push(this.visit(item));
|
|
1674
|
+
}
|
|
1675
|
+
const start = ctx.LCurly?.[0];
|
|
1676
|
+
const end = ctx.RCurly?.[0];
|
|
1677
|
+
const range = start && end ? makeRange(start, end) : { start: dummyPos(), end: dummyPos() };
|
|
1678
|
+
return { items, range };
|
|
1679
|
+
}
|
|
1680
|
+
lastTokenOfLabel(node) {
|
|
1681
|
+
const ch = node.children;
|
|
1682
|
+
if (ch.StringLit?.[0]) return ch.StringLit[0];
|
|
1683
|
+
const ids = ch.Identifier;
|
|
1684
|
+
return ids?.[ids.length - 1];
|
|
1685
|
+
}
|
|
1686
|
+
// Source span covering the label value: the StringLit (quotes included) or
|
|
1687
|
+
// the run of unquoted identifier tokens. Used to rewrite a node's label in
|
|
1688
|
+
// place from the canvas editor.
|
|
1689
|
+
labelValueRange(node) {
|
|
1690
|
+
const ch = node.children;
|
|
1691
|
+
if (ch.StringLit?.[0]) return makeRange(ch.StringLit[0], ch.StringLit[0]);
|
|
1692
|
+
const ids = ch.Identifier;
|
|
1693
|
+
if (ids && ids.length > 0) return makeRange(ids[0], ids[ids.length - 1]);
|
|
1694
|
+
return void 0;
|
|
1695
|
+
}
|
|
1696
|
+
lastTokenOfAttrValue(node) {
|
|
1697
|
+
const ch = node.children;
|
|
1698
|
+
return ch.StringLit?.[0] ?? ch.NumberLit?.[0] ?? ch.Identifier[0];
|
|
1699
|
+
}
|
|
1700
|
+
};
|
|
1701
|
+
function makeNode(id, label, shape, range, idRange, labelRange) {
|
|
1702
|
+
return {
|
|
1703
|
+
kind: "node",
|
|
1704
|
+
id,
|
|
1705
|
+
label,
|
|
1706
|
+
shape,
|
|
1707
|
+
range,
|
|
1708
|
+
idRange,
|
|
1709
|
+
...labelRange ? { labelRange } : {}
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
function makeRange(start, end) {
|
|
1713
|
+
return {
|
|
1714
|
+
start: {
|
|
1715
|
+
line: start.startLine ?? 1,
|
|
1716
|
+
column: start.startColumn ?? 1,
|
|
1717
|
+
offset: start.startOffset
|
|
1718
|
+
},
|
|
1719
|
+
end: {
|
|
1720
|
+
line: end.endLine ?? start.startLine ?? 1,
|
|
1721
|
+
column: (end.endColumn ?? start.startColumn ?? 1) + 1,
|
|
1722
|
+
offset: (end.endOffset ?? start.startOffset) + 1
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
function dummyPos() {
|
|
1727
|
+
return { line: 1, column: 1, offset: 0 };
|
|
1728
|
+
}
|
|
1729
|
+
function unquote(raw) {
|
|
1730
|
+
const body = raw.slice(1, -1);
|
|
1731
|
+
let out = "";
|
|
1732
|
+
for (let i = 0; i < body.length; i++) {
|
|
1733
|
+
const c = body[i];
|
|
1734
|
+
if (c !== "\\" || i === body.length - 1) {
|
|
1735
|
+
out += c;
|
|
1736
|
+
continue;
|
|
1737
|
+
}
|
|
1738
|
+
const next = body[++i];
|
|
1739
|
+
switch (next) {
|
|
1740
|
+
case "n":
|
|
1741
|
+
out += "\n";
|
|
1742
|
+
break;
|
|
1743
|
+
case "t":
|
|
1744
|
+
out += " ";
|
|
1745
|
+
break;
|
|
1746
|
+
case "r":
|
|
1747
|
+
out += "\r";
|
|
1748
|
+
break;
|
|
1749
|
+
default:
|
|
1750
|
+
out += next;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
return out;
|
|
1754
|
+
}
|
|
1755
|
+
function strokeDashToStyle(value) {
|
|
1756
|
+
if (!value) return "solid";
|
|
1757
|
+
const n = Number(value.text);
|
|
1758
|
+
if (!Number.isFinite(n) || n <= 0) return "solid";
|
|
1759
|
+
if (n <= 3) return "dotted";
|
|
1760
|
+
return "dashed";
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
// src/parser/index.ts
|
|
1764
|
+
function emptyRange() {
|
|
1765
|
+
return {
|
|
1766
|
+
start: { line: 1, column: 1, offset: 0 },
|
|
1767
|
+
end: { line: 1, column: 1, offset: 0 }
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
function parse(source) {
|
|
1771
|
+
const lexResult = lexer.tokenize(source);
|
|
1772
|
+
const errors = lexResult.errors.map(lexErrorToParseError);
|
|
1773
|
+
parserInstance.input = lexResult.tokens;
|
|
1774
|
+
const cst = parserInstance.program();
|
|
1775
|
+
const eof = eofPos(source);
|
|
1776
|
+
for (const e of parserInstance.errors) {
|
|
1777
|
+
errors.push(parserErrorToParseError(e, eof));
|
|
1778
|
+
}
|
|
1779
|
+
const visitor = new D2Visitor();
|
|
1780
|
+
let diagram;
|
|
1781
|
+
try {
|
|
1782
|
+
diagram = visitor.visit(cst);
|
|
1783
|
+
} catch (err) {
|
|
1784
|
+
errors.push({
|
|
1785
|
+
message: err.message,
|
|
1786
|
+
range: emptyRange()
|
|
1787
|
+
});
|
|
1788
|
+
return { ok: false, errors };
|
|
1789
|
+
}
|
|
1790
|
+
errors.push(...visitor.errors);
|
|
1791
|
+
if (errors.length > 0) return { ok: false, errors };
|
|
1792
|
+
return { ok: true, diagram };
|
|
1793
|
+
}
|
|
1794
|
+
function lexErrorToParseError(e) {
|
|
1795
|
+
const line = e.line ?? 1;
|
|
1796
|
+
const column = e.column ?? 1;
|
|
1797
|
+
const offset = e.offset ?? 0;
|
|
1798
|
+
const length = e.length ?? 1;
|
|
1799
|
+
return {
|
|
1800
|
+
message: e.message,
|
|
1801
|
+
range: {
|
|
1802
|
+
start: { line, column, offset },
|
|
1803
|
+
end: { line, column: column + length, offset: offset + length }
|
|
1804
|
+
}
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1807
|
+
function parserErrorToParseError(e, eof) {
|
|
1808
|
+
const tok = e.token;
|
|
1809
|
+
const startLine = finite(tok?.startLine, eof.line);
|
|
1810
|
+
const startColumn = finite(tok?.startColumn, eof.column);
|
|
1811
|
+
const startOffset = finite(tok?.startOffset, eof.offset);
|
|
1812
|
+
return {
|
|
1813
|
+
message: e.message,
|
|
1814
|
+
range: {
|
|
1815
|
+
start: { line: startLine, column: startColumn, offset: startOffset },
|
|
1816
|
+
end: {
|
|
1817
|
+
line: finite(tok?.endLine, startLine),
|
|
1818
|
+
column: finite(tok?.endColumn, startColumn) + 1,
|
|
1819
|
+
offset: finite(tok?.endOffset, startOffset) + 1
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
function finite(v, fallback) {
|
|
1825
|
+
return typeof v === "number" && Number.isFinite(v) ? v : fallback;
|
|
1826
|
+
}
|
|
1827
|
+
function eofPos(source) {
|
|
1828
|
+
const nl = source.split("\n");
|
|
1829
|
+
return {
|
|
1830
|
+
line: nl.length,
|
|
1831
|
+
column: (nl[nl.length - 1]?.length ?? 0) + 1,
|
|
1832
|
+
offset: source.length
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
// src/file/layoutSchema.ts
|
|
1837
|
+
var JsonParser = class {
|
|
1838
|
+
constructor(text) {
|
|
1839
|
+
this.text = text;
|
|
1840
|
+
}
|
|
1841
|
+
text;
|
|
1842
|
+
i = 0;
|
|
1843
|
+
line = 1;
|
|
1844
|
+
col = 1;
|
|
1845
|
+
parse() {
|
|
1846
|
+
this.skipWs();
|
|
1847
|
+
const root = this.parseValue();
|
|
1848
|
+
this.skipWs();
|
|
1849
|
+
if (this.i < this.text.length) {
|
|
1850
|
+
throw this.error(`Unexpected trailing content`, this.pos());
|
|
1851
|
+
}
|
|
1852
|
+
return root;
|
|
1853
|
+
}
|
|
1854
|
+
pos() {
|
|
1855
|
+
return { line: this.line, column: this.col, offset: this.i };
|
|
1856
|
+
}
|
|
1857
|
+
rangeFrom(start) {
|
|
1858
|
+
return { start, end: this.pos() };
|
|
1859
|
+
}
|
|
1860
|
+
advance(n = 1) {
|
|
1861
|
+
for (let k = 0; k < n; k++) {
|
|
1862
|
+
const ch = this.text[this.i];
|
|
1863
|
+
if (ch === "\n") {
|
|
1864
|
+
this.line += 1;
|
|
1865
|
+
this.col = 1;
|
|
1866
|
+
} else {
|
|
1867
|
+
this.col += 1;
|
|
1868
|
+
}
|
|
1869
|
+
this.i += 1;
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
peek() {
|
|
1873
|
+
return this.text[this.i];
|
|
1874
|
+
}
|
|
1875
|
+
skipWs() {
|
|
1876
|
+
while (this.i < this.text.length) {
|
|
1877
|
+
const ch = this.text[this.i];
|
|
1878
|
+
if (ch === " " || ch === " " || ch === "\r" || ch === "\n") {
|
|
1879
|
+
this.advance();
|
|
1880
|
+
} else {
|
|
1881
|
+
break;
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
error(message, start) {
|
|
1886
|
+
const end = {
|
|
1887
|
+
line: start.line,
|
|
1888
|
+
column: start.column + 1,
|
|
1889
|
+
offset: Math.min(this.text.length, start.offset + 1)
|
|
1890
|
+
};
|
|
1891
|
+
const err = new Error(message);
|
|
1892
|
+
err.range = { start, end };
|
|
1893
|
+
return err;
|
|
1894
|
+
}
|
|
1895
|
+
parseValue() {
|
|
1896
|
+
this.skipWs();
|
|
1897
|
+
const ch = this.peek();
|
|
1898
|
+
if (ch === void 0) throw this.error("Unexpected end of input", this.pos());
|
|
1899
|
+
if (ch === "{") return this.parseObject();
|
|
1900
|
+
if (ch === "[") return this.parseArray();
|
|
1901
|
+
if (ch === '"') return this.parseString();
|
|
1902
|
+
if (ch === "-" || ch >= "0" && ch <= "9") return this.parseNumber();
|
|
1903
|
+
if (this.text.startsWith("true", this.i)) {
|
|
1904
|
+
const start = this.pos();
|
|
1905
|
+
this.advance(4);
|
|
1906
|
+
return { kind: "boolean", range: this.rangeFrom(start), value: true };
|
|
1907
|
+
}
|
|
1908
|
+
if (this.text.startsWith("false", this.i)) {
|
|
1909
|
+
const start = this.pos();
|
|
1910
|
+
this.advance(5);
|
|
1911
|
+
return { kind: "boolean", range: this.rangeFrom(start), value: false };
|
|
1912
|
+
}
|
|
1913
|
+
if (this.text.startsWith("null", this.i)) {
|
|
1914
|
+
const start = this.pos();
|
|
1915
|
+
this.advance(4);
|
|
1916
|
+
return { kind: "null", range: this.rangeFrom(start) };
|
|
1917
|
+
}
|
|
1918
|
+
throw this.error(`Unexpected character '${ch}'`, this.pos());
|
|
1919
|
+
}
|
|
1920
|
+
parseObject() {
|
|
1921
|
+
const start = this.pos();
|
|
1922
|
+
this.advance();
|
|
1923
|
+
const entries = [];
|
|
1924
|
+
this.skipWs();
|
|
1925
|
+
if (this.peek() === "}") {
|
|
1926
|
+
this.advance();
|
|
1927
|
+
return { kind: "object", range: this.rangeFrom(start), entries };
|
|
1928
|
+
}
|
|
1929
|
+
while (true) {
|
|
1930
|
+
this.skipWs();
|
|
1931
|
+
if (this.peek() !== '"') {
|
|
1932
|
+
throw this.error("Expected string key", this.pos());
|
|
1933
|
+
}
|
|
1934
|
+
const keyStart = this.pos();
|
|
1935
|
+
const keyNode = this.parseString();
|
|
1936
|
+
const keyRange = { start: keyStart, end: this.pos() };
|
|
1937
|
+
this.skipWs();
|
|
1938
|
+
if (this.peek() !== ":") {
|
|
1939
|
+
throw this.error(`Expected ':' after key`, this.pos());
|
|
1940
|
+
}
|
|
1941
|
+
this.advance();
|
|
1942
|
+
const value = this.parseValue();
|
|
1943
|
+
entries.push({ key: keyNode.kind === "string" ? keyNode.value : "", keyRange, value });
|
|
1944
|
+
this.skipWs();
|
|
1945
|
+
const next = this.peek();
|
|
1946
|
+
if (next === ",") {
|
|
1947
|
+
this.advance();
|
|
1948
|
+
continue;
|
|
1949
|
+
}
|
|
1950
|
+
if (next === "}") {
|
|
1951
|
+
this.advance();
|
|
1952
|
+
break;
|
|
1953
|
+
}
|
|
1954
|
+
throw this.error(`Expected ',' or '}'`, this.pos());
|
|
1955
|
+
}
|
|
1956
|
+
return { kind: "object", range: this.rangeFrom(start), entries };
|
|
1957
|
+
}
|
|
1958
|
+
parseArray() {
|
|
1959
|
+
const start = this.pos();
|
|
1960
|
+
this.advance();
|
|
1961
|
+
const items = [];
|
|
1962
|
+
this.skipWs();
|
|
1963
|
+
if (this.peek() === "]") {
|
|
1964
|
+
this.advance();
|
|
1965
|
+
return { kind: "array", range: this.rangeFrom(start), items };
|
|
1966
|
+
}
|
|
1967
|
+
while (true) {
|
|
1968
|
+
items.push(this.parseValue());
|
|
1969
|
+
this.skipWs();
|
|
1970
|
+
const next = this.peek();
|
|
1971
|
+
if (next === ",") {
|
|
1972
|
+
this.advance();
|
|
1973
|
+
continue;
|
|
1974
|
+
}
|
|
1975
|
+
if (next === "]") {
|
|
1976
|
+
this.advance();
|
|
1977
|
+
break;
|
|
1978
|
+
}
|
|
1979
|
+
throw this.error(`Expected ',' or ']'`, this.pos());
|
|
1980
|
+
}
|
|
1981
|
+
return { kind: "array", range: this.rangeFrom(start), items };
|
|
1982
|
+
}
|
|
1983
|
+
parseString() {
|
|
1984
|
+
const start = this.pos();
|
|
1985
|
+
this.advance();
|
|
1986
|
+
let value = "";
|
|
1987
|
+
while (this.i < this.text.length) {
|
|
1988
|
+
const ch = this.peek();
|
|
1989
|
+
if (ch === '"') {
|
|
1990
|
+
this.advance();
|
|
1991
|
+
return { kind: "string", range: this.rangeFrom(start), value };
|
|
1992
|
+
}
|
|
1993
|
+
if (ch === "\n") {
|
|
1994
|
+
throw this.error("Unterminated string", this.pos());
|
|
1995
|
+
}
|
|
1996
|
+
if (ch === "\\") {
|
|
1997
|
+
this.advance();
|
|
1998
|
+
const esc = this.peek();
|
|
1999
|
+
if (esc === void 0) throw this.error("Unterminated escape", this.pos());
|
|
2000
|
+
switch (esc) {
|
|
2001
|
+
case '"':
|
|
2002
|
+
value += '"';
|
|
2003
|
+
this.advance();
|
|
2004
|
+
break;
|
|
2005
|
+
case "\\":
|
|
2006
|
+
value += "\\";
|
|
2007
|
+
this.advance();
|
|
2008
|
+
break;
|
|
2009
|
+
case "/":
|
|
2010
|
+
value += "/";
|
|
2011
|
+
this.advance();
|
|
2012
|
+
break;
|
|
2013
|
+
case "b":
|
|
2014
|
+
value += "\b";
|
|
2015
|
+
this.advance();
|
|
2016
|
+
break;
|
|
2017
|
+
case "f":
|
|
2018
|
+
value += "\f";
|
|
2019
|
+
this.advance();
|
|
2020
|
+
break;
|
|
2021
|
+
case "n":
|
|
2022
|
+
value += "\n";
|
|
2023
|
+
this.advance();
|
|
2024
|
+
break;
|
|
2025
|
+
case "r":
|
|
2026
|
+
value += "\r";
|
|
2027
|
+
this.advance();
|
|
2028
|
+
break;
|
|
2029
|
+
case "t":
|
|
2030
|
+
value += " ";
|
|
2031
|
+
this.advance();
|
|
2032
|
+
break;
|
|
2033
|
+
case "u": {
|
|
2034
|
+
this.advance();
|
|
2035
|
+
const hex = this.text.slice(this.i, this.i + 4);
|
|
2036
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
|
|
2037
|
+
throw this.error("Invalid unicode escape", this.pos());
|
|
2038
|
+
}
|
|
2039
|
+
value += String.fromCharCode(parseInt(hex, 16));
|
|
2040
|
+
this.advance(4);
|
|
2041
|
+
break;
|
|
2042
|
+
}
|
|
2043
|
+
default:
|
|
2044
|
+
throw this.error(`Invalid escape \\${esc}`, this.pos());
|
|
2045
|
+
}
|
|
2046
|
+
continue;
|
|
2047
|
+
}
|
|
2048
|
+
value += ch;
|
|
2049
|
+
this.advance();
|
|
2050
|
+
}
|
|
2051
|
+
throw this.error("Unterminated string", this.pos());
|
|
2052
|
+
}
|
|
2053
|
+
parseNumber() {
|
|
2054
|
+
const start = this.pos();
|
|
2055
|
+
const match = /-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/y;
|
|
2056
|
+
match.lastIndex = this.i;
|
|
2057
|
+
const m = match.exec(this.text);
|
|
2058
|
+
if (!m) throw this.error("Invalid number", start);
|
|
2059
|
+
this.advance(m[0].length);
|
|
2060
|
+
return { kind: "number", range: this.rangeFrom(start), value: parseFloat(m[0]) };
|
|
2061
|
+
}
|
|
2062
|
+
};
|
|
2063
|
+
var PALETTE_COLORS = /* @__PURE__ */ new Set([
|
|
2064
|
+
"black",
|
|
2065
|
+
"gray",
|
|
2066
|
+
"red",
|
|
2067
|
+
"orange",
|
|
2068
|
+
"yellow",
|
|
2069
|
+
"green",
|
|
2070
|
+
"teal",
|
|
2071
|
+
"blue",
|
|
2072
|
+
"purple",
|
|
2073
|
+
"pink"
|
|
2074
|
+
]);
|
|
2075
|
+
var FILL_COLORS = /* @__PURE__ */ new Set([...PALETTE_COLORS, "transparent", "white"]);
|
|
2076
|
+
var SIZES = /* @__PURE__ */ new Set(["S", "M", "L", "XL"]);
|
|
2077
|
+
var LINE_STYLES = /* @__PURE__ */ new Set(["solid", "dashed", "dotted"]);
|
|
2078
|
+
var END_CAPS = /* @__PURE__ */ new Set(["none", "arrow", "dot", "diamond"]);
|
|
2079
|
+
var ICON_POSITIONS = /* @__PURE__ */ new Set(["corner", "top"]);
|
|
2080
|
+
var SIDES = /* @__PURE__ */ new Set(["N", "S", "E", "W"]);
|
|
2081
|
+
var NODE_FIELDS = /* @__PURE__ */ new Set([
|
|
2082
|
+
"cx",
|
|
2083
|
+
"cy",
|
|
2084
|
+
"w",
|
|
2085
|
+
"h",
|
|
2086
|
+
"textSize",
|
|
2087
|
+
"textColor",
|
|
2088
|
+
"borderColor",
|
|
2089
|
+
"borderStyle",
|
|
2090
|
+
"fillColor",
|
|
2091
|
+
"icon",
|
|
2092
|
+
"iconPosition",
|
|
2093
|
+
"shape"
|
|
2094
|
+
]);
|
|
2095
|
+
var SHAPES = /* @__PURE__ */ new Set(["rectangle", "cylinder", "person"]);
|
|
2096
|
+
var EDGE_FIELDS = /* @__PURE__ */ new Set([
|
|
2097
|
+
"color",
|
|
2098
|
+
"lineStyle",
|
|
2099
|
+
"width",
|
|
2100
|
+
"startCap",
|
|
2101
|
+
"endCap",
|
|
2102
|
+
"sourceSide",
|
|
2103
|
+
"targetSide",
|
|
2104
|
+
"labelDx",
|
|
2105
|
+
"labelDy"
|
|
2106
|
+
]);
|
|
2107
|
+
var AREA_FIELDS = /* @__PURE__ */ new Set(["borderColor", "borderStyle", "fillColor"]);
|
|
2108
|
+
var ROOT_FIELDS = /* @__PURE__ */ new Set(["gridSize", "nodes", "edges", "areas"]);
|
|
2109
|
+
var push = (ctx, message, range) => {
|
|
2110
|
+
ctx.errors.push({ message, range });
|
|
2111
|
+
};
|
|
2112
|
+
var validateEnum = (node, allowed, label, ctx) => {
|
|
2113
|
+
if (node.kind !== "string") {
|
|
2114
|
+
push(ctx, `${label} must be a string`, node.range);
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2117
|
+
if (!allowed.has(node.value)) {
|
|
2118
|
+
const list = [...allowed].join(", ");
|
|
2119
|
+
push(ctx, `${label} must be one of: ${list}`, node.range);
|
|
2120
|
+
}
|
|
2121
|
+
};
|
|
2122
|
+
var validateNumber = (node, label, ctx, opts = {}) => {
|
|
2123
|
+
if (node.kind !== "number") {
|
|
2124
|
+
push(ctx, `${label} must be a number`, node.range);
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
if (opts.integer && !Number.isInteger(node.value)) {
|
|
2128
|
+
push(ctx, `${label} must be an integer`, node.range);
|
|
2129
|
+
}
|
|
2130
|
+
if (opts.min !== void 0 && node.value < opts.min) {
|
|
2131
|
+
push(ctx, `${label} must be \u2265 ${opts.min}`, node.range);
|
|
2132
|
+
}
|
|
2133
|
+
};
|
|
2134
|
+
var validateNode = (node, key, ctx) => {
|
|
2135
|
+
if (node.kind !== "object") {
|
|
2136
|
+
push(ctx, `Node "${key}" must be an object`, node.range);
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
2139
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2140
|
+
for (const entry of node.entries) {
|
|
2141
|
+
if (seen.has(entry.key)) {
|
|
2142
|
+
push(ctx, `Duplicate field "${entry.key}"`, entry.keyRange);
|
|
2143
|
+
}
|
|
2144
|
+
seen.add(entry.key);
|
|
2145
|
+
switch (entry.key) {
|
|
2146
|
+
case "cx":
|
|
2147
|
+
case "cy":
|
|
2148
|
+
validateNumber(entry.value, entry.key, ctx);
|
|
2149
|
+
break;
|
|
2150
|
+
case "w":
|
|
2151
|
+
case "h":
|
|
2152
|
+
validateNumber(entry.value, entry.key, ctx, { min: 1 });
|
|
2153
|
+
break;
|
|
2154
|
+
case "textSize":
|
|
2155
|
+
validateEnum(entry.value, SIZES, "textSize", ctx);
|
|
2156
|
+
break;
|
|
2157
|
+
case "textColor":
|
|
2158
|
+
case "borderColor":
|
|
2159
|
+
validateEnum(entry.value, PALETTE_COLORS, entry.key, ctx);
|
|
2160
|
+
break;
|
|
2161
|
+
case "fillColor":
|
|
2162
|
+
validateEnum(entry.value, FILL_COLORS, "fillColor", ctx);
|
|
2163
|
+
break;
|
|
2164
|
+
case "borderStyle":
|
|
2165
|
+
validateEnum(entry.value, LINE_STYLES, "borderStyle", ctx);
|
|
2166
|
+
break;
|
|
2167
|
+
case "iconPosition":
|
|
2168
|
+
validateEnum(entry.value, ICON_POSITIONS, "iconPosition", ctx);
|
|
2169
|
+
break;
|
|
2170
|
+
case "shape":
|
|
2171
|
+
validateEnum(entry.value, SHAPES, "shape", ctx);
|
|
2172
|
+
break;
|
|
2173
|
+
case "icon":
|
|
2174
|
+
if (entry.value.kind !== "string") {
|
|
2175
|
+
push(ctx, "icon must be a string", entry.value.range);
|
|
2176
|
+
} else if (!iconById(entry.value.value)) {
|
|
2177
|
+
push(ctx, `Unknown icon "${entry.value.value}"`, entry.value.range);
|
|
2178
|
+
}
|
|
2179
|
+
break;
|
|
2180
|
+
default:
|
|
2181
|
+
if (!NODE_FIELDS.has(entry.key)) {
|
|
2182
|
+
push(ctx, `Unknown node field "${entry.key}"`, entry.keyRange);
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
for (const req of ["cx", "cy", "w", "h"]) {
|
|
2187
|
+
if (!seen.has(req)) {
|
|
2188
|
+
push(ctx, `Node "${key}" is missing "${req}"`, node.range);
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
};
|
|
2192
|
+
var validateEdge = (node, key, ctx) => {
|
|
2193
|
+
if (node.kind !== "object") {
|
|
2194
|
+
push(ctx, `Edge "${key}" must be an object`, node.range);
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2198
|
+
for (const entry of node.entries) {
|
|
2199
|
+
if (seen.has(entry.key)) {
|
|
2200
|
+
push(ctx, `Duplicate field "${entry.key}"`, entry.keyRange);
|
|
2201
|
+
}
|
|
2202
|
+
seen.add(entry.key);
|
|
2203
|
+
switch (entry.key) {
|
|
2204
|
+
case "color":
|
|
2205
|
+
validateEnum(entry.value, PALETTE_COLORS, "color", ctx);
|
|
2206
|
+
break;
|
|
2207
|
+
case "lineStyle":
|
|
2208
|
+
validateEnum(entry.value, LINE_STYLES, "lineStyle", ctx);
|
|
2209
|
+
break;
|
|
2210
|
+
case "width":
|
|
2211
|
+
validateEnum(entry.value, SIZES, "width", ctx);
|
|
2212
|
+
break;
|
|
2213
|
+
case "startCap":
|
|
2214
|
+
case "endCap":
|
|
2215
|
+
validateEnum(entry.value, END_CAPS, entry.key, ctx);
|
|
2216
|
+
break;
|
|
2217
|
+
case "sourceSide":
|
|
2218
|
+
case "targetSide":
|
|
2219
|
+
validateEnum(entry.value, SIDES, entry.key, ctx);
|
|
2220
|
+
break;
|
|
2221
|
+
case "labelDx":
|
|
2222
|
+
case "labelDy":
|
|
2223
|
+
validateNumber(entry.value, entry.key, ctx, { integer: true });
|
|
2224
|
+
break;
|
|
2225
|
+
default:
|
|
2226
|
+
if (!EDGE_FIELDS.has(entry.key)) {
|
|
2227
|
+
push(ctx, `Unknown edge field "${entry.key}"`, entry.keyRange);
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
};
|
|
2232
|
+
var validateArea = (node, key, ctx) => {
|
|
2233
|
+
if (node.kind !== "object") {
|
|
2234
|
+
push(ctx, `Area "${key}" must be an object`, node.range);
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2237
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2238
|
+
for (const entry of node.entries) {
|
|
2239
|
+
if (seen.has(entry.key)) {
|
|
2240
|
+
push(ctx, `Duplicate field "${entry.key}"`, entry.keyRange);
|
|
2241
|
+
}
|
|
2242
|
+
seen.add(entry.key);
|
|
2243
|
+
switch (entry.key) {
|
|
2244
|
+
case "borderColor":
|
|
2245
|
+
validateEnum(entry.value, PALETTE_COLORS, "borderColor", ctx);
|
|
2246
|
+
break;
|
|
2247
|
+
case "borderStyle":
|
|
2248
|
+
validateEnum(entry.value, LINE_STYLES, "borderStyle", ctx);
|
|
2249
|
+
break;
|
|
2250
|
+
case "fillColor":
|
|
2251
|
+
validateEnum(entry.value, FILL_COLORS, "fillColor", ctx);
|
|
2252
|
+
break;
|
|
2253
|
+
default:
|
|
2254
|
+
if (!AREA_FIELDS.has(entry.key)) {
|
|
2255
|
+
push(ctx, `Unknown area field "${entry.key}"`, entry.keyRange);
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
};
|
|
2260
|
+
var validateMap = (node, label, ctx, validate) => {
|
|
2261
|
+
if (node.kind !== "object") {
|
|
2262
|
+
push(ctx, `${label} must be an object`, node.range);
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2265
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2266
|
+
for (const entry of node.entries) {
|
|
2267
|
+
if (seen.has(entry.key)) {
|
|
2268
|
+
push(ctx, `Duplicate key "${entry.key}"`, entry.keyRange);
|
|
2269
|
+
}
|
|
2270
|
+
seen.add(entry.key);
|
|
2271
|
+
validate(entry.value, entry.key, ctx);
|
|
2272
|
+
}
|
|
2273
|
+
};
|
|
2274
|
+
var validateRoot = (node, ctx) => {
|
|
2275
|
+
if (node.kind !== "object") {
|
|
2276
|
+
push(ctx, "Layout root must be an object", node.range);
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2280
|
+
for (const entry of node.entries) {
|
|
2281
|
+
if (seen.has(entry.key)) {
|
|
2282
|
+
push(ctx, `Duplicate field "${entry.key}"`, entry.keyRange);
|
|
2283
|
+
}
|
|
2284
|
+
seen.add(entry.key);
|
|
2285
|
+
switch (entry.key) {
|
|
2286
|
+
case "gridSize":
|
|
2287
|
+
validateNumber(entry.value, "gridSize", ctx, { integer: true, min: 1 });
|
|
2288
|
+
break;
|
|
2289
|
+
case "nodes":
|
|
2290
|
+
validateMap(entry.value, "nodes", ctx, validateNode);
|
|
2291
|
+
break;
|
|
2292
|
+
case "edges":
|
|
2293
|
+
validateMap(entry.value, "edges", ctx, validateEdge);
|
|
2294
|
+
break;
|
|
2295
|
+
case "areas":
|
|
2296
|
+
validateMap(entry.value, "areas", ctx, validateArea);
|
|
2297
|
+
break;
|
|
2298
|
+
default:
|
|
2299
|
+
if (!ROOT_FIELDS.has(entry.key)) {
|
|
2300
|
+
push(ctx, `Unknown root field "${entry.key}"`, entry.keyRange);
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
for (const req of ["gridSize", "nodes", "edges"]) {
|
|
2305
|
+
if (!seen.has(req)) {
|
|
2306
|
+
push(ctx, `Layout is missing "${req}"`, node.range);
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
};
|
|
2310
|
+
var nodeToValue = (node) => {
|
|
2311
|
+
switch (node.kind) {
|
|
2312
|
+
case "string":
|
|
2313
|
+
return node.value;
|
|
2314
|
+
case "number":
|
|
2315
|
+
return node.value;
|
|
2316
|
+
case "boolean":
|
|
2317
|
+
return node.value;
|
|
2318
|
+
case "null":
|
|
2319
|
+
return null;
|
|
2320
|
+
case "array":
|
|
2321
|
+
return node.items.map(nodeToValue);
|
|
2322
|
+
case "object": {
|
|
2323
|
+
const out = {};
|
|
2324
|
+
for (const entry of node.entries) out[entry.key] = nodeToValue(entry.value);
|
|
2325
|
+
return out;
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
};
|
|
2329
|
+
var validateLayoutJson = (text) => {
|
|
2330
|
+
let root;
|
|
2331
|
+
try {
|
|
2332
|
+
root = new JsonParser(text).parse();
|
|
2333
|
+
} catch (e) {
|
|
2334
|
+
const err = e;
|
|
2335
|
+
return {
|
|
2336
|
+
value: null,
|
|
2337
|
+
errors: [{ message: err.message, range: err.range }]
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
const ctx = { errors: [] };
|
|
2341
|
+
validateRoot(root, ctx);
|
|
2342
|
+
if (ctx.errors.length > 0) {
|
|
2343
|
+
return { value: null, errors: ctx.errors };
|
|
2344
|
+
}
|
|
2345
|
+
return { value: nodeToValue(root), errors: [] };
|
|
2346
|
+
};
|
|
2347
|
+
|
|
2348
|
+
// src/layout/normalize.ts
|
|
2349
|
+
var DEFAULT_NODE_W = 4;
|
|
2350
|
+
var DEFAULT_NODE_H = 2;
|
|
2351
|
+
var START_CX = 4;
|
|
2352
|
+
var START_CY = 2;
|
|
2353
|
+
var COL_STRIDE = 6;
|
|
2354
|
+
var ROW_STRIDE = 4;
|
|
2355
|
+
var WRAP_COLS = 6;
|
|
2356
|
+
function normalizeForRoute(diagram, layout) {
|
|
2357
|
+
const missing = diagram.nodes.filter((n) => !layout.nodes[n.id]);
|
|
2358
|
+
if (missing.length === 0) return layout;
|
|
2359
|
+
const placed = Object.values(layout.nodes);
|
|
2360
|
+
const startCy = placed.length === 0 ? START_CY : Math.ceil(Math.max(...placed.map((n) => n.cy + n.h / 2))) + 2;
|
|
2361
|
+
const nodes = { ...layout.nodes };
|
|
2362
|
+
missing.forEach((node, i) => {
|
|
2363
|
+
const col = i % WRAP_COLS;
|
|
2364
|
+
const row = Math.floor(i / WRAP_COLS);
|
|
2365
|
+
nodes[node.id] = {
|
|
2366
|
+
cx: START_CX + col * COL_STRIDE,
|
|
2367
|
+
cy: startCy + row * ROW_STRIDE,
|
|
2368
|
+
w: DEFAULT_NODE_W,
|
|
2369
|
+
h: DEFAULT_NODE_H
|
|
2370
|
+
};
|
|
2371
|
+
});
|
|
2372
|
+
return { ...layout, nodes };
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
// server/render/model.ts
|
|
2376
|
+
var fallbackLayout = () => ({ gridSize: 40, nodes: {}, edges: {} });
|
|
2377
|
+
var buildRenderModel = async (d2, layoutText) => {
|
|
2378
|
+
const parsed = parse(d2);
|
|
2379
|
+
if (!parsed.ok) {
|
|
2380
|
+
const first = parsed.errors[0];
|
|
2381
|
+
return {
|
|
2382
|
+
error: first ? `${first.range.start.line}:${first.range.start.column} ${first.message}` : "parse error"
|
|
2383
|
+
};
|
|
2384
|
+
}
|
|
2385
|
+
const layout = layoutText !== null ? validateLayoutJson(layoutText).value ?? fallbackLayout() : fallbackLayout();
|
|
2386
|
+
const routed = await route(parsed.diagram, normalizeForRoute(parsed.diagram, layout));
|
|
2387
|
+
const nodes = {};
|
|
2388
|
+
for (const node of parsed.diagram.nodes) {
|
|
2389
|
+
nodes[node.id] = { shape: node.shape, label: node.label };
|
|
2390
|
+
}
|
|
2391
|
+
const edges = {};
|
|
2392
|
+
parsed.diagram.edges.forEach((edge, i) => {
|
|
2393
|
+
edges[makeEdgeId(edge.source, edge.target, i)] = {
|
|
2394
|
+
label: edge.label,
|
|
2395
|
+
style: edge.style,
|
|
2396
|
+
marker: edge.direction
|
|
2397
|
+
};
|
|
2398
|
+
});
|
|
2399
|
+
return { routed, nodes, edges };
|
|
2400
|
+
};
|
|
2401
|
+
|
|
2402
|
+
// server/render/png.ts
|
|
2403
|
+
import { Resvg } from "@resvg/resvg-js";
|
|
2404
|
+
var svgToPng = (svg, opts = {}) => {
|
|
2405
|
+
const scale = opts.scale ?? 2;
|
|
2406
|
+
const resvg = new Resvg(svg, {
|
|
2407
|
+
background: opts.background ?? "#ffffff",
|
|
2408
|
+
fitTo: scale === 1 ? { mode: "original" } : { mode: "zoom", value: scale },
|
|
2409
|
+
// The diagram's text was laid out with approximate metrics, so any clean
|
|
2410
|
+
// sans-serif is fine; load whatever the host has and fall back to a generic.
|
|
2411
|
+
font: { loadSystemFonts: true, defaultFontFamily: "Arial" }
|
|
2412
|
+
});
|
|
2413
|
+
return resvg.render().asPng();
|
|
2414
|
+
};
|
|
2415
|
+
|
|
2416
|
+
// server/render/pngText.ts
|
|
2417
|
+
var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
2418
|
+
var CRC_TABLE = (() => {
|
|
2419
|
+
const table = new Uint32Array(256);
|
|
2420
|
+
for (let n = 0; n < 256; n += 1) {
|
|
2421
|
+
let c = n;
|
|
2422
|
+
for (let k = 0; k < 8; k += 1) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
2423
|
+
table[n] = c >>> 0;
|
|
2424
|
+
}
|
|
2425
|
+
return table;
|
|
2426
|
+
})();
|
|
2427
|
+
var crc32 = (buf) => {
|
|
2428
|
+
let c = 4294967295;
|
|
2429
|
+
for (let i = 0; i < buf.length; i += 1) c = CRC_TABLE[(c ^ buf[i]) & 255] ^ c >>> 8;
|
|
2430
|
+
return (c ^ 4294967295) >>> 0;
|
|
2431
|
+
};
|
|
2432
|
+
var iTXtChunk = (keyword, text) => {
|
|
2433
|
+
const body = Buffer.concat([
|
|
2434
|
+
Buffer.from(keyword, "latin1"),
|
|
2435
|
+
Buffer.from([0]),
|
|
2436
|
+
// keyword null-terminator
|
|
2437
|
+
Buffer.from([0, 0]),
|
|
2438
|
+
// compression flag (0 = none) + method
|
|
2439
|
+
Buffer.from([0]),
|
|
2440
|
+
// empty language tag + null
|
|
2441
|
+
Buffer.from([0]),
|
|
2442
|
+
// empty translated keyword + null
|
|
2443
|
+
Buffer.from(text, "utf8")
|
|
2444
|
+
]);
|
|
2445
|
+
const type = Buffer.from("iTXt", "latin1");
|
|
2446
|
+
const len = Buffer.alloc(4);
|
|
2447
|
+
len.writeUInt32BE(body.length, 0);
|
|
2448
|
+
const crc = Buffer.alloc(4);
|
|
2449
|
+
crc.writeUInt32BE(crc32(Buffer.concat([type, body])), 0);
|
|
2450
|
+
return Buffer.concat([len, type, body, crc]);
|
|
2451
|
+
};
|
|
2452
|
+
var isPng = (png) => png.length >= 33 && png.subarray(0, 8).equals(PNG_SIGNATURE) && png.subarray(12, 16).toString("latin1") === "IHDR";
|
|
2453
|
+
var embedPngText = (png, entries) => {
|
|
2454
|
+
if (entries.length === 0 || !isPng(png)) return png;
|
|
2455
|
+
const ihdrLen = png.readUInt32BE(8);
|
|
2456
|
+
const insertAt = 8 + 4 + 4 + ihdrLen + 4;
|
|
2457
|
+
const chunks = entries.map((e) => iTXtChunk(e.keyword, e.text));
|
|
2458
|
+
return Buffer.concat([png.subarray(0, insertAt), ...chunks, png.subarray(insertAt)]);
|
|
2459
|
+
};
|
|
2460
|
+
var readPngText = (png) => {
|
|
2461
|
+
const out = {};
|
|
2462
|
+
if (!isPng(png)) return out;
|
|
2463
|
+
let off = 8;
|
|
2464
|
+
while (off + 8 <= png.length) {
|
|
2465
|
+
const len = png.readUInt32BE(off);
|
|
2466
|
+
const type = png.subarray(off + 4, off + 8).toString("latin1");
|
|
2467
|
+
const data = png.subarray(off + 8, off + 8 + len);
|
|
2468
|
+
if (type === "tEXt") {
|
|
2469
|
+
const sep = data.indexOf(0);
|
|
2470
|
+
if (sep >= 0) out[data.subarray(0, sep).toString("latin1")] = data.subarray(sep + 1).toString("latin1");
|
|
2471
|
+
} else if (type === "iTXt") {
|
|
2472
|
+
const kwEnd = data.indexOf(0);
|
|
2473
|
+
if (kwEnd >= 0) {
|
|
2474
|
+
const keyword = data.subarray(0, kwEnd).toString("latin1");
|
|
2475
|
+
const langStart = kwEnd + 3;
|
|
2476
|
+
const langEnd = data.indexOf(0, langStart);
|
|
2477
|
+
const transEnd = data.indexOf(0, langEnd + 1);
|
|
2478
|
+
out[keyword] = data.subarray(transEnd + 1).toString("utf8");
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
if (type === "IEND") break;
|
|
2482
|
+
off += 8 + len + 4;
|
|
2483
|
+
}
|
|
2484
|
+
return out;
|
|
2485
|
+
};
|
|
2486
|
+
var PNG_SOURCE_KEYS = {
|
|
2487
|
+
d2: "epure.d2",
|
|
2488
|
+
layout: "epure.layout.json"
|
|
2489
|
+
};
|
|
2490
|
+
var PNG_MARKER_KEYS = {
|
|
2491
|
+
software: "Software",
|
|
2492
|
+
description: "Description"
|
|
2493
|
+
};
|
|
2494
|
+
var EPURE_SOFTWARE = "\xC9pure \u2014 github:theodo-group/epure";
|
|
2495
|
+
var epureDescription = (hasLayout) => [
|
|
2496
|
+
"Architecture diagram made with \xC9pure, a grid-snapped, git-reviewable diagram editor.",
|
|
2497
|
+
`Its editable source is embedded in this PNG as text chunks: "${PNG_SOURCE_KEYS.d2}" is the` + (hasLayout ? ` diagram topology and "${PNG_SOURCE_KEYS.layout}" is the layout.` : " diagram topology."),
|
|
2498
|
+
"Extract it with `npx -y github:theodo-group/epure source <this-file.png>`,",
|
|
2499
|
+
"then edit the .epr.d2 / .epr.layout.json pair and open it live with",
|
|
2500
|
+
"`npx -y github:theodo-group/epure <name>.epr.d2`."
|
|
2501
|
+
].join(" ");
|
|
2502
|
+
var epureMetaEntries = (d2, layoutText) => [
|
|
2503
|
+
{ keyword: PNG_MARKER_KEYS.software, text: EPURE_SOFTWARE },
|
|
2504
|
+
{ keyword: PNG_MARKER_KEYS.description, text: epureDescription(layoutText !== null) },
|
|
2505
|
+
{ keyword: PNG_SOURCE_KEYS.d2, text: d2 },
|
|
2506
|
+
...layoutText !== null ? [{ keyword: PNG_SOURCE_KEYS.layout, text: layoutText }] : []
|
|
2507
|
+
];
|
|
2508
|
+
|
|
2509
|
+
// server/render/svg.tsx
|
|
2510
|
+
import { readFileSync } from "fs";
|
|
2511
|
+
import { join } from "path";
|
|
2512
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
2513
|
+
|
|
2514
|
+
// src/layout/crossings.ts
|
|
2515
|
+
var AX_EPS = 0.5;
|
|
2516
|
+
var END_EPS = 1;
|
|
2517
|
+
var GAP_BASE = 7;
|
|
2518
|
+
var widthOf = (e) => STROKE_WIDTH[e.width ?? "M"];
|
|
2519
|
+
var segmentsOf = (pts) => {
|
|
2520
|
+
const out = [];
|
|
2521
|
+
for (let i = 1; i < pts.length; i += 1) {
|
|
2522
|
+
const a = pts[i - 1];
|
|
2523
|
+
const b = pts[i];
|
|
2524
|
+
out.push({ x1: a.x, y1: a.y, x2: b.x, y2: b.y });
|
|
2525
|
+
}
|
|
2526
|
+
return out;
|
|
2527
|
+
};
|
|
2528
|
+
var isHorizontal = (s) => Math.abs(s.y1 - s.y2) <= AX_EPS && Math.abs(s.x1 - s.x2) > AX_EPS;
|
|
2529
|
+
var isVertical = (s) => Math.abs(s.x1 - s.x2) <= AX_EPS && Math.abs(s.y1 - s.y2) > AX_EPS;
|
|
2530
|
+
var intersect = (h, v) => {
|
|
2531
|
+
const hy = (h.y1 + h.y2) / 2;
|
|
2532
|
+
const vx = (v.x1 + v.x2) / 2;
|
|
2533
|
+
const hxLo = Math.min(h.x1, h.x2);
|
|
2534
|
+
const hxHi = Math.max(h.x1, h.x2);
|
|
2535
|
+
const vyLo = Math.min(v.y1, v.y2);
|
|
2536
|
+
const vyHi = Math.max(v.y1, v.y2);
|
|
2537
|
+
if (vx > hxLo + END_EPS && vx < hxHi - END_EPS && hy > vyLo + END_EPS && hy < vyHi - END_EPS) {
|
|
2538
|
+
return { x: vx, y: hy };
|
|
2539
|
+
}
|
|
2540
|
+
return null;
|
|
2541
|
+
};
|
|
2542
|
+
var computeCrossings = (edges) => {
|
|
2543
|
+
const result = /* @__PURE__ */ new Map();
|
|
2544
|
+
const cache = edges.map((e) => segmentsOf(e.points));
|
|
2545
|
+
for (let i = 0; i < edges.length; i += 1) {
|
|
2546
|
+
for (let j = i + 1; j < edges.length; j += 1) {
|
|
2547
|
+
const under = edges[i];
|
|
2548
|
+
const r = GAP_BASE + widthOf(edges[j]);
|
|
2549
|
+
for (const a of cache[i]) {
|
|
2550
|
+
for (const b of cache[j]) {
|
|
2551
|
+
let pt = null;
|
|
2552
|
+
if (isHorizontal(a) && isVertical(b)) pt = intersect(a, b);
|
|
2553
|
+
else if (isVertical(a) && isHorizontal(b)) pt = intersect(b, a);
|
|
2554
|
+
if (!pt) continue;
|
|
2555
|
+
const list = result.get(under.id);
|
|
2556
|
+
if (list) list.push({ x: pt.x, y: pt.y, r });
|
|
2557
|
+
else result.set(under.id, [{ x: pt.x, y: pt.y, r }]);
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
return result;
|
|
2563
|
+
};
|
|
2564
|
+
|
|
2565
|
+
// server/render/svg.tsx
|
|
2566
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2567
|
+
var DEFAULT_PADDING = 32;
|
|
2568
|
+
var computeBounds = (diagram, edgeMeta) => {
|
|
2569
|
+
let minX = Infinity;
|
|
2570
|
+
let minY = Infinity;
|
|
2571
|
+
let maxX = -Infinity;
|
|
2572
|
+
let maxY = -Infinity;
|
|
2573
|
+
const grow = (x, y) => {
|
|
2574
|
+
minX = Math.min(minX, x);
|
|
2575
|
+
minY = Math.min(minY, y);
|
|
2576
|
+
maxX = Math.max(maxX, x);
|
|
2577
|
+
maxY = Math.max(maxY, y);
|
|
2578
|
+
};
|
|
2579
|
+
for (const a of diagram.areas) {
|
|
2580
|
+
grow(a.x, a.y);
|
|
2581
|
+
grow(a.x + a.w, a.y + a.h);
|
|
2582
|
+
grow(a.x, a.y - 12);
|
|
2583
|
+
}
|
|
2584
|
+
for (const n of diagram.nodes) {
|
|
2585
|
+
grow(n.x, n.y);
|
|
2586
|
+
grow(n.x + n.w, n.y + n.h);
|
|
2587
|
+
grow(n.x + n.w, n.y + n.h + 26);
|
|
2588
|
+
}
|
|
2589
|
+
for (const e of diagram.edges) {
|
|
2590
|
+
for (const p of e.points) grow(p.x, p.y);
|
|
2591
|
+
const label = edgeMeta[e.id]?.label;
|
|
2592
|
+
if (label && e.labelAnchor) {
|
|
2593
|
+
const { w: pillW, h: pillH } = labelPillSize(label);
|
|
2594
|
+
grow(e.labelAnchor.x - pillW / 2, e.labelAnchor.y - pillH / 2);
|
|
2595
|
+
grow(e.labelAnchor.x + pillW / 2, e.labelAnchor.y + pillH / 2);
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
if (!Number.isFinite(minX)) return { x: 0, y: 0, w: 800, h: 600 };
|
|
2599
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
2600
|
+
};
|
|
2601
|
+
var renderSvgString = (model, opts = {}) => {
|
|
2602
|
+
const { routed, nodes, edges } = model;
|
|
2603
|
+
const pad = opts.padding ?? DEFAULT_PADDING;
|
|
2604
|
+
const b = computeBounds(routed, edges);
|
|
2605
|
+
const crossings = computeCrossings(routed.edges);
|
|
2606
|
+
const x = b.x - pad;
|
|
2607
|
+
const y = b.y - pad;
|
|
2608
|
+
const w = Math.max(1, b.w + pad * 2);
|
|
2609
|
+
const h = Math.max(1, b.h + pad * 2);
|
|
2610
|
+
const markup = renderToStaticMarkup(
|
|
2611
|
+
/* @__PURE__ */ jsxs(
|
|
2612
|
+
"svg",
|
|
2613
|
+
{
|
|
2614
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
2615
|
+
viewBox: `${x} ${y} ${w} ${h}`,
|
|
2616
|
+
width: w,
|
|
2617
|
+
height: h,
|
|
2618
|
+
children: [
|
|
2619
|
+
/* @__PURE__ */ jsx(EdgeDefs, {}),
|
|
2620
|
+
/* @__PURE__ */ jsx("rect", { x, y, width: w, height: h, fill: opts.background ?? "#ffffff" }),
|
|
2621
|
+
routed.areas.map((area) => /* @__PURE__ */ jsx(Area, { area }, area.id)),
|
|
2622
|
+
routed.edges.map((edge) => {
|
|
2623
|
+
const m = edges[edge.id] ?? {};
|
|
2624
|
+
return /* @__PURE__ */ jsx(
|
|
2625
|
+
Edge,
|
|
2626
|
+
{
|
|
2627
|
+
edge,
|
|
2628
|
+
label: m.label,
|
|
2629
|
+
style: m.style,
|
|
2630
|
+
marker: m.marker,
|
|
2631
|
+
crossings: crossings.get(edge.id)
|
|
2632
|
+
},
|
|
2633
|
+
edge.id
|
|
2634
|
+
);
|
|
2635
|
+
}),
|
|
2636
|
+
routed.nodes.map((node) => {
|
|
2637
|
+
const m = nodes[node.id] ?? {};
|
|
2638
|
+
return /* @__PURE__ */ jsx(
|
|
2639
|
+
Node,
|
|
2640
|
+
{
|
|
2641
|
+
id: node.id,
|
|
2642
|
+
shape: node.shape ?? m.shape ?? "rectangle",
|
|
2643
|
+
label: m.label,
|
|
2644
|
+
x: node.x,
|
|
2645
|
+
y: node.y,
|
|
2646
|
+
w: node.w,
|
|
2647
|
+
h: node.h,
|
|
2648
|
+
textSize: node.textSize,
|
|
2649
|
+
textColor: node.textColor,
|
|
2650
|
+
borderColor: node.borderColor,
|
|
2651
|
+
borderStyle: node.borderStyle,
|
|
2652
|
+
fillColor: node.fillColor,
|
|
2653
|
+
icon: node.icon,
|
|
2654
|
+
iconPosition: node.iconPosition,
|
|
2655
|
+
gridSize: routed.gridSize
|
|
2656
|
+
},
|
|
2657
|
+
node.id
|
|
2658
|
+
);
|
|
2659
|
+
}),
|
|
2660
|
+
routed.areas.map((area) => /* @__PURE__ */ jsx(AreaLabel, { area }, `label-${area.id}`))
|
|
2661
|
+
]
|
|
2662
|
+
}
|
|
2663
|
+
)
|
|
2664
|
+
);
|
|
2665
|
+
return markup;
|
|
2666
|
+
};
|
|
2667
|
+
var inlineIcons = (svg, iconsDir) => svg.replace(
|
|
2668
|
+
/(xlink:href|href)="\/icons\/([^"]+)"/g,
|
|
2669
|
+
(whole, attr, file) => {
|
|
2670
|
+
try {
|
|
2671
|
+
const bytes = readFileSync(join(iconsDir, file));
|
|
2672
|
+
const mime = file.endsWith(".svg") ? "image/svg+xml" : "image/png";
|
|
2673
|
+
return `${attr}="data:${mime};base64,${bytes.toString("base64")}"`;
|
|
2674
|
+
} catch {
|
|
2675
|
+
return whole;
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
);
|
|
2679
|
+
|
|
2680
|
+
// server/render/index.ts
|
|
2681
|
+
var renderDiagramSvg = async (d2, layoutText, opts = {}) => {
|
|
2682
|
+
const model = await buildRenderModel(d2, layoutText);
|
|
2683
|
+
if ("error" in model) return model;
|
|
2684
|
+
const svg = renderSvgString(model, opts);
|
|
2685
|
+
return opts.iconsDir ? inlineIcons(svg, opts.iconsDir) : svg;
|
|
2686
|
+
};
|
|
2687
|
+
var renderDiagramPng = async (d2, layoutText, opts = {}) => {
|
|
2688
|
+
const svg = await renderDiagramSvg(d2, layoutText, opts);
|
|
2689
|
+
if (typeof svg !== "string") return svg;
|
|
2690
|
+
const png = svgToPng(svg, opts);
|
|
2691
|
+
return embedPngText(png, epureMetaEntries(d2, layoutText));
|
|
2692
|
+
};
|
|
2693
|
+
|
|
2694
|
+
// lib/render.ts
|
|
2695
|
+
var HERE = dirname(fileURLToPath(import.meta.url));
|
|
2696
|
+
var WASM_CANDIDATES = [join2(HERE, "libavoid.wasm"), join2(HERE, "..", "public", "libavoid.wasm")];
|
|
2697
|
+
var wasm = WASM_CANDIDATES.find(existsSync);
|
|
2698
|
+
if (wasm) setLibavoidWasmPath(wasm);
|
|
2699
|
+
var packagedIconsDir = () => {
|
|
2700
|
+
const candidates = [join2(HERE, "..", "dist", "icons"), join2(HERE, "..", "public", "icons")];
|
|
2701
|
+
return candidates.find(existsSync) ?? candidates[0];
|
|
2702
|
+
};
|
|
2703
|
+
export {
|
|
2704
|
+
PNG_MARKER_KEYS,
|
|
2705
|
+
PNG_SOURCE_KEYS,
|
|
2706
|
+
buildRenderModel,
|
|
2707
|
+
embedPngText,
|
|
2708
|
+
epureMetaEntries,
|
|
2709
|
+
inlineIcons,
|
|
2710
|
+
packagedIconsDir,
|
|
2711
|
+
readPngText,
|
|
2712
|
+
renderDiagramPng,
|
|
2713
|
+
renderDiagramSvg,
|
|
2714
|
+
renderSvgString,
|
|
2715
|
+
setLibavoidWasmPath,
|
|
2716
|
+
svgToPng
|
|
2717
|
+
};
|