@pixerate/editor 0.53.0 → 0.54.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.
Files changed (92) hide show
  1. package/dist/agent/index.cjs +14 -6
  2. package/dist/agent/index.cjs.map +1 -1
  3. package/dist/agent/index.d.cts +4 -3
  4. package/dist/agent/index.d.ts +4 -3
  5. package/dist/agent/index.js +12 -4
  6. package/dist/agent/index.js.map +1 -1
  7. package/dist/animation/index.cjs +15 -1
  8. package/dist/animation/index.cjs.map +1 -1
  9. package/dist/animation/index.d.cts +3 -226
  10. package/dist/animation/index.d.ts +3 -226
  11. package/dist/animation/index.js +14 -0
  12. package/dist/bridge/cli.cjs +36 -0
  13. package/dist/bridge/cli.cjs.map +1 -0
  14. package/dist/bridge/cli.d.cts +1 -0
  15. package/dist/bridge/cli.d.ts +1 -0
  16. package/dist/bridge/cli.js +36 -0
  17. package/dist/bridge/cli.js.map +1 -0
  18. package/dist/bridge/index.cjs +16 -0
  19. package/dist/bridge/index.cjs.map +1 -0
  20. package/dist/bridge/index.d.cts +65 -0
  21. package/dist/bridge/index.d.ts +65 -0
  22. package/dist/bridge/index.js +16 -0
  23. package/dist/bridge/index.js.map +1 -0
  24. package/dist/bridge-client-C5OziCRK.d.cts +148 -0
  25. package/dist/bridge-client-C5dRjrz5.d.ts +148 -0
  26. package/dist/canvas/index.cjs +1308 -528
  27. package/dist/canvas/index.cjs.map +1 -1
  28. package/dist/canvas/index.d.cts +309 -5
  29. package/dist/canvas/index.d.ts +309 -5
  30. package/dist/canvas/index.js +1295 -515
  31. package/dist/canvas/index.js.map +1 -1
  32. package/dist/chart/index.cjs +34 -76
  33. package/dist/chart/index.cjs.map +1 -1
  34. package/dist/chart/index.d.cts +9 -38
  35. package/dist/chart/index.d.ts +9 -38
  36. package/dist/chart/index.js +23 -65
  37. package/dist/chart/index.js.map +1 -1
  38. package/dist/chunk-AWKPSOSX.js +401 -0
  39. package/dist/chunk-AWKPSOSX.js.map +1 -0
  40. package/dist/chunk-B6DSIEXA.js +96 -0
  41. package/dist/chunk-B6DSIEXA.js.map +1 -0
  42. package/dist/chunk-EGFUVXKK.cjs +96 -0
  43. package/dist/chunk-EGFUVXKK.cjs.map +1 -0
  44. package/dist/{chunk-AMWXSYCI.js → chunk-GPWNYQ7R.js} +2 -370
  45. package/dist/chunk-GPWNYQ7R.js.map +1 -0
  46. package/dist/chunk-H5K77C7Y.cjs +3848 -0
  47. package/dist/chunk-H5K77C7Y.cjs.map +1 -0
  48. package/dist/chunk-K73CKYUM.cjs +458 -0
  49. package/dist/chunk-K73CKYUM.cjs.map +1 -0
  50. package/dist/chunk-NV22EMOZ.cjs +401 -0
  51. package/dist/chunk-NV22EMOZ.cjs.map +1 -0
  52. package/dist/chunk-PTMIGXZ2.js +467 -0
  53. package/dist/chunk-PTMIGXZ2.js.map +1 -0
  54. package/dist/chunk-QIWFEEWG.cjs +467 -0
  55. package/dist/chunk-QIWFEEWG.cjs.map +1 -0
  56. package/dist/chunk-WWJNC7SX.js +3848 -0
  57. package/dist/chunk-WWJNC7SX.js.map +1 -0
  58. package/dist/commands/index.d.cts +1 -1
  59. package/dist/commands/index.d.ts +1 -1
  60. package/dist/diagram/index.cjs +1146 -0
  61. package/dist/diagram/index.cjs.map +1 -0
  62. package/dist/diagram/index.d.cts +787 -0
  63. package/dist/diagram/index.d.ts +787 -0
  64. package/dist/diagram/index.js +1146 -0
  65. package/dist/diagram/index.js.map +1 -0
  66. package/dist/{easing-DTmX99QG.d.cts → easing-CNGk-W6d.d.cts} +1 -1
  67. package/dist/{easing-DTmX99QG.d.ts → easing-CNGk-W6d.d.ts} +1 -1
  68. package/dist/generation/index.d.cts +2 -2
  69. package/dist/generation/index.d.ts +2 -2
  70. package/dist/image-editor/index.d.cts +1 -1
  71. package/dist/image-editor/index.d.ts +1 -1
  72. package/dist/parameters/index.d.cts +1 -1
  73. package/dist/parameters/index.d.ts +1 -1
  74. package/dist/player-DSMjdgQ9.d.cts +225 -0
  75. package/dist/player-DdfVrKWq.d.ts +225 -0
  76. package/dist/presence-Cb6WhcrQ.d.cts +77 -0
  77. package/dist/presence-Cb6WhcrQ.d.ts +77 -0
  78. package/dist/{registry-CSlH4iUn.d.ts → registry-7AZFuand.d.ts} +1 -1
  79. package/dist/{registry-B4l-lYts.d.cts → registry-DIpIaCkH.d.cts} +1 -1
  80. package/dist/tools/index.cjs +10 -2
  81. package/dist/tools/index.cjs.map +1 -1
  82. package/dist/tools/index.d.cts +3 -74
  83. package/dist/tools/index.d.ts +3 -74
  84. package/dist/tools/index.js +12 -4
  85. package/dist/types-BULrFyuQ.d.cts +316 -0
  86. package/dist/types-BULrFyuQ.d.ts +316 -0
  87. package/dist/vector/index.d.cts +1 -1
  88. package/dist/vector/index.d.ts +1 -1
  89. package/package.json +14 -1
  90. package/dist/chunk-AMWXSYCI.js.map +0 -1
  91. package/dist/chunk-XFYUFVAX.cjs +0 -826
  92. package/dist/chunk-XFYUFVAX.cjs.map +0 -1
