@x12i/diagrams 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -14
- package/dist/chunk-3DJFQRU3.js +2708 -0
- package/dist/chunk-3DJFQRU3.js.map +1 -0
- package/dist/index.d.ts +67 -54
- package/dist/index.js +204 -45
- package/dist/index.js.map +1 -1
- package/dist/messages-ZCyCfyO0.d.ts +857 -0
- package/dist/react.d.ts +348 -0
- package/dist/react.js +2525 -0
- package/dist/react.js.map +1 -0
- package/dist/styles.css +1389 -0
- package/package.json +51 -4
|
@@ -0,0 +1,2708 @@
|
|
|
1
|
+
// src/schema.ts
|
|
2
|
+
var DIAGRAM_VERSION = 1;
|
|
3
|
+
var NODE_KINDS = [
|
|
4
|
+
"start",
|
|
5
|
+
"task",
|
|
6
|
+
"decision",
|
|
7
|
+
"end",
|
|
8
|
+
"group",
|
|
9
|
+
"subflow",
|
|
10
|
+
"note",
|
|
11
|
+
"image",
|
|
12
|
+
"entity",
|
|
13
|
+
"code",
|
|
14
|
+
"chart",
|
|
15
|
+
"agent"
|
|
16
|
+
];
|
|
17
|
+
var NODE_STATUSES = ["neutral", "success", "warning", "danger", "info"];
|
|
18
|
+
var EDGE_KINDS = ["straight", "bezier", "step", "smooth-step"];
|
|
19
|
+
var DIAGRAM_KINDS = ["graph", "flowchart", "dag", "tree"];
|
|
20
|
+
function isContainerKind(kind) {
|
|
21
|
+
return kind === "group" || kind === "subflow";
|
|
22
|
+
}
|
|
23
|
+
function defineDiagram(document) {
|
|
24
|
+
return document;
|
|
25
|
+
}
|
|
26
|
+
function createDiagram(title = "", extra = {}) {
|
|
27
|
+
return { version: DIAGRAM_VERSION, title, nodes: [], edges: [], ...extra };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/ids.ts
|
|
31
|
+
function generateId(prefix = "id", random = Math.random) {
|
|
32
|
+
let suffix = "";
|
|
33
|
+
while (suffix.length < 8) suffix += random().toString(36).slice(2);
|
|
34
|
+
return `${prefix}-${suffix.slice(0, 8)}`;
|
|
35
|
+
}
|
|
36
|
+
function nextId(taken, prefix) {
|
|
37
|
+
const set = taken instanceof Set ? taken : new Set(taken);
|
|
38
|
+
let n = 1;
|
|
39
|
+
while (set.has(`${prefix}${n}`)) n += 1;
|
|
40
|
+
return `${prefix}${n}`;
|
|
41
|
+
}
|
|
42
|
+
function createIdAllocator(taken = []) {
|
|
43
|
+
const used = new Set(taken);
|
|
44
|
+
const counters = /* @__PURE__ */ new Map();
|
|
45
|
+
return {
|
|
46
|
+
next(prefix) {
|
|
47
|
+
let n = counters.get(prefix) ?? 1;
|
|
48
|
+
while (used.has(`${prefix}${n}`)) n += 1;
|
|
49
|
+
counters.set(prefix, n + 1);
|
|
50
|
+
const id = `${prefix}${n}`;
|
|
51
|
+
used.add(id);
|
|
52
|
+
return id;
|
|
53
|
+
},
|
|
54
|
+
reserve(id) {
|
|
55
|
+
used.add(id);
|
|
56
|
+
},
|
|
57
|
+
has(id) {
|
|
58
|
+
return used.has(id);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function idStem(id, fallback) {
|
|
63
|
+
const stem = id.replace(/[-_]?\d+$/, "");
|
|
64
|
+
return stem === "" ? fallback : stem;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/graph.ts
|
|
68
|
+
function buildIndex(doc) {
|
|
69
|
+
const nodesById = /* @__PURE__ */ new Map();
|
|
70
|
+
for (const node of doc.nodes) if (!nodesById.has(node.id)) nodesById.set(node.id, node);
|
|
71
|
+
const edgesById = /* @__PURE__ */ new Map();
|
|
72
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
73
|
+
const incoming = /* @__PURE__ */ new Map();
|
|
74
|
+
for (const edge of doc.edges) {
|
|
75
|
+
if (!edgesById.has(edge.id)) edgesById.set(edge.id, edge);
|
|
76
|
+
if (!nodesById.has(edge.source) || !nodesById.has(edge.target)) continue;
|
|
77
|
+
const out = outgoing.get(edge.source);
|
|
78
|
+
if (out) out.push(edge);
|
|
79
|
+
else outgoing.set(edge.source, [edge]);
|
|
80
|
+
const inc = incoming.get(edge.target);
|
|
81
|
+
if (inc) inc.push(edge);
|
|
82
|
+
else incoming.set(edge.target, [edge]);
|
|
83
|
+
}
|
|
84
|
+
const children = /* @__PURE__ */ new Map();
|
|
85
|
+
for (const node of doc.nodes) {
|
|
86
|
+
if (node.parentId === void 0 || !nodesById.has(node.parentId) || node.parentId === node.id) continue;
|
|
87
|
+
const list = children.get(node.parentId);
|
|
88
|
+
if (list) list.push(node.id);
|
|
89
|
+
else children.set(node.parentId, [node.id]);
|
|
90
|
+
}
|
|
91
|
+
return { nodesById, edgesById, outgoing, incoming, children };
|
|
92
|
+
}
|
|
93
|
+
function getAncestors(index, id) {
|
|
94
|
+
const chain = [];
|
|
95
|
+
const seen = /* @__PURE__ */ new Set([id]);
|
|
96
|
+
let current = index.nodesById.get(id)?.parentId;
|
|
97
|
+
while (current !== void 0 && !seen.has(current) && index.nodesById.has(current)) {
|
|
98
|
+
chain.push(current);
|
|
99
|
+
seen.add(current);
|
|
100
|
+
current = index.nodesById.get(current)?.parentId;
|
|
101
|
+
}
|
|
102
|
+
return chain;
|
|
103
|
+
}
|
|
104
|
+
function getDescendants(index, id) {
|
|
105
|
+
const result = [];
|
|
106
|
+
const seen = /* @__PURE__ */ new Set([id]);
|
|
107
|
+
const stack = [...index.children.get(id) ?? []].reverse();
|
|
108
|
+
while (stack.length > 0) {
|
|
109
|
+
const next = stack.pop();
|
|
110
|
+
if (seen.has(next)) continue;
|
|
111
|
+
seen.add(next);
|
|
112
|
+
result.push(next);
|
|
113
|
+
const kids = index.children.get(next);
|
|
114
|
+
if (kids) for (let i = kids.length - 1; i >= 0; i -= 1) stack.push(kids[i]);
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
function findCycle(doc, index = buildIndex(doc)) {
|
|
119
|
+
const state = /* @__PURE__ */ new Map();
|
|
120
|
+
for (const root of doc.nodes) {
|
|
121
|
+
if (state.has(root.id)) continue;
|
|
122
|
+
const path = [];
|
|
123
|
+
const cursors = [];
|
|
124
|
+
path.push(root.id);
|
|
125
|
+
cursors.push(0);
|
|
126
|
+
state.set(root.id, 1);
|
|
127
|
+
while (path.length > 0) {
|
|
128
|
+
const current = path[path.length - 1];
|
|
129
|
+
const edges = index.outgoing.get(current) ?? [];
|
|
130
|
+
const cursor = cursors[cursors.length - 1];
|
|
131
|
+
if (cursor >= edges.length) {
|
|
132
|
+
state.set(current, 2);
|
|
133
|
+
path.pop();
|
|
134
|
+
cursors.pop();
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
cursors[cursors.length - 1] = cursor + 1;
|
|
138
|
+
const next = edges[cursor].target;
|
|
139
|
+
const seen = state.get(next);
|
|
140
|
+
if (seen === 1) return path.slice(path.indexOf(next));
|
|
141
|
+
if (seen === void 0) {
|
|
142
|
+
state.set(next, 1);
|
|
143
|
+
path.push(next);
|
|
144
|
+
cursors.push(0);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return void 0;
|
|
149
|
+
}
|
|
150
|
+
function hasPath(index, from, to) {
|
|
151
|
+
if (from === to) return true;
|
|
152
|
+
const seen = /* @__PURE__ */ new Set([from]);
|
|
153
|
+
const queue = [from];
|
|
154
|
+
for (let head = 0; head < queue.length; head += 1) {
|
|
155
|
+
for (const edge of index.outgoing.get(queue[head]) ?? []) {
|
|
156
|
+
if (edge.target === to) return true;
|
|
157
|
+
if (!seen.has(edge.target)) {
|
|
158
|
+
seen.add(edge.target);
|
|
159
|
+
queue.push(edge.target);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
function getRoots(doc, index = buildIndex(doc)) {
|
|
166
|
+
return doc.nodes.filter((node) => !index.incoming.has(node.id)).map((node) => node.id);
|
|
167
|
+
}
|
|
168
|
+
function getNeighbors(index, id) {
|
|
169
|
+
const result = /* @__PURE__ */ new Set();
|
|
170
|
+
for (const edge of index.outgoing.get(id) ?? []) result.add(edge.target);
|
|
171
|
+
for (const edge of index.incoming.get(id) ?? []) result.add(edge.source);
|
|
172
|
+
result.delete(id);
|
|
173
|
+
return [...result];
|
|
174
|
+
}
|
|
175
|
+
function topologicalOrder(doc, index = buildIndex(doc)) {
|
|
176
|
+
const indegree = /* @__PURE__ */ new Map();
|
|
177
|
+
for (const node of doc.nodes) indegree.set(node.id, 0);
|
|
178
|
+
for (const edges of index.outgoing.values()) {
|
|
179
|
+
for (const edge of edges) indegree.set(edge.target, (indegree.get(edge.target) ?? 0) + 1);
|
|
180
|
+
}
|
|
181
|
+
const queue = doc.nodes.filter((node) => indegree.get(node.id) === 0).map((node) => node.id);
|
|
182
|
+
const order = [];
|
|
183
|
+
for (let head = 0; head < queue.length; head += 1) {
|
|
184
|
+
const id = queue[head];
|
|
185
|
+
order.push(id);
|
|
186
|
+
for (const edge of index.outgoing.get(id) ?? []) {
|
|
187
|
+
const left = (indegree.get(edge.target) ?? 0) - 1;
|
|
188
|
+
indegree.set(edge.target, left);
|
|
189
|
+
if (left === 0) queue.push(edge.target);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (order.length < doc.nodes.length) {
|
|
193
|
+
const placed = new Set(order);
|
|
194
|
+
for (const node of doc.nodes) if (!placed.has(node.id)) order.push(node.id);
|
|
195
|
+
}
|
|
196
|
+
return order;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/connection.ts
|
|
200
|
+
function resolveConnectionRules(kind, rules = {}) {
|
|
201
|
+
const strict = kind === "dag" || kind === "tree";
|
|
202
|
+
return {
|
|
203
|
+
allowSelfLoops: rules.allowSelfLoops ?? !strict,
|
|
204
|
+
allowDuplicates: rules.allowDuplicates ?? false,
|
|
205
|
+
acyclic: rules.acyclic ?? strict,
|
|
206
|
+
singleParent: rules.singleParent ?? kind === "tree"
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function portTypesCompatible(a, b) {
|
|
210
|
+
return a === void 0 || b === void 0 || a === "any" || b === "any" || a === b;
|
|
211
|
+
}
|
|
212
|
+
var reject = (code, message) => ({ ok: false, code, message });
|
|
213
|
+
function checkPort(node, portId, wanted) {
|
|
214
|
+
if (portId === void 0) return void 0;
|
|
215
|
+
const port = node.ports?.find((candidate) => candidate.id === portId);
|
|
216
|
+
if (!port) return wanted === "out" ? "source-port-missing" : "target-port-missing";
|
|
217
|
+
if (port.direction !== wanted) return wanted === "out" ? "source-port-not-output" : "target-port-not-input";
|
|
218
|
+
return void 0;
|
|
219
|
+
}
|
|
220
|
+
function checkConnection(doc, connection, rules = {}, index = buildIndex(doc)) {
|
|
221
|
+
const resolved = resolveConnectionRules(doc.kind, rules);
|
|
222
|
+
const source = index.nodesById.get(connection.source);
|
|
223
|
+
const target = index.nodesById.get(connection.target);
|
|
224
|
+
if (!source) return reject("unknown-source", `Unknown source node "${connection.source}".`);
|
|
225
|
+
if (!target) return reject("unknown-target", `Unknown target node "${connection.target}".`);
|
|
226
|
+
if (source.locked === true || target.locked === true) {
|
|
227
|
+
return reject("locked", "A locked node cannot be connected.");
|
|
228
|
+
}
|
|
229
|
+
if (source.id === target.id && !resolved.allowSelfLoops) {
|
|
230
|
+
return reject("self-loop", "A node cannot connect to itself in this diagram.");
|
|
231
|
+
}
|
|
232
|
+
if (source.id !== target.id) {
|
|
233
|
+
if (getAncestors(index, target.id).includes(source.id) || getAncestors(index, source.id).includes(target.id)) {
|
|
234
|
+
return reject("container-descendant", "A group cannot connect to a node inside it.");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const sourceProblem = checkPort(source, connection.sourcePort, "out");
|
|
238
|
+
if (sourceProblem) return reject(sourceProblem, `The source port "${connection.sourcePort}" cannot start a connection.`);
|
|
239
|
+
const targetProblem = checkPort(target, connection.targetPort, "in");
|
|
240
|
+
if (targetProblem) return reject(targetProblem, `The target port "${connection.targetPort}" cannot receive a connection.`);
|
|
241
|
+
if (connection.sourcePort !== void 0 && connection.targetPort !== void 0) {
|
|
242
|
+
const sourceType = source.ports?.find((port) => port.id === connection.sourcePort)?.type;
|
|
243
|
+
const targetType = target.ports?.find((port) => port.id === connection.targetPort)?.type;
|
|
244
|
+
if (!portTypesCompatible(sourceType, targetType)) {
|
|
245
|
+
return reject("port-type-mismatch", `Port types "${sourceType}" and "${targetType}" are not compatible.`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (!resolved.allowDuplicates) {
|
|
249
|
+
const duplicate = (index.outgoing.get(source.id) ?? []).some(
|
|
250
|
+
(edge) => edge.target === target.id && edge.sourcePort === connection.sourcePort && edge.targetPort === connection.targetPort
|
|
251
|
+
);
|
|
252
|
+
if (duplicate) return reject("duplicate", "These nodes are already connected.");
|
|
253
|
+
}
|
|
254
|
+
if (resolved.singleParent && (index.incoming.get(target.id)?.length ?? 0) > 0) {
|
|
255
|
+
return reject("multiple-parents", "This node already has a parent.");
|
|
256
|
+
}
|
|
257
|
+
if (resolved.acyclic && hasPath(index, target.id, source.id)) {
|
|
258
|
+
return reject("cycle", "This connection would create a cycle.");
|
|
259
|
+
}
|
|
260
|
+
return { ok: true };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/validate.ts
|
|
264
|
+
var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
265
|
+
function validateDiagram(doc, options = {}) {
|
|
266
|
+
const issues = [];
|
|
267
|
+
const add2 = (issue) => issues.push({ severity: "error", ...issue });
|
|
268
|
+
const kind = options.kind ?? doc.kind ?? "graph";
|
|
269
|
+
if (doc.version !== DIAGRAM_VERSION) {
|
|
270
|
+
add2({
|
|
271
|
+
code: "unsupported-version",
|
|
272
|
+
message: `Unsupported diagram version ${String(doc.version)}. Expected ${DIAGRAM_VERSION}.`,
|
|
273
|
+
path: ["version"]
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
if (typeof doc.title !== "string" || doc.title.trim() === "") {
|
|
277
|
+
add2({
|
|
278
|
+
code: "missing-title",
|
|
279
|
+
severity: "warning",
|
|
280
|
+
message: "Every diagram needs a title that states what it shows.",
|
|
281
|
+
path: ["title"]
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
const index = buildIndex(doc);
|
|
285
|
+
const seenNodes = /* @__PURE__ */ new Set();
|
|
286
|
+
doc.nodes.forEach((node, i) => {
|
|
287
|
+
if (typeof node.id !== "string" || node.id === "") {
|
|
288
|
+
add2({ code: "empty-id", message: "A node needs a non-empty id.", path: ["nodes", i, "id"] });
|
|
289
|
+
} else if (seenNodes.has(node.id)) {
|
|
290
|
+
add2({
|
|
291
|
+
code: "duplicate-node-id",
|
|
292
|
+
message: `Node id "${node.id}" is used more than once.`,
|
|
293
|
+
path: ["nodes", i, "id"],
|
|
294
|
+
nodeId: node.id
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
seenNodes.add(node.id);
|
|
298
|
+
if (!NODE_KINDS.includes(node.kind)) {
|
|
299
|
+
add2({
|
|
300
|
+
code: "invalid-node-kind",
|
|
301
|
+
message: `Unknown node kind "${String(node.kind)}".`,
|
|
302
|
+
path: ["nodes", i, "kind"],
|
|
303
|
+
nodeId: node.id
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
if (node.status !== void 0 && !NODE_STATUSES.includes(node.status)) {
|
|
307
|
+
add2({
|
|
308
|
+
code: "invalid-status",
|
|
309
|
+
message: `Unknown node status "${String(node.status)}".`,
|
|
310
|
+
path: ["nodes", i, "status"],
|
|
311
|
+
nodeId: node.id
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
if (!node.position || !isFiniteNumber(node.position.x) || !isFiniteNumber(node.position.y)) {
|
|
315
|
+
add2({
|
|
316
|
+
code: "invalid-position",
|
|
317
|
+
message: `Node "${node.id}" needs a finite position.`,
|
|
318
|
+
path: ["nodes", i, "position"],
|
|
319
|
+
nodeId: node.id
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
for (const key of ["width", "height"]) {
|
|
323
|
+
const value = node[key];
|
|
324
|
+
if (value !== void 0 && (!isFiniteNumber(value) || value <= 0)) {
|
|
325
|
+
add2({
|
|
326
|
+
code: "invalid-size",
|
|
327
|
+
message: `Node "${node.id}" has an invalid ${key}.`,
|
|
328
|
+
path: ["nodes", i, key],
|
|
329
|
+
nodeId: node.id
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if ((options.warnOnEmptyLabels ?? true) && (typeof node.label !== "string" || node.label.trim() === "")) {
|
|
334
|
+
add2({
|
|
335
|
+
code: "empty-label",
|
|
336
|
+
severity: "warning",
|
|
337
|
+
message: `Node "${node.id}" has no label, so screen readers cannot name it.`,
|
|
338
|
+
path: ["nodes", i, "label"],
|
|
339
|
+
nodeId: node.id
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
const portIds = /* @__PURE__ */ new Set();
|
|
343
|
+
node.ports?.forEach((port, p) => {
|
|
344
|
+
if (portIds.has(port.id)) {
|
|
345
|
+
add2({
|
|
346
|
+
code: "duplicate-port-id",
|
|
347
|
+
message: `Port id "${port.id}" is used twice on node "${node.id}".`,
|
|
348
|
+
path: ["nodes", i, "ports", p, "id"],
|
|
349
|
+
nodeId: node.id
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
portIds.add(port.id);
|
|
353
|
+
});
|
|
354
|
+
if (node.parentId !== void 0) {
|
|
355
|
+
const parent = index.nodesById.get(node.parentId);
|
|
356
|
+
if (node.parentId === node.id) {
|
|
357
|
+
add2({
|
|
358
|
+
code: "parent-self",
|
|
359
|
+
message: `Node "${node.id}" is its own parent.`,
|
|
360
|
+
path: ["nodes", i, "parentId"],
|
|
361
|
+
nodeId: node.id
|
|
362
|
+
});
|
|
363
|
+
} else if (!parent) {
|
|
364
|
+
add2({
|
|
365
|
+
code: "parent-missing",
|
|
366
|
+
message: `Node "${node.id}" refers to a missing parent "${node.parentId}".`,
|
|
367
|
+
path: ["nodes", i, "parentId"],
|
|
368
|
+
nodeId: node.id
|
|
369
|
+
});
|
|
370
|
+
} else {
|
|
371
|
+
if (!isContainerKind(parent.kind)) {
|
|
372
|
+
add2({
|
|
373
|
+
code: "parent-not-container",
|
|
374
|
+
message: `Parent "${parent.id}" is a ${parent.kind}, not a group or subflow.`,
|
|
375
|
+
path: ["nodes", i, "parentId"],
|
|
376
|
+
nodeId: node.id
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
if (hasParentLoop(index, node.id)) {
|
|
380
|
+
add2({
|
|
381
|
+
code: "parent-cycle",
|
|
382
|
+
message: `Node "${node.id}" is inside a loop of groups.`,
|
|
383
|
+
path: ["nodes", i, "parentId"],
|
|
384
|
+
nodeId: node.id
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
const seenEdges = /* @__PURE__ */ new Set();
|
|
391
|
+
const seenPairs = /* @__PURE__ */ new Set();
|
|
392
|
+
doc.edges.forEach((edge, i) => {
|
|
393
|
+
if (typeof edge.id !== "string" || edge.id === "") {
|
|
394
|
+
add2({ code: "empty-id", message: "An edge needs a non-empty id.", path: ["edges", i, "id"] });
|
|
395
|
+
} else if (seenEdges.has(edge.id)) {
|
|
396
|
+
add2({
|
|
397
|
+
code: "duplicate-edge-id",
|
|
398
|
+
message: `Edge id "${edge.id}" is used more than once.`,
|
|
399
|
+
path: ["edges", i, "id"],
|
|
400
|
+
edgeId: edge.id
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
seenEdges.add(edge.id);
|
|
404
|
+
if (edge.kind !== void 0 && !EDGE_KINDS.includes(edge.kind)) {
|
|
405
|
+
add2({
|
|
406
|
+
code: "invalid-edge-kind",
|
|
407
|
+
message: `Unknown edge kind "${String(edge.kind)}".`,
|
|
408
|
+
path: ["edges", i, "kind"],
|
|
409
|
+
edgeId: edge.id
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
if (edge.status !== void 0 && !NODE_STATUSES.includes(edge.status)) {
|
|
413
|
+
add2({
|
|
414
|
+
code: "invalid-status",
|
|
415
|
+
message: `Unknown edge status "${String(edge.status)}".`,
|
|
416
|
+
path: ["edges", i, "status"],
|
|
417
|
+
edgeId: edge.id
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
const source = index.nodesById.get(edge.source);
|
|
421
|
+
const target = index.nodesById.get(edge.target);
|
|
422
|
+
if (!source) {
|
|
423
|
+
add2({
|
|
424
|
+
code: "dangling-source",
|
|
425
|
+
message: `Edge "${edge.id}" starts at a missing node "${edge.source}".`,
|
|
426
|
+
path: ["edges", i, "source"],
|
|
427
|
+
edgeId: edge.id
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
if (!target) {
|
|
431
|
+
add2({
|
|
432
|
+
code: "dangling-target",
|
|
433
|
+
message: `Edge "${edge.id}" ends at a missing node "${edge.target}".`,
|
|
434
|
+
path: ["edges", i, "target"],
|
|
435
|
+
edgeId: edge.id
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
if (!source || !target) return;
|
|
439
|
+
const sourcePort = edge.sourcePort === void 0 ? void 0 : source.ports?.find((p) => p.id === edge.sourcePort);
|
|
440
|
+
const targetPort = edge.targetPort === void 0 ? void 0 : target.ports?.find((p) => p.id === edge.targetPort);
|
|
441
|
+
if (edge.sourcePort !== void 0) {
|
|
442
|
+
if (!sourcePort) {
|
|
443
|
+
add2({
|
|
444
|
+
code: "unknown-source-port",
|
|
445
|
+
message: `Edge "${edge.id}" uses port "${edge.sourcePort}", which node "${source.id}" does not have.`,
|
|
446
|
+
path: ["edges", i, "sourcePort"],
|
|
447
|
+
edgeId: edge.id
|
|
448
|
+
});
|
|
449
|
+
} else if (sourcePort.direction !== "out") {
|
|
450
|
+
add2({
|
|
451
|
+
code: "source-port-direction",
|
|
452
|
+
message: `Port "${sourcePort.id}" on "${source.id}" is an input and cannot start an edge.`,
|
|
453
|
+
path: ["edges", i, "sourcePort"],
|
|
454
|
+
edgeId: edge.id
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (edge.targetPort !== void 0) {
|
|
459
|
+
if (!targetPort) {
|
|
460
|
+
add2({
|
|
461
|
+
code: "unknown-target-port",
|
|
462
|
+
message: `Edge "${edge.id}" uses port "${edge.targetPort}", which node "${target.id}" does not have.`,
|
|
463
|
+
path: ["edges", i, "targetPort"],
|
|
464
|
+
edgeId: edge.id
|
|
465
|
+
});
|
|
466
|
+
} else if (targetPort.direction !== "in") {
|
|
467
|
+
add2({
|
|
468
|
+
code: "target-port-direction",
|
|
469
|
+
message: `Port "${targetPort.id}" on "${target.id}" is an output and cannot end an edge.`,
|
|
470
|
+
path: ["edges", i, "targetPort"],
|
|
471
|
+
edgeId: edge.id
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (sourcePort && targetPort && !portTypesCompatible(sourcePort.type, targetPort.type)) {
|
|
476
|
+
add2({
|
|
477
|
+
code: "port-type-mismatch",
|
|
478
|
+
message: `Edge "${edge.id}" joins incompatible port types "${sourcePort.type}" and "${targetPort.type}".`,
|
|
479
|
+
path: ["edges", i],
|
|
480
|
+
edgeId: edge.id
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
const pair = JSON.stringify([edge.source, edge.sourcePort ?? null, edge.target, edge.targetPort ?? null]);
|
|
484
|
+
if (seenPairs.has(pair)) {
|
|
485
|
+
add2({
|
|
486
|
+
code: "duplicate-edge",
|
|
487
|
+
severity: "warning",
|
|
488
|
+
message: `Edge "${edge.id}" repeats an existing connection.`,
|
|
489
|
+
path: ["edges", i],
|
|
490
|
+
edgeId: edge.id
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
seenPairs.add(pair);
|
|
494
|
+
if (edge.source === edge.target && (kind === "dag" || kind === "tree")) {
|
|
495
|
+
add2({
|
|
496
|
+
code: "self-loop",
|
|
497
|
+
message: `Edge "${edge.id}" connects node "${edge.source}" to itself, which a ${kind} does not allow.`,
|
|
498
|
+
path: ["edges", i],
|
|
499
|
+
edgeId: edge.id
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
if (kind === "dag" || kind === "tree") {
|
|
504
|
+
const cycle = findCycle(doc, index);
|
|
505
|
+
if (cycle && !(cycle.length === 1 && issues.some((issue) => issue.code === "self-loop"))) {
|
|
506
|
+
add2({
|
|
507
|
+
code: "cycle",
|
|
508
|
+
message: `The diagram has a cycle: ${[...cycle, cycle[0]].join(" -> ")}.`,
|
|
509
|
+
path: ["edges"],
|
|
510
|
+
nodeId: cycle[0]
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (kind === "tree") {
|
|
515
|
+
for (const [id, edges] of index.incoming) {
|
|
516
|
+
if (edges.length > 1) {
|
|
517
|
+
add2({
|
|
518
|
+
code: "multiple-parents",
|
|
519
|
+
message: `Node "${id}" has ${edges.length} incoming edges, but a tree node has one parent.`,
|
|
520
|
+
path: ["edges"],
|
|
521
|
+
nodeId: id
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const errors = issues.filter((issue) => issue.severity === "error");
|
|
527
|
+
const warnings = issues.filter((issue) => issue.severity === "warning");
|
|
528
|
+
return { valid: errors.length === 0, issues, errors, warnings };
|
|
529
|
+
}
|
|
530
|
+
function hasParentLoop(index, id) {
|
|
531
|
+
const seen = /* @__PURE__ */ new Set();
|
|
532
|
+
let current = id;
|
|
533
|
+
while (current !== void 0) {
|
|
534
|
+
if (seen.has(current)) return true;
|
|
535
|
+
seen.add(current);
|
|
536
|
+
current = index.nodesById.get(current)?.parentId;
|
|
537
|
+
if (current !== void 0 && !index.nodesById.has(current)) return false;
|
|
538
|
+
}
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// src/geometry.ts
|
|
543
|
+
var GRID_SIZE = 8;
|
|
544
|
+
var DEFAULT_NODE_SIZE = {
|
|
545
|
+
start: { width: 128, height: 48 },
|
|
546
|
+
end: { width: 128, height: 48 },
|
|
547
|
+
task: { width: 176, height: 64 },
|
|
548
|
+
decision: { width: 160, height: 96 },
|
|
549
|
+
group: { width: 320, height: 200 },
|
|
550
|
+
subflow: { width: 320, height: 200 },
|
|
551
|
+
note: { width: 176, height: 96 },
|
|
552
|
+
image: { width: 160, height: 128 },
|
|
553
|
+
entity: { width: 192, height: 128 },
|
|
554
|
+
code: { width: 240, height: 128 },
|
|
555
|
+
chart: { width: 240, height: 160 },
|
|
556
|
+
agent: { width: 192, height: 80 }
|
|
557
|
+
};
|
|
558
|
+
var COLLAPSED_HEIGHT = 48;
|
|
559
|
+
var TEXT_METRICS = { charWidth: 7, lineHeight: 20, nodePaddingX: 32, nodePaddingY: 24 };
|
|
560
|
+
var roundUp = (value, step) => Math.ceil(value / step) * step;
|
|
561
|
+
function estimateTextWidth(text, charWidth = TEXT_METRICS.charWidth) {
|
|
562
|
+
let units = 0;
|
|
563
|
+
for (const char of text) {
|
|
564
|
+
const code = char.codePointAt(0) ?? 0;
|
|
565
|
+
units += code >= 11904 ? 2 : 1;
|
|
566
|
+
}
|
|
567
|
+
return units * charWidth;
|
|
568
|
+
}
|
|
569
|
+
function wrapText(text, maxWidth, charWidth = TEXT_METRICS.charWidth) {
|
|
570
|
+
const maxChars = Math.max(1, Math.floor(maxWidth / charWidth));
|
|
571
|
+
const lines = [];
|
|
572
|
+
for (const paragraph of text.split(/\r?\n/)) {
|
|
573
|
+
let line = "";
|
|
574
|
+
for (const word of paragraph.split(/\s+/).filter(Boolean)) {
|
|
575
|
+
let rest = word;
|
|
576
|
+
while (rest.length > maxChars) {
|
|
577
|
+
if (line !== "") {
|
|
578
|
+
lines.push(line);
|
|
579
|
+
line = "";
|
|
580
|
+
}
|
|
581
|
+
lines.push(rest.slice(0, maxChars));
|
|
582
|
+
rest = rest.slice(maxChars);
|
|
583
|
+
}
|
|
584
|
+
const candidate = line === "" ? rest : `${line} ${rest}`;
|
|
585
|
+
if (candidate.length <= maxChars) line = candidate;
|
|
586
|
+
else {
|
|
587
|
+
lines.push(line);
|
|
588
|
+
line = rest;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
lines.push(line);
|
|
592
|
+
}
|
|
593
|
+
return lines.length === 0 ? [""] : lines;
|
|
594
|
+
}
|
|
595
|
+
function dataList(node, key) {
|
|
596
|
+
const value = node.data?.[key];
|
|
597
|
+
return Array.isArray(value) ? value : [];
|
|
598
|
+
}
|
|
599
|
+
function getNodeSize(node) {
|
|
600
|
+
const base = DEFAULT_NODE_SIZE[node.kind] ?? DEFAULT_NODE_SIZE.task;
|
|
601
|
+
if (node.collapsed === true && isContainerKind(node.kind)) {
|
|
602
|
+
return { width: node.width ?? base.width, height: COLLAPSED_HEIGHT };
|
|
603
|
+
}
|
|
604
|
+
if (node.width !== void 0 && node.height !== void 0) return { width: node.width, height: node.height };
|
|
605
|
+
let { width, height } = base;
|
|
606
|
+
switch (node.kind) {
|
|
607
|
+
case "start":
|
|
608
|
+
case "end":
|
|
609
|
+
case "task":
|
|
610
|
+
case "agent":
|
|
611
|
+
case "note":
|
|
612
|
+
case "decision": {
|
|
613
|
+
const diamond = node.kind === "decision";
|
|
614
|
+
const usable = (w) => diamond ? w * 0.5 : w - TEXT_METRICS.nodePaddingX;
|
|
615
|
+
const needed = estimateTextWidth(node.label);
|
|
616
|
+
if (needed > usable(width)) {
|
|
617
|
+
const maxWidth = base.width * 2;
|
|
618
|
+
const wanted = diamond ? needed * 2 : needed + TEXT_METRICS.nodePaddingX;
|
|
619
|
+
width = Math.min(maxWidth, roundUp(wanted, 8));
|
|
620
|
+
const lines = wrapText(node.label, usable(width)).length;
|
|
621
|
+
const textHeight = lines * TEXT_METRICS.lineHeight;
|
|
622
|
+
height = Math.max(base.height, roundUp(diamond ? textHeight * 2 + 16 : textHeight + TEXT_METRICS.nodePaddingY + 8, 8));
|
|
623
|
+
}
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
case "entity": {
|
|
627
|
+
const rows = dataList(node, "fields").length;
|
|
628
|
+
height = Math.max(base.height, roundUp(32 + rows * 24 + 8, 8));
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
case "code": {
|
|
632
|
+
const code = typeof node.data?.code === "string" ? node.data.code : "";
|
|
633
|
+
const lines = Math.min(12, code === "" ? 0 : code.split(/\r?\n/).length);
|
|
634
|
+
height = Math.max(base.height, roundUp(40 + lines * TEXT_METRICS.lineHeight, 8));
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
default:
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
return { width: node.width ?? width, height: node.height ?? height };
|
|
641
|
+
}
|
|
642
|
+
function getNodeBounds(node) {
|
|
643
|
+
const size = getNodeSize(node);
|
|
644
|
+
return { x: node.position.x, y: node.position.y, ...size };
|
|
645
|
+
}
|
|
646
|
+
function rectCenter(rect) {
|
|
647
|
+
return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
|
|
648
|
+
}
|
|
649
|
+
function unionRects(rects) {
|
|
650
|
+
let minX = Infinity;
|
|
651
|
+
let minY = Infinity;
|
|
652
|
+
let maxX = -Infinity;
|
|
653
|
+
let maxY = -Infinity;
|
|
654
|
+
for (const rect of rects) {
|
|
655
|
+
minX = Math.min(minX, rect.x);
|
|
656
|
+
minY = Math.min(minY, rect.y);
|
|
657
|
+
maxX = Math.max(maxX, rect.x + rect.width);
|
|
658
|
+
maxY = Math.max(maxY, rect.y + rect.height);
|
|
659
|
+
}
|
|
660
|
+
if (minX === Infinity) return { x: 0, y: 0, width: 0, height: 0 };
|
|
661
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
662
|
+
}
|
|
663
|
+
function rectsIntersect(a, b) {
|
|
664
|
+
return a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height;
|
|
665
|
+
}
|
|
666
|
+
function rectContainsPoint(rect, point) {
|
|
667
|
+
return point.x >= rect.x && point.x <= rect.x + rect.width && point.y >= rect.y && point.y <= rect.y + rect.height;
|
|
668
|
+
}
|
|
669
|
+
function getAbsolutePositions(doc, index = buildIndex(doc)) {
|
|
670
|
+
const result = /* @__PURE__ */ new Map();
|
|
671
|
+
const resolve = (id) => {
|
|
672
|
+
const known = result.get(id);
|
|
673
|
+
if (known) return known;
|
|
674
|
+
const chain = [];
|
|
675
|
+
const seen = /* @__PURE__ */ new Set();
|
|
676
|
+
let current = index.nodesById.get(id);
|
|
677
|
+
let base = { x: 0, y: 0 };
|
|
678
|
+
while (current) {
|
|
679
|
+
if (seen.has(current.id)) break;
|
|
680
|
+
seen.add(current.id);
|
|
681
|
+
const cached = result.get(current.id);
|
|
682
|
+
if (cached) {
|
|
683
|
+
base = cached;
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
chain.push(current);
|
|
687
|
+
current = current.parentId === void 0 ? void 0 : index.nodesById.get(current.parentId);
|
|
688
|
+
}
|
|
689
|
+
for (let i = chain.length - 1; i >= 0; i -= 1) {
|
|
690
|
+
const node = chain[i];
|
|
691
|
+
base = { x: base.x + node.position.x, y: base.y + node.position.y };
|
|
692
|
+
result.set(node.id, base);
|
|
693
|
+
}
|
|
694
|
+
return result.get(id) ?? base;
|
|
695
|
+
};
|
|
696
|
+
for (const node of doc.nodes) resolve(node.id);
|
|
697
|
+
return result;
|
|
698
|
+
}
|
|
699
|
+
function getAbsolutePosition(doc, id) {
|
|
700
|
+
return getAbsolutePositions(doc).get(id);
|
|
701
|
+
}
|
|
702
|
+
function getAbsoluteBounds(doc, index = buildIndex(doc)) {
|
|
703
|
+
const positions = getAbsolutePositions(doc, index);
|
|
704
|
+
const bounds = /* @__PURE__ */ new Map();
|
|
705
|
+
for (const node of doc.nodes) {
|
|
706
|
+
const at = positions.get(node.id) ?? node.position;
|
|
707
|
+
bounds.set(node.id, { x: at.x, y: at.y, ...getNodeSize(node) });
|
|
708
|
+
}
|
|
709
|
+
return bounds;
|
|
710
|
+
}
|
|
711
|
+
function getDocumentBounds(doc) {
|
|
712
|
+
return unionRects(getAbsoluteBounds(doc).values());
|
|
713
|
+
}
|
|
714
|
+
function snapValue(value, grid = GRID_SIZE) {
|
|
715
|
+
return grid > 0 ? Math.round(value / grid) * grid : value;
|
|
716
|
+
}
|
|
717
|
+
function snapPoint(point, grid = GRID_SIZE) {
|
|
718
|
+
return { x: snapValue(point.x, grid), y: snapValue(point.y, grid) };
|
|
719
|
+
}
|
|
720
|
+
function directionSides(direction) {
|
|
721
|
+
switch (direction) {
|
|
722
|
+
case "TB":
|
|
723
|
+
return { source: "bottom", target: "top" };
|
|
724
|
+
case "BT":
|
|
725
|
+
return { source: "top", target: "bottom" };
|
|
726
|
+
case "LR":
|
|
727
|
+
return { source: "right", target: "left" };
|
|
728
|
+
case "RL":
|
|
729
|
+
return { source: "left", target: "right" };
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
function mirrorDirection(direction, rtl) {
|
|
733
|
+
if (!rtl) return direction;
|
|
734
|
+
if (direction === "LR") return "RL";
|
|
735
|
+
if (direction === "RL") return "LR";
|
|
736
|
+
return direction;
|
|
737
|
+
}
|
|
738
|
+
function isHorizontal(direction) {
|
|
739
|
+
return direction === "LR" || direction === "RL";
|
|
740
|
+
}
|
|
741
|
+
function getPortPlacements(node, direction = "TB", rtl = false) {
|
|
742
|
+
const sides = directionSides(direction);
|
|
743
|
+
const mirror2 = (offset, side) => rtl && (side === "top" || side === "bottom") ? 1 - offset : offset;
|
|
744
|
+
if (!node.ports || node.ports.length === 0) {
|
|
745
|
+
const placements2 = [];
|
|
746
|
+
if (node.kind !== "start") placements2.push({ id: void 0, direction: "in", side: sides.target, offset: 0.5 });
|
|
747
|
+
if (node.kind !== "end") placements2.push({ id: void 0, direction: "out", side: sides.source, offset: 0.5 });
|
|
748
|
+
return placements2;
|
|
749
|
+
}
|
|
750
|
+
const placements = [];
|
|
751
|
+
for (const dir of ["in", "out"]) {
|
|
752
|
+
const ports = node.ports.filter((port) => port.direction === dir);
|
|
753
|
+
const side = dir === "in" ? sides.target : sides.source;
|
|
754
|
+
ports.forEach((port, i) => {
|
|
755
|
+
const placement = {
|
|
756
|
+
id: port.id,
|
|
757
|
+
direction: dir,
|
|
758
|
+
side,
|
|
759
|
+
offset: mirror2((i + 1) / (ports.length + 1), side)
|
|
760
|
+
};
|
|
761
|
+
if (port.type !== void 0) placement.type = port.type;
|
|
762
|
+
placements.push(placement);
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
return placements;
|
|
766
|
+
}
|
|
767
|
+
function sideAnchor(rect, side, offset = 0.5) {
|
|
768
|
+
switch (side) {
|
|
769
|
+
case "top":
|
|
770
|
+
return { x: rect.x + rect.width * offset, y: rect.y };
|
|
771
|
+
case "bottom":
|
|
772
|
+
return { x: rect.x + rect.width * offset, y: rect.y + rect.height };
|
|
773
|
+
case "left":
|
|
774
|
+
return { x: rect.x, y: rect.y + rect.height * offset };
|
|
775
|
+
case "right":
|
|
776
|
+
return { x: rect.x + rect.width, y: rect.y + rect.height * offset };
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
var SIDE_VECTOR = {
|
|
780
|
+
top: { x: 0, y: -1 },
|
|
781
|
+
right: { x: 1, y: 0 },
|
|
782
|
+
bottom: { x: 0, y: 1 },
|
|
783
|
+
left: { x: -1, y: 0 }
|
|
784
|
+
};
|
|
785
|
+
function sideVector(side) {
|
|
786
|
+
return SIDE_VECTOR[side];
|
|
787
|
+
}
|
|
788
|
+
var fmt = (value) => {
|
|
789
|
+
const rounded = Math.round(value * 100) / 100;
|
|
790
|
+
return Object.is(rounded, -0) ? "0" : String(rounded);
|
|
791
|
+
};
|
|
792
|
+
var add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
|
|
793
|
+
var sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
|
|
794
|
+
var scale = (a, k) => ({ x: a.x * k, y: a.y * k });
|
|
795
|
+
var dot = (a, b) => a.x * b.x + a.y * b.y;
|
|
796
|
+
var same = (a, b) => a.x === b.x && a.y === b.y;
|
|
797
|
+
var distance = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
|
|
798
|
+
function controlOffset(delta, curvature) {
|
|
799
|
+
return delta >= 0 ? 0.5 * delta : curvature * 25 * Math.sqrt(-delta);
|
|
800
|
+
}
|
|
801
|
+
function controlPoint(side, from, to, curvature) {
|
|
802
|
+
switch (side) {
|
|
803
|
+
case "left":
|
|
804
|
+
return { x: from.x - controlOffset(from.x - to.x, curvature), y: from.y };
|
|
805
|
+
case "right":
|
|
806
|
+
return { x: from.x + controlOffset(to.x - from.x, curvature), y: from.y };
|
|
807
|
+
case "top":
|
|
808
|
+
return { x: from.x, y: from.y - controlOffset(from.y - to.y, curvature) };
|
|
809
|
+
case "bottom":
|
|
810
|
+
return { x: from.x, y: from.y + controlOffset(to.y - from.y, curvature) };
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
function sidestep(a, b, gap) {
|
|
814
|
+
return Math.abs(a - b) >= gap * 2 ? (a + b) / 2 : Math.max(a, b) + gap * 2;
|
|
815
|
+
}
|
|
816
|
+
function orthogonalPoints(s, ss, t, ts, gap) {
|
|
817
|
+
const sv = SIDE_VECTOR[ss];
|
|
818
|
+
const tv = SIDE_VECTOR[ts];
|
|
819
|
+
const s1 = add(s, scale(sv, gap));
|
|
820
|
+
const t1 = add(t, scale(tv, gap));
|
|
821
|
+
const sVertical = sv.y !== 0;
|
|
822
|
+
const tVertical = tv.y !== 0;
|
|
823
|
+
let points;
|
|
824
|
+
if (sVertical === tVertical) {
|
|
825
|
+
const opposite = sv.x === -tv.x && sv.y === -tv.y;
|
|
826
|
+
if (opposite && dot(sub(t1, s1), sv) >= 0) {
|
|
827
|
+
if (sVertical) {
|
|
828
|
+
const y = (s.y + t.y) / 2;
|
|
829
|
+
points = [s, { x: s.x, y }, { x: t.x, y }, t];
|
|
830
|
+
} else {
|
|
831
|
+
const x = (s.x + t.x) / 2;
|
|
832
|
+
points = [s, { x, y: s.y }, { x, y: t.y }, t];
|
|
833
|
+
}
|
|
834
|
+
} else if (!opposite) {
|
|
835
|
+
if (sVertical) {
|
|
836
|
+
const y = sv.y > 0 ? Math.max(s1.y, t1.y) : Math.min(s1.y, t1.y);
|
|
837
|
+
points = [s, { x: s.x, y }, { x: t.x, y }, t];
|
|
838
|
+
} else {
|
|
839
|
+
const x = sv.x > 0 ? Math.max(s1.x, t1.x) : Math.min(s1.x, t1.x);
|
|
840
|
+
points = [s, { x, y: s.y }, { x, y: t.y }, t];
|
|
841
|
+
}
|
|
842
|
+
} else if (sVertical) {
|
|
843
|
+
const x = sidestep(s.x, t.x, gap);
|
|
844
|
+
points = [s, s1, { x, y: s1.y }, { x, y: t1.y }, t1, t];
|
|
845
|
+
} else {
|
|
846
|
+
const y = sidestep(s.y, t.y, gap);
|
|
847
|
+
points = [s, s1, { x: s1.x, y }, { x: t1.x, y }, t1, t];
|
|
848
|
+
}
|
|
849
|
+
} else if (sVertical) {
|
|
850
|
+
const corner = { x: s.x, y: t.y };
|
|
851
|
+
const leavesOk = dot(sub(corner, s), sv) >= gap;
|
|
852
|
+
const arrivesOk = dot(sub(t, corner), scale(tv, -1)) >= gap;
|
|
853
|
+
points = leavesOk && arrivesOk ? [s, corner, t] : [s, s1, { x: s1.x, y: t1.y }, t1, t];
|
|
854
|
+
} else {
|
|
855
|
+
const corner = { x: t.x, y: s.y };
|
|
856
|
+
const leavesOk = dot(sub(corner, s), sv) >= gap;
|
|
857
|
+
const arrivesOk = dot(sub(t, corner), scale(tv, -1)) >= gap;
|
|
858
|
+
points = leavesOk && arrivesOk ? [s, corner, t] : [s, s1, { x: t1.x, y: s1.y }, t1, t];
|
|
859
|
+
}
|
|
860
|
+
return simplify(points);
|
|
861
|
+
}
|
|
862
|
+
function simplify(points) {
|
|
863
|
+
const out = [];
|
|
864
|
+
for (const point of points) {
|
|
865
|
+
const last = out[out.length - 1];
|
|
866
|
+
if (last && same(last, point)) continue;
|
|
867
|
+
out.push(point);
|
|
868
|
+
}
|
|
869
|
+
for (let i = out.length - 2; i >= 1; i -= 1) {
|
|
870
|
+
const a = out[i - 1];
|
|
871
|
+
const b = out[i];
|
|
872
|
+
const c = out[i + 1];
|
|
873
|
+
if (a.x === b.x && b.x === c.x || a.y === b.y && b.y === c.y) out.splice(i, 1);
|
|
874
|
+
}
|
|
875
|
+
return out;
|
|
876
|
+
}
|
|
877
|
+
function polylineMidpoint(points) {
|
|
878
|
+
let total = 0;
|
|
879
|
+
for (let i = 1; i < points.length; i += 1) total += distance(points[i - 1], points[i]);
|
|
880
|
+
let remaining = total / 2;
|
|
881
|
+
for (let i = 1; i < points.length; i += 1) {
|
|
882
|
+
const a = points[i - 1];
|
|
883
|
+
const b = points[i];
|
|
884
|
+
const length = distance(a, b);
|
|
885
|
+
if (remaining <= length && length > 0) {
|
|
886
|
+
const t = remaining / length;
|
|
887
|
+
return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
|
|
888
|
+
}
|
|
889
|
+
remaining -= length;
|
|
890
|
+
}
|
|
891
|
+
return points[0] ?? { x: 0, y: 0 };
|
|
892
|
+
}
|
|
893
|
+
function polylinePath(points, radius) {
|
|
894
|
+
const first = points[0];
|
|
895
|
+
if (!first) return "";
|
|
896
|
+
let d = `M ${fmt(first.x)} ${fmt(first.y)}`;
|
|
897
|
+
for (let i = 1; i < points.length; i += 1) {
|
|
898
|
+
const prev = points[i - 1];
|
|
899
|
+
const current = points[i];
|
|
900
|
+
const next = points[i + 1];
|
|
901
|
+
if (radius > 0 && next) {
|
|
902
|
+
const r = Math.min(radius, distance(prev, current) / 2, distance(current, next) / 2);
|
|
903
|
+
const toPrev = sub(prev, current);
|
|
904
|
+
const toNext = sub(next, current);
|
|
905
|
+
const a = add(current, scale(toPrev, r / (distance(prev, current) || 1)));
|
|
906
|
+
const b = add(current, scale(toNext, r / (distance(current, next) || 1)));
|
|
907
|
+
d += ` L ${fmt(a.x)} ${fmt(a.y)} Q ${fmt(current.x)} ${fmt(current.y)} ${fmt(b.x)} ${fmt(b.y)}`;
|
|
908
|
+
} else {
|
|
909
|
+
d += ` L ${fmt(current.x)} ${fmt(current.y)}`;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return d;
|
|
913
|
+
}
|
|
914
|
+
function computeEdgePath(input) {
|
|
915
|
+
const { source, target, sourceSide, targetSide } = input;
|
|
916
|
+
const kind = input.kind ?? "smooth-step";
|
|
917
|
+
const offset = input.offset ?? 24;
|
|
918
|
+
const radius = input.radius ?? 8;
|
|
919
|
+
const curvature = input.curvature ?? 0.25;
|
|
920
|
+
if (input.loopSize !== void 0) {
|
|
921
|
+
const size = input.loopSize;
|
|
922
|
+
const bulge = sideVector(sourceSide).y !== 0 ? { x: 1, y: 0 } : { x: 0, y: 1 };
|
|
923
|
+
const c1 = add(add(source, scale(SIDE_VECTOR[sourceSide], size)), scale(bulge, size));
|
|
924
|
+
const c2 = add(add(target, scale(SIDE_VECTOR[targetSide], size)), scale(bulge, size));
|
|
925
|
+
const label = {
|
|
926
|
+
x: (source.x + 3 * c1.x + 3 * c2.x + target.x) / 8,
|
|
927
|
+
y: (source.y + 3 * c1.y + 3 * c2.y + target.y) / 8
|
|
928
|
+
};
|
|
929
|
+
return {
|
|
930
|
+
path: `M ${fmt(source.x)} ${fmt(source.y)} C ${fmt(c1.x)} ${fmt(c1.y)} ${fmt(c2.x)} ${fmt(c2.y)} ${fmt(target.x)} ${fmt(target.y)}`,
|
|
931
|
+
label,
|
|
932
|
+
points: [source, c1, c2, target]
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
if (kind === "straight") {
|
|
936
|
+
return {
|
|
937
|
+
path: `M ${fmt(source.x)} ${fmt(source.y)} L ${fmt(target.x)} ${fmt(target.y)}`,
|
|
938
|
+
label: { x: (source.x + target.x) / 2, y: (source.y + target.y) / 2 },
|
|
939
|
+
points: [source, target]
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
if (kind === "bezier") {
|
|
943
|
+
const c1 = controlPoint(sourceSide, source, target, curvature);
|
|
944
|
+
const c2 = controlPoint(targetSide, target, source, curvature);
|
|
945
|
+
return {
|
|
946
|
+
path: `M ${fmt(source.x)} ${fmt(source.y)} C ${fmt(c1.x)} ${fmt(c1.y)} ${fmt(c2.x)} ${fmt(c2.y)} ${fmt(target.x)} ${fmt(target.y)}`,
|
|
947
|
+
label: {
|
|
948
|
+
x: (source.x + 3 * c1.x + 3 * c2.x + target.x) / 8,
|
|
949
|
+
y: (source.y + 3 * c1.y + 3 * c2.y + target.y) / 8
|
|
950
|
+
},
|
|
951
|
+
points: [source, c1, c2, target]
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
const points = orthogonalPoints(source, sourceSide, target, targetSide, offset);
|
|
955
|
+
return {
|
|
956
|
+
path: polylinePath(points, kind === "smooth-step" ? radius : 0),
|
|
957
|
+
label: polylineMidpoint(points),
|
|
958
|
+
points
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
function createGeometryContext(doc, options = {}) {
|
|
962
|
+
const index = buildIndex(doc);
|
|
963
|
+
return {
|
|
964
|
+
doc,
|
|
965
|
+
index,
|
|
966
|
+
direction: options.direction ?? "TB",
|
|
967
|
+
rtl: options.rtl ?? false,
|
|
968
|
+
edgeKind: options.edgeKind ?? "smooth-step",
|
|
969
|
+
bounds: getAbsoluteBounds(doc, index)
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
function anchorFor(node, rect, wanted, portId, direction, rtl) {
|
|
973
|
+
const placements = getPortPlacements(node, direction, rtl);
|
|
974
|
+
const sides = directionSides(direction);
|
|
975
|
+
const match = placements.find((placement) => placement.direction === wanted && placement.id === portId) ?? placements.find((placement) => placement.direction === wanted && portId === void 0);
|
|
976
|
+
const side = match?.side ?? (wanted === "out" ? sides.source : sides.target);
|
|
977
|
+
return { point: sideAnchor(rect, side, match?.offset ?? 0.5), side };
|
|
978
|
+
}
|
|
979
|
+
function getEdgeGeometry(context, edge) {
|
|
980
|
+
const sourceNode = context.index.nodesById.get(edge.source);
|
|
981
|
+
const targetNode = context.index.nodesById.get(edge.target);
|
|
982
|
+
const sourceRect = context.bounds.get(edge.source);
|
|
983
|
+
const targetRect = context.bounds.get(edge.target);
|
|
984
|
+
if (!sourceNode || !targetNode || !sourceRect || !targetRect) return void 0;
|
|
985
|
+
const from = anchorFor(sourceNode, sourceRect, "out", edge.sourcePort, context.direction, context.rtl);
|
|
986
|
+
const to = anchorFor(targetNode, targetRect, "in", edge.targetPort, context.direction, context.rtl);
|
|
987
|
+
const selfLoop = edge.source === edge.target;
|
|
988
|
+
const halfExtent = sideVector(from.side).y !== 0 ? sourceRect.width / 2 : sourceRect.height / 2;
|
|
989
|
+
const path = computeEdgePath({
|
|
990
|
+
source: from.point,
|
|
991
|
+
target: to.point,
|
|
992
|
+
sourceSide: from.side,
|
|
993
|
+
targetSide: to.side,
|
|
994
|
+
kind: edge.kind ?? context.edgeKind,
|
|
995
|
+
...selfLoop ? { loopSize: Math.ceil(halfExtent / 0.75) + 24 } : {}
|
|
996
|
+
});
|
|
997
|
+
return { ...path, source: from.point, target: to.point, sourceSide: from.side, targetSide: to.side, selfLoop };
|
|
998
|
+
}
|
|
999
|
+
function getReadingOrder(doc, options = {}) {
|
|
1000
|
+
const { direction = "TB", rtl = false, tolerance = 24 } = options;
|
|
1001
|
+
const index = buildIndex(doc);
|
|
1002
|
+
const horizontal = isHorizontal(direction);
|
|
1003
|
+
const primarySign = direction === "BT" || direction === "RL" ? -1 : 1;
|
|
1004
|
+
const secondarySign = horizontal ? 1 : rtl ? -1 : 1;
|
|
1005
|
+
const sortSiblings = (ids) => {
|
|
1006
|
+
const entries = ids.map((id) => {
|
|
1007
|
+
const node = index.nodesById.get(id);
|
|
1008
|
+
const c = rectCenter(getNodeBounds(node));
|
|
1009
|
+
return { id, p: (horizontal ? c.x : c.y) * primarySign, s: (horizontal ? c.y : c.x) * secondarySign };
|
|
1010
|
+
});
|
|
1011
|
+
entries.sort((a, b) => a.p - b.p);
|
|
1012
|
+
const rows = [];
|
|
1013
|
+
let anchor = -Infinity;
|
|
1014
|
+
for (const entry of entries) {
|
|
1015
|
+
const row = rows[rows.length - 1];
|
|
1016
|
+
if (row && entry.p - anchor <= tolerance) row.push(entry);
|
|
1017
|
+
else {
|
|
1018
|
+
rows.push([entry]);
|
|
1019
|
+
anchor = entry.p;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
return rows.flatMap((row) => row.sort((a, b) => a.s - b.s).map((entry) => entry.id));
|
|
1023
|
+
};
|
|
1024
|
+
const result = [];
|
|
1025
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1026
|
+
const visit = (id) => {
|
|
1027
|
+
if (visited.has(id)) return;
|
|
1028
|
+
visited.add(id);
|
|
1029
|
+
result.push(id);
|
|
1030
|
+
const kids = index.children.get(id);
|
|
1031
|
+
if (kids) for (const child of sortSiblings(kids)) visit(child);
|
|
1032
|
+
};
|
|
1033
|
+
const roots = doc.nodes.filter((node) => node.parentId === void 0 || !index.nodesById.has(node.parentId) || node.parentId === node.id).map((node) => node.id);
|
|
1034
|
+
for (const id of sortSiblings(roots)) visit(id);
|
|
1035
|
+
for (const node of doc.nodes) visit(node.id);
|
|
1036
|
+
return result;
|
|
1037
|
+
}
|
|
1038
|
+
var FOCUS_VECTOR = {
|
|
1039
|
+
up: { x: 0, y: -1 },
|
|
1040
|
+
down: { x: 0, y: 1 },
|
|
1041
|
+
left: { x: -1, y: 0 },
|
|
1042
|
+
right: { x: 1, y: 0 }
|
|
1043
|
+
};
|
|
1044
|
+
function findDirectionalNeighbor(doc, fromId, direction, options = {}) {
|
|
1045
|
+
const index = buildIndex(doc);
|
|
1046
|
+
const bounds = getAbsoluteBounds(doc, index);
|
|
1047
|
+
const origin = bounds.get(fromId);
|
|
1048
|
+
if (!origin) return void 0;
|
|
1049
|
+
const from = rectCenter(origin);
|
|
1050
|
+
const vector = FOCUS_VECTOR[direction];
|
|
1051
|
+
const connected = new Set(getNeighbors(index, fromId));
|
|
1052
|
+
const allowed = options.candidates === void 0 ? void 0 : new Set(options.candidates);
|
|
1053
|
+
let best;
|
|
1054
|
+
for (const node of doc.nodes) {
|
|
1055
|
+
if (node.id === fromId || allowed && !allowed.has(node.id)) continue;
|
|
1056
|
+
const rect = bounds.get(node.id);
|
|
1057
|
+
if (!rect) continue;
|
|
1058
|
+
const delta = sub(rectCenter(rect), from);
|
|
1059
|
+
const along = dot(delta, vector);
|
|
1060
|
+
const across = Math.abs(delta.x * vector.y - delta.y * vector.x);
|
|
1061
|
+
if (along <= 0 || across > along * 2) continue;
|
|
1062
|
+
const score = along + across * 2;
|
|
1063
|
+
const linked = connected.has(node.id);
|
|
1064
|
+
if (!best || linked && !best.linked || linked === best.linked && score < best.score) {
|
|
1065
|
+
best = { id: node.id, score, linked };
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
return best?.id;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// src/mutations.ts
|
|
1072
|
+
var DiagramMutationError = class extends Error {
|
|
1073
|
+
code;
|
|
1074
|
+
/** For `invalid-connection`: the `ConnectionRejection` code. */
|
|
1075
|
+
detail;
|
|
1076
|
+
constructor(code, message, detail) {
|
|
1077
|
+
super(message);
|
|
1078
|
+
this.name = "DiagramMutationError";
|
|
1079
|
+
this.code = code;
|
|
1080
|
+
if (detail !== void 0) this.detail = detail;
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
var withoutUndefined = (value) => {
|
|
1084
|
+
const result = { ...value };
|
|
1085
|
+
for (const key of Object.keys(result)) if (result[key] === void 0) delete result[key];
|
|
1086
|
+
return result;
|
|
1087
|
+
};
|
|
1088
|
+
function nextNodeId(doc, prefix = "node") {
|
|
1089
|
+
return nextId(
|
|
1090
|
+
doc.nodes.map((node) => node.id),
|
|
1091
|
+
prefix
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
function nextEdgeId(doc, prefix = "edge") {
|
|
1095
|
+
return nextId(
|
|
1096
|
+
doc.edges.map((edge) => edge.id),
|
|
1097
|
+
prefix
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
function createNode(doc, input) {
|
|
1101
|
+
const id = input.id ?? nextNodeId(doc, input.kind);
|
|
1102
|
+
return withoutUndefined({
|
|
1103
|
+
...input,
|
|
1104
|
+
id,
|
|
1105
|
+
label: input.label ?? "",
|
|
1106
|
+
position: input.position ?? { x: 0, y: 0 }
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
function requireContainer(doc, parentId) {
|
|
1110
|
+
const parent = doc.nodes.find((node) => node.id === parentId);
|
|
1111
|
+
if (!parent) throw new DiagramMutationError("parent-not-found", `Parent node "${parentId}" does not exist.`);
|
|
1112
|
+
if (!isContainerKind(parent.kind)) {
|
|
1113
|
+
throw new DiagramMutationError("parent-not-container", `Node "${parentId}" is a ${parent.kind}, not a group or subflow.`);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
function addNode(doc, input) {
|
|
1117
|
+
const node = createNode(doc, input);
|
|
1118
|
+
if (doc.nodes.some((existing) => existing.id === node.id)) {
|
|
1119
|
+
throw new DiagramMutationError("duplicate-id", `A node with id "${node.id}" already exists.`);
|
|
1120
|
+
}
|
|
1121
|
+
if (node.parentId !== void 0) requireContainer(doc, node.parentId);
|
|
1122
|
+
return { ...doc, nodes: [...doc.nodes, node] };
|
|
1123
|
+
}
|
|
1124
|
+
function updateNode(doc, id, patch) {
|
|
1125
|
+
const current = doc.nodes.find((node) => node.id === id);
|
|
1126
|
+
if (!current) throw new DiagramMutationError("node-not-found", `No node with id "${id}".`);
|
|
1127
|
+
const resolved = typeof patch === "function" ? patch(current) : patch;
|
|
1128
|
+
const { ...rest } = resolved;
|
|
1129
|
+
delete rest.id;
|
|
1130
|
+
if (rest.parentId !== void 0) requireContainer(doc, rest.parentId);
|
|
1131
|
+
const next = withoutUndefined({ ...current, ...rest });
|
|
1132
|
+
return { ...doc, nodes: doc.nodes.map((node) => node === current ? next : node) };
|
|
1133
|
+
}
|
|
1134
|
+
function removeNodes(doc, ids, options = {}) {
|
|
1135
|
+
const index = buildIndex(doc);
|
|
1136
|
+
const removed = /* @__PURE__ */ new Set();
|
|
1137
|
+
for (const id of ids) {
|
|
1138
|
+
if (!index.nodesById.has(id)) continue;
|
|
1139
|
+
removed.add(id);
|
|
1140
|
+
if (options.children === "remove") for (const descendant of getDescendants(index, id)) removed.add(descendant);
|
|
1141
|
+
}
|
|
1142
|
+
if (removed.size === 0) return doc;
|
|
1143
|
+
const nodes = [];
|
|
1144
|
+
for (const node of doc.nodes) {
|
|
1145
|
+
if (removed.has(node.id)) continue;
|
|
1146
|
+
if (node.parentId !== void 0 && removed.has(node.parentId)) {
|
|
1147
|
+
let x = node.position.x;
|
|
1148
|
+
let y = node.position.y;
|
|
1149
|
+
let parentId = node.parentId;
|
|
1150
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1151
|
+
while (parentId !== void 0 && removed.has(parentId) && !seen.has(parentId)) {
|
|
1152
|
+
seen.add(parentId);
|
|
1153
|
+
const parent = index.nodesById.get(parentId);
|
|
1154
|
+
x += parent.position.x;
|
|
1155
|
+
y += parent.position.y;
|
|
1156
|
+
parentId = parent.parentId;
|
|
1157
|
+
}
|
|
1158
|
+
const released = { ...node, position: { x, y } };
|
|
1159
|
+
if (parentId === void 0 || removed.has(parentId)) delete released.parentId;
|
|
1160
|
+
else released.parentId = parentId;
|
|
1161
|
+
nodes.push(released);
|
|
1162
|
+
} else {
|
|
1163
|
+
nodes.push(node);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
const edges = doc.edges.filter((edge) => !removed.has(edge.source) && !removed.has(edge.target));
|
|
1167
|
+
return { ...doc, nodes, edges };
|
|
1168
|
+
}
|
|
1169
|
+
function removeNode(doc, id, options = {}) {
|
|
1170
|
+
if (!doc.nodes.some((node) => node.id === id)) throw new DiagramMutationError("node-not-found", `No node with id "${id}".`);
|
|
1171
|
+
return removeNodes(doc, [id], options);
|
|
1172
|
+
}
|
|
1173
|
+
function moveNodes(doc, positions) {
|
|
1174
|
+
let changed = false;
|
|
1175
|
+
const nodes = doc.nodes.map((node) => {
|
|
1176
|
+
const next = positions[node.id];
|
|
1177
|
+
if (!next || next.x === node.position.x && next.y === node.position.y) return node;
|
|
1178
|
+
changed = true;
|
|
1179
|
+
return { ...node, position: { x: next.x, y: next.y } };
|
|
1180
|
+
});
|
|
1181
|
+
return changed ? { ...doc, nodes } : doc;
|
|
1182
|
+
}
|
|
1183
|
+
function translateNodes(doc, ids, dx, dy) {
|
|
1184
|
+
const wanted = new Set(ids);
|
|
1185
|
+
if (dx === 0 && dy === 0 || wanted.size === 0) return doc;
|
|
1186
|
+
const nodes = doc.nodes.map((node) => {
|
|
1187
|
+
if (!wanted.has(node.id)) return node;
|
|
1188
|
+
return { ...node, position: { x: node.position.x + dx, y: node.position.y + dy } };
|
|
1189
|
+
});
|
|
1190
|
+
return { ...doc, nodes };
|
|
1191
|
+
}
|
|
1192
|
+
function resizeNode(doc, id, size, position) {
|
|
1193
|
+
return updateNode(doc, id, { width: size.width, height: size.height, ...position ? { position } : {} });
|
|
1194
|
+
}
|
|
1195
|
+
function setNodeParent(doc, id, parentId) {
|
|
1196
|
+
const index = buildIndex(doc);
|
|
1197
|
+
const node = index.nodesById.get(id);
|
|
1198
|
+
if (!node) throw new DiagramMutationError("node-not-found", `No node with id "${id}".`);
|
|
1199
|
+
if (parentId !== void 0) {
|
|
1200
|
+
requireContainer(doc, parentId);
|
|
1201
|
+
if (parentId === id || getAncestors(index, parentId).includes(id)) {
|
|
1202
|
+
throw new DiagramMutationError("cannot-group", "A group cannot be moved inside itself.");
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
const absolute = getAbsolutePositions(doc, index);
|
|
1206
|
+
const at = absolute.get(id);
|
|
1207
|
+
const base = parentId === void 0 ? { x: 0, y: 0 } : absolute.get(parentId);
|
|
1208
|
+
const next = { ...node, position: { x: at.x - base.x, y: at.y - base.y } };
|
|
1209
|
+
if (parentId === void 0) delete next.parentId;
|
|
1210
|
+
else next.parentId = parentId;
|
|
1211
|
+
return { ...doc, nodes: doc.nodes.map((candidate) => candidate === node ? next : candidate) };
|
|
1212
|
+
}
|
|
1213
|
+
function connect(doc, input, options = {}) {
|
|
1214
|
+
const { validate = true, ...rules } = options;
|
|
1215
|
+
if (validate) {
|
|
1216
|
+
const check = checkConnection(doc, input, rules);
|
|
1217
|
+
if (!check.ok) throw new DiagramMutationError("invalid-connection", check.message, check.code);
|
|
1218
|
+
}
|
|
1219
|
+
const id = input.id ?? nextEdgeId(doc);
|
|
1220
|
+
if (doc.edges.some((edge2) => edge2.id === id)) {
|
|
1221
|
+
throw new DiagramMutationError("duplicate-id", `An edge with id "${id}" already exists.`);
|
|
1222
|
+
}
|
|
1223
|
+
const edge = withoutUndefined({ ...input, id });
|
|
1224
|
+
return { ...doc, edges: [...doc.edges, edge] };
|
|
1225
|
+
}
|
|
1226
|
+
function disconnect(doc, target) {
|
|
1227
|
+
if (typeof target === "string") {
|
|
1228
|
+
if (!doc.edges.some((edge) => edge.id === target)) {
|
|
1229
|
+
throw new DiagramMutationError("edge-not-found", `No edge with id "${target}".`);
|
|
1230
|
+
}
|
|
1231
|
+
return { ...doc, edges: doc.edges.filter((edge) => edge.id !== target) };
|
|
1232
|
+
}
|
|
1233
|
+
const edges = doc.edges.filter(
|
|
1234
|
+
(edge) => !(edge.source === target.source && edge.target === target.target && (target.sourcePort === void 0 || edge.sourcePort === target.sourcePort) && (target.targetPort === void 0 || edge.targetPort === target.targetPort))
|
|
1235
|
+
);
|
|
1236
|
+
return edges.length === doc.edges.length ? doc : { ...doc, edges };
|
|
1237
|
+
}
|
|
1238
|
+
function removeEdges(doc, ids) {
|
|
1239
|
+
const wanted = new Set(ids);
|
|
1240
|
+
if (wanted.size === 0) return doc;
|
|
1241
|
+
const edges = doc.edges.filter((edge) => !wanted.has(edge.id));
|
|
1242
|
+
return edges.length === doc.edges.length ? doc : { ...doc, edges };
|
|
1243
|
+
}
|
|
1244
|
+
function updateEdge(doc, id, patch) {
|
|
1245
|
+
const current = doc.edges.find((edge) => edge.id === id);
|
|
1246
|
+
if (!current) throw new DiagramMutationError("edge-not-found", `No edge with id "${id}".`);
|
|
1247
|
+
const next = withoutUndefined({ ...current, ...patch, id });
|
|
1248
|
+
return { ...doc, edges: doc.edges.map((edge) => edge === current ? next : edge) };
|
|
1249
|
+
}
|
|
1250
|
+
function groupNodes(doc, ids, options = {}) {
|
|
1251
|
+
const index = buildIndex(doc);
|
|
1252
|
+
const selected = /* @__PURE__ */ new Set();
|
|
1253
|
+
for (const id2 of ids) if (index.nodesById.has(id2)) selected.add(id2);
|
|
1254
|
+
const members = [...selected].filter((id2) => !getAncestors(index, id2).some((ancestor) => selected.has(ancestor)));
|
|
1255
|
+
if (members.length === 0) throw new DiagramMutationError("cannot-group", "Select at least one node to group.");
|
|
1256
|
+
const parents = new Set(members.map((id2) => index.nodesById.get(id2)?.parentId));
|
|
1257
|
+
if (parents.size > 1) throw new DiagramMutationError("cannot-group", "Nodes must share the same parent to be grouped.");
|
|
1258
|
+
const parentId = [...parents][0];
|
|
1259
|
+
const kind = options.kind ?? "group";
|
|
1260
|
+
const padding = options.padding ?? 32;
|
|
1261
|
+
const header = options.header ?? 40;
|
|
1262
|
+
const id = options.id ?? nextNodeId(doc, kind);
|
|
1263
|
+
if (index.nodesById.has(id)) throw new DiagramMutationError("duplicate-id", `A node with id "${id}" already exists.`);
|
|
1264
|
+
const bounds = getAbsoluteBounds(doc, index);
|
|
1265
|
+
const box = unionRects(members.map((member) => bounds.get(member)));
|
|
1266
|
+
const groupAbsolute = { x: box.x - padding, y: box.y - padding - header };
|
|
1267
|
+
const parentAbsolute = parentId === void 0 ? { x: 0, y: 0 } : getAbsolutePositions(doc, index).get(parentId);
|
|
1268
|
+
const group = {
|
|
1269
|
+
id,
|
|
1270
|
+
kind,
|
|
1271
|
+
label: options.label ?? "",
|
|
1272
|
+
position: { x: groupAbsolute.x - parentAbsolute.x, y: groupAbsolute.y - parentAbsolute.y },
|
|
1273
|
+
width: box.width + padding * 2,
|
|
1274
|
+
height: box.height + padding * 2 + header
|
|
1275
|
+
};
|
|
1276
|
+
if (parentId !== void 0) group.parentId = parentId;
|
|
1277
|
+
const memberSet = new Set(members);
|
|
1278
|
+
const nodes = [];
|
|
1279
|
+
let inserted = false;
|
|
1280
|
+
for (const node of doc.nodes) {
|
|
1281
|
+
if (memberSet.has(node.id)) {
|
|
1282
|
+
if (!inserted) {
|
|
1283
|
+
nodes.push(group);
|
|
1284
|
+
inserted = true;
|
|
1285
|
+
}
|
|
1286
|
+
const at = bounds.get(node.id);
|
|
1287
|
+
nodes.push({ ...node, parentId: id, position: { x: at.x - groupAbsolute.x, y: at.y - groupAbsolute.y } });
|
|
1288
|
+
} else {
|
|
1289
|
+
nodes.push(node);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
return { ...doc, nodes };
|
|
1293
|
+
}
|
|
1294
|
+
function ungroupNode(doc, groupId) {
|
|
1295
|
+
return removeNode(doc, groupId, { children: "release" });
|
|
1296
|
+
}
|
|
1297
|
+
function extractFragment(doc, ids) {
|
|
1298
|
+
const index = buildIndex(doc);
|
|
1299
|
+
const wanted = /* @__PURE__ */ new Set();
|
|
1300
|
+
for (const id of ids) {
|
|
1301
|
+
if (!index.nodesById.has(id)) continue;
|
|
1302
|
+
wanted.add(id);
|
|
1303
|
+
for (const descendant of getDescendants(index, id)) wanted.add(descendant);
|
|
1304
|
+
}
|
|
1305
|
+
const absolute = getAbsolutePositions(doc, index);
|
|
1306
|
+
const nodes = [];
|
|
1307
|
+
for (const node of doc.nodes) {
|
|
1308
|
+
if (!wanted.has(node.id)) continue;
|
|
1309
|
+
if (node.parentId !== void 0 && wanted.has(node.parentId)) {
|
|
1310
|
+
nodes.push(structuredClone(node));
|
|
1311
|
+
} else {
|
|
1312
|
+
const copy = structuredClone(node);
|
|
1313
|
+
delete copy.parentId;
|
|
1314
|
+
const at = absolute.get(node.id);
|
|
1315
|
+
copy.position = { x: at.x, y: at.y };
|
|
1316
|
+
nodes.push(copy);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
const edges = doc.edges.filter((edge) => wanted.has(edge.source) && wanted.has(edge.target)).map((edge) => structuredClone(edge));
|
|
1320
|
+
return { nodes, edges };
|
|
1321
|
+
}
|
|
1322
|
+
function pasteFragment(doc, fragment, options = {}) {
|
|
1323
|
+
const offset = options.offset ?? { x: 24, y: 24 };
|
|
1324
|
+
if (options.parentId !== void 0) requireContainer(doc, options.parentId);
|
|
1325
|
+
const parentAbsolute = options.parentId === void 0 ? { x: 0, y: 0 } : getAbsolutePositions(doc).get(options.parentId);
|
|
1326
|
+
const nodeIds = createIdAllocator(doc.nodes.map((node) => node.id));
|
|
1327
|
+
const edgeIds = createIdAllocator(doc.edges.map((edge) => edge.id));
|
|
1328
|
+
const idMap = {};
|
|
1329
|
+
for (const node of fragment.nodes) idMap[node.id] = nodeIds.next(idStem(node.id, node.kind));
|
|
1330
|
+
const fragmentIds = new Set(fragment.nodes.map((node) => node.id));
|
|
1331
|
+
const rootIds = [];
|
|
1332
|
+
const nodes = fragment.nodes.map((node) => {
|
|
1333
|
+
const copy = structuredClone(node);
|
|
1334
|
+
copy.id = idMap[node.id];
|
|
1335
|
+
if (node.parentId !== void 0 && fragmentIds.has(node.parentId)) {
|
|
1336
|
+
copy.parentId = idMap[node.parentId];
|
|
1337
|
+
} else {
|
|
1338
|
+
delete copy.parentId;
|
|
1339
|
+
if (options.parentId !== void 0) copy.parentId = options.parentId;
|
|
1340
|
+
copy.position = {
|
|
1341
|
+
x: node.position.x + offset.x - parentAbsolute.x,
|
|
1342
|
+
y: node.position.y + offset.y - parentAbsolute.y
|
|
1343
|
+
};
|
|
1344
|
+
rootIds.push(copy.id);
|
|
1345
|
+
}
|
|
1346
|
+
return copy;
|
|
1347
|
+
});
|
|
1348
|
+
const edges = fragment.edges.filter((edge) => fragmentIds.has(edge.source) && fragmentIds.has(edge.target)).map((edge) => {
|
|
1349
|
+
const copy = structuredClone(edge);
|
|
1350
|
+
copy.id = edgeIds.next(idStem(edge.id, "edge"));
|
|
1351
|
+
copy.source = idMap[edge.source];
|
|
1352
|
+
copy.target = idMap[edge.target];
|
|
1353
|
+
return copy;
|
|
1354
|
+
});
|
|
1355
|
+
return {
|
|
1356
|
+
document: { ...doc, nodes: [...doc.nodes, ...nodes], edges: [...doc.edges, ...edges] },
|
|
1357
|
+
nodeIds: nodes.map((node) => node.id),
|
|
1358
|
+
rootIds,
|
|
1359
|
+
edgeIds: edges.map((edge) => edge.id),
|
|
1360
|
+
idMap
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
function duplicateNodes(doc, ids, options = {}) {
|
|
1364
|
+
const selected = [...ids];
|
|
1365
|
+
const index = buildIndex(doc);
|
|
1366
|
+
const fragment = extractFragment(doc, selected);
|
|
1367
|
+
const parents = new Set(
|
|
1368
|
+
selected.filter((id) => index.nodesById.has(id)).map((id) => index.nodesById.get(id)?.parentId)
|
|
1369
|
+
);
|
|
1370
|
+
const parentId = parents.size === 1 ? [...parents][0] : void 0;
|
|
1371
|
+
return pasteFragment(doc, fragment, { ...parentId !== void 0 ? { parentId } : {}, ...options });
|
|
1372
|
+
}
|
|
1373
|
+
function setAbsolute(doc, targets) {
|
|
1374
|
+
const absolute = getAbsolutePositions(doc);
|
|
1375
|
+
const nodes = doc.nodes.map((node) => {
|
|
1376
|
+
const target = targets.get(node.id);
|
|
1377
|
+
if (!target) return node;
|
|
1378
|
+
const parent = node.parentId === void 0 ? void 0 : absolute.get(node.parentId);
|
|
1379
|
+
return { ...node, position: { x: target.x - (parent?.x ?? 0), y: target.y - (parent?.y ?? 0) } };
|
|
1380
|
+
});
|
|
1381
|
+
return { ...doc, nodes };
|
|
1382
|
+
}
|
|
1383
|
+
function alignNodes(doc, ids, alignment) {
|
|
1384
|
+
const bounds = getAbsoluteBounds(doc);
|
|
1385
|
+
const rects = [...new Set(ids)].flatMap((id) => {
|
|
1386
|
+
const rect = bounds.get(id);
|
|
1387
|
+
return rect ? [{ id, rect }] : [];
|
|
1388
|
+
});
|
|
1389
|
+
if (rects.length < 2) return doc;
|
|
1390
|
+
const box = unionRects(rects.map((entry) => entry.rect));
|
|
1391
|
+
const targets = /* @__PURE__ */ new Map();
|
|
1392
|
+
for (const { id, rect } of rects) {
|
|
1393
|
+
let { x, y } = rect;
|
|
1394
|
+
if (alignment === "left") x = box.x;
|
|
1395
|
+
else if (alignment === "right") x = box.x + box.width - rect.width;
|
|
1396
|
+
else if (alignment === "center") x = box.x + (box.width - rect.width) / 2;
|
|
1397
|
+
else if (alignment === "top") y = box.y;
|
|
1398
|
+
else if (alignment === "bottom") y = box.y + box.height - rect.height;
|
|
1399
|
+
else y = box.y + (box.height - rect.height) / 2;
|
|
1400
|
+
targets.set(id, { x, y });
|
|
1401
|
+
}
|
|
1402
|
+
return setAbsolute(doc, targets);
|
|
1403
|
+
}
|
|
1404
|
+
function distributeNodes(doc, ids, axis) {
|
|
1405
|
+
const bounds = getAbsoluteBounds(doc);
|
|
1406
|
+
const horizontal = axis === "horizontal";
|
|
1407
|
+
const entries = [...new Set(ids)].flatMap((id) => {
|
|
1408
|
+
const rect = bounds.get(id);
|
|
1409
|
+
return rect ? [{ id, rect }] : [];
|
|
1410
|
+
}).sort((a, b) => horizontal ? a.rect.x - b.rect.x : a.rect.y - b.rect.y);
|
|
1411
|
+
if (entries.length < 3) return doc;
|
|
1412
|
+
const first = entries[0];
|
|
1413
|
+
const last = entries[entries.length - 1];
|
|
1414
|
+
const total = horizontal ? last.rect.x + last.rect.width - first.rect.x : last.rect.y + last.rect.height - first.rect.y;
|
|
1415
|
+
const occupied = entries.reduce((sum, entry) => sum + (horizontal ? entry.rect.width : entry.rect.height), 0);
|
|
1416
|
+
const gap = (total - occupied) / (entries.length - 1);
|
|
1417
|
+
const targets = /* @__PURE__ */ new Map();
|
|
1418
|
+
let cursor = horizontal ? first.rect.x : first.rect.y;
|
|
1419
|
+
for (const { id, rect } of entries) {
|
|
1420
|
+
targets.set(id, horizontal ? { x: cursor, y: rect.y } : { x: rect.x, y: cursor });
|
|
1421
|
+
cursor += (horizontal ? rect.width : rect.height) + gap;
|
|
1422
|
+
}
|
|
1423
|
+
return setAbsolute(doc, targets);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// src/layout.ts
|
|
1427
|
+
import { Graph, layout as dagreLayout } from "@dagrejs/dagre";
|
|
1428
|
+
import { cluster, hierarchy, tree } from "d3-hierarchy";
|
|
1429
|
+
var ROOT = "\0root";
|
|
1430
|
+
var VIRTUAL = "\0virtual";
|
|
1431
|
+
function layoutDiagram(doc, options = {}) {
|
|
1432
|
+
const algorithm = options.algorithm ?? "layered";
|
|
1433
|
+
const rtl = options.rtl ?? false;
|
|
1434
|
+
const direction = mirrorDirection(options.direction ?? "TB", rtl);
|
|
1435
|
+
const spacing = typeof options.spacing === "number" ? { node: options.spacing } : options.spacing ?? {};
|
|
1436
|
+
const resolved = {
|
|
1437
|
+
algorithm,
|
|
1438
|
+
direction,
|
|
1439
|
+
rtl,
|
|
1440
|
+
nodeSep: spacing.node ?? 48,
|
|
1441
|
+
rankSep: spacing.rank ?? 80,
|
|
1442
|
+
pad: spacing.group ?? 32,
|
|
1443
|
+
header: options.groupHeader ?? 40,
|
|
1444
|
+
grid: options.grid ?? GRID_SIZE,
|
|
1445
|
+
options
|
|
1446
|
+
};
|
|
1447
|
+
const index = buildIndex(doc);
|
|
1448
|
+
const parentOf = /* @__PURE__ */ new Map();
|
|
1449
|
+
for (const node of doc.nodes) {
|
|
1450
|
+
let parent = node.parentId !== node.id && node.parentId !== void 0 && index.nodesById.has(node.parentId) ? node.parentId : void 0;
|
|
1451
|
+
const seen = /* @__PURE__ */ new Set([node.id]);
|
|
1452
|
+
for (let walker = parent; walker !== void 0; walker = index.nodesById.get(walker)?.parentId) {
|
|
1453
|
+
if (seen.has(walker)) {
|
|
1454
|
+
parent = void 0;
|
|
1455
|
+
break;
|
|
1456
|
+
}
|
|
1457
|
+
seen.add(walker);
|
|
1458
|
+
}
|
|
1459
|
+
parentOf.set(node.id, parent);
|
|
1460
|
+
}
|
|
1461
|
+
const kidsOf = /* @__PURE__ */ new Map();
|
|
1462
|
+
for (const node of doc.nodes) {
|
|
1463
|
+
const key = parentOf.get(node.id) ?? ROOT;
|
|
1464
|
+
const list = kidsOf.get(key);
|
|
1465
|
+
if (list) list.push(node.id);
|
|
1466
|
+
else kidsOf.set(key, [node.id]);
|
|
1467
|
+
}
|
|
1468
|
+
const pathTo = (id) => {
|
|
1469
|
+
const chain = [id];
|
|
1470
|
+
for (let p = parentOf.get(id); p !== void 0; p = parentOf.get(p)) chain.push(p);
|
|
1471
|
+
return chain.reverse();
|
|
1472
|
+
};
|
|
1473
|
+
const levelEdges = /* @__PURE__ */ new Map();
|
|
1474
|
+
for (const edge of doc.edges) {
|
|
1475
|
+
if (edge.source === edge.target || !index.nodesById.has(edge.source) || !index.nodesById.has(edge.target)) continue;
|
|
1476
|
+
const a = pathTo(edge.source);
|
|
1477
|
+
const b = pathTo(edge.target);
|
|
1478
|
+
let common = 0;
|
|
1479
|
+
while (common < a.length && common < b.length && a[common] === b[common]) common += 1;
|
|
1480
|
+
if (common === a.length || common === b.length) continue;
|
|
1481
|
+
const level = common === 0 ? ROOT : a[common - 1];
|
|
1482
|
+
const ua = a[common];
|
|
1483
|
+
const ub = b[common];
|
|
1484
|
+
const map = levelEdges.get(level) ?? /* @__PURE__ */ new Map();
|
|
1485
|
+
levelEdges.set(level, map);
|
|
1486
|
+
const key = `${ua}\0${ub}`;
|
|
1487
|
+
const existing = map.get(key);
|
|
1488
|
+
const labelWidth = edge.label ? estimateTextWidth(edge.label) + 16 : 0;
|
|
1489
|
+
const labelHeight = edge.label ? 24 : 0;
|
|
1490
|
+
if (existing) {
|
|
1491
|
+
existing.labelWidth = Math.max(existing.labelWidth, labelWidth);
|
|
1492
|
+
existing.labelHeight = Math.max(existing.labelHeight, labelHeight);
|
|
1493
|
+
} else {
|
|
1494
|
+
map.set(key, { source: ua, target: ub, labelWidth, labelHeight });
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
const positions = {};
|
|
1498
|
+
const sizes = {};
|
|
1499
|
+
const fitted = /* @__PURE__ */ new Map();
|
|
1500
|
+
const layoutLevel = (parent) => {
|
|
1501
|
+
const kids = kidsOf.get(parent) ?? [];
|
|
1502
|
+
for (const kid of kids) {
|
|
1503
|
+
if (!kidsOf.has(kid)) continue;
|
|
1504
|
+
const content = layoutLevel(kid);
|
|
1505
|
+
const width = snapUp(content.width + resolved.pad * 2, resolved.grid);
|
|
1506
|
+
const height = snapUp(content.height + resolved.pad * 2 + resolved.header, resolved.grid);
|
|
1507
|
+
const collapsed = index.nodesById.get(kid)?.collapsed === true;
|
|
1508
|
+
fitted.set(kid, collapsed ? { width, height: COLLAPSED_HEIGHT } : { width, height });
|
|
1509
|
+
sizes[kid] = { width, height };
|
|
1510
|
+
for (const child of kidsOf.get(kid) ?? []) {
|
|
1511
|
+
const at = positions[child];
|
|
1512
|
+
positions[child] = { x: at.x + resolved.pad, y: at.y + resolved.pad + resolved.header };
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
const items = kids.map((id) => {
|
|
1516
|
+
const size = fitted.get(id) ?? getNodeSize(index.nodesById.get(id));
|
|
1517
|
+
return { id, width: size.width, height: size.height };
|
|
1518
|
+
});
|
|
1519
|
+
const edges = [...levelEdges.get(parent)?.values() ?? []];
|
|
1520
|
+
let placement = place(items, edges, resolved);
|
|
1521
|
+
placement = normalize(placement, items, resolved);
|
|
1522
|
+
const mirrorX = resolved.rtl && (algorithm === "radial" || !isHorizontal(options.direction ?? "TB"));
|
|
1523
|
+
if (mirrorX) placement = mirror(placement, items);
|
|
1524
|
+
placement = normalize(placement, items, resolved);
|
|
1525
|
+
for (const item of items) {
|
|
1526
|
+
const at = placement.get(item.id);
|
|
1527
|
+
positions[item.id] = { x: at.x, y: at.y };
|
|
1528
|
+
}
|
|
1529
|
+
return extent(placement, items);
|
|
1530
|
+
};
|
|
1531
|
+
layoutLevel(ROOT);
|
|
1532
|
+
const origin = options.origin ?? { x: 0, y: 0 };
|
|
1533
|
+
for (const id of kidsOf.get(ROOT) ?? []) {
|
|
1534
|
+
const at = positions[id];
|
|
1535
|
+
positions[id] = { x: at.x + origin.x, y: at.y + origin.y };
|
|
1536
|
+
}
|
|
1537
|
+
const absolute = /* @__PURE__ */ new Map();
|
|
1538
|
+
const resolveAbsolute = (id) => {
|
|
1539
|
+
const known = absolute.get(id);
|
|
1540
|
+
if (known) return known;
|
|
1541
|
+
const own = positions[id];
|
|
1542
|
+
const parent = parentOf.get(id);
|
|
1543
|
+
const base = parent === void 0 ? { x: 0, y: 0 } : resolveAbsolute(parent);
|
|
1544
|
+
const result = { x: base.x + own.x, y: base.y + own.y };
|
|
1545
|
+
absolute.set(id, result);
|
|
1546
|
+
return result;
|
|
1547
|
+
};
|
|
1548
|
+
const rects = [];
|
|
1549
|
+
for (const node of doc.nodes) {
|
|
1550
|
+
const at = resolveAbsolute(node.id);
|
|
1551
|
+
const size = fitted.get(node.id) ?? getNodeSize(node);
|
|
1552
|
+
rects.push({ x: at.x, y: at.y, ...size });
|
|
1553
|
+
}
|
|
1554
|
+
return { positions, sizes, bounds: unionRects(rects), algorithm, direction };
|
|
1555
|
+
}
|
|
1556
|
+
function applyLayout(doc, result) {
|
|
1557
|
+
return {
|
|
1558
|
+
...doc,
|
|
1559
|
+
nodes: doc.nodes.map((node) => {
|
|
1560
|
+
const position = result.positions[node.id];
|
|
1561
|
+
const size = result.sizes[node.id];
|
|
1562
|
+
if (!position && !size) return node;
|
|
1563
|
+
return { ...node, ...position ? { position: { ...position } } : {}, ...size ? { width: size.width, height: size.height } : {} };
|
|
1564
|
+
})
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
function layoutedDiagram(doc, options = {}) {
|
|
1568
|
+
return applyLayout(doc, layoutDiagram(doc, options));
|
|
1569
|
+
}
|
|
1570
|
+
function snapUp(value, grid) {
|
|
1571
|
+
return grid > 0 ? Math.ceil(value / grid) * grid : value;
|
|
1572
|
+
}
|
|
1573
|
+
function extent(placement, items) {
|
|
1574
|
+
let width = 0;
|
|
1575
|
+
let height = 0;
|
|
1576
|
+
for (const item of items) {
|
|
1577
|
+
const at = placement.get(item.id);
|
|
1578
|
+
width = Math.max(width, at.x + item.width);
|
|
1579
|
+
height = Math.max(height, at.y + item.height);
|
|
1580
|
+
}
|
|
1581
|
+
return { width, height };
|
|
1582
|
+
}
|
|
1583
|
+
function normalize(placement, items, resolved) {
|
|
1584
|
+
let minX = Infinity;
|
|
1585
|
+
let minY = Infinity;
|
|
1586
|
+
for (const item of items) {
|
|
1587
|
+
const at = placement.get(item.id);
|
|
1588
|
+
minX = Math.min(minX, at.x);
|
|
1589
|
+
minY = Math.min(minY, at.y);
|
|
1590
|
+
}
|
|
1591
|
+
const result = /* @__PURE__ */ new Map();
|
|
1592
|
+
for (const item of items) {
|
|
1593
|
+
const at = placement.get(item.id);
|
|
1594
|
+
result.set(item.id, {
|
|
1595
|
+
x: snapValue(at.x - (minX === Infinity ? 0 : minX), resolved.grid),
|
|
1596
|
+
y: snapValue(at.y - (minY === Infinity ? 0 : minY), resolved.grid)
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
return result;
|
|
1600
|
+
}
|
|
1601
|
+
function mirror(placement, items) {
|
|
1602
|
+
let right = 0;
|
|
1603
|
+
for (const item of items) right = Math.max(right, placement.get(item.id).x + item.width);
|
|
1604
|
+
const result = /* @__PURE__ */ new Map();
|
|
1605
|
+
for (const item of items) {
|
|
1606
|
+
const at = placement.get(item.id);
|
|
1607
|
+
result.set(item.id, { x: right - (at.x + item.width), y: at.y });
|
|
1608
|
+
}
|
|
1609
|
+
return result;
|
|
1610
|
+
}
|
|
1611
|
+
function place(items, edges, resolved) {
|
|
1612
|
+
if (items.length === 0) return /* @__PURE__ */ new Map();
|
|
1613
|
+
switch (resolved.algorithm) {
|
|
1614
|
+
case "tree":
|
|
1615
|
+
return placeTree(items, edges, resolved);
|
|
1616
|
+
case "radial":
|
|
1617
|
+
return placeRadial(items, edges, resolved);
|
|
1618
|
+
case "grid":
|
|
1619
|
+
return placeGrid(items, resolved);
|
|
1620
|
+
case "layered":
|
|
1621
|
+
return placeLayered(items, edges, resolved);
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
var LARGE_LEVEL = 400;
|
|
1625
|
+
function placeLayered(items, edges, resolved) {
|
|
1626
|
+
const graph = new Graph();
|
|
1627
|
+
const ranker = resolved.options.ranker ?? (items.length + edges.length > LARGE_LEVEL ? "tight-tree" : "network-simplex");
|
|
1628
|
+
graph.setGraph({
|
|
1629
|
+
rankdir: resolved.direction,
|
|
1630
|
+
nodesep: resolved.nodeSep,
|
|
1631
|
+
ranksep: resolved.rankSep,
|
|
1632
|
+
edgesep: Math.max(8, Math.round(resolved.nodeSep / 2)),
|
|
1633
|
+
marginx: 0,
|
|
1634
|
+
marginy: 0,
|
|
1635
|
+
ranker
|
|
1636
|
+
});
|
|
1637
|
+
graph.setDefaultEdgeLabel(() => ({}));
|
|
1638
|
+
for (const item of [...items].reverse()) graph.setNode(item.id, { width: item.width, height: item.height });
|
|
1639
|
+
for (const edge of [...edges].reverse()) {
|
|
1640
|
+
graph.setEdge(
|
|
1641
|
+
edge.source,
|
|
1642
|
+
edge.target,
|
|
1643
|
+
edge.labelWidth > 0 ? { width: edge.labelWidth, height: edge.labelHeight, labelpos: "c" } : {}
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1646
|
+
dagreLayout(graph);
|
|
1647
|
+
const placement = /* @__PURE__ */ new Map();
|
|
1648
|
+
for (const item of items) {
|
|
1649
|
+
const laid = graph.node(item.id);
|
|
1650
|
+
placement.set(item.id, laid ? { x: laid.x - item.width / 2, y: laid.y - item.height / 2 } : { x: 0, y: 0 });
|
|
1651
|
+
}
|
|
1652
|
+
return placement;
|
|
1653
|
+
}
|
|
1654
|
+
function buildForest(items, edges, rootId) {
|
|
1655
|
+
const known = new Set(items.map((item) => item.id));
|
|
1656
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
1657
|
+
const hasIncoming = /* @__PURE__ */ new Set();
|
|
1658
|
+
for (const edge of edges) {
|
|
1659
|
+
const list = outgoing.get(edge.source);
|
|
1660
|
+
if (list) list.push(edge.target);
|
|
1661
|
+
else outgoing.set(edge.source, [edge.target]);
|
|
1662
|
+
hasIncoming.add(edge.target);
|
|
1663
|
+
}
|
|
1664
|
+
const assigned = /* @__PURE__ */ new Set();
|
|
1665
|
+
const forest = [];
|
|
1666
|
+
const grow = (id) => {
|
|
1667
|
+
if (assigned.has(id)) return;
|
|
1668
|
+
assigned.add(id);
|
|
1669
|
+
const root = { id, children: [] };
|
|
1670
|
+
const lookup = /* @__PURE__ */ new Map([[id, root]]);
|
|
1671
|
+
const queue = [id];
|
|
1672
|
+
for (let head = 0; head < queue.length; head += 1) {
|
|
1673
|
+
const current = queue[head];
|
|
1674
|
+
const parent = lookup.get(current);
|
|
1675
|
+
for (const target of outgoing.get(current) ?? []) {
|
|
1676
|
+
if (assigned.has(target)) continue;
|
|
1677
|
+
assigned.add(target);
|
|
1678
|
+
const child = { id: target, children: [] };
|
|
1679
|
+
parent.children.push(child);
|
|
1680
|
+
lookup.set(target, child);
|
|
1681
|
+
queue.push(target);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
forest.push(root);
|
|
1685
|
+
};
|
|
1686
|
+
if (rootId !== void 0 && known.has(rootId)) grow(rootId);
|
|
1687
|
+
for (const item of items) if (!hasIncoming.has(item.id)) grow(item.id);
|
|
1688
|
+
for (const item of items) grow(item.id);
|
|
1689
|
+
return forest;
|
|
1690
|
+
}
|
|
1691
|
+
function runTreeLayout(items, edges, resolved, separation) {
|
|
1692
|
+
const forest = buildForest(items, edges, resolved.options.root);
|
|
1693
|
+
const virtual = forest.length > 1;
|
|
1694
|
+
const top = virtual ? { id: VIRTUAL, children: forest } : forest[0];
|
|
1695
|
+
const root = hierarchy(top, (d) => d.children);
|
|
1696
|
+
const laid = resolved.options.treeVariant === "cluster" ? cluster().nodeSize([1, 1]).separation(separation)(root) : tree().nodeSize([1, 1]).separation(separation)(root);
|
|
1697
|
+
return { root: laid, virtual };
|
|
1698
|
+
}
|
|
1699
|
+
function placeTree(items, edges, resolved) {
|
|
1700
|
+
const sizes = new Map(items.map((item) => [item.id, item]));
|
|
1701
|
+
const horizontal = isHorizontal(resolved.direction);
|
|
1702
|
+
const breadthOf = (id) => {
|
|
1703
|
+
const item = sizes.get(id);
|
|
1704
|
+
return item ? horizontal ? item.height : item.width : 0;
|
|
1705
|
+
};
|
|
1706
|
+
const depthExtentOf = (id) => {
|
|
1707
|
+
const item = sizes.get(id);
|
|
1708
|
+
return item ? horizontal ? item.width : item.height : 0;
|
|
1709
|
+
};
|
|
1710
|
+
const { root, virtual } = runTreeLayout(
|
|
1711
|
+
items,
|
|
1712
|
+
edges,
|
|
1713
|
+
resolved,
|
|
1714
|
+
(a, b) => (breadthOf(a.data.id) + breadthOf(b.data.id)) / 2 + resolved.nodeSep
|
|
1715
|
+
);
|
|
1716
|
+
const nodes = root.descendants().filter((node) => node.data.id !== VIRTUAL);
|
|
1717
|
+
const base = virtual ? 1 : 0;
|
|
1718
|
+
const maxDepth = Math.max(0, ...nodes.map((node) => node.depth - base));
|
|
1719
|
+
const levelOf = (node) => resolved.options.treeVariant === "cluster" && !node.children ? maxDepth : node.depth - base;
|
|
1720
|
+
const thickness = Array.from({ length: maxDepth + 1 }, () => 0);
|
|
1721
|
+
for (const node of nodes) {
|
|
1722
|
+
const level = levelOf(node);
|
|
1723
|
+
thickness[level] = Math.max(thickness[level], depthExtentOf(node.data.id));
|
|
1724
|
+
}
|
|
1725
|
+
const centers = [];
|
|
1726
|
+
let cursor = 0;
|
|
1727
|
+
for (const level of thickness) {
|
|
1728
|
+
centers.push(cursor + level / 2);
|
|
1729
|
+
cursor += level + resolved.rankSep;
|
|
1730
|
+
}
|
|
1731
|
+
const total = cursor - resolved.rankSep;
|
|
1732
|
+
const placement = /* @__PURE__ */ new Map();
|
|
1733
|
+
for (const node of nodes) {
|
|
1734
|
+
const item = sizes.get(node.data.id);
|
|
1735
|
+
let along = centers[levelOf(node)];
|
|
1736
|
+
if (resolved.direction === "BT" || resolved.direction === "RL") along = total - along;
|
|
1737
|
+
const breadth = node.x;
|
|
1738
|
+
const center = horizontal ? { x: along, y: breadth } : { x: breadth, y: along };
|
|
1739
|
+
placement.set(item.id, { x: center.x - item.width / 2, y: center.y - item.height / 2 });
|
|
1740
|
+
}
|
|
1741
|
+
return placement;
|
|
1742
|
+
}
|
|
1743
|
+
function placeRadial(items, edges, resolved) {
|
|
1744
|
+
const sizes = new Map(items.map((item) => [item.id, item]));
|
|
1745
|
+
const thick = (id) => {
|
|
1746
|
+
const item = sizes.get(id);
|
|
1747
|
+
return item ? Math.max(item.width, item.height) : 0;
|
|
1748
|
+
};
|
|
1749
|
+
const { root } = runTreeLayout(items, edges, resolved, (a, b) => a.parent === b.parent ? 1 : 2);
|
|
1750
|
+
const nodes = root.descendants();
|
|
1751
|
+
const xs = nodes.map((node) => node.x);
|
|
1752
|
+
const minX = Math.min(...xs);
|
|
1753
|
+
const span = Math.max(...xs) - minX;
|
|
1754
|
+
const slots = span + 2;
|
|
1755
|
+
const maxDepth = Math.max(...nodes.map((node) => node.depth));
|
|
1756
|
+
const widest = Array.from({ length: maxDepth + 1 }, () => 0);
|
|
1757
|
+
for (const node of nodes) widest[node.depth] = Math.max(widest[node.depth], thick(node.data.id));
|
|
1758
|
+
const radius = [0];
|
|
1759
|
+
for (let depth = 1; depth <= maxDepth; depth += 1) {
|
|
1760
|
+
const previous = radius[depth - 1];
|
|
1761
|
+
const current = widest[depth];
|
|
1762
|
+
const fromInside = previous + resolved.rankSep + (widest[depth - 1] + current) / 2;
|
|
1763
|
+
const fromCircumference = slots * (current + resolved.nodeSep) / (2 * Math.PI);
|
|
1764
|
+
radius.push(Math.max(fromInside, fromCircumference));
|
|
1765
|
+
}
|
|
1766
|
+
const placement = /* @__PURE__ */ new Map();
|
|
1767
|
+
for (const node of nodes) {
|
|
1768
|
+
if (node.data.id === VIRTUAL) continue;
|
|
1769
|
+
const item = sizes.get(node.data.id);
|
|
1770
|
+
const angle = (node.x - minX) / slots * 2 * Math.PI - Math.PI / 2;
|
|
1771
|
+
const r = radius[node.depth];
|
|
1772
|
+
placement.set(item.id, {
|
|
1773
|
+
x: r * Math.cos(angle) - item.width / 2,
|
|
1774
|
+
y: r * Math.sin(angle) - item.height / 2
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
return placement;
|
|
1778
|
+
}
|
|
1779
|
+
function placeGrid(items, resolved) {
|
|
1780
|
+
const perLine = Math.max(1, Math.floor(resolved.options.columns ?? Math.ceil(Math.sqrt(items.length))));
|
|
1781
|
+
const horizontal = isHorizontal(resolved.direction);
|
|
1782
|
+
const lineCount = Math.ceil(items.length / perLine);
|
|
1783
|
+
const at = (i) => ({ line: Math.floor(i / perLine), slot: i % perLine });
|
|
1784
|
+
const lineThickness = Array.from({ length: lineCount }, () => 0);
|
|
1785
|
+
const slotThickness = Array.from({ length: perLine }, () => 0);
|
|
1786
|
+
items.forEach((item, i) => {
|
|
1787
|
+
const { line, slot } = at(i);
|
|
1788
|
+
lineThickness[line] = Math.max(lineThickness[line], horizontal ? item.width : item.height);
|
|
1789
|
+
slotThickness[slot] = Math.max(slotThickness[slot], horizontal ? item.height : item.width);
|
|
1790
|
+
});
|
|
1791
|
+
const offsets = (sizes, gap) => {
|
|
1792
|
+
let cursor = 0;
|
|
1793
|
+
return sizes.map((size) => {
|
|
1794
|
+
const start = cursor;
|
|
1795
|
+
cursor += size + gap;
|
|
1796
|
+
return start;
|
|
1797
|
+
});
|
|
1798
|
+
};
|
|
1799
|
+
const lineStart = offsets(lineThickness, resolved.rankSep / 2);
|
|
1800
|
+
const slotStart = offsets(slotThickness, resolved.nodeSep);
|
|
1801
|
+
const lineTotal = (lineStart[lineCount - 1] ?? 0) + (lineThickness[lineCount - 1] ?? 0);
|
|
1802
|
+
const reverseLines = resolved.direction === "BT" || resolved.direction === "RL";
|
|
1803
|
+
const placement = /* @__PURE__ */ new Map();
|
|
1804
|
+
items.forEach((item, i) => {
|
|
1805
|
+
const { line, slot } = at(i);
|
|
1806
|
+
const thickness = lineThickness[line];
|
|
1807
|
+
let along = lineStart[line];
|
|
1808
|
+
if (reverseLines) along = lineTotal - along - thickness;
|
|
1809
|
+
const across = slotStart[slot];
|
|
1810
|
+
placement.set(item.id, horizontal ? { x: along, y: across } : { x: across, y: along });
|
|
1811
|
+
});
|
|
1812
|
+
return placement;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
// src/history.ts
|
|
1816
|
+
function createHistory(initial) {
|
|
1817
|
+
return { past: [], present: initial, future: [] };
|
|
1818
|
+
}
|
|
1819
|
+
function canUndo(state) {
|
|
1820
|
+
return state.past.length > 0;
|
|
1821
|
+
}
|
|
1822
|
+
function canRedo(state) {
|
|
1823
|
+
return state.future.length > 0;
|
|
1824
|
+
}
|
|
1825
|
+
function historyReducer(state, action, options = {}) {
|
|
1826
|
+
const limit = Math.max(1, options.limit ?? 100);
|
|
1827
|
+
switch (action.type) {
|
|
1828
|
+
case "set": {
|
|
1829
|
+
if (Object.is(action.value, state.present)) return state;
|
|
1830
|
+
if (action.coalesceKey !== void 0 && action.coalesceKey === state.lastKey) {
|
|
1831
|
+
return { past: state.past, present: action.value, future: [], lastKey: action.coalesceKey };
|
|
1832
|
+
}
|
|
1833
|
+
const past = [...state.past, state.present];
|
|
1834
|
+
if (past.length > limit) past.splice(0, past.length - limit);
|
|
1835
|
+
const next = { past, present: action.value, future: [] };
|
|
1836
|
+
if (action.coalesceKey !== void 0) next.lastKey = action.coalesceKey;
|
|
1837
|
+
return next;
|
|
1838
|
+
}
|
|
1839
|
+
case "undo": {
|
|
1840
|
+
const previous = state.past[state.past.length - 1];
|
|
1841
|
+
if (state.past.length === 0 || previous === void 0) return state;
|
|
1842
|
+
return { past: state.past.slice(0, -1), present: previous, future: [state.present, ...state.future] };
|
|
1843
|
+
}
|
|
1844
|
+
case "redo": {
|
|
1845
|
+
const next = state.future[0];
|
|
1846
|
+
if (state.future.length === 0 || next === void 0) return state;
|
|
1847
|
+
return { past: [...state.past, state.present], present: next, future: state.future.slice(1) };
|
|
1848
|
+
}
|
|
1849
|
+
case "reset":
|
|
1850
|
+
return createHistory(action.value);
|
|
1851
|
+
case "clear":
|
|
1852
|
+
return createHistory(state.present);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
// src/outline.ts
|
|
1857
|
+
function diagramToOutline(diagram, { connector = "to", maxPaths = 100 } = {}) {
|
|
1858
|
+
const labels = new Map(diagram.nodes.map((node) => [node.id, node.label]));
|
|
1859
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
1860
|
+
const hasIncoming = /* @__PURE__ */ new Set();
|
|
1861
|
+
for (const edge of diagram.edges) {
|
|
1862
|
+
if (!labels.has(edge.source) || !labels.has(edge.target)) continue;
|
|
1863
|
+
outgoing.set(edge.source, [...outgoing.get(edge.source) ?? [], edge.target]);
|
|
1864
|
+
hasIncoming.add(edge.target);
|
|
1865
|
+
}
|
|
1866
|
+
const lines = [];
|
|
1867
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1868
|
+
const label = (id) => labels.get(id) ?? id;
|
|
1869
|
+
const sentence = (ids) => ids.map(label).join(` ${connector} `);
|
|
1870
|
+
const walk = (id, trail) => {
|
|
1871
|
+
if (lines.length >= maxPaths) return;
|
|
1872
|
+
visited.add(id);
|
|
1873
|
+
const path = [...trail, id];
|
|
1874
|
+
const next = outgoing.get(id) ?? [];
|
|
1875
|
+
if (next.length === 0) {
|
|
1876
|
+
lines.push(sentence(path));
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
for (const target of next) {
|
|
1880
|
+
if (path.includes(target)) lines.push(sentence([...path, target]));
|
|
1881
|
+
else walk(target, path);
|
|
1882
|
+
}
|
|
1883
|
+
};
|
|
1884
|
+
for (const node of diagram.nodes) {
|
|
1885
|
+
if (!hasIncoming.has(node.id)) walk(node.id, []);
|
|
1886
|
+
}
|
|
1887
|
+
for (const node of diagram.nodes) {
|
|
1888
|
+
if (!visited.has(node.id)) walk(node.id, []);
|
|
1889
|
+
}
|
|
1890
|
+
return lines;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
// src/outline-tree.ts
|
|
1894
|
+
function diagramToOutlineTree(doc, options = {}) {
|
|
1895
|
+
const index = buildIndex(doc);
|
|
1896
|
+
const order = getReadingOrder(doc, {
|
|
1897
|
+
...options.direction === void 0 ? {} : { direction: options.direction },
|
|
1898
|
+
...options.rtl === void 0 ? {} : { rtl: options.rtl }
|
|
1899
|
+
});
|
|
1900
|
+
const items = /* @__PURE__ */ new Map();
|
|
1901
|
+
for (const id of order) {
|
|
1902
|
+
const node = index.nodesById.get(id);
|
|
1903
|
+
const item = {
|
|
1904
|
+
id,
|
|
1905
|
+
label: node.label,
|
|
1906
|
+
kind: node.kind,
|
|
1907
|
+
incoming: (index.incoming.get(id) ?? []).map((edge) => connection(edge.id, edge.source, edge.label)),
|
|
1908
|
+
outgoing: (index.outgoing.get(id) ?? []).map((edge) => connection(edge.id, edge.target, edge.label)),
|
|
1909
|
+
children: []
|
|
1910
|
+
};
|
|
1911
|
+
if (node.status !== void 0) item.status = node.status;
|
|
1912
|
+
if (node.description !== void 0) item.description = node.description;
|
|
1913
|
+
items.set(id, item);
|
|
1914
|
+
}
|
|
1915
|
+
function connection(edgeId, nodeId, edgeLabel) {
|
|
1916
|
+
const result = { edgeId, nodeId, label: index.nodesById.get(nodeId)?.label ?? nodeId };
|
|
1917
|
+
if (edgeLabel !== void 0 && edgeLabel !== "") result.edgeLabel = edgeLabel;
|
|
1918
|
+
return result;
|
|
1919
|
+
}
|
|
1920
|
+
const roots = [];
|
|
1921
|
+
const position = new Map(order.map((id, i) => [id, i]));
|
|
1922
|
+
for (const id of order) {
|
|
1923
|
+
const item = items.get(id);
|
|
1924
|
+
const parent = index.nodesById.get(id)?.parentId;
|
|
1925
|
+
const parentItem = parent === void 0 || parent === id ? void 0 : items.get(parent);
|
|
1926
|
+
if (parentItem && (position.get(parent) ?? Infinity) < (position.get(id) ?? 0)) parentItem.children.push(item);
|
|
1927
|
+
else roots.push(item);
|
|
1928
|
+
}
|
|
1929
|
+
return roots;
|
|
1930
|
+
}
|
|
1931
|
+
function summarizeNodes(doc) {
|
|
1932
|
+
const index = buildIndex(doc);
|
|
1933
|
+
const result = /* @__PURE__ */ new Map();
|
|
1934
|
+
for (const node of doc.nodes) {
|
|
1935
|
+
const summary = {
|
|
1936
|
+
id: node.id,
|
|
1937
|
+
kind: node.kind,
|
|
1938
|
+
label: node.label,
|
|
1939
|
+
incoming: index.incoming.get(node.id)?.length ?? 0,
|
|
1940
|
+
outgoing: index.outgoing.get(node.id)?.length ?? 0
|
|
1941
|
+
};
|
|
1942
|
+
if (node.status !== void 0) summary.status = node.status;
|
|
1943
|
+
if (node.description !== void 0) summary.description = node.description;
|
|
1944
|
+
result.set(node.id, summary);
|
|
1945
|
+
}
|
|
1946
|
+
return result;
|
|
1947
|
+
}
|
|
1948
|
+
|
|
1949
|
+
// src/svg.ts
|
|
1950
|
+
import { cssVar } from "@x12i/tokens";
|
|
1951
|
+
var defaultPalette = {
|
|
1952
|
+
surface: cssVar("color.bg.surface"),
|
|
1953
|
+
canvas: cssVar("color.bg.canvas"),
|
|
1954
|
+
text: cssVar("color.text.primary"),
|
|
1955
|
+
textMuted: cssVar("color.text.secondary"),
|
|
1956
|
+
textInverse: cssVar("color.text.inverse"),
|
|
1957
|
+
border: cssVar("color.border.default"),
|
|
1958
|
+
borderSubtle: cssVar("color.border.subtle"),
|
|
1959
|
+
edge: cssVar("color.border.strong"),
|
|
1960
|
+
accent: cssVar("color.accent.default"),
|
|
1961
|
+
accentSubtle: cssVar("color.accent.subtle"),
|
|
1962
|
+
onAccent: cssVar("color.accent.on-accent"),
|
|
1963
|
+
inverse: cssVar("color.bg.inverse"),
|
|
1964
|
+
status: {
|
|
1965
|
+
neutral: {
|
|
1966
|
+
background: cssVar("color.bg.surface"),
|
|
1967
|
+
foreground: cssVar("color.text.primary"),
|
|
1968
|
+
border: cssVar("color.border.default")
|
|
1969
|
+
},
|
|
1970
|
+
success: {
|
|
1971
|
+
background: cssVar("color.status.success.background"),
|
|
1972
|
+
foreground: cssVar("color.status.success.foreground"),
|
|
1973
|
+
border: cssVar("color.status.success.border")
|
|
1974
|
+
},
|
|
1975
|
+
warning: {
|
|
1976
|
+
background: cssVar("color.status.warning.background"),
|
|
1977
|
+
foreground: cssVar("color.status.warning.foreground"),
|
|
1978
|
+
border: cssVar("color.status.warning.border")
|
|
1979
|
+
},
|
|
1980
|
+
danger: {
|
|
1981
|
+
background: cssVar("color.status.danger.background"),
|
|
1982
|
+
foreground: cssVar("color.status.danger.foreground"),
|
|
1983
|
+
border: cssVar("color.status.danger.border")
|
|
1984
|
+
},
|
|
1985
|
+
info: {
|
|
1986
|
+
background: cssVar("color.status.info.background"),
|
|
1987
|
+
foreground: cssVar("color.status.info.foreground"),
|
|
1988
|
+
border: cssVar("color.status.info.border")
|
|
1989
|
+
}
|
|
1990
|
+
},
|
|
1991
|
+
fontSans: cssVar("font.family.sans"),
|
|
1992
|
+
fontMono: cssVar("font.family.mono")
|
|
1993
|
+
};
|
|
1994
|
+
function resolvePalette(override) {
|
|
1995
|
+
if (!override) return defaultPalette;
|
|
1996
|
+
const status = { ...defaultPalette.status };
|
|
1997
|
+
for (const key of Object.keys(override.status ?? {})) {
|
|
1998
|
+
status[key] = { ...defaultPalette.status[key], ...override.status?.[key] };
|
|
1999
|
+
}
|
|
2000
|
+
const rest = { ...override };
|
|
2001
|
+
delete rest.status;
|
|
2002
|
+
return { ...defaultPalette, ...rest, status };
|
|
2003
|
+
}
|
|
2004
|
+
var XML_INVALID = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\uFFFE\uFFFF]/g;
|
|
2005
|
+
function escapeXml(text) {
|
|
2006
|
+
return text.replace(XML_INVALID, "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2007
|
+
}
|
|
2008
|
+
function serializeSvg(element) {
|
|
2009
|
+
const attrs = Object.entries(element.attrs ?? {}).filter((entry) => entry[1] !== void 0).map(([name, value]) => ` ${name}="${escapeXml(String(value))}"`).join("");
|
|
2010
|
+
const style = element.style && Object.keys(element.style).length > 0 ? ` style="${escapeXml(
|
|
2011
|
+
Object.entries(element.style).map(([name, value]) => `${name}:${value}`).join(";")
|
|
2012
|
+
)}"` : "";
|
|
2013
|
+
const children = element.children ?? [];
|
|
2014
|
+
if (children.length === 0) return `<${element.tag}${attrs}${style}/>`;
|
|
2015
|
+
const inner = children.map((child) => typeof child === "string" ? escapeXml(child) : serializeSvg(child)).join("");
|
|
2016
|
+
return `<${element.tag}${attrs}${style}>${inner}</${element.tag}>`;
|
|
2017
|
+
}
|
|
2018
|
+
var el = (tag, attrs, style, children) => {
|
|
2019
|
+
const element = { tag };
|
|
2020
|
+
if (attrs) element.attrs = attrs;
|
|
2021
|
+
if (style) element.style = style;
|
|
2022
|
+
if (children) element.children = children;
|
|
2023
|
+
return element;
|
|
2024
|
+
};
|
|
2025
|
+
var num = (value) => Math.round(value * 100) / 100;
|
|
2026
|
+
var SAFE_IMAGE = /^(?:https?:\/\/|data:image\/(?:png|gif|jpe?g|webp|avif);base64,)/i;
|
|
2027
|
+
function isSafeImageUrl(value) {
|
|
2028
|
+
return typeof value === "string" && SAFE_IMAGE.test(value);
|
|
2029
|
+
}
|
|
2030
|
+
function dataText(node, key) {
|
|
2031
|
+
const value = node.data?.[key];
|
|
2032
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
2033
|
+
}
|
|
2034
|
+
function diagramToSvgTree(doc, options = {}) {
|
|
2035
|
+
const palette = resolvePalette(options.palette);
|
|
2036
|
+
const prefix = options.idPrefix ?? "kit-diagram";
|
|
2037
|
+
const padding = options.padding ?? 24;
|
|
2038
|
+
const rtl = options.rtl ?? false;
|
|
2039
|
+
const highlight = new Set(options.highlight ?? []);
|
|
2040
|
+
const showEdgeLabels = options.showEdgeLabels ?? true;
|
|
2041
|
+
const ctx = createGeometryContext(doc, {
|
|
2042
|
+
...options.direction === void 0 ? {} : { direction: options.direction },
|
|
2043
|
+
rtl,
|
|
2044
|
+
...options.edgeKind === void 0 ? {} : { edgeKind: options.edgeKind }
|
|
2045
|
+
});
|
|
2046
|
+
const hidden = /* @__PURE__ */ new Set();
|
|
2047
|
+
for (const node of doc.nodes) {
|
|
2048
|
+
if (getAncestors(ctx.index, node.id).some((id) => ctx.index.nodesById.get(id)?.collapsed === true)) hidden.add(node.id);
|
|
2049
|
+
}
|
|
2050
|
+
const visibleNodes = doc.nodes.filter((node) => !hidden.has(node.id));
|
|
2051
|
+
const edges = [];
|
|
2052
|
+
for (const edge of doc.edges) {
|
|
2053
|
+
if (hidden.has(edge.source) || hidden.has(edge.target)) continue;
|
|
2054
|
+
const geometry = getEdgeGeometry(ctx, edge);
|
|
2055
|
+
if (geometry) edges.push({ edge, geometry });
|
|
2056
|
+
}
|
|
2057
|
+
const rects = visibleNodes.map((node) => ctx.bounds.get(node.id));
|
|
2058
|
+
for (const { edge, geometry } of edges) {
|
|
2059
|
+
for (const point of geometry.points) rects.push({ x: point.x, y: point.y, width: 0, height: 0 });
|
|
2060
|
+
if (showEdgeLabels && edge.label) {
|
|
2061
|
+
const width = estimateTextWidth(edge.label) + 16;
|
|
2062
|
+
rects.push({ x: geometry.label.x - width / 2, y: geometry.label.y - 12, width, height: 24 });
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
const box = unionRects(rects);
|
|
2066
|
+
const view = rects.length === 0 ? { x: 0, y: 0, width: 160, height: 80 } : { x: box.x - padding, y: box.y - padding, width: box.width + padding * 2, height: box.height + padding * 2 };
|
|
2067
|
+
const tones = /* @__PURE__ */ new Set(["neutral"]);
|
|
2068
|
+
for (const { edge } of edges) tones.add(edge.status ?? "neutral");
|
|
2069
|
+
const markerId = (tone) => `${prefix}-arrow-${tone}`;
|
|
2070
|
+
const markerColor = (tone) => tone === "neutral" ? palette.edge : palette.status[tone].border;
|
|
2071
|
+
const defs = el(
|
|
2072
|
+
"defs",
|
|
2073
|
+
void 0,
|
|
2074
|
+
void 0,
|
|
2075
|
+
[...tones].map(
|
|
2076
|
+
(tone) => el(
|
|
2077
|
+
"marker",
|
|
2078
|
+
{
|
|
2079
|
+
id: markerId(tone),
|
|
2080
|
+
viewBox: "0 0 10 10",
|
|
2081
|
+
markerWidth: 8,
|
|
2082
|
+
markerHeight: 8,
|
|
2083
|
+
refX: 9,
|
|
2084
|
+
refY: 5,
|
|
2085
|
+
orient: "auto-start-reverse",
|
|
2086
|
+
markerUnits: "userSpaceOnUse"
|
|
2087
|
+
},
|
|
2088
|
+
void 0,
|
|
2089
|
+
[el("path", { d: "M 0 0 L 10 5 L 0 10 z" }, { fill: markerColor(tone) })]
|
|
2090
|
+
)
|
|
2091
|
+
)
|
|
2092
|
+
);
|
|
2093
|
+
const depth = (node) => getAncestors(ctx.index, node.id).length;
|
|
2094
|
+
const containers = visibleNodes.filter((node) => isContainerKind(node.kind)).sort((a, b) => depth(a) - depth(b));
|
|
2095
|
+
const others = visibleNodes.filter((node) => !isContainerKind(node.kind));
|
|
2096
|
+
const colorsFor = (node) => {
|
|
2097
|
+
if (node.status !== void 0 && node.status !== "neutral") {
|
|
2098
|
+
const tone = palette.status[node.status];
|
|
2099
|
+
return { fill: tone.background, stroke: tone.border, text: tone.foreground };
|
|
2100
|
+
}
|
|
2101
|
+
switch (node.kind) {
|
|
2102
|
+
case "start":
|
|
2103
|
+
return { fill: palette.accentSubtle, stroke: palette.accent, text: palette.text };
|
|
2104
|
+
case "end":
|
|
2105
|
+
return { fill: palette.inverse, stroke: palette.inverse, text: palette.textInverse };
|
|
2106
|
+
case "note": {
|
|
2107
|
+
const tone = palette.status.warning;
|
|
2108
|
+
return { fill: tone.background, stroke: tone.border, text: tone.foreground };
|
|
2109
|
+
}
|
|
2110
|
+
case "agent":
|
|
2111
|
+
return { fill: palette.accentSubtle, stroke: palette.accent, text: palette.text };
|
|
2112
|
+
case "group":
|
|
2113
|
+
case "subflow":
|
|
2114
|
+
return { fill: "none", stroke: palette.border, text: palette.textMuted };
|
|
2115
|
+
default:
|
|
2116
|
+
return { fill: palette.surface, stroke: palette.border, text: palette.text };
|
|
2117
|
+
}
|
|
2118
|
+
};
|
|
2119
|
+
const strokeWidth = (node) => highlight.has(node.id) || node.kind === "end" ? cssVar("border.width.emphasis") : cssVar("border.width.default");
|
|
2120
|
+
const textLines = (lines2, center, colors, extra = {}) => {
|
|
2121
|
+
const lineHeight = TEXT_METRICS.lineHeight;
|
|
2122
|
+
const startY = center.y - (lines2.length - 1) * lineHeight / 2;
|
|
2123
|
+
return lines2.map(
|
|
2124
|
+
(line, i) => el(
|
|
2125
|
+
"text",
|
|
2126
|
+
{
|
|
2127
|
+
x: num(center.x),
|
|
2128
|
+
y: num(startY + i * lineHeight),
|
|
2129
|
+
"text-anchor": "middle",
|
|
2130
|
+
"dominant-baseline": "central",
|
|
2131
|
+
...rtl ? { direction: "rtl" } : {}
|
|
2132
|
+
},
|
|
2133
|
+
{ fill: colors.text, ...extra },
|
|
2134
|
+
[line]
|
|
2135
|
+
)
|
|
2136
|
+
);
|
|
2137
|
+
};
|
|
2138
|
+
const startText = (text, rect, y, fill, extra = {}, padX = 12) => el(
|
|
2139
|
+
"text",
|
|
2140
|
+
{
|
|
2141
|
+
x: num(rtl ? rect.x + rect.width - padX : rect.x + padX),
|
|
2142
|
+
y: num(y),
|
|
2143
|
+
"text-anchor": "start",
|
|
2144
|
+
"dominant-baseline": "central",
|
|
2145
|
+
...rtl ? { direction: "rtl" } : {}
|
|
2146
|
+
},
|
|
2147
|
+
{ fill, ...extra },
|
|
2148
|
+
[text]
|
|
2149
|
+
);
|
|
2150
|
+
const nodeElement = (node) => {
|
|
2151
|
+
const rect = ctx.bounds.get(node.id);
|
|
2152
|
+
const colors = colorsFor(node);
|
|
2153
|
+
const center = rectCenter(rect);
|
|
2154
|
+
const accent = highlight.has(node.id);
|
|
2155
|
+
const shapeStyle = {
|
|
2156
|
+
fill: colors.fill,
|
|
2157
|
+
stroke: accent ? palette.accent : colors.stroke,
|
|
2158
|
+
"stroke-width": strokeWidth(node)
|
|
2159
|
+
};
|
|
2160
|
+
const children = [];
|
|
2161
|
+
const geometry = (x, y, w, h) => ({ x: num(x), y: num(y), width: num(w), height: num(h) });
|
|
2162
|
+
switch (node.kind) {
|
|
2163
|
+
case "start":
|
|
2164
|
+
case "end":
|
|
2165
|
+
children.push(el("rect", { ...geometry(rect.x, rect.y, rect.width, rect.height), rx: num(rect.height / 2) }, shapeStyle));
|
|
2166
|
+
children.push(...textLines(wrapText(node.label, rect.width - 32), center, colors));
|
|
2167
|
+
break;
|
|
2168
|
+
case "decision": {
|
|
2169
|
+
const { x, y, width, height } = rect;
|
|
2170
|
+
const points = [`${num(x + width / 2)},${num(y)}`, `${num(x + width)},${num(y + height / 2)}`, `${num(x + width / 2)},${num(y + height)}`, `${num(x)},${num(y + height / 2)}`].join(" ");
|
|
2171
|
+
children.push(el("polygon", { points }, { ...shapeStyle, "stroke-linejoin": "round" }));
|
|
2172
|
+
children.push(...textLines(wrapText(node.label, width * 0.5), center, colors));
|
|
2173
|
+
break;
|
|
2174
|
+
}
|
|
2175
|
+
case "note": {
|
|
2176
|
+
const fold = 16;
|
|
2177
|
+
const { x, y, width, height } = rect;
|
|
2178
|
+
const d = `M ${num(x)} ${num(y)} H ${num(x + width - fold)} L ${num(x + width)} ${num(y + fold)} V ${num(y + height)} H ${num(x)} Z`;
|
|
2179
|
+
children.push(el("path", { d }, shapeStyle));
|
|
2180
|
+
children.push(
|
|
2181
|
+
el("path", { d: `M ${num(x + width - fold)} ${num(y)} V ${num(y + fold)} H ${num(x + width)}` }, { fill: "none", stroke: shapeStyle.stroke, "stroke-width": strokeWidth(node) })
|
|
2182
|
+
);
|
|
2183
|
+
children.push(...textLines(wrapText(node.label, width - 32), center, colors));
|
|
2184
|
+
break;
|
|
2185
|
+
}
|
|
2186
|
+
case "group":
|
|
2187
|
+
case "subflow": {
|
|
2188
|
+
children.push(
|
|
2189
|
+
el("rect", { ...geometry(rect.x, rect.y, rect.width, rect.height), rx: 8 }, {
|
|
2190
|
+
...shapeStyle,
|
|
2191
|
+
"stroke-dasharray": node.kind === "group" ? "6 4" : "none"
|
|
2192
|
+
})
|
|
2193
|
+
);
|
|
2194
|
+
children.push(startText(node.label, rect, rect.y + 20, colors.text, { "font-weight": "600" }));
|
|
2195
|
+
break;
|
|
2196
|
+
}
|
|
2197
|
+
case "entity": {
|
|
2198
|
+
const fields = Array.isArray(node.data?.fields) ? node.data?.fields : [];
|
|
2199
|
+
children.push(el("rect", { ...geometry(rect.x, rect.y, rect.width, rect.height), rx: 6 }, shapeStyle));
|
|
2200
|
+
children.push(
|
|
2201
|
+
el("path", { d: `M ${num(rect.x)} ${num(rect.y + 32)} H ${num(rect.x + rect.width)}` }, { fill: "none", stroke: palette.borderSubtle })
|
|
2202
|
+
);
|
|
2203
|
+
children.push(startText(node.label, rect, rect.y + 16, colors.text, { "font-weight": "600" }));
|
|
2204
|
+
fields.forEach((field, i) => {
|
|
2205
|
+
const name = typeof field === "string" ? field : String(field?.name ?? "");
|
|
2206
|
+
const type = typeof field === "object" && field !== null ? String(field.type ?? "") : "";
|
|
2207
|
+
const y = rect.y + 32 + 12 + i * 24;
|
|
2208
|
+
children.push(startText(name, rect, y, colors.text));
|
|
2209
|
+
if (type !== "") {
|
|
2210
|
+
children.push(
|
|
2211
|
+
el(
|
|
2212
|
+
"text",
|
|
2213
|
+
{
|
|
2214
|
+
x: num(rtl ? rect.x + 12 : rect.x + rect.width - 12),
|
|
2215
|
+
y: num(y),
|
|
2216
|
+
"text-anchor": "end",
|
|
2217
|
+
"dominant-baseline": "central",
|
|
2218
|
+
...rtl ? { direction: "rtl" } : {}
|
|
2219
|
+
},
|
|
2220
|
+
{ fill: palette.textMuted },
|
|
2221
|
+
[type]
|
|
2222
|
+
)
|
|
2223
|
+
);
|
|
2224
|
+
}
|
|
2225
|
+
});
|
|
2226
|
+
break;
|
|
2227
|
+
}
|
|
2228
|
+
case "code": {
|
|
2229
|
+
const code = dataText(node, "code") ?? "";
|
|
2230
|
+
children.push(el("rect", { ...geometry(rect.x, rect.y, rect.width, rect.height), rx: 6 }, { ...shapeStyle, fill: palette.canvas }));
|
|
2231
|
+
children.push(startText(node.label, rect, rect.y + 16, palette.textMuted, { "font-size": cssVar("text.caption.size") }));
|
|
2232
|
+
code.split(/\r?\n/).slice(0, 12).forEach((line, i) => {
|
|
2233
|
+
children.push(
|
|
2234
|
+
el(
|
|
2235
|
+
"text",
|
|
2236
|
+
{
|
|
2237
|
+
x: num(rect.x + 12),
|
|
2238
|
+
y: num(rect.y + 40 + i * TEXT_METRICS.lineHeight + 10),
|
|
2239
|
+
"text-anchor": "start",
|
|
2240
|
+
"dominant-baseline": "central",
|
|
2241
|
+
direction: "ltr"
|
|
2242
|
+
},
|
|
2243
|
+
{ fill: palette.text, "font-family": palette.fontMono, "font-size": cssVar("text.code.size"), "white-space": "pre" },
|
|
2244
|
+
[line]
|
|
2245
|
+
)
|
|
2246
|
+
);
|
|
2247
|
+
});
|
|
2248
|
+
break;
|
|
2249
|
+
}
|
|
2250
|
+
case "image": {
|
|
2251
|
+
children.push(el("rect", { ...geometry(rect.x, rect.y, rect.width, rect.height), rx: 6 }, shapeStyle));
|
|
2252
|
+
const src = dataText(node, "src");
|
|
2253
|
+
if (isSafeImageUrl(src)) {
|
|
2254
|
+
children.push(
|
|
2255
|
+
el("image", {
|
|
2256
|
+
href: src,
|
|
2257
|
+
...geometry(rect.x + 1, rect.y + 1, rect.width - 2, rect.height - 28),
|
|
2258
|
+
preserveAspectRatio: "xMidYMid slice"
|
|
2259
|
+
})
|
|
2260
|
+
);
|
|
2261
|
+
} else {
|
|
2262
|
+
const gx = rect.x + rect.width / 2;
|
|
2263
|
+
const gy = rect.y + (rect.height - 24) / 2;
|
|
2264
|
+
children.push(
|
|
2265
|
+
el("path", { d: `M ${num(gx - 20)} ${num(gy + 12)} L ${num(gx - 6)} ${num(gy - 6)} L ${num(gx + 4)} ${num(gy + 6)} L ${num(gx + 10)} ${num(gy)} L ${num(gx + 20)} ${num(gy + 12)} Z` }, { fill: "none", stroke: palette.textMuted, "stroke-width": cssVar("border.width.default"), "stroke-linejoin": "round" })
|
|
2266
|
+
);
|
|
2267
|
+
}
|
|
2268
|
+
children.push(...textLines([node.label], { x: center.x, y: rect.y + rect.height - 14 }, colors));
|
|
2269
|
+
break;
|
|
2270
|
+
}
|
|
2271
|
+
case "chart": {
|
|
2272
|
+
children.push(el("rect", { ...geometry(rect.x, rect.y, rect.width, rect.height), rx: 6 }, shapeStyle));
|
|
2273
|
+
const baseY = rect.y + rect.height - 40;
|
|
2274
|
+
[0.4, 0.7, 0.55, 0.9].forEach((ratio, i) => {
|
|
2275
|
+
const h = (rect.height - 64) * ratio;
|
|
2276
|
+
children.push(el("rect", geometry(rect.x + 24 + i * 24, baseY - h, 16, h), { fill: palette.accent }));
|
|
2277
|
+
});
|
|
2278
|
+
children.push(...textLines([node.label], { x: center.x, y: rect.y + rect.height - 16 }, colors));
|
|
2279
|
+
break;
|
|
2280
|
+
}
|
|
2281
|
+
case "agent": {
|
|
2282
|
+
children.push(el("rect", { ...geometry(rect.x, rect.y, rect.width, rect.height), rx: 12 }, shapeStyle));
|
|
2283
|
+
const cx = rtl ? rect.x + rect.width - 28 : rect.x + 28;
|
|
2284
|
+
children.push(el("circle", { cx: num(cx), cy: num(center.y), r: 12 }, { fill: palette.accent }));
|
|
2285
|
+
const role = dataText(node, "role");
|
|
2286
|
+
const textX = rtl ? rect.x + rect.width - 52 : rect.x + 52;
|
|
2287
|
+
const anchor = { "text-anchor": "start", "dominant-baseline": "central", ...rtl ? { direction: "rtl" } : {} };
|
|
2288
|
+
children.push(
|
|
2289
|
+
el("text", { x: num(textX), y: num(role ? center.y - 9 : center.y), ...anchor }, { fill: colors.text, "font-weight": "600" }, [node.label])
|
|
2290
|
+
);
|
|
2291
|
+
if (role) {
|
|
2292
|
+
children.push(
|
|
2293
|
+
el("text", { x: num(textX), y: num(center.y + 11), ...anchor }, { fill: palette.textMuted, "font-size": cssVar("text.caption.size") }, [role])
|
|
2294
|
+
);
|
|
2295
|
+
}
|
|
2296
|
+
break;
|
|
2297
|
+
}
|
|
2298
|
+
default: {
|
|
2299
|
+
children.push(el("rect", { ...geometry(rect.x, rect.y, rect.width, rect.height), rx: 6 }, shapeStyle));
|
|
2300
|
+
children.push(...textLines(wrapText(node.label, rect.width - 32), center, colors));
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
return el("g", { "data-node-id": node.id, "data-kind": node.kind, ...node.status ? { "data-status": node.status } : {} }, void 0, children);
|
|
2304
|
+
};
|
|
2305
|
+
const edgeElement = ({ edge, geometry }) => {
|
|
2306
|
+
const tone = edge.status ?? "neutral";
|
|
2307
|
+
const color = markerColor(tone);
|
|
2308
|
+
const children = [
|
|
2309
|
+
el(
|
|
2310
|
+
"path",
|
|
2311
|
+
{
|
|
2312
|
+
d: geometry.path,
|
|
2313
|
+
"marker-end": `url(#${markerId(tone)})`,
|
|
2314
|
+
...edge.bidirectional ? { "marker-start": `url(#${markerId(tone)})` } : {}
|
|
2315
|
+
},
|
|
2316
|
+
{
|
|
2317
|
+
fill: "none",
|
|
2318
|
+
stroke: color,
|
|
2319
|
+
"stroke-width": cssVar("border.width.default"),
|
|
2320
|
+
"stroke-linecap": "round",
|
|
2321
|
+
// A static picture cannot animate, so an animated edge is drawn dashed.
|
|
2322
|
+
...edge.dashed || edge.animated ? { "stroke-dasharray": "6 4" } : {}
|
|
2323
|
+
}
|
|
2324
|
+
)
|
|
2325
|
+
];
|
|
2326
|
+
if (showEdgeLabels && edge.label) {
|
|
2327
|
+
const width = estimateTextWidth(edge.label) + 16;
|
|
2328
|
+
children.push(
|
|
2329
|
+
el("rect", { x: num(geometry.label.x - width / 2), y: num(geometry.label.y - 12), width: num(width), height: 24, rx: 4 }, { fill: palette.surface, stroke: palette.borderSubtle })
|
|
2330
|
+
);
|
|
2331
|
+
children.push(
|
|
2332
|
+
el(
|
|
2333
|
+
"text",
|
|
2334
|
+
{ x: num(geometry.label.x), y: num(geometry.label.y), "text-anchor": "middle", "dominant-baseline": "central", ...rtl ? { direction: "rtl" } : {} },
|
|
2335
|
+
{ fill: palette.textMuted, "font-size": cssVar("text.caption.size") },
|
|
2336
|
+
[edge.label]
|
|
2337
|
+
)
|
|
2338
|
+
);
|
|
2339
|
+
}
|
|
2340
|
+
return el("g", { "data-edge-id": edge.id, ...edge.status ? { "data-status": edge.status } : {} }, void 0, children);
|
|
2341
|
+
};
|
|
2342
|
+
const title = options.title ?? doc.title;
|
|
2343
|
+
const lines = diagramToOutline(doc, options.connector === void 0 ? {} : { connector: options.connector });
|
|
2344
|
+
const description = options.description ?? [doc.description, ...lines].filter((part) => Boolean(part)).join(". ");
|
|
2345
|
+
const titleId = `${prefix}-title`;
|
|
2346
|
+
const descId = `${prefix}-desc`;
|
|
2347
|
+
return el(
|
|
2348
|
+
"svg",
|
|
2349
|
+
{
|
|
2350
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
2351
|
+
viewBox: `${num(view.x)} ${num(view.y)} ${num(view.width)} ${num(view.height)}`,
|
|
2352
|
+
...options.width === void 0 ? {} : { width: options.width },
|
|
2353
|
+
...options.height === void 0 ? {} : { height: options.height },
|
|
2354
|
+
role: "img",
|
|
2355
|
+
"aria-labelledby": description === "" ? titleId : `${titleId} ${descId}`,
|
|
2356
|
+
...options.lang === void 0 ? {} : { lang: options.lang },
|
|
2357
|
+
...rtl ? { direction: "rtl" } : {},
|
|
2358
|
+
"data-kit": "diagram-static"
|
|
2359
|
+
},
|
|
2360
|
+
{ "font-family": palette.fontSans, "font-size": cssVar("text.label.size") },
|
|
2361
|
+
[
|
|
2362
|
+
el("title", { id: titleId }, void 0, [title]),
|
|
2363
|
+
...description === "" ? [] : [el("desc", { id: descId }, void 0, [description])],
|
|
2364
|
+
defs,
|
|
2365
|
+
el("g", { "data-layer": "groups" }, void 0, containers.map(nodeElement)),
|
|
2366
|
+
el("g", { "data-layer": "edges" }, void 0, edges.map(edgeElement)),
|
|
2367
|
+
el("g", { "data-layer": "nodes" }, void 0, others.map(nodeElement))
|
|
2368
|
+
]
|
|
2369
|
+
);
|
|
2370
|
+
}
|
|
2371
|
+
function diagramToSvg(doc, options = {}) {
|
|
2372
|
+
const svg = serializeSvg(diagramToSvgTree(doc, options));
|
|
2373
|
+
return options.standalone ? `<?xml version="1.0" encoding="UTF-8"?>
|
|
2374
|
+
${svg}` : svg;
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
// src/io.ts
|
|
2378
|
+
var DiagramImportError = class extends Error {
|
|
2379
|
+
code;
|
|
2380
|
+
issues;
|
|
2381
|
+
constructor(code, message, issues = []) {
|
|
2382
|
+
super(message);
|
|
2383
|
+
this.name = "DiagramImportError";
|
|
2384
|
+
this.code = code;
|
|
2385
|
+
this.issues = issues;
|
|
2386
|
+
}
|
|
2387
|
+
};
|
|
2388
|
+
function exportDiagramJson(doc, options = {}) {
|
|
2389
|
+
const { version, title, ...rest } = doc;
|
|
2390
|
+
return JSON.stringify({ version, title, ...rest }, null, options.space ?? 2);
|
|
2391
|
+
}
|
|
2392
|
+
function exportDiagramSvg(doc, options = {}) {
|
|
2393
|
+
return diagramToSvg(doc, { ...options, standalone: true });
|
|
2394
|
+
}
|
|
2395
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2396
|
+
function importDiagram(input, options = {}) {
|
|
2397
|
+
let raw = input;
|
|
2398
|
+
if (typeof input === "string") {
|
|
2399
|
+
try {
|
|
2400
|
+
raw = JSON.parse(input);
|
|
2401
|
+
} catch (error) {
|
|
2402
|
+
throw new DiagramImportError("invalid-json", `The diagram is not valid JSON: ${error.message}`);
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
if (!isRecord(raw)) throw new DiagramImportError("not-an-object", "A diagram must be a JSON object.");
|
|
2406
|
+
let data = { ...raw };
|
|
2407
|
+
let version = data.version;
|
|
2408
|
+
if (version === void 0 && options.assumeVersion !== void 0) version = options.assumeVersion;
|
|
2409
|
+
if (typeof version !== "number" || !Number.isInteger(version)) {
|
|
2410
|
+
throw new DiagramImportError("missing-version", "The diagram has no numeric version field.");
|
|
2411
|
+
}
|
|
2412
|
+
if (version > DIAGRAM_VERSION) {
|
|
2413
|
+
throw new DiagramImportError(
|
|
2414
|
+
"unsupported-version",
|
|
2415
|
+
`The diagram is version ${version}, but this library reads up to version ${DIAGRAM_VERSION}. Update @x12i/diagrams.`
|
|
2416
|
+
);
|
|
2417
|
+
}
|
|
2418
|
+
const fromVersion = version;
|
|
2419
|
+
for (let current = version; current < DIAGRAM_VERSION; current += 1) {
|
|
2420
|
+
const step = options.migrations?.[current];
|
|
2421
|
+
if (!step) {
|
|
2422
|
+
throw new DiagramImportError("missing-migration", `No migration from diagram version ${current} to ${current + 1}.`);
|
|
2423
|
+
}
|
|
2424
|
+
data = { ...step(data), version: current + 1 };
|
|
2425
|
+
}
|
|
2426
|
+
data.version = DIAGRAM_VERSION;
|
|
2427
|
+
if (options.migrate && fromVersion < DIAGRAM_VERSION) data = { ...options.migrate(data, fromVersion), version: DIAGRAM_VERSION };
|
|
2428
|
+
if (!Array.isArray(data.nodes) || !Array.isArray(data.edges)) {
|
|
2429
|
+
throw new DiagramImportError("invalid-structure", "A diagram needs `nodes` and `edges` arrays.");
|
|
2430
|
+
}
|
|
2431
|
+
for (const [i, node] of data.nodes.entries()) {
|
|
2432
|
+
if (!isRecord(node) || typeof node.id !== "string") {
|
|
2433
|
+
throw new DiagramImportError("invalid-structure", `Node ${i} needs a string id.`);
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
for (const [i, edge] of data.edges.entries()) {
|
|
2437
|
+
if (!isRecord(edge) || typeof edge.id !== "string" || typeof edge.source !== "string" || typeof edge.target !== "string") {
|
|
2438
|
+
throw new DiagramImportError("invalid-structure", `Edge ${i} needs string id, source and target fields.`);
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
const document = { ...data, title: typeof data.title === "string" ? data.title : "" };
|
|
2442
|
+
const { issues, errors } = validateDiagram(document);
|
|
2443
|
+
if (options.strict && errors.length > 0) {
|
|
2444
|
+
throw new DiagramImportError("invalid-structure", `The diagram has ${errors.length} validation errors. First: ${errors[0]?.message}`, issues);
|
|
2445
|
+
}
|
|
2446
|
+
const result = { document, issues };
|
|
2447
|
+
if (fromVersion < DIAGRAM_VERSION) result.migratedFrom = fromVersion;
|
|
2448
|
+
return result;
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
// src/messages.ts
|
|
2452
|
+
function formatMessage(message, values = {}) {
|
|
2453
|
+
if (typeof message === "function") return message(values);
|
|
2454
|
+
return message.replace(/\{(\w+)\}/g, (match, name) => {
|
|
2455
|
+
const value = values[name];
|
|
2456
|
+
return value === void 0 ? match : String(value);
|
|
2457
|
+
});
|
|
2458
|
+
}
|
|
2459
|
+
var defaultMessages = {
|
|
2460
|
+
canvas: {
|
|
2461
|
+
label: "Diagram: {title}",
|
|
2462
|
+
instructions: "Press Tab to move between nodes. Arrow keys jump to the nearest node in that direction. Press Space to select a node, Enter to rename it, Delete to remove the selection, and C to start a connection. Hold Shift with an arrow key to nudge the selection. Press Escape to clear the selection. The diagram outline lists every node and connection.",
|
|
2463
|
+
instructionsReadOnly: "Press Tab to move between nodes. Arrow keys jump to the nearest node in that direction. The diagram outline lists every node and connection.",
|
|
2464
|
+
empty: "This diagram is empty."
|
|
2465
|
+
},
|
|
2466
|
+
kinds: {
|
|
2467
|
+
start: "Start",
|
|
2468
|
+
end: "End",
|
|
2469
|
+
task: "Task",
|
|
2470
|
+
decision: "Decision",
|
|
2471
|
+
group: "Group",
|
|
2472
|
+
note: "Note",
|
|
2473
|
+
image: "Image",
|
|
2474
|
+
entity: "Entity",
|
|
2475
|
+
code: "Code",
|
|
2476
|
+
chart: "Chart",
|
|
2477
|
+
agent: "Agent",
|
|
2478
|
+
subflow: "Subflow"
|
|
2479
|
+
},
|
|
2480
|
+
statuses: {
|
|
2481
|
+
neutral: "Neutral",
|
|
2482
|
+
success: "Success",
|
|
2483
|
+
warning: "Warning",
|
|
2484
|
+
danger: "Danger",
|
|
2485
|
+
info: "Info"
|
|
2486
|
+
},
|
|
2487
|
+
node: {
|
|
2488
|
+
label: "{label}, {kind}. Incoming connections: {incoming}. Outgoing connections: {outgoing}.",
|
|
2489
|
+
status: "Status: {status}.",
|
|
2490
|
+
roleDescription: "node",
|
|
2491
|
+
editLabel: "Label for {label}",
|
|
2492
|
+
collapse: "Collapse {label}",
|
|
2493
|
+
expand: "Expand {label}",
|
|
2494
|
+
port: "Connection point",
|
|
2495
|
+
childCount: "{count} items"
|
|
2496
|
+
},
|
|
2497
|
+
edge: {
|
|
2498
|
+
label: "Connection from {source} to {target}",
|
|
2499
|
+
labelWithText: "{label}. Connection from {source} to {target}",
|
|
2500
|
+
roleDescription: "connection",
|
|
2501
|
+
editLabel: "Label for the connection from {source} to {target}"
|
|
2502
|
+
},
|
|
2503
|
+
toolbar: {
|
|
2504
|
+
label: "Diagram tools",
|
|
2505
|
+
addNode: "Add node",
|
|
2506
|
+
palette: "Node types",
|
|
2507
|
+
undo: "Undo",
|
|
2508
|
+
redo: "Redo",
|
|
2509
|
+
delete: "Delete selection",
|
|
2510
|
+
duplicate: "Duplicate selection",
|
|
2511
|
+
autoLayout: "Arrange automatically",
|
|
2512
|
+
group: "Group selection",
|
|
2513
|
+
ungroup: "Ungroup",
|
|
2514
|
+
zoomIn: "Zoom in",
|
|
2515
|
+
zoomOut: "Zoom out",
|
|
2516
|
+
fitView: "Fit diagram to view",
|
|
2517
|
+
showMiniMap: "Show minimap",
|
|
2518
|
+
hideMiniMap: "Hide minimap",
|
|
2519
|
+
showGrid: "Show grid",
|
|
2520
|
+
hideGrid: "Hide grid",
|
|
2521
|
+
controlsLabel: "View controls"
|
|
2522
|
+
},
|
|
2523
|
+
minimap: "Minimap",
|
|
2524
|
+
announce: {
|
|
2525
|
+
nodeAdded: "Added {kind}: {label}.",
|
|
2526
|
+
nodeRemoved: "Removed {label}.",
|
|
2527
|
+
itemsRemoved: "Items removed: {count}.",
|
|
2528
|
+
connected: "Connected {source} to {target}.",
|
|
2529
|
+
disconnected: "Removed the connection from {source} to {target}.",
|
|
2530
|
+
moved: "Moved {label} to x {x}, y {y}.",
|
|
2531
|
+
resized: "Resized {label} to {width} by {height}.",
|
|
2532
|
+
renamed: "Renamed to {label}.",
|
|
2533
|
+
selected: "Selected {label}.",
|
|
2534
|
+
selectedMany: "Selected items: {count}.",
|
|
2535
|
+
selectionCleared: "Selection cleared.",
|
|
2536
|
+
undone: "Undid the last change.",
|
|
2537
|
+
redone: "Redid the change.",
|
|
2538
|
+
nothingToUndo: "Nothing to undo.",
|
|
2539
|
+
nothingToRedo: "Nothing to redo.",
|
|
2540
|
+
layoutApplied: "Layout applied.",
|
|
2541
|
+
grouped: "Grouped into {label}.",
|
|
2542
|
+
ungrouped: "Ungrouped {label}.",
|
|
2543
|
+
duplicated: "Duplicated items: {count}.",
|
|
2544
|
+
copied: "Copied items: {count}.",
|
|
2545
|
+
pasted: "Pasted items: {count}.",
|
|
2546
|
+
connectStarted: "Connecting from {label}. Move to the target node and press Enter, or press Escape to cancel.",
|
|
2547
|
+
connectCancelled: "Connection cancelled.",
|
|
2548
|
+
locked: "{label} is locked."
|
|
2549
|
+
},
|
|
2550
|
+
connection: {
|
|
2551
|
+
"unknown-source": "That node no longer exists.",
|
|
2552
|
+
"unknown-target": "That node no longer exists.",
|
|
2553
|
+
"self-loop": "A node cannot connect to itself in this diagram.",
|
|
2554
|
+
duplicate: "These nodes are already connected.",
|
|
2555
|
+
locked: "A locked node cannot be connected.",
|
|
2556
|
+
"container-descendant": "A group cannot connect to a node inside it.",
|
|
2557
|
+
"source-port-missing": "That connection point does not exist.",
|
|
2558
|
+
"target-port-missing": "That connection point does not exist.",
|
|
2559
|
+
"source-port-not-output": "That connection point only receives connections.",
|
|
2560
|
+
"target-port-not-input": "That connection point only starts connections.",
|
|
2561
|
+
"port-type-mismatch": "These connection points have different types and cannot be joined.",
|
|
2562
|
+
cycle: "This connection would create a cycle, which this diagram does not allow.",
|
|
2563
|
+
"multiple-parents": "This node already has a parent.",
|
|
2564
|
+
custom: "This connection is not allowed."
|
|
2565
|
+
},
|
|
2566
|
+
outline: {
|
|
2567
|
+
title: "Diagram outline",
|
|
2568
|
+
nodes: "Nodes",
|
|
2569
|
+
paths: "Paths",
|
|
2570
|
+
connector: "to",
|
|
2571
|
+
connectsTo: "Connects to",
|
|
2572
|
+
receivesFrom: "Receives from",
|
|
2573
|
+
noConnections: "No connections",
|
|
2574
|
+
goToNode: "Show {label} in the diagram",
|
|
2575
|
+
empty: "This diagram has no nodes.",
|
|
2576
|
+
status: "Status: {status}",
|
|
2577
|
+
contains: "Contains",
|
|
2578
|
+
disconnect: "Remove the connection from {source} to {target}"
|
|
2579
|
+
},
|
|
2580
|
+
mermaid: {
|
|
2581
|
+
loading: "Rendering diagram\u2026",
|
|
2582
|
+
renderError: "The diagram could not be rendered.",
|
|
2583
|
+
loadError: "Mermaid could not be loaded. Install the mermaid package and pass it with the loadMermaid prop.",
|
|
2584
|
+
showSource: "Show source",
|
|
2585
|
+
hideSource: "Hide source",
|
|
2586
|
+
sourceLabel: "Diagram source"
|
|
2587
|
+
}
|
|
2588
|
+
};
|
|
2589
|
+
function isRecord2(value) {
|
|
2590
|
+
return typeof value === "object" && value !== null;
|
|
2591
|
+
}
|
|
2592
|
+
function merge(base, override) {
|
|
2593
|
+
if (!isRecord2(override) || typeof base === "string" || typeof base === "function") {
|
|
2594
|
+
return override === void 0 ? base : override;
|
|
2595
|
+
}
|
|
2596
|
+
const result = { ...base };
|
|
2597
|
+
for (const [key, value] of Object.entries(override)) {
|
|
2598
|
+
if (value !== void 0) result[key] = merge(base[key], value);
|
|
2599
|
+
}
|
|
2600
|
+
return result;
|
|
2601
|
+
}
|
|
2602
|
+
function resolveMessages(override) {
|
|
2603
|
+
return override ? merge(defaultMessages, override) : defaultMessages;
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
export {
|
|
2607
|
+
DIAGRAM_VERSION,
|
|
2608
|
+
NODE_KINDS,
|
|
2609
|
+
NODE_STATUSES,
|
|
2610
|
+
EDGE_KINDS,
|
|
2611
|
+
DIAGRAM_KINDS,
|
|
2612
|
+
isContainerKind,
|
|
2613
|
+
defineDiagram,
|
|
2614
|
+
createDiagram,
|
|
2615
|
+
generateId,
|
|
2616
|
+
nextId,
|
|
2617
|
+
createIdAllocator,
|
|
2618
|
+
idStem,
|
|
2619
|
+
buildIndex,
|
|
2620
|
+
getAncestors,
|
|
2621
|
+
getDescendants,
|
|
2622
|
+
findCycle,
|
|
2623
|
+
hasPath,
|
|
2624
|
+
getRoots,
|
|
2625
|
+
getNeighbors,
|
|
2626
|
+
topologicalOrder,
|
|
2627
|
+
resolveConnectionRules,
|
|
2628
|
+
portTypesCompatible,
|
|
2629
|
+
checkConnection,
|
|
2630
|
+
validateDiagram,
|
|
2631
|
+
GRID_SIZE,
|
|
2632
|
+
DEFAULT_NODE_SIZE,
|
|
2633
|
+
COLLAPSED_HEIGHT,
|
|
2634
|
+
TEXT_METRICS,
|
|
2635
|
+
estimateTextWidth,
|
|
2636
|
+
wrapText,
|
|
2637
|
+
getNodeSize,
|
|
2638
|
+
getNodeBounds,
|
|
2639
|
+
rectCenter,
|
|
2640
|
+
unionRects,
|
|
2641
|
+
rectsIntersect,
|
|
2642
|
+
rectContainsPoint,
|
|
2643
|
+
getAbsolutePositions,
|
|
2644
|
+
getAbsolutePosition,
|
|
2645
|
+
getAbsoluteBounds,
|
|
2646
|
+
getDocumentBounds,
|
|
2647
|
+
snapValue,
|
|
2648
|
+
snapPoint,
|
|
2649
|
+
directionSides,
|
|
2650
|
+
mirrorDirection,
|
|
2651
|
+
isHorizontal,
|
|
2652
|
+
getPortPlacements,
|
|
2653
|
+
sideAnchor,
|
|
2654
|
+
sideVector,
|
|
2655
|
+
computeEdgePath,
|
|
2656
|
+
createGeometryContext,
|
|
2657
|
+
getEdgeGeometry,
|
|
2658
|
+
getReadingOrder,
|
|
2659
|
+
findDirectionalNeighbor,
|
|
2660
|
+
DiagramMutationError,
|
|
2661
|
+
nextNodeId,
|
|
2662
|
+
nextEdgeId,
|
|
2663
|
+
createNode,
|
|
2664
|
+
addNode,
|
|
2665
|
+
updateNode,
|
|
2666
|
+
removeNodes,
|
|
2667
|
+
removeNode,
|
|
2668
|
+
moveNodes,
|
|
2669
|
+
translateNodes,
|
|
2670
|
+
resizeNode,
|
|
2671
|
+
setNodeParent,
|
|
2672
|
+
connect,
|
|
2673
|
+
disconnect,
|
|
2674
|
+
removeEdges,
|
|
2675
|
+
updateEdge,
|
|
2676
|
+
groupNodes,
|
|
2677
|
+
ungroupNode,
|
|
2678
|
+
extractFragment,
|
|
2679
|
+
pasteFragment,
|
|
2680
|
+
duplicateNodes,
|
|
2681
|
+
alignNodes,
|
|
2682
|
+
distributeNodes,
|
|
2683
|
+
layoutDiagram,
|
|
2684
|
+
applyLayout,
|
|
2685
|
+
layoutedDiagram,
|
|
2686
|
+
createHistory,
|
|
2687
|
+
canUndo,
|
|
2688
|
+
canRedo,
|
|
2689
|
+
historyReducer,
|
|
2690
|
+
diagramToOutline,
|
|
2691
|
+
diagramToOutlineTree,
|
|
2692
|
+
summarizeNodes,
|
|
2693
|
+
defaultPalette,
|
|
2694
|
+
resolvePalette,
|
|
2695
|
+
escapeXml,
|
|
2696
|
+
serializeSvg,
|
|
2697
|
+
isSafeImageUrl,
|
|
2698
|
+
diagramToSvgTree,
|
|
2699
|
+
diagramToSvg,
|
|
2700
|
+
DiagramImportError,
|
|
2701
|
+
exportDiagramJson,
|
|
2702
|
+
exportDiagramSvg,
|
|
2703
|
+
importDiagram,
|
|
2704
|
+
formatMessage,
|
|
2705
|
+
defaultMessages,
|
|
2706
|
+
resolveMessages
|
|
2707
|
+
};
|
|
2708
|
+
//# sourceMappingURL=chunk-3DJFQRU3.js.map
|