callman-core 1.28.1 → 1.29.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/dist/ai/index.d.ts +1 -1
- package/dist/ai/index.d.ts.map +1 -1
- package/dist/ai/index.js.map +1 -1
- package/dist/ai/outputSchemas.d.ts +3 -0
- package/dist/ai/outputSchemas.d.ts.map +1 -1
- package/dist/ai/outputSchemas.js +1 -0
- package/dist/ai/outputSchemas.js.map +1 -1
- package/dist/ai/parse/diagram.d.ts.map +1 -1
- package/dist/ai/parse/diagram.js +6 -3
- package/dist/ai/parse/diagram.js.map +1 -1
- package/dist/ai/prompts/diagram.d.ts +7 -3
- package/dist/ai/prompts/diagram.d.ts.map +1 -1
- package/dist/ai/prompts/diagram.js +44 -5
- package/dist/ai/prompts/diagram.js.map +1 -1
- package/dist/ai/types.d.ts +6 -0
- package/dist/ai/types.d.ts.map +1 -1
- package/dist/diagram/bpmn/derive.d.ts +74 -0
- package/dist/diagram/bpmn/derive.d.ts.map +1 -0
- package/dist/diagram/bpmn/derive.js +392 -0
- package/dist/diagram/bpmn/derive.js.map +1 -0
- package/dist/diagram/bpmn/index.d.ts +7 -0
- package/dist/diagram/bpmn/index.d.ts.map +1 -0
- package/dist/diagram/bpmn/index.js +12 -0
- package/dist/diagram/bpmn/index.js.map +1 -0
- package/dist/diagram/bpmn/patch.d.ts +14 -0
- package/dist/diagram/bpmn/patch.d.ts.map +1 -0
- package/dist/diagram/bpmn/patch.js +770 -0
- package/dist/diagram/bpmn/patch.js.map +1 -0
- package/dist/diagram/bpmn/place.d.ts +4 -0
- package/dist/diagram/bpmn/place.d.ts.map +1 -0
- package/dist/diagram/bpmn/place.js +65 -0
- package/dist/diagram/bpmn/place.js.map +1 -0
- package/dist/diagram/bpmn/serialize.d.ts +12 -0
- package/dist/diagram/bpmn/serialize.d.ts.map +1 -0
- package/dist/diagram/bpmn/serialize.js +292 -0
- package/dist/diagram/bpmn/serialize.js.map +1 -0
- package/dist/diagram/bpmn/types.d.ts +42 -0
- package/dist/diagram/bpmn/types.d.ts.map +1 -0
- package/dist/diagram/bpmn/types.js +84 -0
- package/dist/diagram/bpmn/types.js.map +1 -0
- package/dist/diagram/bpmn/xml.d.ts +60 -0
- package/dist/diagram/bpmn/xml.d.ts.map +1 -0
- package/dist/diagram/bpmn/xml.js +342 -0
- package/dist/diagram/bpmn/xml.js.map +1 -0
- package/dist/diagram/elkGraph.d.ts +1 -3
- package/dist/diagram/elkGraph.d.ts.map +1 -1
- package/dist/diagram/elkGraph.js +1 -0
- package/dist/diagram/elkGraph.js.map +1 -1
- package/dist/diagram/fixtures.d.ts +10 -0
- package/dist/diagram/fixtures.d.ts.map +1 -0
- package/dist/diagram/fixtures.js +113 -0
- package/dist/diagram/fixtures.js.map +1 -0
- package/dist/diagram/geometry.d.ts +27 -0
- package/dist/diagram/geometry.d.ts.map +1 -0
- package/dist/diagram/geometry.js +122 -0
- package/dist/diagram/geometry.js.map +1 -0
- package/dist/diagram/history.d.ts +36 -0
- package/dist/diagram/history.d.ts.map +1 -0
- package/dist/diagram/history.js +54 -0
- package/dist/diagram/history.js.map +1 -0
- package/dist/diagram/ids.d.ts +5 -0
- package/dist/diagram/ids.d.ts.map +1 -0
- package/dist/diagram/ids.js +52 -0
- package/dist/diagram/ids.js.map +1 -0
- package/dist/diagram/index.d.ts +14 -2
- package/dist/diagram/index.d.ts.map +1 -1
- package/dist/diagram/index.js +20 -8
- package/dist/diagram/index.js.map +1 -1
- package/dist/diagram/layout.d.ts +107 -0
- package/dist/diagram/layout.d.ts.map +1 -0
- package/dist/diagram/layout.js +356 -0
- package/dist/diagram/layout.js.map +1 -0
- package/dist/diagram/mermaid/derive.d.ts +55 -0
- package/dist/diagram/mermaid/derive.d.ts.map +1 -0
- package/dist/diagram/mermaid/derive.js +343 -0
- package/dist/diagram/mermaid/derive.js.map +1 -0
- package/dist/diagram/mermaid/index.d.ts +8 -0
- package/dist/diagram/mermaid/index.d.ts.map +1 -0
- package/dist/diagram/mermaid/index.js +24 -0
- package/dist/diagram/mermaid/index.js.map +1 -0
- package/dist/diagram/mermaid/parse.d.ts +96 -0
- package/dist/diagram/mermaid/parse.d.ts.map +1 -0
- package/dist/diagram/mermaid/parse.js +672 -0
- package/dist/diagram/mermaid/parse.js.map +1 -0
- package/dist/diagram/mermaid/patch.d.ts +16 -0
- package/dist/diagram/mermaid/patch.d.ts.map +1 -0
- package/dist/diagram/mermaid/patch.js +463 -0
- package/dist/diagram/mermaid/patch.js.map +1 -0
- package/dist/diagram/mermaid/serialize.d.ts +14 -0
- package/dist/diagram/mermaid/serialize.d.ts.map +1 -0
- package/dist/diagram/mermaid/serialize.js +125 -0
- package/dist/diagram/mermaid/serialize.js.map +1 -0
- package/dist/diagram/mermaid/shapes.d.ts +11 -0
- package/dist/diagram/mermaid/shapes.d.ts.map +1 -0
- package/dist/diagram/mermaid/shapes.js +320 -0
- package/dist/diagram/mermaid/shapes.js.map +1 -0
- package/dist/diagram/migrations.d.ts +1 -0
- package/dist/diagram/migrations.d.ts.map +1 -1
- package/dist/diagram/migrations.js +7 -2
- package/dist/diagram/migrations.js.map +1 -1
- package/dist/diagram/normalize.d.ts +9 -1
- package/dist/diagram/normalize.d.ts.map +1 -1
- package/dist/diagram/normalize.js +194 -0
- package/dist/diagram/normalize.js.map +1 -1
- package/dist/diagram/ops.d.ts +146 -0
- package/dist/diagram/ops.d.ts.map +1 -0
- package/dist/diagram/ops.js +823 -0
- package/dist/diagram/ops.js.map +1 -0
- package/dist/diagram/opsReference.d.ts +3 -0
- package/dist/diagram/opsReference.d.ts.map +1 -0
- package/dist/diagram/opsReference.js +51 -0
- package/dist/diagram/opsReference.js.map +1 -0
- package/dist/diagram/patchInput.d.ts +193 -0
- package/dist/diagram/patchInput.d.ts.map +1 -0
- package/dist/diagram/patchInput.js +621 -0
- package/dist/diagram/patchInput.js.map +1 -0
- package/dist/diagram/routing/batch.d.ts +45 -0
- package/dist/diagram/routing/batch.d.ts.map +1 -0
- package/dist/diagram/routing/batch.js +136 -0
- package/dist/diagram/routing/batch.js.map +1 -0
- package/dist/diagram/routing/curves.d.ts +7 -0
- package/dist/diagram/routing/curves.d.ts.map +1 -0
- package/dist/diagram/routing/curves.js +120 -0
- package/dist/diagram/routing/curves.js.map +1 -0
- package/dist/diagram/routing/grid.d.ts +15 -0
- package/dist/diagram/routing/grid.d.ts.map +1 -0
- package/dist/diagram/routing/grid.js +245 -0
- package/dist/diagram/routing/grid.js.map +1 -0
- package/dist/diagram/routing/index.d.ts +10 -0
- package/dist/diagram/routing/index.d.ts.map +1 -0
- package/dist/diagram/routing/index.js +10 -0
- package/dist/diagram/routing/index.js.map +1 -0
- package/dist/diagram/routing/legacy.d.ts +6 -0
- package/dist/diagram/routing/legacy.d.ts.map +1 -0
- package/dist/diagram/routing/legacy.js +27 -0
- package/dist/diagram/routing/legacy.js.map +1 -0
- package/dist/diagram/routing/nudge.d.ts +4 -0
- package/dist/diagram/routing/nudge.d.ts.map +1 -0
- package/dist/diagram/routing/nudge.js +96 -0
- package/dist/diagram/routing/nudge.js.map +1 -0
- package/dist/diagram/routing/orthogonal.d.ts +6 -0
- package/dist/diagram/routing/orthogonal.d.ts.map +1 -0
- package/dist/diagram/routing/orthogonal.js +56 -0
- package/dist/diagram/routing/orthogonal.js.map +1 -0
- package/dist/diagram/routing/path.d.ts +16 -0
- package/dist/diagram/routing/path.d.ts.map +1 -0
- package/dist/diagram/routing/path.js +128 -0
- package/dist/diagram/routing/path.js.map +1 -0
- package/dist/diagram/routing/sides.d.ts +22 -0
- package/dist/diagram/routing/sides.d.ts.map +1 -0
- package/dist/diagram/routing/sides.js +74 -0
- package/dist/diagram/routing/sides.js.map +1 -0
- package/dist/diagram/routing/types.d.ts +46 -0
- package/dist/diagram/routing/types.d.ts.map +1 -0
- package/dist/diagram/routing/types.js +11 -0
- package/dist/diagram/routing/types.js.map +1 -0
- package/dist/diagram/schema.d.ts +70 -37
- package/dist/diagram/schema.d.ts.map +1 -1
- package/dist/diagram/schema.js +62 -8
- package/dist/diagram/schema.js.map +1 -1
- package/dist/diagram/styleInput.d.ts +41 -0
- package/dist/diagram/styleInput.d.ts.map +1 -0
- package/dist/diagram/styleInput.js +105 -0
- package/dist/diagram/styleInput.js.map +1 -0
- package/dist/diagram/views.d.ts +9 -0
- package/dist/diagram/views.d.ts.map +1 -0
- package/dist/diagram/views.js +42 -0
- package/dist/diagram/views.js.map +1 -0
- package/dist-cjs/ai/index.js.map +1 -1
- package/dist-cjs/ai/outputSchemas.js +1 -0
- package/dist-cjs/ai/outputSchemas.js.map +1 -1
- package/dist-cjs/ai/parse/diagram.js +6 -3
- package/dist-cjs/ai/parse/diagram.js.map +1 -1
- package/dist-cjs/ai/prompts/diagram.js +44 -5
- package/dist-cjs/ai/prompts/diagram.js.map +1 -1
- package/dist-cjs/diagram/bpmn/derive.js +398 -0
- package/dist-cjs/diagram/bpmn/derive.js.map +1 -0
- package/dist-cjs/diagram/bpmn/index.js +31 -0
- package/dist-cjs/diagram/bpmn/index.js.map +1 -0
- package/dist-cjs/diagram/bpmn/patch.js +774 -0
- package/dist-cjs/diagram/bpmn/patch.js.map +1 -0
- package/dist-cjs/diagram/bpmn/place.js +69 -0
- package/dist-cjs/diagram/bpmn/place.js.map +1 -0
- package/dist-cjs/diagram/bpmn/serialize.js +299 -0
- package/dist-cjs/diagram/bpmn/serialize.js.map +1 -0
- package/dist-cjs/diagram/bpmn/types.js +99 -0
- package/dist-cjs/diagram/bpmn/types.js.map +1 -0
- package/dist-cjs/diagram/bpmn/xml.js +367 -0
- package/dist-cjs/diagram/bpmn/xml.js.map +1 -0
- package/dist-cjs/diagram/elkGraph.js +1 -0
- package/dist-cjs/diagram/elkGraph.js.map +1 -1
- package/dist-cjs/diagram/fixtures.js +117 -0
- package/dist-cjs/diagram/fixtures.js.map +1 -0
- package/dist-cjs/diagram/geometry.js +143 -0
- package/dist-cjs/diagram/geometry.js.map +1 -0
- package/dist-cjs/diagram/history.js +65 -0
- package/dist-cjs/diagram/history.js.map +1 -0
- package/dist-cjs/diagram/ids.js +58 -0
- package/dist-cjs/diagram/ids.js.map +1 -0
- package/dist-cjs/diagram/index.js +23 -8
- package/dist-cjs/diagram/index.js.map +1 -1
- package/dist-cjs/diagram/layout.js +364 -0
- package/dist-cjs/diagram/layout.js.map +1 -0
- package/dist-cjs/diagram/mermaid/derive.js +353 -0
- package/dist-cjs/diagram/mermaid/derive.js.map +1 -0
- package/dist-cjs/diagram/mermaid/index.js +44 -0
- package/dist-cjs/diagram/mermaid/index.js.map +1 -0
- package/dist-cjs/diagram/mermaid/parse.js +681 -0
- package/dist-cjs/diagram/mermaid/parse.js.map +1 -0
- package/dist-cjs/diagram/mermaid/patch.js +468 -0
- package/dist-cjs/diagram/mermaid/patch.js.map +1 -0
- package/dist-cjs/diagram/mermaid/serialize.js +133 -0
- package/dist-cjs/diagram/mermaid/serialize.js.map +1 -0
- package/dist-cjs/diagram/mermaid/shapes.js +327 -0
- package/dist-cjs/diagram/mermaid/shapes.js.map +1 -0
- package/dist-cjs/diagram/migrations.js +9 -3
- package/dist-cjs/diagram/migrations.js.map +1 -1
- package/dist-cjs/diagram/normalize.js +198 -1
- package/dist-cjs/diagram/normalize.js.map +1 -1
- package/dist-cjs/diagram/ops.js +829 -0
- package/dist-cjs/diagram/ops.js.map +1 -0
- package/dist-cjs/diagram/opsReference.js +55 -0
- package/dist-cjs/diagram/opsReference.js.map +1 -0
- package/dist-cjs/diagram/patchInput.js +636 -0
- package/dist-cjs/diagram/patchInput.js.map +1 -0
- package/dist-cjs/diagram/routing/batch.js +142 -0
- package/dist-cjs/diagram/routing/batch.js.map +1 -0
- package/dist-cjs/diagram/routing/curves.js +127 -0
- package/dist-cjs/diagram/routing/curves.js.map +1 -0
- package/dist-cjs/diagram/routing/grid.js +250 -0
- package/dist-cjs/diagram/routing/grid.js.map +1 -0
- package/dist-cjs/diagram/routing/index.js +29 -0
- package/dist-cjs/diagram/routing/index.js.map +1 -0
- package/dist-cjs/diagram/routing/legacy.js +32 -0
- package/dist-cjs/diagram/routing/legacy.js.map +1 -0
- package/dist-cjs/diagram/routing/nudge.js +100 -0
- package/dist-cjs/diagram/routing/nudge.js.map +1 -0
- package/dist-cjs/diagram/routing/orthogonal.js +62 -0
- package/dist-cjs/diagram/routing/orthogonal.js.map +1 -0
- package/dist-cjs/diagram/routing/path.js +137 -0
- package/dist-cjs/diagram/routing/path.js.map +1 -0
- package/dist-cjs/diagram/routing/sides.js +82 -0
- package/dist-cjs/diagram/routing/sides.js.map +1 -0
- package/dist-cjs/diagram/routing/types.js +15 -0
- package/dist-cjs/diagram/routing/types.js.map +1 -0
- package/dist-cjs/diagram/schema.js +64 -9
- package/dist-cjs/diagram/schema.js.map +1 -1
- package/dist-cjs/diagram/styleInput.js +113 -0
- package/dist-cjs/diagram/styleInput.js.map +1 -0
- package/dist-cjs/diagram/views.js +46 -0
- package/dist-cjs/diagram/views.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
// Diagram OPERATIONS — the one way a document changes.
|
|
2
|
+
//
|
|
3
|
+
// Every edit, whether it comes from the canvas, an AI reply, an MCP tool, a
|
|
4
|
+
// Mermaid text edit or an import, is expressed as a list of `DiagramOp`s and
|
|
5
|
+
// applied by `applyDiagramOps`. The function is pure and deterministic (ids
|
|
6
|
+
// are supplied by the caller, no clock unless `now` is passed), atomic (an
|
|
7
|
+
// error leaves the input untouched) and returns the INVERSE ops, so undo/redo
|
|
8
|
+
// is a property of the model rather than of any editor. Untouched node/edge
|
|
9
|
+
// objects keep their identity (structural sharing) so renderers can memoise.
|
|
10
|
+
//
|
|
11
|
+
// Vocabulary (16 ops). Canonical, stored-shape ops — the compact authoring
|
|
12
|
+
// vocabulary models write (`parent`, `semantic`, CSS colors…) is lowered onto
|
|
13
|
+
// these by patchInput.ts.
|
|
14
|
+
import { absolutePosition, descendantIds, indexNodes, isDescendantOf, toParentRelative, } from "./geometry.js";
|
|
15
|
+
import { mermaidSourceHash } from "./mermaid/index.js";
|
|
16
|
+
import { parseMermaid, syncOpsFromStructure } from "./mermaid/derive.js";
|
|
17
|
+
import { patchMermaidSource } from "./mermaid/patch.js";
|
|
18
|
+
import { parseBpmn, syncOpsFromBpmn } from "./bpmn/derive.js";
|
|
19
|
+
import { patchBpmnSource } from "./bpmn/patch.js";
|
|
20
|
+
import { sortPreOrder } from "./normalize.js";
|
|
21
|
+
export class DiagramOpError extends Error {
|
|
22
|
+
code;
|
|
23
|
+
data;
|
|
24
|
+
opIndex;
|
|
25
|
+
constructor(code, message, opIndex = -1, data) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "DiagramOpError";
|
|
28
|
+
this.code = code;
|
|
29
|
+
this.opIndex = opIndex;
|
|
30
|
+
this.data = data;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const MISSING_CODES = new Set([
|
|
34
|
+
"DIAGRAM_NODE_NOT_FOUND",
|
|
35
|
+
"DIAGRAM_EDGE_NOT_FOUND",
|
|
36
|
+
"DIAGRAM_PARENT_NOT_FOUND",
|
|
37
|
+
"DIAGRAM_EDGE_ENDPOINT_NOT_FOUND",
|
|
38
|
+
"DIAGRAM_NODE_LOCKED",
|
|
39
|
+
]);
|
|
40
|
+
// --- helpers ------------------------------------------------------------------
|
|
41
|
+
const compact = (value) => {
|
|
42
|
+
const out = {};
|
|
43
|
+
for (const [key, entry] of Object.entries(value))
|
|
44
|
+
if (entry !== undefined)
|
|
45
|
+
out[key] = entry;
|
|
46
|
+
return out;
|
|
47
|
+
};
|
|
48
|
+
const isLegacyPinned = (edge) => Boolean(edge.sourceAnchor || edge.targetAnchor) && edge.routing === "straight";
|
|
49
|
+
const sameStyleValue = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
50
|
+
// Apply a StyleChanges merge and return [next, inverse].
|
|
51
|
+
const mergeStyle = (base, changes) => {
|
|
52
|
+
if (changes === null) {
|
|
53
|
+
return { next: undefined, inverse: base ? { ...base } : null };
|
|
54
|
+
}
|
|
55
|
+
const next = { ...(base ?? {}) };
|
|
56
|
+
const inverse = {};
|
|
57
|
+
for (const [key, value] of Object.entries(changes)) {
|
|
58
|
+
if (value === undefined)
|
|
59
|
+
continue;
|
|
60
|
+
const prior = base?.[key];
|
|
61
|
+
inverse[key] = prior === undefined ? null : prior;
|
|
62
|
+
if (value === null)
|
|
63
|
+
delete next[key];
|
|
64
|
+
else
|
|
65
|
+
next[key] = value;
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
next: Object.keys(next).length > 0 ? next : undefined,
|
|
69
|
+
inverse: inverse,
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
const mergeData = (base, changes) => {
|
|
73
|
+
if (changes === null) {
|
|
74
|
+
return { next: undefined, inverse: base ? { ...base } : null };
|
|
75
|
+
}
|
|
76
|
+
const next = { ...(base ?? {}) };
|
|
77
|
+
const inverse = {};
|
|
78
|
+
for (const [key, value] of Object.entries(changes)) {
|
|
79
|
+
if (value === undefined)
|
|
80
|
+
continue;
|
|
81
|
+
const prior = base?.[key];
|
|
82
|
+
inverse[key] = prior === undefined ? null : prior;
|
|
83
|
+
if (value === null)
|
|
84
|
+
delete next[key];
|
|
85
|
+
else
|
|
86
|
+
next[key] = value;
|
|
87
|
+
}
|
|
88
|
+
return { next: Object.keys(next).length > 0 ? next : undefined, inverse };
|
|
89
|
+
};
|
|
90
|
+
// Set-or-delete an optional field; returns the prior value for the inverse
|
|
91
|
+
// (`null` when it was absent).
|
|
92
|
+
const assign = (target, key, value) => {
|
|
93
|
+
const prior = target[key];
|
|
94
|
+
if (value === undefined)
|
|
95
|
+
return prior === undefined ? null : prior;
|
|
96
|
+
if (value === null)
|
|
97
|
+
delete target[key];
|
|
98
|
+
else
|
|
99
|
+
target[key] = value;
|
|
100
|
+
return prior === undefined ? null : prior;
|
|
101
|
+
};
|
|
102
|
+
// --- working state --------------------------------------------------------------
|
|
103
|
+
class WorkingDoc {
|
|
104
|
+
nodes;
|
|
105
|
+
edges;
|
|
106
|
+
title;
|
|
107
|
+
canvas;
|
|
108
|
+
source;
|
|
109
|
+
changedNodes = new Set();
|
|
110
|
+
changedEdges = new Set();
|
|
111
|
+
addedNodes = [];
|
|
112
|
+
addedEdges = [];
|
|
113
|
+
removedNodes = [];
|
|
114
|
+
removedEdges = [];
|
|
115
|
+
rerouted = new Set();
|
|
116
|
+
warnings = [];
|
|
117
|
+
index = null;
|
|
118
|
+
edgeIndex = null;
|
|
119
|
+
constructor(doc) {
|
|
120
|
+
this.nodes = doc.nodes.slice();
|
|
121
|
+
this.edges = doc.edges.slice();
|
|
122
|
+
this.title = doc.meta.title;
|
|
123
|
+
this.canvas = doc.canvas;
|
|
124
|
+
this.source = doc.source;
|
|
125
|
+
}
|
|
126
|
+
byId() {
|
|
127
|
+
this.index ??= indexNodes(this.nodes);
|
|
128
|
+
return this.index;
|
|
129
|
+
}
|
|
130
|
+
edgesById() {
|
|
131
|
+
this.edgeIndex ??= new Map(this.edges.map((edge) => [edge.id, edge]));
|
|
132
|
+
return this.edgeIndex;
|
|
133
|
+
}
|
|
134
|
+
node(id) {
|
|
135
|
+
return this.byId().get(id);
|
|
136
|
+
}
|
|
137
|
+
edge(id) {
|
|
138
|
+
return this.edgesById().get(id);
|
|
139
|
+
}
|
|
140
|
+
invalidate() {
|
|
141
|
+
this.index = null;
|
|
142
|
+
this.edgeIndex = null;
|
|
143
|
+
}
|
|
144
|
+
setNode(next) {
|
|
145
|
+
const at = this.nodes.findIndex((node) => node.id === next.id);
|
|
146
|
+
if (at < 0)
|
|
147
|
+
throw new Error(`setNode: ${next.id} missing`);
|
|
148
|
+
this.nodes[at] = next;
|
|
149
|
+
this.changedNodes.add(next.id);
|
|
150
|
+
this.invalidate();
|
|
151
|
+
}
|
|
152
|
+
setEdge(next) {
|
|
153
|
+
const at = this.edges.findIndex((edge) => edge.id === next.id);
|
|
154
|
+
if (at < 0)
|
|
155
|
+
throw new Error(`setEdge: ${next.id} missing`);
|
|
156
|
+
this.edges[at] = next;
|
|
157
|
+
this.changedEdges.add(next.id);
|
|
158
|
+
this.invalidate();
|
|
159
|
+
}
|
|
160
|
+
siblings(parentId) {
|
|
161
|
+
return this.nodes.filter((node) => (node.parentId ?? undefined) === parentId);
|
|
162
|
+
}
|
|
163
|
+
siblingIndex(id) {
|
|
164
|
+
const node = this.node(id);
|
|
165
|
+
if (!node)
|
|
166
|
+
return -1;
|
|
167
|
+
return this.siblings(node.parentId).findIndex((sibling) => sibling.id === id);
|
|
168
|
+
}
|
|
169
|
+
// Insert `node` so that it sits at `index` among its siblings (array
|
|
170
|
+
// position = right after the previous sibling's subtree, or after the parent
|
|
171
|
+
// for index 0, or at the end for the root level).
|
|
172
|
+
insertNode(node, index) {
|
|
173
|
+
const siblings = this.siblings(node.parentId);
|
|
174
|
+
const at = index === undefined ? siblings.length : Math.max(0, Math.min(index, siblings.length));
|
|
175
|
+
let arrayPos;
|
|
176
|
+
if (at === 0) {
|
|
177
|
+
if (node.parentId) {
|
|
178
|
+
arrayPos = this.nodes.findIndex((candidate) => candidate.id === node.parentId) + 1;
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
arrayPos = 0;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
const previous = siblings[at - 1];
|
|
186
|
+
const subtree = descendantIds(previous.id, this.nodes);
|
|
187
|
+
const lastId = subtree.length > 0 ? subtree[subtree.length - 1] : previous.id;
|
|
188
|
+
arrayPos = this.nodes.findIndex((candidate) => candidate.id === lastId) + 1;
|
|
189
|
+
}
|
|
190
|
+
this.nodes.splice(arrayPos, 0, node);
|
|
191
|
+
this.invalidate();
|
|
192
|
+
}
|
|
193
|
+
// Remove a node (and only that node) from the array.
|
|
194
|
+
detachNode(id) {
|
|
195
|
+
const at = this.nodes.findIndex((node) => node.id === id);
|
|
196
|
+
if (at < 0)
|
|
197
|
+
return undefined;
|
|
198
|
+
const [removed] = this.nodes.splice(at, 1);
|
|
199
|
+
this.invalidate();
|
|
200
|
+
return removed;
|
|
201
|
+
}
|
|
202
|
+
// Move a subtree so its root sits at `index` among the siblings of `parentId`.
|
|
203
|
+
placeSubtree(id, parentId, index) {
|
|
204
|
+
const subtreeIds = [id, ...descendantIds(id, this.nodes)];
|
|
205
|
+
const subtree = subtreeIds.map((subtreeId) => this.node(subtreeId));
|
|
206
|
+
this.nodes = this.nodes.filter((node) => !subtreeIds.includes(node.id));
|
|
207
|
+
this.invalidate();
|
|
208
|
+
const root = { ...subtree[0], ...(parentId ? { parentId } : {}) };
|
|
209
|
+
if (!parentId)
|
|
210
|
+
delete root.parentId;
|
|
211
|
+
this.insertNode(root, index);
|
|
212
|
+
const rootPos = this.nodes.findIndex((node) => node.id === id);
|
|
213
|
+
this.nodes.splice(rootPos + 1, 0, ...subtree.slice(1));
|
|
214
|
+
this.invalidate();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// --- the apply --------------------------------------------------------------------
|
|
218
|
+
export const applyDiagramOps = (doc, ops, options = {}) => {
|
|
219
|
+
const onMissing = options.onMissing ?? "error";
|
|
220
|
+
const work = new WorkingDoc(doc);
|
|
221
|
+
const inverse = [];
|
|
222
|
+
const skipped = [];
|
|
223
|
+
let replaced = null;
|
|
224
|
+
const fail = (code, message, index, data) => {
|
|
225
|
+
throw new DiagramOpError(code, message, index, data);
|
|
226
|
+
};
|
|
227
|
+
// Clears a legacy pinned route / manual bends and records the inverse.
|
|
228
|
+
const clearRoute = (edge, mode) => {
|
|
229
|
+
const pinned = isLegacyPinned(edge);
|
|
230
|
+
const clearWaypoints = mode === "all" || pinned;
|
|
231
|
+
if (!pinned && !(clearWaypoints && edge.waypoints))
|
|
232
|
+
return edge;
|
|
233
|
+
const next = { ...edge };
|
|
234
|
+
const restore = {};
|
|
235
|
+
if (pinned) {
|
|
236
|
+
restore.sourceAnchor = edge.sourceAnchor ?? null;
|
|
237
|
+
restore.targetAnchor = edge.targetAnchor ?? null;
|
|
238
|
+
restore.routing = edge.routing ?? null;
|
|
239
|
+
delete next.sourceAnchor;
|
|
240
|
+
delete next.targetAnchor;
|
|
241
|
+
delete next.routing;
|
|
242
|
+
}
|
|
243
|
+
if (clearWaypoints && edge.waypoints) {
|
|
244
|
+
restore.waypoints = edge.waypoints;
|
|
245
|
+
delete next.waypoints;
|
|
246
|
+
}
|
|
247
|
+
inverse.push({ op: "updateEdge", id: edge.id, changes: restore });
|
|
248
|
+
work.rerouted.add(edge.id);
|
|
249
|
+
return next;
|
|
250
|
+
};
|
|
251
|
+
// After a subtree moved by `delta` (absolute), fix the edges: bends inside
|
|
252
|
+
// travel, pinned routes touching it are cleared, bends leaving it are kept
|
|
253
|
+
// or cleared per option.
|
|
254
|
+
const settleEdgesAfterMove = (subtree, delta) => {
|
|
255
|
+
for (const edge of work.edges) {
|
|
256
|
+
const sourceIn = Boolean(edge.source && subtree.has(edge.source));
|
|
257
|
+
const targetIn = Boolean(edge.target && subtree.has(edge.target));
|
|
258
|
+
if (!sourceIn && !targetIn)
|
|
259
|
+
continue;
|
|
260
|
+
let next = edge;
|
|
261
|
+
if (sourceIn && targetIn) {
|
|
262
|
+
if (edge.waypoints && (delta.x !== 0 || delta.y !== 0)) {
|
|
263
|
+
inverse.push({ op: "updateEdge", id: edge.id, changes: { waypoints: edge.waypoints } });
|
|
264
|
+
next = { ...edge, waypoints: edge.waypoints.map((p) => ({ x: p.x + delta.x, y: p.y + delta.y })) };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
next = clearRoute(edge, options.moveClearsWaypoints ? "all" : "pinned");
|
|
269
|
+
}
|
|
270
|
+
if (next !== edge)
|
|
271
|
+
work.setEdge(next);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
const applyNodeChanges = (index, id, changes) => {
|
|
275
|
+
const node = work.node(id);
|
|
276
|
+
if (!node)
|
|
277
|
+
return fail("DIAGRAM_NODE_NOT_FOUND", `Node "${id}" does not exist.`, index);
|
|
278
|
+
const next = { ...node };
|
|
279
|
+
const restore = {};
|
|
280
|
+
if (changes.type !== undefined) {
|
|
281
|
+
restore.type = node.type;
|
|
282
|
+
next.type = changes.type;
|
|
283
|
+
}
|
|
284
|
+
if (changes.label !== undefined)
|
|
285
|
+
restore.label = assign(next, "label", changes.label === "" ? null : changes.label);
|
|
286
|
+
if (changes.rotation !== undefined)
|
|
287
|
+
restore.rotation = assign(next, "rotation", changes.rotation === 0 ? null : changes.rotation);
|
|
288
|
+
if (changes.locked !== undefined)
|
|
289
|
+
restore.locked = assign(next, "locked", changes.locked ? true : null);
|
|
290
|
+
if (changes.hidden !== undefined)
|
|
291
|
+
restore.hidden = assign(next, "hidden", changes.hidden ? true : null);
|
|
292
|
+
if (changes.collapsed !== undefined)
|
|
293
|
+
restore.collapsed = assign(next, "collapsed", changes.collapsed ? true : null);
|
|
294
|
+
if (changes.collapsedSize !== undefined)
|
|
295
|
+
restore.collapsedSize = assign(next, "collapsedSize", changes.collapsedSize);
|
|
296
|
+
if (changes.style !== undefined) {
|
|
297
|
+
const merged = mergeStyle(node.style, changes.style);
|
|
298
|
+
restore.style = merged.inverse;
|
|
299
|
+
if (merged.next)
|
|
300
|
+
next.style = merged.next;
|
|
301
|
+
else
|
|
302
|
+
delete next.style;
|
|
303
|
+
}
|
|
304
|
+
if (changes.data !== undefined) {
|
|
305
|
+
const merged = mergeData(node.data, changes.data);
|
|
306
|
+
restore.data = merged.inverse;
|
|
307
|
+
if (merged.next)
|
|
308
|
+
next.data = merged.next;
|
|
309
|
+
else
|
|
310
|
+
delete next.data;
|
|
311
|
+
}
|
|
312
|
+
if (Object.keys(restore).length === 0)
|
|
313
|
+
return;
|
|
314
|
+
inverse.push({ op: "updateNode", id, changes: restore });
|
|
315
|
+
work.setNode(compact(next));
|
|
316
|
+
};
|
|
317
|
+
const applyEdgeChanges = (index, id, changes) => {
|
|
318
|
+
const edge = work.edge(id);
|
|
319
|
+
if (!edge)
|
|
320
|
+
return fail("DIAGRAM_EDGE_NOT_FOUND", `Edge "${id}" does not exist.`, index);
|
|
321
|
+
let next = { ...edge };
|
|
322
|
+
const restore = {};
|
|
323
|
+
let endpointChanged = false;
|
|
324
|
+
for (const end of ["source", "target"]) {
|
|
325
|
+
const value = changes[end];
|
|
326
|
+
if (value === undefined)
|
|
327
|
+
continue;
|
|
328
|
+
if (!work.node(value))
|
|
329
|
+
return fail("DIAGRAM_EDGE_ENDPOINT_NOT_FOUND", `Edge endpoint "${value}" does not exist.`, index);
|
|
330
|
+
if (value !== edge[end]) {
|
|
331
|
+
restore[end] = edge[end];
|
|
332
|
+
next[end] = value;
|
|
333
|
+
endpointChanged = true;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (changes.sourcePoint !== undefined)
|
|
337
|
+
restore.sourcePoint = assign(next, "sourcePoint", changes.sourcePoint);
|
|
338
|
+
if (changes.targetPoint !== undefined)
|
|
339
|
+
restore.targetPoint = assign(next, "targetPoint", changes.targetPoint);
|
|
340
|
+
if (changes.sourceHandle !== undefined)
|
|
341
|
+
restore.sourceHandle = assign(next, "sourceHandle", changes.sourceHandle);
|
|
342
|
+
if (changes.targetHandle !== undefined)
|
|
343
|
+
restore.targetHandle = assign(next, "targetHandle", changes.targetHandle);
|
|
344
|
+
if (changes.label !== undefined)
|
|
345
|
+
restore.label = assign(next, "label", changes.label === "" ? null : changes.label);
|
|
346
|
+
if (changes.labelPosition !== undefined)
|
|
347
|
+
restore.labelPosition = assign(next, "labelPosition", changes.labelPosition);
|
|
348
|
+
if (changes.labelOffset !== undefined)
|
|
349
|
+
restore.labelOffset = assign(next, "labelOffset", changes.labelOffset);
|
|
350
|
+
if (changes.routing !== undefined)
|
|
351
|
+
restore.routing = assign(next, "routing", changes.routing);
|
|
352
|
+
if (changes.waypoints !== undefined)
|
|
353
|
+
restore.waypoints = assign(next, "waypoints", changes.waypoints && changes.waypoints.length > 0 ? changes.waypoints : null);
|
|
354
|
+
if (changes.markerStart !== undefined)
|
|
355
|
+
restore.markerStart = assign(next, "markerStart", changes.markerStart === "none" ? null : changes.markerStart);
|
|
356
|
+
if (changes.markerEnd !== undefined)
|
|
357
|
+
restore.markerEnd = assign(next, "markerEnd", changes.markerEnd);
|
|
358
|
+
if (changes.sourceAnchor !== undefined)
|
|
359
|
+
restore.sourceAnchor = assign(next, "sourceAnchor", changes.sourceAnchor);
|
|
360
|
+
if (changes.targetAnchor !== undefined)
|
|
361
|
+
restore.targetAnchor = assign(next, "targetAnchor", changes.targetAnchor);
|
|
362
|
+
if (changes.data !== undefined) {
|
|
363
|
+
const merged = mergeData(edge.data, changes.data);
|
|
364
|
+
restore.data = merged.inverse;
|
|
365
|
+
if (merged.next)
|
|
366
|
+
next.data = merged.next;
|
|
367
|
+
else
|
|
368
|
+
delete next.data;
|
|
369
|
+
}
|
|
370
|
+
if (changes.style !== undefined) {
|
|
371
|
+
const merged = mergeStyle(edge.style, changes.style);
|
|
372
|
+
restore.style = merged.inverse;
|
|
373
|
+
if (merged.next)
|
|
374
|
+
next.style = merged.next;
|
|
375
|
+
else
|
|
376
|
+
delete next.style;
|
|
377
|
+
}
|
|
378
|
+
if (Object.keys(restore).length === 0)
|
|
379
|
+
return;
|
|
380
|
+
if (endpointChanged) {
|
|
381
|
+
// Re-pointing an end invalidates the saved route. Its restore is pushed
|
|
382
|
+
// BEFORE the field restore so that, reversed for undo, the endpoints go
|
|
383
|
+
// back first (a no-op clear on an already-clear edge) and the route last.
|
|
384
|
+
const cleared = clearRoute(edge, "all");
|
|
385
|
+
next = { ...next };
|
|
386
|
+
for (const key of ["sourceAnchor", "targetAnchor", "routing", "waypoints"]) {
|
|
387
|
+
// Keep whatever the caller set explicitly in the same op.
|
|
388
|
+
if (!(key in cleared) && changes[key] === undefined)
|
|
389
|
+
delete next[key];
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
inverse.push({ op: "updateEdge", id, changes: restore });
|
|
393
|
+
work.setEdge(compact(next));
|
|
394
|
+
};
|
|
395
|
+
const removeEdgeAt = (id) => {
|
|
396
|
+
const at = work.edges.findIndex((edge) => edge.id === id);
|
|
397
|
+
if (at < 0)
|
|
398
|
+
return;
|
|
399
|
+
const [edge] = work.edges.splice(at, 1);
|
|
400
|
+
work.invalidate();
|
|
401
|
+
work.removedEdges.push(id);
|
|
402
|
+
inverse.push({ op: "addEdge", edge, index: at });
|
|
403
|
+
};
|
|
404
|
+
const run = (op, index) => {
|
|
405
|
+
switch (op.op) {
|
|
406
|
+
case "setTitle": {
|
|
407
|
+
const title = op.title.trim();
|
|
408
|
+
if (!title)
|
|
409
|
+
return fail("DIAGRAM_INVALID_OP", "setTitle: title must not be empty", index);
|
|
410
|
+
if (title === work.title)
|
|
411
|
+
return;
|
|
412
|
+
inverse.push({ op: "setTitle", title: work.title });
|
|
413
|
+
work.title = title;
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
case "addNode": {
|
|
417
|
+
const node = compact({ ...op.node });
|
|
418
|
+
if (!node.id)
|
|
419
|
+
return fail("DIAGRAM_INVALID_OP", "addNode: node.id is required", index);
|
|
420
|
+
if (work.node(node.id) || work.edge(node.id))
|
|
421
|
+
return fail("DIAGRAM_DUPLICATE_NODE_ID", `A node with id "${node.id}" already exists.`, index);
|
|
422
|
+
if (node.parentId && !work.node(node.parentId))
|
|
423
|
+
return fail("DIAGRAM_PARENT_NOT_FOUND", `Parent "${node.parentId}" does not exist.`, index);
|
|
424
|
+
work.insertNode(node, op.index);
|
|
425
|
+
work.addedNodes.push(node.id);
|
|
426
|
+
inverse.push({ op: "removeNode", id: node.id, children: "delete" });
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
case "updateNode":
|
|
430
|
+
return applyNodeChanges(index, op.id, op.changes);
|
|
431
|
+
case "removeNode": {
|
|
432
|
+
const node = work.node(op.id);
|
|
433
|
+
if (!node)
|
|
434
|
+
return fail("DIAGRAM_NODE_NOT_FOUND", `Node "${op.id}" does not exist.`, index);
|
|
435
|
+
const mode = op.children ?? "lift";
|
|
436
|
+
const byId = work.byId();
|
|
437
|
+
const subtree = new Set([op.id, ...(mode === "delete" ? descendantIds(op.id, work.nodes) : [])]);
|
|
438
|
+
// Edges touching anything removed go first (their inverses run last).
|
|
439
|
+
for (const edge of work.edges.slice()) {
|
|
440
|
+
if ((edge.source && subtree.has(edge.source)) || (edge.target && subtree.has(edge.target)))
|
|
441
|
+
removeEdgeAt(edge.id);
|
|
442
|
+
}
|
|
443
|
+
// Lift direct children to the removed node's parent, keeping their
|
|
444
|
+
// absolute position.
|
|
445
|
+
if (mode === "lift") {
|
|
446
|
+
for (const child of work.siblings(op.id)) {
|
|
447
|
+
const childIndex = work.siblingIndex(child.id);
|
|
448
|
+
const childAbs = absolutePosition(child, byId);
|
|
449
|
+
const lifted = { ...child, position: toParentRelative(childAbs, node.parentId, byId) };
|
|
450
|
+
if (node.parentId)
|
|
451
|
+
lifted.parentId = node.parentId;
|
|
452
|
+
else
|
|
453
|
+
delete lifted.parentId;
|
|
454
|
+
inverse.push({ op: "reparent", id: child.id, parentId: op.id, position: child.position, index: childIndex });
|
|
455
|
+
work.setNode(lifted);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const removedInOrder = work.nodes.filter((candidate) => subtree.has(candidate.id));
|
|
459
|
+
for (const candidate of removedInOrder.slice().reverse()) {
|
|
460
|
+
const siblingIndex = work.siblingIndex(candidate.id);
|
|
461
|
+
work.detachNode(candidate.id);
|
|
462
|
+
work.removedNodes.push(candidate.id);
|
|
463
|
+
// Re-added in forward order: parents before children.
|
|
464
|
+
inverse.push({ op: "addNode", node: candidate, index: siblingIndex });
|
|
465
|
+
}
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
case "addEdge": {
|
|
469
|
+
const edge = compact({ ...op.edge });
|
|
470
|
+
if (!edge.id)
|
|
471
|
+
return fail("DIAGRAM_INVALID_OP", "addEdge: edge.id is required", index);
|
|
472
|
+
if (work.edge(edge.id) || work.node(edge.id))
|
|
473
|
+
return fail("DIAGRAM_DUPLICATE_EDGE_ID", `An edge with id "${edge.id}" already exists.`, index);
|
|
474
|
+
for (const end of ["source", "target"]) {
|
|
475
|
+
const value = edge[end];
|
|
476
|
+
if (value === undefined) {
|
|
477
|
+
if (!edge[end === "source" ? "sourcePoint" : "targetPoint"])
|
|
478
|
+
return fail("DIAGRAM_INVALID_OP", `addEdge: ${end} is required`, index);
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
if (!work.node(value))
|
|
482
|
+
return fail("DIAGRAM_EDGE_ENDPOINT_NOT_FOUND", `Edge endpoint "${value}" does not exist.`, index);
|
|
483
|
+
}
|
|
484
|
+
const at = op.index === undefined ? work.edges.length : Math.max(0, Math.min(op.index, work.edges.length));
|
|
485
|
+
work.edges.splice(at, 0, edge);
|
|
486
|
+
work.invalidate();
|
|
487
|
+
work.addedEdges.push(edge.id);
|
|
488
|
+
inverse.push({ op: "removeEdge", id: edge.id });
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
case "updateEdge":
|
|
492
|
+
return applyEdgeChanges(index, op.id, op.changes);
|
|
493
|
+
case "removeEdge": {
|
|
494
|
+
if (!work.edge(op.id))
|
|
495
|
+
return fail("DIAGRAM_EDGE_NOT_FOUND", `Edge "${op.id}" does not exist.`, index);
|
|
496
|
+
removeEdgeAt(op.id);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
case "moveNodes": {
|
|
500
|
+
const restore = [];
|
|
501
|
+
for (const move of op.moves) {
|
|
502
|
+
const node = work.node(move.id);
|
|
503
|
+
if (!node)
|
|
504
|
+
return fail("DIAGRAM_NODE_NOT_FOUND", `Node "${move.id}" does not exist.`, index);
|
|
505
|
+
if (node.locked)
|
|
506
|
+
return fail("DIAGRAM_NODE_LOCKED", `Node "${move.id}" is locked.`, index);
|
|
507
|
+
if (node.position.x === move.position.x && node.position.y === move.position.y)
|
|
508
|
+
continue;
|
|
509
|
+
const delta = { x: move.position.x - node.position.x, y: move.position.y - node.position.y };
|
|
510
|
+
restore.push({ id: move.id, position: node.position });
|
|
511
|
+
work.setNode({ ...node, position: move.position });
|
|
512
|
+
settleEdgesAfterMove(new Set([move.id, ...descendantIds(move.id, work.nodes)]), delta);
|
|
513
|
+
}
|
|
514
|
+
if (restore.length > 0)
|
|
515
|
+
inverse.push({ op: "moveNodes", moves: restore });
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
case "resizeNode": {
|
|
519
|
+
const node = work.node(op.id);
|
|
520
|
+
if (!node)
|
|
521
|
+
return fail("DIAGRAM_NODE_NOT_FOUND", `Node "${op.id}" does not exist.`, index);
|
|
522
|
+
if (node.locked)
|
|
523
|
+
return fail("DIAGRAM_NODE_LOCKED", `Node "${op.id}" is locked.`, index);
|
|
524
|
+
if (!(op.size.width > 0 && op.size.height > 0))
|
|
525
|
+
return fail("DIAGRAM_INVALID_OP", "resizeNode: size must be positive", index);
|
|
526
|
+
const sameSize = node.size.width === op.size.width && node.size.height === op.size.height;
|
|
527
|
+
const samePos = !op.position || (op.position.x === node.position.x && op.position.y === node.position.y);
|
|
528
|
+
if (sameSize && samePos)
|
|
529
|
+
return;
|
|
530
|
+
const next = { ...node, size: op.size, ...(op.position ? { position: op.position } : {}) };
|
|
531
|
+
if (node.collapsed && node.collapsedSize) {
|
|
532
|
+
// Resizing a folded container sets its FOLDED size.
|
|
533
|
+
next.size = node.size;
|
|
534
|
+
next.collapsedSize = op.size;
|
|
535
|
+
}
|
|
536
|
+
work.setNode(next);
|
|
537
|
+
if (op.position) {
|
|
538
|
+
// Children stay put on the canvas: shift them by the opposite delta.
|
|
539
|
+
const delta = { x: op.position.x - node.position.x, y: op.position.y - node.position.y };
|
|
540
|
+
for (const child of work.siblings(op.id)) {
|
|
541
|
+
inverse.push({ op: "moveNodes", moves: [{ id: child.id, position: child.position }] });
|
|
542
|
+
work.setNode({ ...child, position: { x: child.position.x - delta.x, y: child.position.y - delta.y } });
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
// Edges into a resized node: pinned routes are stale.
|
|
546
|
+
for (const edge of work.edges) {
|
|
547
|
+
if (edge.source === op.id || edge.target === op.id) {
|
|
548
|
+
const next2 = clearRoute(edge, "pinned");
|
|
549
|
+
if (next2 !== edge)
|
|
550
|
+
work.setEdge(next2);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
// Primary inverse LAST: reversed for undo it runs first, when the edges
|
|
554
|
+
// are still clear (no double-clearing), and the route restores follow.
|
|
555
|
+
if (node.collapsed && node.collapsedSize) {
|
|
556
|
+
inverse.push({ op: "updateNode", id: op.id, changes: { collapsedSize: node.collapsedSize } });
|
|
557
|
+
if (op.position)
|
|
558
|
+
inverse.push({ op: "moveNodes", moves: [{ id: op.id, position: node.position }] });
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
inverse.push({ op: "resizeNode", id: op.id, size: node.size, position: op.position ? node.position : undefined });
|
|
562
|
+
}
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
case "reparent": {
|
|
566
|
+
const node = work.node(op.id);
|
|
567
|
+
if (!node)
|
|
568
|
+
return fail("DIAGRAM_NODE_NOT_FOUND", `Node "${op.id}" does not exist.`, index);
|
|
569
|
+
if (node.locked)
|
|
570
|
+
return fail("DIAGRAM_NODE_LOCKED", `Node "${op.id}" is locked.`, index);
|
|
571
|
+
const parentId = op.parentId ?? undefined;
|
|
572
|
+
if (parentId) {
|
|
573
|
+
if (parentId === op.id)
|
|
574
|
+
return fail("DIAGRAM_PARENT_CYCLE", `Node "${op.id}" cannot contain itself.`, index);
|
|
575
|
+
if (!work.node(parentId))
|
|
576
|
+
return fail("DIAGRAM_PARENT_NOT_FOUND", `Parent "${parentId}" does not exist.`, index);
|
|
577
|
+
if (isDescendantOf(parentId, op.id, work.byId()))
|
|
578
|
+
return fail("DIAGRAM_PARENT_CYCLE", `Node "${op.id}" cannot be moved into its own descendant "${parentId}".`, index);
|
|
579
|
+
}
|
|
580
|
+
const before = absolutePosition(node, work.byId());
|
|
581
|
+
const priorIndex = work.siblingIndex(op.id);
|
|
582
|
+
work.placeSubtree(op.id, parentId, op.index);
|
|
583
|
+
const placed = work.node(op.id);
|
|
584
|
+
work.setNode({ ...placed, position: op.position });
|
|
585
|
+
const after = absolutePosition(work.node(op.id), work.byId());
|
|
586
|
+
settleEdgesAfterMove(new Set([op.id, ...descendantIds(op.id, work.nodes)]), { x: after.x - before.x, y: after.y - before.y });
|
|
587
|
+
// Primary inverse last (see resizeNode).
|
|
588
|
+
inverse.push({ op: "reparent", id: op.id, parentId: node.parentId ?? null, position: node.position, index: priorIndex });
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
case "reorder": {
|
|
592
|
+
if (op.kind === "edge") {
|
|
593
|
+
const at = work.edges.findIndex((edge) => edge.id === op.id);
|
|
594
|
+
if (at < 0)
|
|
595
|
+
return fail("DIAGRAM_EDGE_NOT_FOUND", `Edge "${op.id}" does not exist.`, index);
|
|
596
|
+
const target = Math.max(0, Math.min(op.index, work.edges.length - 1));
|
|
597
|
+
if (target === at)
|
|
598
|
+
return;
|
|
599
|
+
const [edge] = work.edges.splice(at, 1);
|
|
600
|
+
work.edges.splice(target, 0, edge);
|
|
601
|
+
work.invalidate();
|
|
602
|
+
work.changedEdges.add(op.id);
|
|
603
|
+
inverse.push({ op: "reorder", kind: "edge", id: op.id, index: at });
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
const node = work.node(op.id);
|
|
607
|
+
if (!node)
|
|
608
|
+
return fail("DIAGRAM_NODE_NOT_FOUND", `Node "${op.id}" does not exist.`, index);
|
|
609
|
+
const priorIndex = work.siblingIndex(op.id);
|
|
610
|
+
const siblingCount = work.siblings(node.parentId).length;
|
|
611
|
+
const target = Math.max(0, Math.min(op.index, siblingCount - 1));
|
|
612
|
+
if (target === priorIndex)
|
|
613
|
+
return;
|
|
614
|
+
// placeSubtree removes the subtree first, so `target` is already the
|
|
615
|
+
// index among the remaining siblings.
|
|
616
|
+
work.placeSubtree(op.id, node.parentId, target);
|
|
617
|
+
work.changedNodes.add(op.id);
|
|
618
|
+
inverse.push({ op: "reorder", kind: "node", id: op.id, index: priorIndex });
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
case "setCanvas": {
|
|
622
|
+
const next = { ...(work.canvas ?? {}) };
|
|
623
|
+
const restore = {};
|
|
624
|
+
for (const [key, value] of Object.entries(op.changes)) {
|
|
625
|
+
if (value === undefined)
|
|
626
|
+
continue;
|
|
627
|
+
const prior = next[key];
|
|
628
|
+
if (sameStyleValue(prior, value === null ? undefined : value))
|
|
629
|
+
continue;
|
|
630
|
+
restore[key] = prior === undefined ? null : prior;
|
|
631
|
+
if (value === null)
|
|
632
|
+
delete next[key];
|
|
633
|
+
else
|
|
634
|
+
next[key] = value;
|
|
635
|
+
}
|
|
636
|
+
if (Object.keys(restore).length === 0)
|
|
637
|
+
return;
|
|
638
|
+
inverse.push({ op: "setCanvas", changes: restore });
|
|
639
|
+
work.canvas = next;
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
case "setViewport": {
|
|
643
|
+
const prior = work.canvas?.viewport;
|
|
644
|
+
if (prior && prior.x === op.viewport.x && prior.y === op.viewport.y && prior.zoom === op.viewport.zoom)
|
|
645
|
+
return;
|
|
646
|
+
inverse.push(prior ? { op: "setViewport", viewport: prior } : { op: "setCanvas", changes: { viewport: null } });
|
|
647
|
+
work.canvas = { ...(work.canvas ?? {}), viewport: op.viewport };
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
case "replaceDocument": {
|
|
651
|
+
// Restore point = the working state right before the replacement.
|
|
652
|
+
inverse.push({
|
|
653
|
+
op: "replaceDocument",
|
|
654
|
+
document: compact({
|
|
655
|
+
...(replaced ?? doc),
|
|
656
|
+
meta: { ...(replaced ?? doc).meta, title: work.title },
|
|
657
|
+
canvas: work.canvas,
|
|
658
|
+
nodes: sortPreOrder(work.nodes),
|
|
659
|
+
edges: work.edges.slice(),
|
|
660
|
+
source: work.source,
|
|
661
|
+
}),
|
|
662
|
+
});
|
|
663
|
+
replaced = op.document;
|
|
664
|
+
const fresh = new WorkingDoc(op.document);
|
|
665
|
+
work.nodes = fresh.nodes;
|
|
666
|
+
work.edges = fresh.edges;
|
|
667
|
+
work.title = fresh.title;
|
|
668
|
+
work.canvas = fresh.canvas;
|
|
669
|
+
work.source = fresh.source;
|
|
670
|
+
work.invalidate();
|
|
671
|
+
for (const node of work.nodes)
|
|
672
|
+
work.changedNodes.add(node.id);
|
|
673
|
+
for (const edge of work.edges)
|
|
674
|
+
work.changedEdges.add(edge.id);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
case "setSource": {
|
|
678
|
+
const prior = work.source;
|
|
679
|
+
if (sameStyleValue(prior, op.source ?? undefined))
|
|
680
|
+
return;
|
|
681
|
+
const next = op.source ? { ...op.source } : undefined;
|
|
682
|
+
if (next && next.format === "bpmn" && op.resync !== false && next.text !== prior?.text) {
|
|
683
|
+
// The XML is the structure AND geometry truth: derive the model from it.
|
|
684
|
+
const parsed = parseBpmn(next.text);
|
|
685
|
+
next.hash = mermaidSourceHash(next.text);
|
|
686
|
+
next.diagramType = "bpmn";
|
|
687
|
+
next.valid = parsed.ok;
|
|
688
|
+
if (parsed.ok) {
|
|
689
|
+
const sync = syncOpsFromBpmn(snapshotDoc(), parsed.structure);
|
|
690
|
+
for (const syncOp of sync.ops)
|
|
691
|
+
run(syncOp, index);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (next && next.format === "mermaid" && op.resync !== false && next.text !== prior?.text) {
|
|
695
|
+
// The text is the structure truth: parse it and bring the nodes/edges
|
|
696
|
+
// in line. A text that does not parse is kept (the user is mid-edit)
|
|
697
|
+
// and flagged; the structure stays what it was.
|
|
698
|
+
const parsed = parseMermaid(next.text);
|
|
699
|
+
next.hash = mermaidSourceHash(next.text);
|
|
700
|
+
if (parsed.ok) {
|
|
701
|
+
next.valid = true;
|
|
702
|
+
next.diagramType = parsed.cst.diagramType ?? next.diagramType;
|
|
703
|
+
const snapshot = snapshotDoc();
|
|
704
|
+
const sync = syncOpsFromStructure(snapshot, parsed.structure);
|
|
705
|
+
for (const syncOp of sync.ops)
|
|
706
|
+
run(syncOp, index);
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
next.valid = false;
|
|
710
|
+
if (parsed.cst.diagramType && parsed.cst.diagramType !== "flowchart")
|
|
711
|
+
next.diagramType = parsed.cst.diagramType;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
work.source = next;
|
|
715
|
+
// Primary inverse last: reversed for undo it runs first and restores
|
|
716
|
+
// the text verbatim (no resync), then the structural inverses follow.
|
|
717
|
+
inverse.push({ op: "setSource", source: prior ?? null, resync: false });
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
case "batch": {
|
|
721
|
+
const mark = inverse.length;
|
|
722
|
+
for (const inner of op.ops)
|
|
723
|
+
run(inner, index);
|
|
724
|
+
const innerInverse = inverse.splice(mark).reverse();
|
|
725
|
+
inverse.push({ op: "batch", ops: innerInverse, label: op.label });
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
default:
|
|
729
|
+
return fail("DIAGRAM_INVALID_OP", `Unknown op "${op.op}"`, index);
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
const snapshotDoc = () => compact({
|
|
733
|
+
...(replaced ?? doc),
|
|
734
|
+
meta: { ...(replaced ?? doc).meta, title: work.title },
|
|
735
|
+
canvas: work.canvas,
|
|
736
|
+
nodes: sortPreOrder(work.nodes),
|
|
737
|
+
edges: work.edges.slice(),
|
|
738
|
+
source: work.source,
|
|
739
|
+
});
|
|
740
|
+
ops.forEach((op, index) => {
|
|
741
|
+
try {
|
|
742
|
+
run(op, index);
|
|
743
|
+
}
|
|
744
|
+
catch (error) {
|
|
745
|
+
if (onMissing === "skip" && error instanceof DiagramOpError && MISSING_CODES.has(error.code)) {
|
|
746
|
+
skipped.push({ index, code: error.code, message: error.message });
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
throw error;
|
|
750
|
+
}
|
|
751
|
+
});
|
|
752
|
+
// Source-backed document: mirror the structural changes onto the text as
|
|
753
|
+
// minimal edits. Skipped when this batch itself set the source (a text edit
|
|
754
|
+
// or an undo restoring one) — the text is then already the truth.
|
|
755
|
+
let sourceUnmapped = [];
|
|
756
|
+
const touchesSource = (list) => list.some((op) => op.op === "setSource" || op.op === "replaceDocument" || (op.op === "batch" && touchesSource(op.ops)));
|
|
757
|
+
if (work.source?.format === "mermaid" && !touchesSource(ops) && skipped.length < ops.length) {
|
|
758
|
+
const prior = work.source;
|
|
759
|
+
const patched = patchMermaidSource(prior.text, ops, { before: doc, after: snapshotDoc() });
|
|
760
|
+
sourceUnmapped = patched.unmapped;
|
|
761
|
+
if (patched.changed) {
|
|
762
|
+
work.source = { ...prior, text: patched.text, hash: mermaidSourceHash(patched.text), valid: true };
|
|
763
|
+
// Pushed last → first on undo: the text goes back verbatim before the
|
|
764
|
+
// structural inverses run (which then skip patching, see above).
|
|
765
|
+
inverse.push({ op: "setSource", source: prior, resync: false });
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (work.source?.format === "bpmn" && !touchesSource(ops) && skipped.length < ops.length) {
|
|
769
|
+
const prior = work.source;
|
|
770
|
+
const patched = patchBpmnSource(prior.text, ops, { before: doc, after: snapshotDoc() });
|
|
771
|
+
sourceUnmapped = patched.unmapped;
|
|
772
|
+
for (const warning of patched.warnings)
|
|
773
|
+
work.warnings.push(warning);
|
|
774
|
+
if (patched.changed) {
|
|
775
|
+
work.source = { ...prior, text: patched.text, hash: mermaidSourceHash(patched.text), valid: true };
|
|
776
|
+
// The XML is the truth, so the model follows it back: what the patch
|
|
777
|
+
// decided (placement, waypoints, cascades) is derived into the model
|
|
778
|
+
// as ordinary ops whose inverses sit before the text's own inverse.
|
|
779
|
+
const parsed = parseBpmn(patched.text);
|
|
780
|
+
if (parsed.ok) {
|
|
781
|
+
const sync = syncOpsFromBpmn(snapshotDoc(), parsed.structure);
|
|
782
|
+
for (const syncOp of sync.ops) {
|
|
783
|
+
try {
|
|
784
|
+
run(syncOp, -1);
|
|
785
|
+
}
|
|
786
|
+
catch {
|
|
787
|
+
// A sync op that cannot apply leaves the model as the ops made it.
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
inverse.push({ op: "setSource", source: prior, resync: false });
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
const nodes = sortPreOrder(work.nodes);
|
|
795
|
+
const meta = {
|
|
796
|
+
...(replaced ?? doc).meta,
|
|
797
|
+
title: work.title,
|
|
798
|
+
...(options.now ? { updatedAt: options.now } : {}),
|
|
799
|
+
};
|
|
800
|
+
const base = replaced ?? doc;
|
|
801
|
+
const next = compact({
|
|
802
|
+
...base,
|
|
803
|
+
meta,
|
|
804
|
+
canvas: work.canvas,
|
|
805
|
+
nodes,
|
|
806
|
+
edges: work.edges,
|
|
807
|
+
source: work.source,
|
|
808
|
+
});
|
|
809
|
+
return {
|
|
810
|
+
doc: next,
|
|
811
|
+
inverse: inverse.slice().reverse(),
|
|
812
|
+
changed: { nodes: [...work.changedNodes], edges: [...work.changedEdges] },
|
|
813
|
+
added: { nodes: work.addedNodes, edges: work.addedEdges },
|
|
814
|
+
removed: { nodes: work.removedNodes, edges: work.removedEdges },
|
|
815
|
+
rerouted: [...work.rerouted],
|
|
816
|
+
warnings: work.warnings,
|
|
817
|
+
skipped,
|
|
818
|
+
sourceUnmapped,
|
|
819
|
+
};
|
|
820
|
+
};
|
|
821
|
+
// Convenience for hosts that only need the document.
|
|
822
|
+
export const applyOps = (doc, ops, options) => applyDiagramOps(doc, ops, options).doc;
|
|
823
|
+
//# sourceMappingURL=ops.js.map
|