@@ -0,0 +1,1146 @@
1
+ import {
2
+ HistoryManager
3
+ } from "../chunk-QVZZGT4X.js";
4
+ import {
5
+ PRESENCE_COLORS,
6
+ PerformanceQueue,
7
+ PresenceStore,
8
+ createInstantPresenceClock,
9
+ realPresenceClock,
10
+ tweenPresenceCursor
11
+ } from "../chunk-B6DSIEXA.js";
12
+ import {
13
+ CLASS_RELATIONS,
14
+ ER_CARDINALITIES,
15
+ ER_PATTERNS,
16
+ FLOWCHART_SHAPES,
17
+ META_COMMENT_PREFIX,
18
+ META_FRONTMATTER_KEY,
19
+ STATE_SHAPES,
20
+ applyEdit,
21
+ applyEdits,
22
+ canonicalShape,
23
+ classGrammar,
24
+ createLineAdapter,
25
+ createPassthroughAdapter,
26
+ detectFamily,
27
+ detectFamilyFromHeader,
28
+ emptyMeta,
29
+ erGrammar,
30
+ flowchartAdapter,
31
+ formatChain,
32
+ formatLink,
33
+ formatNode,
34
+ getDiagramAdapter,
35
+ isMetaEmpty,
36
+ mergeMeta,
37
+ parseAttributes,
38
+ parseChain,
39
+ parseFlowchartSource,
40
+ parseLink,
41
+ parseMembers,
42
+ parseMermaid,
43
+ parseStatement,
44
+ quoteLabelIfNeeded,
45
+ registerDiagramAdapter,
46
+ relationFor,
47
+ resolveShape,
48
+ serializeMermaid,
49
+ serializeMeta,
50
+ splitSource,
51
+ splitStatements,
52
+ stateGrammar,
53
+ supportTierOf
54
+ } from "../chunk-WWJNC7SX.js";
55
+
56
+ // src/diagram/types.ts
57
+ var DIRECTIONS = ["TB", "TD", "BT", "LR", "RL"];
58
+
59
+ // src/diagram/source/find.ts
60
+ var MERMAID_INFO = /^(mermaid|mmd)\b/i;
61
+ function findMermaidSources(text) {
62
+ const clean = text.replace(/^\uFEFF/, "");
63
+ const blocks = fencedBlocks(clean);
64
+ if (blocks.length) {
65
+ const found = [];
66
+ for (const block of blocks) {
67
+ if (block.info && !MERMAID_INFO.test(block.info)) continue;
68
+ const item = asDiagram(block.body, block.heading, !block.info);
69
+ if (item) found.push(item);
70
+ }
71
+ if (found.length) return found;
72
+ }
73
+ const whole = asDiagram(clean.trim(), void 0, true);
74
+ return whole ? [whole] : [];
75
+ }
76
+ function asDiagram(source, heading, strict = false) {
77
+ if (!source.trim()) return void 0;
78
+ const split = splitSource(source);
79
+ const family = detectFamilyFromHeader(split.header);
80
+ if (family === "unknown") return void 0;
81
+ if (strict && !/^\s*([a-z]|C4)/.test(split.header)) return void 0;
82
+ const fmTitle = split.preamble.frontmatter?.title;
83
+ const title = typeof fmTitle === "string" && fmTitle.trim() ? fmTitle.trim() : heading;
84
+ return { source: source.trimEnd() + "\n", family, ...title ? { title } : {} };
85
+ }
86
+ function fencedBlocks(text) {
87
+ const lines = text.split(/\r\n|\n/);
88
+ const blocks = [];
89
+ let heading;
90
+ for (let i = 0; i < lines.length; i++) {
91
+ const open = /^ {0,3}(`{3,}(?!`)|~{3,}(?!~))([^`]*)$/.exec(lines[i]);
92
+ if (!open) {
93
+ const h = headingText(lines[i]);
94
+ if (h) heading = h;
95
+ continue;
96
+ }
97
+ const fence = open[1];
98
+ const close = new RegExp(`^ {0,3}${fence[0] === "`" ? "`" : "~"}{${fence.length},}$`);
99
+ const body = [];
100
+ let j = i + 1;
101
+ while (j < lines.length && !close.test(lines[j].trimEnd())) body.push(lines[j++]);
102
+ blocks.push({ info: open[2].trim(), body: body.join("\n"), heading });
103
+ i = j;
104
+ }
105
+ return blocks;
106
+ }
107
+ function headingText(line) {
108
+ const m = /^ {0,3}#{1,6}([ \t].*)$/.exec(line);
109
+ if (!m) return void 0;
110
+ let text = m[1].trim();
111
+ let end = text.length;
112
+ while (end > 0 && text[end - 1] === "#") end--;
113
+ if (end === 0 || text[end - 1] === " " || text[end - 1] === " ") text = text.slice(0, end).trim();
114
+ return text || void 0;
115
+ }
116
+
117
+ // src/diagram/controller.ts
118
+ var EDITABLE_DIAGRAM_FAMILIES = ["flowchart", "state", "class", "er"];
119
+ function diagramShapes(family) {
120
+ if (family === "flowchart") return FLOWCHART_SHAPES.map((shape) => shape.name);
121
+ if (family === "state") return STATE_SHAPES.map((shape) => shape.name);
122
+ return [];
123
+ }
124
+ function diagramSupportsGroups(family) {
125
+ return family === "flowchart" || family === "state" || family === "class";
126
+ }
127
+ var quote = (ids) => [...ids].map((id) => `"${id}"`).join(", ");
128
+ function validateDiagramEdit(doc, op) {
129
+ const { graph, family } = doc;
130
+ if (op.type === "replaceSource") return null;
131
+ if (doc.tier !== "editable")
132
+ return `This ${family} diagram can only be edited as Mermaid text; replace the source instead.`;
133
+ const nodes = new Set(graph.nodes.map((n) => n.id));
134
+ const groups = new Set(graph.groups.map((g) => g.id));
135
+ const edges = new Set(graph.edges.map((e) => e.id));
136
+ const anyIds = () => nodes.size ? `existing node ids are ${quote([...nodes].slice(0, 20))}` : "the diagram has no nodes yet";
137
+ const node = (id, allowGroup = false) => nodes.has(id) || allowGroup && groups.has(id) ? null : `There is no node "${id}"; ${anyIds()}.`;
138
+ const group = (id) => id === void 0 || groups.has(id) ? null : `There is no group "${id}"; ${groups.size ? `existing groups are ${quote(groups)}` : "the diagram has no groups"}.`;
139
+ const edge = (id) => edges.has(id) ? null : `There is no edge "${id}"; ${edges.size ? `existing edge ids are ${quote([...edges].slice(0, 20))}` : "the diagram has no edges yet"}.`;
140
+ const shape = (name) => {
141
+ if (name === void 0) return null;
142
+ const shapes = diagramShapes(family);
143
+ if (!shapes.length) return `Nodes of a ${family} diagram have no shapes to choose from.`;
144
+ return shapes.includes(name) ? null : `Unknown shape "${name}"; use one of ${quote(shapes)}.`;
145
+ };
146
+ const free = (id) => !/^[A-Za-z0-9_][\w.-]*$/.test(id) ? `Id "${id}" is not a valid Mermaid id; use letters, digits, "_", "-" or ".".` : nodes.has(id) || groups.has(id) ? `Id "${id}" is already used; choose another.` : null;
147
+ const first = (...problems) => problems.find((p) => p) ?? null;
148
+ switch (op.type) {
149
+ case "addNode":
150
+ return first(free(op.node.id), shape(op.node.shape), group(op.node.parentId));
151
+ case "removeNodes":
152
+ return first(...op.ids.map((id) => node(id, true)));
153
+ case "renameNode":
154
+ return node(op.id, true);
155
+ case "setNodeShape":
156
+ return first(node(op.id), shape(op.shape));
157
+ case "setNodeClasses":
158
+ case "setNodeStyles":
159
+ case "setNodeLink":
160
+ case "setNodeData":
161
+ case "resizeNode":
162
+ return node(op.id, true);
163
+ case "moveNodes":
164
+ return first(...op.moves.map((m) => node(m.id, true)));
165
+ case "addEdge":
166
+ return first(node(op.edge.source, true), node(op.edge.target, true));
167
+ case "removeEdges":
168
+ return first(...op.ids.map(edge));
169
+ case "setEdgeLabel":
170
+ case "setEdgeStyle":
171
+ case "setEdgeData":
172
+ return edge(op.id);
173
+ case "reconnectEdge":
174
+ return first(edge(op.id), node(op.source, true), node(op.target, true));
175
+ case "groupNodes":
176
+ if (!diagramSupportsGroups(family)) return `${family} diagrams have no groups.`;
177
+ return first(free(op.groupId), ...op.nodeIds.map((id) => node(id, true)), group(op.parentId));
178
+ case "ungroup":
179
+ return group(op.groupId);
180
+ case "renameGroup":
181
+ case "setGroupDirection":
182
+ return group(op.id);
183
+ case "setNodeParent":
184
+ return first(...op.ids.map((id) => node(id, true)), group(op.parentId));
185
+ case "setDirection":
186
+ return DIRECTIONS.includes(op.direction) ? null : `Unknown direction "${op.direction}"; use one of ${quote(DIRECTIONS)}.`;
187
+ default:
188
+ return null;
189
+ }
190
+ }
191
+ var DiagramController = class {
192
+ history;
193
+ doc;
194
+ selection = [];
195
+ issue = null;
196
+ readOnly;
197
+ revision = 0;
198
+ applying = false;
199
+ gestureKey = null;
200
+ gestureCount = 0;
201
+ onChange;
202
+ listeners = /* @__PURE__ */ new Set();
203
+ unsubscribeHistory;
204
+ state;
205
+ constructor(options = {}) {
206
+ this.history = options.history ?? new HistoryManager();
207
+ this.onChange = options.onChange;
208
+ this.readOnly = options.readOnly ?? false;
209
+ this.doc = parseMermaid(options.source ?? "flowchart TD\n");
210
+ this.state = this.computeState();
211
+ this.connect();
212
+ }
213
+ connect() {
214
+ this.unsubscribeHistory?.();
215
+ this.unsubscribeHistory = this.history.subscribe((h) => {
216
+ const s = this.state;
217
+ if (this.applying || s.canUndo === h.canUndo && s.canRedo === h.canRedo && s.undoLabel === h.undoName && s.redoLabel === h.redoName) return;
218
+ this.notify(false);
219
+ });
220
+ }
221
+ /** Stops listening to the history (subscribing again reconnects). */
222
+ destroy() {
223
+ this.unsubscribeHistory?.();
224
+ this.unsubscribeHistory = void 0;
225
+ }
226
+ /** The current snapshot (stable until the next change). */
227
+ getState = () => this.state;
228
+ /** Calls `listener` after every change. Returns an unsubscribe function. */
229
+ subscribe = (listener) => {
230
+ if (!this.unsubscribeHistory) this.connect();
231
+ this.listeners.add(listener);
232
+ return () => {
233
+ this.listeners.delete(listener);
234
+ };
235
+ };
236
+ /** The current document (immutable; edit through the controller). */
237
+ getDocument() {
238
+ return this.doc;
239
+ }
240
+ getSource() {
241
+ return this.doc.source;
242
+ }
243
+ computeState() {
244
+ const h = this.history.getState();
245
+ return {
246
+ document: this.doc,
247
+ source: this.doc.source,
248
+ family: this.doc.family,
249
+ editable: this.doc.tier === "editable" && !this.readOnly,
250
+ readOnly: this.readOnly,
251
+ selection: this.selection,
252
+ issue: this.issue,
253
+ canUndo: h.canUndo,
254
+ canRedo: h.canRedo,
255
+ undoLabel: h.undoName,
256
+ redoLabel: h.redoName,
257
+ revision: this.revision
258
+ };
259
+ }
260
+ notify(sourceChanged) {
261
+ this.revision++;
262
+ this.state = this.computeState();
263
+ if (sourceChanged) this.onChange?.(this.doc.source, this.doc);
264
+ const state = this.state;
265
+ this.listeners.forEach((listener) => {
266
+ try {
267
+ listener(state);
268
+ } catch (error) {
269
+ console.error("[DiagramController] Error in state listener:", error);
270
+ }
271
+ });
272
+ }
273
+ setDocument(doc) {
274
+ this.applying = true;
275
+ try {
276
+ this.doc = doc;
277
+ const ids = new Set([...doc.graph.nodes, ...doc.graph.groups, ...doc.graph.edges].map((x) => x.id));
278
+ this.selection = this.selection.filter((id) => ids.has(id));
279
+ } finally {
280
+ this.applying = false;
281
+ }
282
+ this.notify(true);
283
+ }
284
+ commit(label, next) {
285
+ const before = this.doc;
286
+ this.issue = null;
287
+ if (next.source === before.source) {
288
+ this.notify(false);
289
+ return;
290
+ }
291
+ const command = {
292
+ name: label,
293
+ tag: "diagram",
294
+ execute: () => this.setDocument(next),
295
+ undo: () => this.setDocument(before),
296
+ redo: () => this.setDocument(next),
297
+ byteSize: (before.source.length + next.source.length) * 2
298
+ };
299
+ if (this.gestureKey !== null) this.history.executeMerged(command, { key: `diagram.gesture:${this.gestureKey}:${this.gestureCount}` });
300
+ else this.history.execute(command);
301
+ }
302
+ reject(message) {
303
+ this.issue = message;
304
+ this.notify(false);
305
+ return { ok: false, message };
306
+ }
307
+ /**
308
+ * Applies edits as one undo step. All-or-nothing: every edit is checked against the
309
+ * document as it stands after the edits before it, and nothing changes when one fails.
310
+ */
311
+ apply(ops, label = "Edit diagram") {
312
+ const list = Array.isArray(ops) ? ops : [ops];
313
+ if (this.readOnly) return this.reject("The diagram is read-only.");
314
+ let doc = this.doc;
315
+ for (const op of list) {
316
+ const problem = validateDiagramEdit(doc, op);
317
+ if (problem) return this.reject(problem);
318
+ doc = applyEdits(doc, [op]);
319
+ }
320
+ this.commit(label, doc);
321
+ return { ok: true, document: this.doc };
322
+ }
323
+ /**
324
+ * Replaces the whole source. `clearHistory` drops the undo stack (opening a file);
325
+ * otherwise the replacement is one undo step.
326
+ */
327
+ load(source, options = {}) {
328
+ const next = parseMermaid(source, { metaCarrier: this.doc.metaCarrier });
329
+ if (options.clearHistory) {
330
+ this.history.clear();
331
+ this.issue = null;
332
+ this.selection = [];
333
+ this.setDocument(next);
334
+ return { ok: true, document: next };
335
+ }
336
+ if (this.readOnly) return this.reject("The diagram is read-only.");
337
+ this.commit("Replace diagram", next);
338
+ return { ok: true, document: this.doc };
339
+ }
340
+ setReadOnly(readOnly) {
341
+ if (this.readOnly === readOnly) return;
342
+ this.readOnly = readOnly;
343
+ this.notify(false);
344
+ }
345
+ /** Selects node, group or edge ids (unknown ids are dropped). */
346
+ select(ids) {
347
+ const known = new Set([...this.doc.graph.nodes, ...this.doc.graph.groups, ...this.doc.graph.edges].map((x) => x.id));
348
+ const next = ids.filter((id) => known.has(id));
349
+ if (next.length === this.selection.length && next.every((id, i) => id === this.selection[i])) return;
350
+ this.selection = next;
351
+ this.notify(false);
352
+ }
353
+ /** Edits until `endGesture` (e.g. the moves of one drag) fold into one undo step. */
354
+ beginGesture(key) {
355
+ this.gestureKey = key;
356
+ this.gestureCount++;
357
+ }
358
+ endGesture() {
359
+ this.gestureKey = null;
360
+ }
361
+ undo() {
362
+ return this.history.undo();
363
+ }
364
+ redo() {
365
+ return this.history.redo();
366
+ }
367
+ /** A node id not used by any node or group: `prefix` followed by the next free number. */
368
+ nextId(prefix = "N") {
369
+ const used = new Set([...this.doc.graph.nodes, ...this.doc.graph.groups].map((x) => x.id));
370
+ let i = 1;
371
+ while (used.has(`${prefix}${i}`)) i++;
372
+ return `${prefix}${i}`;
373
+ }
374
+ };
375
+
376
+ // src/diagram/tools.ts
377
+ var READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
378
+ var WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false };
379
+ var WRITE_IDEMPOTENT = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
380
+ var DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
381
+ var ID = { type: "string", minLength: 1, description: 'A node id from get_diagram (e.g. "A").' };
382
+ var IDS = (what) => ({ type: "array", items: { type: "string", minLength: 1 }, minItems: 1, description: what });
383
+ var EDGE_ID = { type: "string", minLength: 1, description: 'An edge id from get_diagram (e.g. "L_A_B_0").' };
384
+ var LABEL = (what) => ({ type: "string", description: what });
385
+ var object = (properties, required = []) => ({
386
+ type: "object",
387
+ properties,
388
+ ...required.length ? { required } : {},
389
+ additionalProperties: false
390
+ });
391
+ function describeDiagram(doc) {
392
+ const { graph } = doc;
393
+ return {
394
+ family: doc.family,
395
+ editable: doc.tier === "editable",
396
+ direction: graph.direction,
397
+ ...graph.title ? { title: graph.title } : {},
398
+ nodes: graph.nodes.map((n) => ({
399
+ id: n.id,
400
+ label: n.label,
401
+ shape: n.shape,
402
+ ...n.parentId ? { parentId: n.parentId } : {},
403
+ ...n.classes.length ? { classes: n.classes } : {},
404
+ ...n.styles.length ? { styles: n.styles } : {},
405
+ ...n.data?.synthetic ? { synthetic: true } : {}
406
+ })),
407
+ edges: graph.edges.map((e) => ({
408
+ id: e.id,
409
+ source: e.source,
410
+ target: e.target,
411
+ ...e.label ? { label: e.label } : {},
412
+ pattern: e.pattern,
413
+ arrowEnd: e.arrowEnd
414
+ })),
415
+ groups: graph.groups.map((g) => ({
416
+ id: g.id,
417
+ label: g.label,
418
+ ...g.parentId ? { parentId: g.parentId } : {},
419
+ ...g.direction ? { direction: g.direction } : {}
420
+ })),
421
+ shapes: diagramShapes(doc.family),
422
+ positions: Object.fromEntries(Object.entries(doc.meta.layout ?? {}).map(([id, [x, y]]) => [id, { x: Math.round(x), y: Math.round(y) }])),
423
+ ...doc.diagnostics.length ? { diagnostics: doc.diagnostics.map((d) => ({ severity: d.severity, message: d.message, ...d.line ? { line: d.line } : {} })) } : {}
424
+ };
425
+ }
426
+ function createDiagramToolProvider(controller, options = {}) {
427
+ const readOnlyReason = () => {
428
+ const r = typeof options.readOnly === "function" ? options.readOnly() : options.readOnly;
429
+ if (r) return typeof r === "string" ? r : "This diagram is read-only.";
430
+ return controller.getState().readOnly ? "This diagram is read-only." : null;
431
+ };
432
+ const doc = () => controller.getDocument();
433
+ const actor = options.actor ?? { id: "agent", name: "Agent" };
434
+ const apply = async (ops, label) => {
435
+ const result = options.director ? await options.director.perform(actor, Array.isArray(ops) ? ops : [ops], label) : controller.apply(ops, label);
436
+ if (!result.ok) throw new Error(result.message);
437
+ return result.document;
438
+ };
439
+ const edgeIds = () => new Set(doc().graph.edges.map((e) => e.id));
440
+ const newEdges = (before, after) => after.graph.edges.filter((e) => !before.has(e.id)).map((e) => ({ id: e.id, source: e.source, target: e.target, ...e.label ? { label: e.label } : {} }));
441
+ const nodeOut = (d, id) => {
442
+ const n = d.graph.nodes.find((x) => x.id === id) ?? d.graph.groups.find((x) => x.id === id);
443
+ return n ? { id: n.id, label: n.label, ..."shape" in n ? { shape: n.shape } : {}, ...n.parentId ? { parentId: n.parentId } : {} } : { id };
444
+ };
445
+ const pinned = (moves) => {
446
+ const shown = options.getPositions?.();
447
+ if (!shown) return moves;
448
+ const stored = doc().meta.layout ?? {};
449
+ const taken = new Set(moves.map((m) => m.id));
450
+ return [...moves, ...Object.entries(shown).filter(([id]) => !taken.has(id) && !stored[id]).map(([id, position]) => ({ id, position }))];
451
+ };
452
+ const requireEditable = () => {
453
+ if (doc().tier !== "editable")
454
+ throw new Error(`This ${doc().family} diagram can only be changed as Mermaid text; use load_diagram_mermaid.`);
455
+ };
456
+ const getDiagram = {
457
+ name: "get_diagram",
458
+ title: "Read the diagram",
459
+ description: "Returns the diagram: its Mermaid family (flowchart, state, class, er; other families are text only), direction, nodes (id, label, shape, parentId), edges (id, source, target, label), groups (subgraphs / composite states), the shapes nodes can take, stored canvas positions, any parse diagnostics and the selection. Use the ids it returns with the write tools.",
460
+ inputSchema: object({ includeSource: { type: "boolean", description: "Also return the Mermaid text (default false)." } }),
461
+ annotations: { title: "Read the diagram", ...READ },
462
+ handler: async ({ includeSource = false }) => {
463
+ const reason = readOnlyReason();
464
+ return {
465
+ ...describeDiagram(doc()),
466
+ selection: controller.getState().selection,
467
+ ...includeSource ? { source: doc().source } : {},
468
+ ...reason ? { readOnly: reason } : {}
469
+ };
470
+ }
471
+ };
472
+ const exportMermaid = {
473
+ name: "export_diagram_mermaid",
474
+ title: "Export the diagram as Mermaid",
475
+ description: 'Returns the diagram as Mermaid text. Canvas positions are kept in a "%% @pixerate {\u2026}" comment that Mermaid ignores; pass withLayout false to leave it out.',
476
+ inputSchema: object({ withLayout: { type: "boolean", description: "Keep the %% @pixerate layout comment (default true)." } }),
477
+ annotations: { title: "Export the diagram as Mermaid", ...READ },
478
+ handler: async ({ withLayout = true }) => {
479
+ const source = doc().source;
480
+ return { family: doc().family, source: withLayout ? source : source.replace(/^[ \t]*%%\s*@(?:pixerate|ursula)\s+\{.*\}[ \t]*(\r?\n|$)/gm, "") };
481
+ }
482
+ };
483
+ const findDiagrams = {
484
+ name: "find_mermaid_diagrams",
485
+ title: "Find Mermaid diagrams in text",
486
+ description: "Finds the Mermaid diagrams in a piece of text (Markdown with ```mermaid blocks, a chat reply, or a bare diagram) without changing anything. Returns each diagram's source, family and title; pass a source to load_diagram_mermaid to open it.",
487
+ inputSchema: object({ text: { type: "string", description: "The text to search." } }, ["text"]),
488
+ annotations: { title: "Find Mermaid diagrams in text", ...READ },
489
+ handler: ({ text }) => ({ diagrams: findMermaidSources(text) })
490
+ };
491
+ const loadMermaid = {
492
+ name: "load_diagram_mermaid",
493
+ title: "Replace the diagram with Mermaid",
494
+ description: "Replaces the whole diagram with Mermaid text (one undo step). Flowchart, state, class and ER diagrams become editable on the canvas; other families are kept as text. Returns the family, node and edge counts and any parse diagnostics. Prefer the per-operation tools for small changes: they keep the rest of the text as written.",
495
+ inputSchema: object({ source: { type: "string", minLength: 1, description: 'Mermaid text, starting with a header such as "flowchart LR".' } }, ["source"]),
496
+ annotations: { title: "Replace the diagram with Mermaid", ...DESTRUCTIVE },
497
+ handler: async ({ source }) => {
498
+ const fenced = /^\s*(`{3,}|~{3,})/m.test(source) ? findMermaidSources(source) : [];
499
+ if (fenced.length > 1) throw new Error(`The text holds ${fenced.length} diagrams; pass one (find_mermaid_diagrams lists them).`);
500
+ const text = fenced[0]?.source ?? source;
501
+ if (detectFamily(text) === "unknown")
502
+ throw new Error('The text has no Mermaid header (e.g. "flowchart LR", "stateDiagram-v2", "classDiagram", "erDiagram"); nothing was changed.');
503
+ const result = options.director ? await options.director.playSource(actor, text) : controller.load(text);
504
+ if (!result.ok) throw new Error(result.message);
505
+ const d = result.document;
506
+ return {
507
+ family: d.family,
508
+ editable: d.tier === "editable",
509
+ nodes: d.graph.nodes.length,
510
+ edges: d.graph.edges.length,
511
+ groups: d.graph.groups.length,
512
+ ...d.diagnostics.length ? { diagnostics: d.diagnostics.map((x) => x.message) } : {}
513
+ };
514
+ }
515
+ };
516
+ const addNode = {
517
+ name: "add_diagram_node",
518
+ title: "Add a node",
519
+ description: "Adds a node to the diagram, optionally inside a group and connected from an existing node. Without a position the canvas places it beside its neighbours. Returns the node and any edge created.",
520
+ inputSchema: object(
521
+ {
522
+ label: LABEL("Text shown in the node."),
523
+ id: { type: "string", pattern: "^[A-Za-z0-9_][\\w.-]*$", description: 'Mermaid id (default: the next free "N<number>").' },
524
+ shape: { type: "string", description: 'One of the shapes get_diagram lists (default "rect"). Flowchart and state diagrams only.' },
525
+ parentId: { type: "string", description: "Group (subgraph) to add the node to." },
526
+ from: { type: "string", description: "An existing node to connect to the new node." },
527
+ edgeLabel: LABEL('Label of the edge from "from" (needs "from").'),
528
+ x: { type: "number", description: "Left edge in canvas px (needs y)." },
529
+ y: { type: "number", description: "Top edge in canvas px (needs x)." }
530
+ },
531
+ ["label"]
532
+ ),
533
+ annotations: { title: "Add a node", ...WRITE },
534
+ handler: async ({ label, id, shape, parentId, from, edgeLabel, x, y }) => {
535
+ requireEditable();
536
+ if (x === void 0 !== (y === void 0)) throw new Error("Give both x and y, or neither to place the node automatically.");
537
+ if (edgeLabel !== void 0 && from === void 0) throw new Error('edgeLabel needs "from", the node the edge starts at.');
538
+ const nodeId = id ?? controller.nextId();
539
+ const before = edgeIds();
540
+ const ops = [
541
+ { type: "addNode", node: { id: nodeId, label, ...shape ? { shape } : {}, ...parentId ? { parentId } : {} }, ...x !== void 0 ? { position: [x, y] } : {} }
542
+ ];
543
+ if (from !== void 0) ops.push({ type: "addEdge", edge: { source: from, target: nodeId, ...edgeLabel ? { label: edgeLabel } : {} } });
544
+ const d = await apply(ops, "Add node");
545
+ const edges = newEdges(before, d);
546
+ return { node: nodeOut(d, nodeId), ...edges.length ? { edge: edges[0] } : {} };
547
+ }
548
+ };
549
+ const removeElements = {
550
+ name: "remove_diagram_nodes",
551
+ title: "Remove nodes or groups",
552
+ description: "Deletes nodes with every edge that touches them. A group id removes the group but keeps its contents. Returns the removed ids and the ids of the edges that went with them.",
553
+ inputSchema: object({ ids: IDS("Node or group ids from get_diagram.") }, ["ids"]),
554
+ annotations: { title: "Remove nodes or groups", ...DESTRUCTIVE },
555
+ handler: async ({ ids }) => {
556
+ requireEditable();
557
+ const before = doc().graph.edges.map((e) => e.id);
558
+ const d = await apply({ type: "removeNodes", ids }, ids.length === 1 ? "Delete node" : "Delete nodes");
559
+ const after = new Set(d.graph.edges.map((e) => e.id));
560
+ return { removed: ids, removedEdges: before.filter((id) => !after.has(id)) };
561
+ }
562
+ };
563
+ const renameNode = {
564
+ name: "rename_diagram_node",
565
+ title: "Rename a node or group",
566
+ description: "Changes the label of a node or group. Returns the node with its new label.",
567
+ inputSchema: object({ id: { ...ID, description: "A node or group id from get_diagram." }, label: LABEL("The new label.") }, ["id", "label"]),
568
+ annotations: { title: "Rename a node or group", ...WRITE_IDEMPOTENT },
569
+ handler: async ({ id, label }) => {
570
+ requireEditable();
571
+ const isGroup = doc().graph.groups.some((g) => g.id === id);
572
+ const d = await apply(isGroup ? { type: "renameGroup", id, label } : { type: "renameNode", id, label }, "Rename");
573
+ return { node: nodeOut(d, id) };
574
+ }
575
+ };
576
+ const setShape = {
577
+ name: "set_diagram_node_shape",
578
+ title: "Change a node's shape",
579
+ description: "Changes a node's shape (flowchart and state diagrams). Use one of the shapes get_diagram lists. Returns the node.",
580
+ inputSchema: object({ id: ID, shape: { type: "string", minLength: 1, description: 'Shape name, e.g. "rect", "rounded", "diam", "cyl", "circle".' } }, ["id", "shape"]),
581
+ annotations: { title: "Change a node's shape", ...WRITE_IDEMPOTENT },
582
+ handler: async ({ id, shape }) => {
583
+ requireEditable();
584
+ return { node: nodeOut(await apply({ type: "setNodeShape", id, shape }, "Change shape"), id) };
585
+ }
586
+ };
587
+ const setStyle = {
588
+ name: "set_diagram_node_style",
589
+ title: "Style a node",
590
+ description: 'Replaces a node\'s inline Mermaid styles (written as a `style` statement), e.g. ["fill:#fde68a", "stroke:#b45309", "stroke-width:2px"]. An empty list removes them. Returns the node with its styles.',
591
+ inputSchema: object(
592
+ { id: ID, styles: { type: "array", items: { type: "string", pattern: "^[\\w-]+:[^,;]+$" }, description: 'CSS-like "property:value" pairs.' } },
593
+ ["id", "styles"]
594
+ ),
595
+ annotations: { title: "Style a node", ...WRITE_IDEMPOTENT },
596
+ handler: async ({ id, styles }) => {
597
+ requireEditable();
598
+ const d = await apply({ type: "setNodeStyles", id, styles }, "Style node");
599
+ return { id, styles: d.graph.nodes.find((n) => n.id === id)?.styles ?? [] };
600
+ }
601
+ };
602
+ const connect = {
603
+ name: "connect_diagram_nodes",
604
+ title: "Connect two nodes",
605
+ description: "Draws an edge from one existing node to another, with an optional label. Returns the new edge with its id.",
606
+ inputSchema: object({ source: { ...ID, description: "Node the edge starts at." }, target: { ...ID, description: "Node the edge ends at." }, label: LABEL("Edge label (optional).") }, ["source", "target"]),
607
+ annotations: { title: "Connect two nodes", ...WRITE },
608
+ handler: async ({ source, target, label }) => {
609
+ requireEditable();
610
+ const before = edgeIds();
611
+ const d = await apply({ type: "addEdge", edge: { source, target, ...label ? { label } : {} } }, "Connect");
612
+ return { edge: newEdges(before, d)[0] ?? null };
613
+ }
614
+ };
615
+ const removeEdges = {
616
+ name: "remove_diagram_edges",
617
+ title: "Remove edges",
618
+ description: "Deletes edges by id; the nodes stay. Returns the removed ids.",
619
+ inputSchema: object({ ids: IDS("Edge ids from get_diagram.") }, ["ids"]),
620
+ annotations: { title: "Remove edges", ...DESTRUCTIVE },
621
+ handler: async ({ ids }) => {
622
+ requireEditable();
623
+ await apply({ type: "removeEdges", ids }, ids.length === 1 ? "Delete edge" : "Delete edges");
624
+ return { removed: ids };
625
+ }
626
+ };
627
+ const labelEdge = {
628
+ name: "set_diagram_edge_label",
629
+ title: "Label an edge",
630
+ description: "Sets an edge's label; an empty string removes it. Returns the edge.",
631
+ inputSchema: object({ id: EDGE_ID, label: LABEL('The new label ("" removes it).') }, ["id", "label"]),
632
+ annotations: { title: "Label an edge", ...WRITE_IDEMPOTENT },
633
+ handler: async ({ id, label }) => {
634
+ requireEditable();
635
+ const d = await apply({ type: "setEdgeLabel", id, label }, "Label edge");
636
+ const e = d.graph.edges.find((x) => x.id === id);
637
+ return { edge: e ? { id: e.id, source: e.source, target: e.target, label: e.label ?? "" } : { id, label } };
638
+ }
639
+ };
640
+ const reconnect = {
641
+ name: "reconnect_diagram_edge",
642
+ title: "Reconnect an edge",
643
+ description: "Moves an edge to new endpoints, keeping its label and style. Returns the edge (its id may change with its endpoints).",
644
+ inputSchema: object({ id: EDGE_ID, source: { ...ID, description: "New start node." }, target: { ...ID, description: "New end node." } }, ["id", "source", "target"]),
645
+ annotations: { title: "Reconnect an edge", ...WRITE },
646
+ handler: async ({ id, source, target }) => {
647
+ requireEditable();
648
+ const index = doc().graph.edges.findIndex((e2) => e2.id === id);
649
+ const d = await apply({ type: "reconnectEdge", id, source, target }, "Reconnect edge");
650
+ const e = d.graph.edges.find((x) => x.id === id) ?? d.graph.edges.find((x, i) => i === index && x.source === source && x.target === target) ?? d.graph.edges.find((x) => x.source === source && x.target === target);
651
+ return { edge: e ? { id: e.id, source: e.source, target: e.target, ...e.label ? { label: e.label } : {} } : null };
652
+ }
653
+ };
654
+ const setDirection = {
655
+ name: "set_diagram_direction",
656
+ title: "Set the layout direction",
657
+ description: "Sets the direction the diagram flows in: TB / TD (top to bottom), BT, LR (left to right) or RL. Returns the direction.",
658
+ inputSchema: object({ direction: { type: "string", enum: [...DIRECTIONS], description: "TB, TD, BT, LR or RL." } }, ["direction"]),
659
+ annotations: { title: "Set the layout direction", ...WRITE_IDEMPOTENT },
660
+ handler: async ({ direction }) => {
661
+ requireEditable();
662
+ return { direction: (await apply({ type: "setDirection", direction }, "Change direction")).graph.direction };
663
+ }
664
+ };
665
+ const group = {
666
+ name: "group_diagram_nodes",
667
+ title: "Group nodes",
668
+ description: "Puts nodes into a new group (a flowchart subgraph, composite state or class namespace). Returns the group id.",
669
+ inputSchema: object(
670
+ {
671
+ nodeIds: IDS("Node ids to put in the group."),
672
+ label: LABEL("Group title."),
673
+ id: { type: "string", pattern: "^[A-Za-z0-9_][\\w.-]*$", description: 'Group id (default: the next free "G<number>").' }
674
+ },
675
+ ["nodeIds", "label"]
676
+ ),
677
+ annotations: { title: "Group nodes", ...WRITE },
678
+ handler: async ({ nodeIds, label, id }) => {
679
+ requireEditable();
680
+ if (!diagramSupportsGroups(doc().family)) throw new Error(`${doc().family} diagrams have no groups.`);
681
+ const groupId = id ?? controller.nextId("G");
682
+ await apply({ type: "groupNodes", groupId, label, nodeIds }, "Group");
683
+ return { group: { id: groupId, label, nodeIds } };
684
+ }
685
+ };
686
+ const ungroup = {
687
+ name: "ungroup_diagram_group",
688
+ title: "Ungroup",
689
+ description: "Removes a group and keeps its nodes (they move to the group's parent). Returns the ids that were in it.",
690
+ inputSchema: object({ id: { type: "string", minLength: 1, description: "A group id from get_diagram." } }, ["id"]),
691
+ annotations: { title: "Ungroup", ...DESTRUCTIVE },
692
+ handler: async ({ id }) => {
693
+ requireEditable();
694
+ const members = [...doc().graph.nodes, ...doc().graph.groups].filter((x) => x.parentId === id).map((x) => x.id);
695
+ await apply({ type: "ungroup", groupId: id }, "Ungroup");
696
+ return { ungrouped: id, members };
697
+ }
698
+ };
699
+ const move = {
700
+ name: "move_diagram_nodes",
701
+ title: "Move nodes",
702
+ description: "Moves nodes or groups on the canvas to absolute positions (top-left, canvas px). Positions are stored in the %% @pixerate comment; the Mermaid statements do not change. Returns the new positions.",
703
+ inputSchema: object(
704
+ {
705
+ moves: {
706
+ type: "array",
707
+ minItems: 1,
708
+ description: "One entry per node.",
709
+ items: object({ id: { ...ID, description: "Node or group id." }, x: { type: "number", description: "Left edge in px." }, y: { type: "number", description: "Top edge in px." } }, ["id", "x", "y"])
710
+ }
711
+ },
712
+ ["moves"]
713
+ ),
714
+ annotations: { title: "Move nodes", ...WRITE_IDEMPOTENT },
715
+ handler: async ({ moves }) => {
716
+ const list = moves;
717
+ requireEditable();
718
+ await apply({ type: "moveNodes", moves: pinned(list.map((m) => ({ id: m.id, position: [m.x, m.y] }))) }, "Move");
719
+ return { moved: moves };
720
+ }
721
+ };
722
+ const undo = {
723
+ name: "undo_diagram",
724
+ title: "Undo a diagram edit",
725
+ description: "Undoes the last diagram edit. Returns whether something was undone and its label.",
726
+ inputSchema: object({}),
727
+ annotations: { title: "Undo a diagram edit", ...WRITE },
728
+ handler: async () => {
729
+ const label = controller.getState().undoLabel;
730
+ const undone = controller.undo();
731
+ return { undone, ...undone ? { label } : {} };
732
+ }
733
+ };
734
+ const redo = {
735
+ name: "redo_diagram",
736
+ title: "Redo a diagram edit",
737
+ description: "Redoes the last undone diagram edit. Returns whether something was redone and its label.",
738
+ inputSchema: object({}),
739
+ annotations: { title: "Redo a diagram edit", ...WRITE },
740
+ handler: async () => {
741
+ const label = controller.getState().redoLabel;
742
+ const redone = controller.redo();
743
+ return { redone, ...redone ? { label } : {} };
744
+ }
745
+ };
746
+ const reads = [getDiagram, exportMermaid, findDiagrams];
747
+ const writes = [loadMermaid, addNode, removeElements, renameNode, setShape, setStyle, connect, removeEdges, labelEdge, reconnect, setDirection, group, ungroup, move, undo, redo];
748
+ return {
749
+ id: options.id ?? "diagram",
750
+ namespace: options.namespace,
751
+ tools: () => readOnlyReason() ? reads : [...reads, ...writes],
752
+ subscribe: (notify) => {
753
+ let last = readOnlyReason();
754
+ return controller.subscribe(() => {
755
+ const now = readOnlyReason();
756
+ if (now !== last) {
757
+ last = now;
758
+ notify();
759
+ }
760
+ });
761
+ }
762
+ };
763
+ }
764
+
765
+ // src/diagram/draft.ts
766
+ function signature(graph) {
767
+ return JSON.stringify([
768
+ graph.title ?? "",
769
+ graph.nodes.map((n) => [n.id, n.label, n.parentId ?? "", n.data ?? null]),
770
+ graph.edges.map((e) => [e.source, e.target, e.label ?? ""]),
771
+ graph.groups.map((g) => [g.id, g.label, g.parentId ?? ""]),
772
+ graph.data ?? null
773
+ ]);
774
+ }
775
+ var isMeta = (line) => /^\s*%%\s*@(?:pixerate|ursula)\b/.test(line);
776
+ function bodyLines(lines, headerIndex) {
777
+ const body = lines.slice(headerIndex + 1).filter((l) => !isMeta(l));
778
+ while (body.length && !body[body.length - 1].trim()) body.pop();
779
+ return body;
780
+ }
781
+ function headerIndexOf(lines, header) {
782
+ const wanted = header.trim();
783
+ return wanted ? lines.findIndex((l) => l.trim() === wanted) : -1;
784
+ }
785
+ function draftFrames(current, target, options = {}) {
786
+ const maxFrames = Math.max(2, options.maxFrames ?? 40);
787
+ const now = parseMermaid(current);
788
+ const goal = parseMermaid(target);
789
+ if (goal.family !== now.family || goal.diagnostics.length) return null;
790
+ const eol = goal.eol;
791
+ const lines = target.split(/\r?\n/);
792
+ const at = headerIndexOf(lines, goal.header);
793
+ if (at < 0) return null;
794
+ const currentLines = current.split(/\r?\n/);
795
+ const currentAt = headerIndexOf(currentLines, now.header);
796
+ if (currentAt < 0) return null;
797
+ const have = bodyLines(currentLines, currentAt);
798
+ const body = lines.map((line, index) => ({ line, index })).slice(at + 1).filter(({ line }) => !isMeta(line));
799
+ for (let i = 0; i < have.length; i++) {
800
+ if (body[i]?.line !== have[i]) return null;
801
+ }
802
+ const frames = [];
803
+ let last = signature(now.graph);
804
+ for (let i = have.length; i < body.length; i++) {
805
+ const end = body[i].index;
806
+ const candidate = lines.slice(0, end + 1).join(eol) + eol;
807
+ const doc = parseMermaid(candidate);
808
+ if (doc.family !== goal.family || doc.diagnostics.length) continue;
809
+ const sig = signature(doc.graph);
810
+ if (sig === last) continue;
811
+ frames.push(candidate);
812
+ last = sig;
813
+ }
814
+ if (!frames.length) return null;
815
+ if (last === signature(goal.graph)) frames[frames.length - 1] = target;
816
+ else frames.push(target);
817
+ if (frames.length <= maxFrames) return frames;
818
+ const sampled = [];
819
+ for (let k = 1; k <= maxFrames; k++) {
820
+ sampled.push(frames[Math.round(k * frames.length / maxFrames) - 1]);
821
+ }
822
+ return sampled;
823
+ }
824
+ function changedItems(before, after) {
825
+ const old = /* @__PURE__ */ new Map();
826
+ for (const n of before.nodes) old.set(n.id, JSON.stringify([n.label, n.parentId, n.data]));
827
+ for (const g of before.groups) old.set(g.id, JSON.stringify([g.label, g.parentId]));
828
+ const out = [];
829
+ for (const n of after.nodes)
830
+ if (old.get(n.id) !== JSON.stringify([n.label, n.parentId, n.data])) out.push(n.id);
831
+ for (const g of after.groups)
832
+ if (old.get(g.id) !== JSON.stringify([g.label, g.parentId])) out.push(g.id);
833
+ return out;
834
+ }
835
+
836
+ // src/diagram/director.ts
837
+ var center = (place, id) => {
838
+ const p = place.positions[id];
839
+ if (!p) return null;
840
+ const s = place.sizes[id] ?? { width: 160, height: 56 };
841
+ return { x: p[0] + s.width / 2, y: p[1] + s.height / 2 };
842
+ };
843
+ function freeSpot(place) {
844
+ let maxX = 0;
845
+ let maxY = 0;
846
+ let any = false;
847
+ for (const [id, [x, y]] of Object.entries(place.positions)) {
848
+ const s = place.sizes[id] ?? { width: 160, height: 56 };
849
+ maxX = Math.max(maxX, x + s.width);
850
+ maxY = Math.max(maxY, y + s.height);
851
+ any = true;
852
+ }
853
+ return any ? { x: maxX + 60, y: maxY / 2 } : { x: 80, y: 40 };
854
+ }
855
+ var NEW_NODE = { width: 160, height: 56 };
856
+ function besideFree(place, from, direction) {
857
+ const d = direction.toUpperCase();
858
+ const along = d === "LR" ? { x: 1, y: 0 } : d === "RL" ? { x: -1, y: 0 } : d === "BT" ? { x: 0, y: -1 } : { x: 0, y: 1 };
859
+ const side = { x: along.y, y: along.x };
860
+ const rects = Object.entries(place.positions).map(([id, [x, y]]) => ({ x, y, ...place.sizes[id] ?? NEW_NODE }));
861
+ const free = (c) => !rects.some((r) => c.x - NEW_NODE.width / 2 - 20 < r.x + r.width && c.x + NEW_NODE.width / 2 + 20 > r.x && c.y - NEW_NODE.height / 2 - 20 < r.y + r.height && c.y + NEW_NODE.height / 2 + 20 > r.y);
862
+ const base = { x: from.x + along.x * 240, y: from.y + along.y * 140 };
863
+ for (let i = 0; i < 24; i++) {
864
+ const k = i === 0 ? 0 : (i % 2 ? 1 : -1) * Math.ceil(i / 2);
865
+ const c = { x: base.x + side.x * k * 200, y: base.y + side.y * k * 90 };
866
+ if (free(c)) return c;
867
+ }
868
+ return base;
869
+ }
870
+ function planDiagramEdit(doc, place, op, speed = 1, next) {
871
+ const ms = (n) => Math.round(n / Math.max(0.1, speed));
872
+ const at = (id) => center(place, id);
873
+ const click = (to) => to ? [{ type: "move", to, ms: ms(380) }, { type: "press" }, { type: "release" }] : [];
874
+ const edgeMid = (id) => {
875
+ const e = doc.graph.edges.find((x) => x.id === id);
876
+ const a = e && at(e.source);
877
+ const b = e && at(e.target);
878
+ return a && b ? { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 } : null;
879
+ };
880
+ switch (op.type) {
881
+ case "addNode": {
882
+ const from = next?.type === "addEdge" && next.edge.target === op.node.id ? at(next.edge.source) : null;
883
+ const to = op.position ? { x: op.position[0] + 80, y: op.position[1] + 28 } : from ? besideFree(place, from, doc.graph.direction) : freeSpot(place);
884
+ const label = op.node.label ?? op.node.id;
885
+ return [...click(to), { type: "type", text: label, ms: ms(Math.min(900, 60 * label.length + 200)) }, { type: "commit" }];
886
+ }
887
+ case "addEdge": {
888
+ const a = at(op.edge.source);
889
+ const b = at(op.edge.target) ?? (a ? { x: a.x + 260, y: a.y } : null);
890
+ if (!a || !b) return [{ type: "commit" }];
891
+ return [
892
+ { type: "move", to: a, ms: ms(380) },
893
+ { type: "press" },
894
+ { type: "move", to: b, ms: ms(520) },
895
+ { type: "release" },
896
+ ...op.edge.label ? [{ type: "type", text: op.edge.label, ms: ms(500) }] : [],
897
+ { type: "commit" }
898
+ ];
899
+ }
900
+ case "moveNodes": {
901
+ const steps = [];
902
+ for (const move of op.moves) {
903
+ const from = at(move.id);
904
+ const size = place.sizes[move.id] ?? { width: 160, height: 56 };
905
+ if (!from) continue;
906
+ steps.push(
907
+ { type: "move", to: from, ms: ms(380) },
908
+ { type: "press" },
909
+ { type: "drag", id: move.id, to: { x: move.position[0] + size.width / 2, y: move.position[1] + size.height / 2 }, position: move.position, ms: ms(520) },
910
+ { type: "release" }
911
+ );
912
+ }
913
+ return [...steps, { type: "commit" }];
914
+ }
915
+ case "renameNode":
916
+ case "renameGroup":
917
+ return [...click(at(op.id)), { type: "press" }, { type: "release" }, { type: "type", text: op.label, ms: ms(Math.min(900, 60 * op.label.length + 200)) }, { type: "commit" }];
918
+ case "removeNodes":
919
+ return [...click(at(op.ids[0])), { type: "say", text: "Delete", ms: ms(300) }, { type: "commit" }];
920
+ case "removeEdges":
921
+ return [...click(edgeMid(op.ids[0])), { type: "say", text: "Delete", ms: ms(300) }, { type: "commit" }];
922
+ case "setEdgeLabel":
923
+ return [...click(edgeMid(op.id)), { type: "type", text: op.label, ms: ms(500) }, { type: "commit" }];
924
+ case "reconnectEdge": {
925
+ const a = at(op.source);
926
+ const b = at(op.target);
927
+ return [...click(edgeMid(op.id)), ...a && b ? [{ type: "move", to: a, ms: ms(300) }, { type: "press" }, { type: "move", to: b, ms: ms(500) }, { type: "release" }] : [], { type: "commit" }];
928
+ }
929
+ case "setNodeShape":
930
+ case "setNodeStyles":
931
+ case "setNodeClasses":
932
+ case "setNodeLink":
933
+ case "setNodeData":
934
+ case "resizeNode":
935
+ return [...click(at(op.id)), { type: "commit" }];
936
+ case "groupNodes": {
937
+ const points = op.nodeIds.map(at).filter((p) => !!p);
938
+ if (!points.length) return [{ type: "commit" }];
939
+ const min = { x: Math.min(...points.map((p) => p.x)) - 90, y: Math.min(...points.map((p) => p.y)) - 40 };
940
+ const max = { x: Math.max(...points.map((p) => p.x)) + 90, y: Math.max(...points.map((p) => p.y)) + 40 };
941
+ return [{ type: "move", to: min, ms: ms(380) }, { type: "press" }, { type: "move", to: max, ms: ms(520) }, { type: "release" }, { type: "type", text: op.label, ms: ms(500) }, { type: "commit" }];
942
+ }
943
+ default:
944
+ return [{ type: "commit" }];
945
+ }
946
+ }
947
+ var DiagramDirector = class {
948
+ constructor(controller, presence, options) {
949
+ this.controller = controller;
950
+ this.options = options;
951
+ this.presence = presence;
952
+ this.clock = options.clock ?? realPresenceClock;
953
+ this.queue = new PerformanceQueue(presence, {
954
+ clock: this.clock,
955
+ linger: options.linger,
956
+ beginGesture: (key) => controller.beginGesture(key),
957
+ endGesture: () => controller.endGesture()
958
+ });
959
+ }
960
+ controller;
961
+ options;
962
+ presence;
963
+ clock;
964
+ queue;
965
+ /** Acts out `ops` in order for `actor` as one undo step. */
966
+ perform(actor, ops, label = "Edit diagram") {
967
+ let check = this.controller.getDocument();
968
+ if (this.controller.getState().readOnly) return Promise.resolve({ ok: false, message: "The diagram is read-only." });
969
+ for (const op of ops) {
970
+ const problem = validateDiagramEdit(check, op);
971
+ if (problem) return Promise.resolve({ ok: false, message: problem });
972
+ check = applyEdits(check, [op]);
973
+ }
974
+ return this.queue.run(actor, async () => {
975
+ const mark = this.controller.history.mark();
976
+ const fail = (message) => {
977
+ this.controller.history.revertTo(mark);
978
+ return { ok: false, message };
979
+ };
980
+ if (ops.some((op) => op.type === "addNode" || op.type === "moveNodes")) {
981
+ const problem = this.pin(label);
982
+ if (problem) return fail(problem);
983
+ }
984
+ for (let i = 0; i < ops.length; i++) {
985
+ const op = ops[i];
986
+ const steps = planDiagramEdit(this.controller.getDocument(), this.options.placement(), op, this.options.speed, ops[i + 1]);
987
+ for (const step of steps) {
988
+ const problem = await this.act(actor.id, step, op, label);
989
+ if (problem) return fail(problem);
990
+ }
991
+ }
992
+ return { ok: true, document: this.controller.getDocument() };
993
+ });
994
+ }
995
+ /**
996
+ * Replaces the diagram with `source`, drawing it statement by statement when it extends
997
+ * what is there (Ursula's draft playback): each frame lands, the cursor points at what
998
+ * appeared, and the whole write is one undo step. Otherwise it lands at once.
999
+ */
1000
+ playSource(actor, source, label = "Replace diagram") {
1001
+ if (this.controller.getState().readOnly) return Promise.resolve({ ok: false, message: "The diagram is read-only." });
1002
+ return this.queue.run(actor, async () => {
1003
+ const frames = draftFrames(this.controller.getSource(), source, { maxFrames: this.options.maxDraftFrames }) ?? [source];
1004
+ const pause = Math.round(260 / Math.max(0.1, this.options.speed ?? 1));
1005
+ for (const frame of frames) {
1006
+ const before = this.controller.getDocument().graph;
1007
+ const result = this.controller.apply({ type: "replaceSource", source: frame }, label);
1008
+ if (!result.ok) return result;
1009
+ const fresh = changedItems(before, result.document.graph);
1010
+ const target = fresh.length ? center(this.options.placement(), fresh[fresh.length - 1]) : null;
1011
+ const actorNow = this.presence.get(actor.id);
1012
+ if (target && actorNow) await tweenPresenceCursor(this.presence, this.clock, actor.id, actorNow, target, pause);
1013
+ else await this.clock.wait(pause);
1014
+ }
1015
+ return { ok: true, document: this.controller.getDocument() };
1016
+ });
1017
+ }
1018
+ /** Stores the drawn position of every node and group that has none yet. */
1019
+ pin(label) {
1020
+ const doc = this.controller.getDocument();
1021
+ const stored = doc.meta.layout ?? {};
1022
+ const known = new Set([...doc.graph.nodes, ...doc.graph.groups].map((x) => x.id));
1023
+ const moves = Object.entries(this.options.placement().positions).filter(([id]) => known.has(id) && !stored[id]).map(([id, position]) => ({ id, position }));
1024
+ if (!moves.length) return null;
1025
+ const result = this.controller.apply({ type: "moveNodes", moves }, label);
1026
+ return result.ok ? null : result.message;
1027
+ }
1028
+ /** Runs one step; returns a problem sentence when the edit can no longer be applied. */
1029
+ async act(id, step, op, label) {
1030
+ const where = () => {
1031
+ const a = this.presence.get(id);
1032
+ return a ? { x: a.x, y: a.y } : { x: 0, y: 0 };
1033
+ };
1034
+ switch (step.type) {
1035
+ case "say":
1036
+ this.presence.set({ id, message: step.text });
1037
+ await this.clock.wait(step.ms);
1038
+ return null;
1039
+ case "type": {
1040
+ const chars = [...step.text];
1041
+ const each = step.ms / Math.max(1, chars.length);
1042
+ for (let i = 1; i <= chars.length; i++) {
1043
+ this.presence.set({ id, message: chars.slice(0, i).join("") });
1044
+ await this.clock.wait(each);
1045
+ }
1046
+ return null;
1047
+ }
1048
+ case "move":
1049
+ await tweenPresenceCursor(this.presence, this.clock, id, where(), step.to, step.ms);
1050
+ return null;
1051
+ case "press":
1052
+ this.presence.set({ id, pressed: true });
1053
+ return null;
1054
+ case "release":
1055
+ this.presence.set({ id, pressed: false });
1056
+ return null;
1057
+ case "drag": {
1058
+ const start = this.options.placement().positions[step.id];
1059
+ let problem = null;
1060
+ await tweenPresenceCursor(this.presence, this.clock, id, where(), step.to, step.ms, (e) => {
1061
+ if (problem || !start) return;
1062
+ const position = [start[0] + (step.position[0] - start[0]) * e, start[1] + (step.position[1] - start[1]) * e];
1063
+ const result = this.controller.apply({ type: "moveNodes", moves: [{ id: step.id, position }] }, label);
1064
+ if (!result.ok) problem = result.message;
1065
+ });
1066
+ return problem;
1067
+ }
1068
+ case "commit": {
1069
+ const placed = op.type === "addNode" && !op.position ? { ...op, position: [Math.round(where().x - 80), Math.round(where().y - 28)] } : op;
1070
+ const result = this.controller.apply(placed, label);
1071
+ return result.ok ? null : result.message;
1072
+ }
1073
+ }
1074
+ }
1075
+ };
1076
+ function planDiagramEdits(source, place, ops, speed = 1) {
1077
+ let doc = parseMermaid(source);
1078
+ return ops.map((op, i) => {
1079
+ const steps = planDiagramEdit(doc, place, op, speed, ops[i + 1]);
1080
+ doc = applyEdits(doc, [op]);
1081
+ return steps;
1082
+ });
1083
+ }
1084
+ export {
1085
+ CLASS_RELATIONS,
1086
+ DIRECTIONS,
1087
+ DiagramController,
1088
+ DiagramDirector,
1089
+ EDITABLE_DIAGRAM_FAMILIES,
1090
+ ER_CARDINALITIES,
1091
+ ER_PATTERNS,
1092
+ FLOWCHART_SHAPES,
1093
+ META_COMMENT_PREFIX,
1094
+ META_FRONTMATTER_KEY,
1095
+ PRESENCE_COLORS,
1096
+ PerformanceQueue,
1097
+ PresenceStore,
1098
+ STATE_SHAPES,
1099
+ applyEdit,
1100
+ applyEdits,
1101
+ canonicalShape,
1102
+ changedItems,
1103
+ classGrammar,
1104
+ createDiagramToolProvider,
1105
+ createInstantPresenceClock,
1106
+ createLineAdapter,
1107
+ createPassthroughAdapter,
1108
+ describeDiagram,
1109
+ detectFamily,
1110
+ detectFamilyFromHeader,
1111
+ diagramShapes,
1112
+ diagramSupportsGroups,
1113
+ draftFrames,
1114
+ emptyMeta,
1115
+ erGrammar,
1116
+ findMermaidSources,
1117
+ flowchartAdapter,
1118
+ formatChain,
1119
+ formatLink,
1120
+ formatNode,
1121
+ getDiagramAdapter,
1122
+ isMetaEmpty,
1123
+ mergeMeta,
1124
+ parseAttributes,
1125
+ parseChain,
1126
+ parseFlowchartSource,
1127
+ parseLink,
1128
+ parseMembers,
1129
+ parseMermaid,
1130
+ parseStatement,
1131
+ planDiagramEdit,
1132
+ planDiagramEdits,
1133
+ quoteLabelIfNeeded,
1134
+ realPresenceClock,
1135
+ registerDiagramAdapter,
1136
+ relationFor,
1137
+ resolveShape,
1138
+ serializeMermaid,
1139
+ serializeMeta,
1140
+ splitSource,
1141
+ splitStatements,
1142
+ stateGrammar,
1143
+ supportTierOf,
1144
+ validateDiagramEdit
1145
+ };
1146
+ //# sourceMappingURL=index.js.map