@aetherwing/fcp-drawio 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/dist/adapter.d.ts +38 -0
- package/dist/adapter.js +259 -0
- package/dist/adapter.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/layout/elk-layout.d.ts +49 -0
- package/dist/layout/elk-layout.js +144 -0
- package/dist/layout/elk-layout.js.map +1 -0
- package/dist/lib/drawio-cli.d.ts +22 -0
- package/dist/lib/drawio-cli.js +88 -0
- package/dist/lib/drawio-cli.js.map +1 -0
- package/dist/lib/node-types.d.ts +22 -0
- package/dist/lib/node-types.js +174 -0
- package/dist/lib/node-types.js.map +1 -0
- package/dist/lib/stencils/aws.d.ts +2 -0
- package/dist/lib/stencils/aws.js +69 -0
- package/dist/lib/stencils/aws.js.map +1 -0
- package/dist/lib/stencils/azure.d.ts +2 -0
- package/dist/lib/stencils/azure.js +54 -0
- package/dist/lib/stencils/azure.js.map +1 -0
- package/dist/lib/stencils/cisco.d.ts +2 -0
- package/dist/lib/stencils/cisco.js +30 -0
- package/dist/lib/stencils/cisco.js.map +1 -0
- package/dist/lib/stencils/gcp.d.ts +2 -0
- package/dist/lib/stencils/gcp.js +38 -0
- package/dist/lib/stencils/gcp.js.map +1 -0
- package/dist/lib/stencils/ibm.d.ts +2 -0
- package/dist/lib/stencils/ibm.js +32 -0
- package/dist/lib/stencils/ibm.js.map +1 -0
- package/dist/lib/stencils/index.d.ts +10 -0
- package/dist/lib/stencils/index.js +33 -0
- package/dist/lib/stencils/index.js.map +1 -0
- package/dist/lib/stencils/k8s.d.ts +2 -0
- package/dist/lib/stencils/k8s.js +32 -0
- package/dist/lib/stencils/k8s.js.map +1 -0
- package/dist/lib/stencils/types.d.ts +14 -0
- package/dist/lib/stencils/types.js +2 -0
- package/dist/lib/stencils/types.js.map +1 -0
- package/dist/lib/themes.d.ts +8 -0
- package/dist/lib/themes.js +32 -0
- package/dist/lib/themes.js.map +1 -0
- package/dist/model/defaults.d.ts +3 -0
- package/dist/model/defaults.js +26 -0
- package/dist/model/defaults.js.map +1 -0
- package/dist/model/diagram-model.d.ts +110 -0
- package/dist/model/diagram-model.js +938 -0
- package/dist/model/diagram-model.js.map +1 -0
- package/dist/model/event-log.d.ts +30 -0
- package/dist/model/event-log.js +112 -0
- package/dist/model/event-log.js.map +1 -0
- package/dist/model/id.d.ts +9 -0
- package/dist/model/id.js +35 -0
- package/dist/model/id.js.map +1 -0
- package/dist/model/reference-registry.d.ts +33 -0
- package/dist/model/reference-registry.js +143 -0
- package/dist/model/reference-registry.js.map +1 -0
- package/dist/model/spatial.d.ts +20 -0
- package/dist/model/spatial.js +59 -0
- package/dist/model/spatial.js.map +1 -0
- package/dist/parser/parse-op.d.ts +18 -0
- package/dist/parser/parse-op.js +430 -0
- package/dist/parser/parse-op.js.map +1 -0
- package/dist/parser/resolve-ref.d.ts +27 -0
- package/dist/parser/resolve-ref.js +232 -0
- package/dist/parser/resolve-ref.js.map +1 -0
- package/dist/parser/tokenizer.d.ts +6 -0
- package/dist/parser/tokenizer.js +7 -0
- package/dist/parser/tokenizer.js.map +1 -0
- package/dist/serialization/connector-intelligence.d.ts +35 -0
- package/dist/serialization/connector-intelligence.js +336 -0
- package/dist/serialization/connector-intelligence.js.map +1 -0
- package/dist/serialization/deserialize.d.ts +6 -0
- package/dist/serialization/deserialize.js +511 -0
- package/dist/serialization/deserialize.js.map +1 -0
- package/dist/serialization/serialize.d.ts +15 -0
- package/dist/serialization/serialize.js +332 -0
- package/dist/serialization/serialize.js.map +1 -0
- package/dist/server/intent-layer.d.ts +48 -0
- package/dist/server/intent-layer.js +1322 -0
- package/dist/server/intent-layer.js.map +1 -0
- package/dist/server/mcp-server.d.ts +7 -0
- package/dist/server/mcp-server.js +26 -0
- package/dist/server/mcp-server.js.map +1 -0
- package/dist/server/model-map.d.ts +8 -0
- package/dist/server/model-map.js +240 -0
- package/dist/server/model-map.js.map +1 -0
- package/dist/server/query-handler.d.ts +19 -0
- package/dist/server/query-handler.js +148 -0
- package/dist/server/query-handler.js.map +1 -0
- package/dist/server/response-formatter.d.ts +56 -0
- package/dist/server/response-formatter.js +351 -0
- package/dist/server/response-formatter.js.map +1 -0
- package/dist/server/session-handler.d.ts +6 -0
- package/dist/server/session-handler.js +127 -0
- package/dist/server/session-handler.js.map +1 -0
- package/dist/types/index.d.ts +238 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/verb-specs.d.ts +6 -0
- package/dist/verb-specs.js +144 -0
- package/dist/verb-specs.js.map +1 -0
- package/package.json +39 -0
|
@@ -0,0 +1,938 @@
|
|
|
1
|
+
import { EventLog } from "@aetherwing/fcp-core";
|
|
2
|
+
import { nextShapeId, nextEdgeId, nextGroupId, nextPageId, nextLayerId, nextSequence } from "./id.js";
|
|
3
|
+
import { createDefaultStyle, createDefaultEdgeStyle } from "./defaults.js";
|
|
4
|
+
import { ReferenceRegistry } from "./reference-registry.js";
|
|
5
|
+
import { NODE_TYPES, computeDefaultSize } from "../lib/node-types.js";
|
|
6
|
+
import { THEMES, isThemeName } from "../lib/themes.js";
|
|
7
|
+
import { boundsOverlap, computePushVector, isDownstream } from "./spatial.js";
|
|
8
|
+
const DEFAULT_GAP = 60;
|
|
9
|
+
const FIRST_SHAPE_POS = { x: 200, y: 200 };
|
|
10
|
+
export class DiagramModel {
|
|
11
|
+
diagram;
|
|
12
|
+
eventLog;
|
|
13
|
+
registry;
|
|
14
|
+
constructor() {
|
|
15
|
+
this.eventLog = new EventLog();
|
|
16
|
+
this.registry = new ReferenceRegistry();
|
|
17
|
+
this.diagram = this.createEmptyDiagram("Untitled");
|
|
18
|
+
}
|
|
19
|
+
// ── Diagram lifecycle ────────────────────────────────────
|
|
20
|
+
createNew(title) {
|
|
21
|
+
this.diagram = this.createEmptyDiagram(title);
|
|
22
|
+
this.eventLog = new EventLog();
|
|
23
|
+
this.rebuildRegistry();
|
|
24
|
+
}
|
|
25
|
+
createEmptyDiagram(title) {
|
|
26
|
+
const pageId = nextPageId();
|
|
27
|
+
const layerId = nextLayerId();
|
|
28
|
+
const page = {
|
|
29
|
+
id: pageId,
|
|
30
|
+
name: "Page-1",
|
|
31
|
+
shapes: new Map(),
|
|
32
|
+
edges: new Map(),
|
|
33
|
+
groups: new Map(),
|
|
34
|
+
layers: [{ id: layerId, name: "Default", visible: true, locked: false, order: 0 }],
|
|
35
|
+
defaultLayer: layerId,
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
id: crypto.randomUUID(),
|
|
39
|
+
title,
|
|
40
|
+
filePath: null,
|
|
41
|
+
pages: [page],
|
|
42
|
+
activePage: pageId,
|
|
43
|
+
customTypes: new Map(),
|
|
44
|
+
customThemes: new Map(),
|
|
45
|
+
loadedStencilPacks: new Set(),
|
|
46
|
+
metadata: {
|
|
47
|
+
host: "fcp-drawio",
|
|
48
|
+
modified: new Date().toISOString(),
|
|
49
|
+
version: "0.2.0",
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// ── Page access ──────────────────────────────────────────
|
|
54
|
+
getActivePage() {
|
|
55
|
+
const page = this.diagram.pages.find((p) => p.id === this.diagram.activePage);
|
|
56
|
+
if (!page)
|
|
57
|
+
throw new Error("No active page");
|
|
58
|
+
return page;
|
|
59
|
+
}
|
|
60
|
+
getPageByName(name) {
|
|
61
|
+
return this.diagram.pages.find((p) => p.name === name);
|
|
62
|
+
}
|
|
63
|
+
switchPage(name) {
|
|
64
|
+
const page = this.getPageByName(name);
|
|
65
|
+
if (!page)
|
|
66
|
+
return null;
|
|
67
|
+
this.diagram.activePage = page.id;
|
|
68
|
+
this.rebuildRegistry();
|
|
69
|
+
return page;
|
|
70
|
+
}
|
|
71
|
+
addPage(name) {
|
|
72
|
+
const layerId = nextLayerId();
|
|
73
|
+
const page = {
|
|
74
|
+
id: nextPageId(),
|
|
75
|
+
name,
|
|
76
|
+
shapes: new Map(),
|
|
77
|
+
edges: new Map(),
|
|
78
|
+
groups: new Map(),
|
|
79
|
+
layers: [{ id: layerId, name: "Default", visible: true, locked: false, order: 0 }],
|
|
80
|
+
defaultLayer: layerId,
|
|
81
|
+
};
|
|
82
|
+
this.diagram.pages.push(page);
|
|
83
|
+
this.emit({ type: "page_added", page });
|
|
84
|
+
this.diagram.activePage = page.id;
|
|
85
|
+
this.rebuildRegistry();
|
|
86
|
+
return page;
|
|
87
|
+
}
|
|
88
|
+
removePage(name) {
|
|
89
|
+
if (this.diagram.pages.length <= 1)
|
|
90
|
+
return false;
|
|
91
|
+
const idx = this.diagram.pages.findIndex((p) => p.name === name);
|
|
92
|
+
if (idx === -1)
|
|
93
|
+
return false;
|
|
94
|
+
const [removed] = this.diagram.pages.splice(idx, 1);
|
|
95
|
+
this.emit({ type: "page_removed", page: removed });
|
|
96
|
+
if (this.diagram.activePage === removed.id) {
|
|
97
|
+
this.diagram.activePage = this.diagram.pages[0].id;
|
|
98
|
+
this.rebuildRegistry();
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
// ── Shape CRUD ───────────────────────────────────────────
|
|
103
|
+
addShape(label, type, options = {}) {
|
|
104
|
+
const page = this.getActivePage();
|
|
105
|
+
const now = nextSequence();
|
|
106
|
+
// Compute size
|
|
107
|
+
const computedSize = options.size ?? computeDefaultSize(type, label);
|
|
108
|
+
// Compute position
|
|
109
|
+
const position = this.computePosition(page, options, computedSize);
|
|
110
|
+
// Build style — skip default theme if stencil provides its own colors
|
|
111
|
+
const effectiveTheme = options.skipDefaultTheme ? undefined : options.theme;
|
|
112
|
+
const style = this.buildShapeStyle(type, effectiveTheme, options.skipDefaultTheme);
|
|
113
|
+
const shape = {
|
|
114
|
+
id: nextShapeId(),
|
|
115
|
+
label,
|
|
116
|
+
type,
|
|
117
|
+
bounds: { x: position.x, y: position.y, width: computedSize.width, height: computedSize.height },
|
|
118
|
+
style,
|
|
119
|
+
parentGroup: options.inGroup ?? null,
|
|
120
|
+
layer: page.defaultLayer,
|
|
121
|
+
metadata: {},
|
|
122
|
+
baseStyleOverride: options.baseStyleOverride,
|
|
123
|
+
createdAt: now,
|
|
124
|
+
modifiedAt: now,
|
|
125
|
+
};
|
|
126
|
+
page.shapes.set(shape.id, shape);
|
|
127
|
+
// If placed in a group, add to group membership
|
|
128
|
+
if (options.inGroup) {
|
|
129
|
+
const group = page.groups.get(options.inGroup);
|
|
130
|
+
if (group) {
|
|
131
|
+
group.memberIds.add(shape.id);
|
|
132
|
+
this.recomputeGroupBounds(group, page);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
this.emit({ type: "shape_created", shape });
|
|
136
|
+
this.rebuildRegistry();
|
|
137
|
+
return shape;
|
|
138
|
+
}
|
|
139
|
+
modifyShape(id, changes) {
|
|
140
|
+
const page = this.getActivePage();
|
|
141
|
+
const shape = page.shapes.get(id);
|
|
142
|
+
if (!shape)
|
|
143
|
+
return null;
|
|
144
|
+
const before = {};
|
|
145
|
+
const after = {};
|
|
146
|
+
for (const [key, value] of Object.entries(changes)) {
|
|
147
|
+
if (value !== undefined) {
|
|
148
|
+
before[key] = shape[key];
|
|
149
|
+
after[key] = value;
|
|
150
|
+
shape[key] = value;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
shape.modifiedAt = nextSequence();
|
|
154
|
+
this.emit({ type: "shape_modified", id, before, after });
|
|
155
|
+
this.rebuildRegistry();
|
|
156
|
+
return shape;
|
|
157
|
+
}
|
|
158
|
+
removeShape(id) {
|
|
159
|
+
const page = this.getActivePage();
|
|
160
|
+
const shape = page.shapes.get(id);
|
|
161
|
+
if (!shape)
|
|
162
|
+
return null;
|
|
163
|
+
// Remove connected edges
|
|
164
|
+
const edgesToRemove = [];
|
|
165
|
+
for (const [edgeId, edge] of page.edges) {
|
|
166
|
+
if (edge.sourceId === id || edge.targetId === id) {
|
|
167
|
+
edgesToRemove.push(edgeId);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
for (const edgeId of edgesToRemove) {
|
|
171
|
+
this.removeEdge(edgeId);
|
|
172
|
+
}
|
|
173
|
+
// Remove from group
|
|
174
|
+
if (shape.parentGroup) {
|
|
175
|
+
const group = page.groups.get(shape.parentGroup);
|
|
176
|
+
if (group) {
|
|
177
|
+
group.memberIds.delete(id);
|
|
178
|
+
this.recomputeGroupBounds(group, page);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
page.shapes.delete(id);
|
|
182
|
+
this.emit({ type: "shape_deleted", shape });
|
|
183
|
+
this.rebuildRegistry();
|
|
184
|
+
return shape;
|
|
185
|
+
}
|
|
186
|
+
// ── Edge CRUD ────────────────────────────────────────────
|
|
187
|
+
addEdge(sourceId, targetId, options = {}) {
|
|
188
|
+
const page = this.getActivePage();
|
|
189
|
+
if (!page.shapes.has(sourceId) || !page.shapes.has(targetId))
|
|
190
|
+
return null;
|
|
191
|
+
const now = nextSequence();
|
|
192
|
+
const edgeStyle = { ...createDefaultEdgeStyle(), ...options.style };
|
|
193
|
+
const edge = {
|
|
194
|
+
id: nextEdgeId(),
|
|
195
|
+
sourceId,
|
|
196
|
+
targetId,
|
|
197
|
+
label: options.label ?? null,
|
|
198
|
+
style: edgeStyle,
|
|
199
|
+
waypoints: [],
|
|
200
|
+
sourceArrow: options.sourceArrow ?? "none",
|
|
201
|
+
targetArrow: options.targetArrow ?? "arrow",
|
|
202
|
+
createdAt: now,
|
|
203
|
+
modifiedAt: now,
|
|
204
|
+
};
|
|
205
|
+
page.edges.set(edge.id, edge);
|
|
206
|
+
this.emit({ type: "edge_created", edge });
|
|
207
|
+
this.rebuildRegistry();
|
|
208
|
+
return edge;
|
|
209
|
+
}
|
|
210
|
+
removeEdge(id) {
|
|
211
|
+
const page = this.getActivePage();
|
|
212
|
+
const edge = page.edges.get(id);
|
|
213
|
+
if (!edge)
|
|
214
|
+
return null;
|
|
215
|
+
page.edges.delete(id);
|
|
216
|
+
this.emit({ type: "edge_deleted", edge });
|
|
217
|
+
this.rebuildRegistry();
|
|
218
|
+
return edge;
|
|
219
|
+
}
|
|
220
|
+
modifyEdge(id, changes) {
|
|
221
|
+
const page = this.getActivePage();
|
|
222
|
+
const edge = page.edges.get(id);
|
|
223
|
+
if (!edge)
|
|
224
|
+
return null;
|
|
225
|
+
const before = {};
|
|
226
|
+
const after = {};
|
|
227
|
+
for (const [key, value] of Object.entries(changes)) {
|
|
228
|
+
if (value !== undefined) {
|
|
229
|
+
before[key] = edge[key];
|
|
230
|
+
after[key] = value;
|
|
231
|
+
edge[key] = value;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
edge.modifiedAt = nextSequence();
|
|
235
|
+
this.emit({ type: "edge_modified", id, before, after });
|
|
236
|
+
return edge;
|
|
237
|
+
}
|
|
238
|
+
findEdge(sourceId, targetId) {
|
|
239
|
+
const page = this.getActivePage();
|
|
240
|
+
for (const edge of page.edges.values()) {
|
|
241
|
+
if (edge.sourceId === sourceId && edge.targetId === targetId)
|
|
242
|
+
return edge;
|
|
243
|
+
}
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
// ── Group operations ─────────────────────────────────────
|
|
247
|
+
createGroup(name, memberIds) {
|
|
248
|
+
const page = this.getActivePage();
|
|
249
|
+
// Validate all members exist
|
|
250
|
+
for (const id of memberIds) {
|
|
251
|
+
if (!page.shapes.has(id))
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
const group = {
|
|
255
|
+
id: nextGroupId(),
|
|
256
|
+
name,
|
|
257
|
+
memberIds: new Set(memberIds),
|
|
258
|
+
isContainer: true,
|
|
259
|
+
collapsed: false,
|
|
260
|
+
bounds: { x: 0, y: 0, width: 0, height: 0 },
|
|
261
|
+
style: createDefaultStyle(),
|
|
262
|
+
};
|
|
263
|
+
// Set parent group on each member
|
|
264
|
+
for (const id of memberIds) {
|
|
265
|
+
const shape = page.shapes.get(id);
|
|
266
|
+
shape.parentGroup = group.id;
|
|
267
|
+
}
|
|
268
|
+
this.recomputeGroupBounds(group, page);
|
|
269
|
+
page.groups.set(group.id, group);
|
|
270
|
+
this.emit({ type: "group_created", group });
|
|
271
|
+
this.rebuildRegistry();
|
|
272
|
+
return group;
|
|
273
|
+
}
|
|
274
|
+
dissolveGroup(groupId) {
|
|
275
|
+
const page = this.getActivePage();
|
|
276
|
+
const group = page.groups.get(groupId);
|
|
277
|
+
if (!group)
|
|
278
|
+
return null;
|
|
279
|
+
// Clear parent group from members
|
|
280
|
+
for (const id of group.memberIds) {
|
|
281
|
+
const shape = page.shapes.get(id);
|
|
282
|
+
if (shape)
|
|
283
|
+
shape.parentGroup = null;
|
|
284
|
+
}
|
|
285
|
+
page.groups.delete(groupId);
|
|
286
|
+
this.emit({ type: "group_dissolved", group });
|
|
287
|
+
this.rebuildRegistry();
|
|
288
|
+
return group;
|
|
289
|
+
}
|
|
290
|
+
getGroupByName(name) {
|
|
291
|
+
const page = this.getActivePage();
|
|
292
|
+
for (const group of page.groups.values()) {
|
|
293
|
+
if (group.name === name)
|
|
294
|
+
return group;
|
|
295
|
+
}
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
// ── Layer CRUD ─────────────────────────────────────────
|
|
299
|
+
addLayer(name) {
|
|
300
|
+
const page = this.getActivePage();
|
|
301
|
+
const layer = {
|
|
302
|
+
id: nextLayerId(),
|
|
303
|
+
name,
|
|
304
|
+
visible: true,
|
|
305
|
+
locked: false,
|
|
306
|
+
order: page.layers.length,
|
|
307
|
+
};
|
|
308
|
+
page.layers.push(layer);
|
|
309
|
+
this.emit({ type: "layer_created", layer, pageId: page.id });
|
|
310
|
+
return layer;
|
|
311
|
+
}
|
|
312
|
+
modifyLayer(layerId, changes) {
|
|
313
|
+
const page = this.getActivePage();
|
|
314
|
+
const layer = page.layers.find((l) => l.id === layerId);
|
|
315
|
+
if (!layer)
|
|
316
|
+
return null;
|
|
317
|
+
const before = {};
|
|
318
|
+
const after = {};
|
|
319
|
+
for (const [key, value] of Object.entries(changes)) {
|
|
320
|
+
if (value !== undefined) {
|
|
321
|
+
before[key] = layer[key];
|
|
322
|
+
after[key] = value;
|
|
323
|
+
layer[key] = value;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
this.emit({ type: "layer_modified", pageId: page.id, layerId, before, after });
|
|
327
|
+
return layer;
|
|
328
|
+
}
|
|
329
|
+
// ── Flow direction ─────────────────────────────────────
|
|
330
|
+
setFlowDirection(dir) {
|
|
331
|
+
const page = this.getActivePage();
|
|
332
|
+
const before = page.flowDirection;
|
|
333
|
+
page.flowDirection = dir;
|
|
334
|
+
this.emit({ type: "flow_direction_changed", pageId: page.id, before, after: dir });
|
|
335
|
+
}
|
|
336
|
+
// ── Title ──────────────────────────────────────────────
|
|
337
|
+
setTitle(title) {
|
|
338
|
+
const before = this.diagram.title;
|
|
339
|
+
this.diagram.title = title;
|
|
340
|
+
this.emit({ type: "title_changed", before, after: title });
|
|
341
|
+
}
|
|
342
|
+
// ── Custom types ─────────────────────────────────────────
|
|
343
|
+
defineCustomType(name, base, options = {}) {
|
|
344
|
+
const ct = {
|
|
345
|
+
name,
|
|
346
|
+
base,
|
|
347
|
+
theme: options.theme,
|
|
348
|
+
badge: options.badge,
|
|
349
|
+
defaultSize: options.size,
|
|
350
|
+
};
|
|
351
|
+
this.diagram.customTypes.set(name, ct);
|
|
352
|
+
return ct;
|
|
353
|
+
}
|
|
354
|
+
defineCustomTheme(name, fill, stroke, fontColor) {
|
|
355
|
+
const ct = { name, fill, stroke, fontColor };
|
|
356
|
+
this.diagram.customThemes.set(name, ct);
|
|
357
|
+
return ct;
|
|
358
|
+
}
|
|
359
|
+
// ── Checkpoints and undo ─────────────────────────────────
|
|
360
|
+
checkpoint(name) {
|
|
361
|
+
this.eventLog.checkpoint(name);
|
|
362
|
+
}
|
|
363
|
+
undo(count = 1) {
|
|
364
|
+
const events = this.eventLog.undo(count);
|
|
365
|
+
for (const event of events) {
|
|
366
|
+
this.reverseEvent(event);
|
|
367
|
+
}
|
|
368
|
+
this.rebuildRegistry();
|
|
369
|
+
return events;
|
|
370
|
+
}
|
|
371
|
+
undoTo(checkpointName) {
|
|
372
|
+
const events = this.eventLog.undoTo(checkpointName);
|
|
373
|
+
if (!events)
|
|
374
|
+
return null;
|
|
375
|
+
for (const event of events) {
|
|
376
|
+
this.reverseEvent(event);
|
|
377
|
+
}
|
|
378
|
+
this.rebuildRegistry();
|
|
379
|
+
return events;
|
|
380
|
+
}
|
|
381
|
+
redo(count = 1) {
|
|
382
|
+
const events = this.eventLog.redo(count);
|
|
383
|
+
for (const event of events) {
|
|
384
|
+
this.replayEvent(event);
|
|
385
|
+
}
|
|
386
|
+
this.rebuildRegistry();
|
|
387
|
+
return events;
|
|
388
|
+
}
|
|
389
|
+
getHistory(count) {
|
|
390
|
+
return this.eventLog.recent(count);
|
|
391
|
+
}
|
|
392
|
+
canUndo() {
|
|
393
|
+
return this.eventLog.canUndo();
|
|
394
|
+
}
|
|
395
|
+
canRedo() {
|
|
396
|
+
return this.eventLog.canRedo();
|
|
397
|
+
}
|
|
398
|
+
/** Compact state digest for drift detection. */
|
|
399
|
+
getDigest() {
|
|
400
|
+
const page = this.getActivePage();
|
|
401
|
+
const pageIdx = this.diagram.pages.findIndex(p => p.id === this.diagram.activePage) + 1;
|
|
402
|
+
const totalPages = this.diagram.pages.length;
|
|
403
|
+
const bounds = this.computeCanvasBounds();
|
|
404
|
+
const canvasStr = bounds ? `${Math.round(bounds.width)}x${Math.round(bounds.height)} ` : "";
|
|
405
|
+
return `[${page.shapes.size}s ${page.edges.size}e ${page.groups.size}g ${canvasStr}p:${pageIdx}/${totalPages}]`;
|
|
406
|
+
}
|
|
407
|
+
/** Compute the bounding box of all shapes and groups on the active page. */
|
|
408
|
+
computeCanvasBounds() {
|
|
409
|
+
const page = this.getActivePage();
|
|
410
|
+
if (page.shapes.size === 0)
|
|
411
|
+
return null;
|
|
412
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
413
|
+
for (const shape of page.shapes.values()) {
|
|
414
|
+
minX = Math.min(minX, shape.bounds.x);
|
|
415
|
+
minY = Math.min(minY, shape.bounds.y);
|
|
416
|
+
maxX = Math.max(maxX, shape.bounds.x + shape.bounds.width);
|
|
417
|
+
maxY = Math.max(maxY, shape.bounds.y + shape.bounds.height);
|
|
418
|
+
}
|
|
419
|
+
for (const group of page.groups.values()) {
|
|
420
|
+
minX = Math.min(minX, group.bounds.x);
|
|
421
|
+
minY = Math.min(minY, group.bounds.y);
|
|
422
|
+
maxX = Math.max(maxX, group.bounds.x + group.bounds.width);
|
|
423
|
+
maxY = Math.max(maxY, group.bounds.y + group.bounds.height);
|
|
424
|
+
}
|
|
425
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Resolve a named canvas region to absolute coordinates.
|
|
429
|
+
* Regions: top-left, top-center, top-right, middle-left, center, middle-right,
|
|
430
|
+
* bottom-left, bottom-center, bottom-right
|
|
431
|
+
* Centers the entity of given size within the region.
|
|
432
|
+
*/
|
|
433
|
+
resolveCanvasRegion(region, entitySize) {
|
|
434
|
+
const canvas = this.computeCanvasBounds();
|
|
435
|
+
if (!canvas) {
|
|
436
|
+
// No shapes yet — place in a default 800x600 canvas
|
|
437
|
+
return this.resolveRegionInBounds({ x: 0, y: 0, width: 800, height: 600 }, region, entitySize);
|
|
438
|
+
}
|
|
439
|
+
// Add margin around existing content
|
|
440
|
+
const margin = 60;
|
|
441
|
+
const expanded = {
|
|
442
|
+
x: canvas.x - margin,
|
|
443
|
+
y: canvas.y - margin,
|
|
444
|
+
width: canvas.width + margin * 2,
|
|
445
|
+
height: canvas.height + margin * 2,
|
|
446
|
+
};
|
|
447
|
+
return this.resolveRegionInBounds(expanded, region, entitySize);
|
|
448
|
+
}
|
|
449
|
+
resolveRegionInBounds(bounds, region, entitySize) {
|
|
450
|
+
const thirdW = bounds.width / 3;
|
|
451
|
+
const thirdH = bounds.height / 3;
|
|
452
|
+
let col; // 0=left, 1=center, 2=right
|
|
453
|
+
let row; // 0=top, 1=middle, 2=bottom
|
|
454
|
+
switch (region) {
|
|
455
|
+
case "top-left":
|
|
456
|
+
row = 0;
|
|
457
|
+
col = 0;
|
|
458
|
+
break;
|
|
459
|
+
case "top-center":
|
|
460
|
+
row = 0;
|
|
461
|
+
col = 1;
|
|
462
|
+
break;
|
|
463
|
+
case "top-right":
|
|
464
|
+
row = 0;
|
|
465
|
+
col = 2;
|
|
466
|
+
break;
|
|
467
|
+
case "middle-left":
|
|
468
|
+
row = 1;
|
|
469
|
+
col = 0;
|
|
470
|
+
break;
|
|
471
|
+
case "center":
|
|
472
|
+
row = 1;
|
|
473
|
+
col = 1;
|
|
474
|
+
break;
|
|
475
|
+
case "middle-right":
|
|
476
|
+
row = 1;
|
|
477
|
+
col = 2;
|
|
478
|
+
break;
|
|
479
|
+
case "bottom-left":
|
|
480
|
+
row = 2;
|
|
481
|
+
col = 0;
|
|
482
|
+
break;
|
|
483
|
+
case "bottom-center":
|
|
484
|
+
row = 2;
|
|
485
|
+
col = 1;
|
|
486
|
+
break;
|
|
487
|
+
case "bottom-right":
|
|
488
|
+
row = 2;
|
|
489
|
+
col = 2;
|
|
490
|
+
break;
|
|
491
|
+
default: return null;
|
|
492
|
+
}
|
|
493
|
+
// Center entity within the region cell
|
|
494
|
+
const cellX = bounds.x + col * thirdW;
|
|
495
|
+
const cellY = bounds.y + row * thirdH;
|
|
496
|
+
return {
|
|
497
|
+
x: Math.round(cellX + (thirdW - entitySize.width) / 2),
|
|
498
|
+
y: Math.round(cellY + (thirdH - entitySize.height) / 2),
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
/** Public wrapper for recomputing group bounds. */
|
|
502
|
+
recomputeGroupBoundsPublic(groupId) {
|
|
503
|
+
const page = this.getActivePage();
|
|
504
|
+
const group = page.groups.get(groupId);
|
|
505
|
+
if (group)
|
|
506
|
+
this.recomputeGroupBounds(group, page);
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Detect and resolve collisions after moving an entity.
|
|
510
|
+
* Pushes overlapping downstream items in the flow direction.
|
|
511
|
+
* Returns the number of items shifted.
|
|
512
|
+
*/
|
|
513
|
+
detectAndResolveCollisions(entityId, isGroup, maxDepth = 5) {
|
|
514
|
+
const page = this.getActivePage();
|
|
515
|
+
const flowDir = page.flowDirection ?? "TB";
|
|
516
|
+
// Get the bounds of the moved entity
|
|
517
|
+
let movedBounds;
|
|
518
|
+
if (isGroup) {
|
|
519
|
+
const group = page.groups.get(entityId);
|
|
520
|
+
if (!group)
|
|
521
|
+
return 0;
|
|
522
|
+
movedBounds = group.bounds;
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
const shape = page.shapes.get(entityId);
|
|
526
|
+
if (!shape)
|
|
527
|
+
return 0;
|
|
528
|
+
movedBounds = shape.bounds;
|
|
529
|
+
}
|
|
530
|
+
const groupedShapeIds = new Set();
|
|
531
|
+
for (const group of page.groups.values()) {
|
|
532
|
+
for (const id of group.memberIds)
|
|
533
|
+
groupedShapeIds.add(id);
|
|
534
|
+
}
|
|
535
|
+
const entities = [];
|
|
536
|
+
// Add ungrouped shapes (excluding the moved entity)
|
|
537
|
+
for (const shape of page.shapes.values()) {
|
|
538
|
+
if (groupedShapeIds.has(shape.id))
|
|
539
|
+
continue;
|
|
540
|
+
if (!isGroup && shape.id === entityId)
|
|
541
|
+
continue;
|
|
542
|
+
entities.push({ id: shape.id, bounds: shape.bounds, isGroup: false });
|
|
543
|
+
}
|
|
544
|
+
// Add groups (excluding the moved group)
|
|
545
|
+
for (const group of page.groups.values()) {
|
|
546
|
+
if (isGroup && group.id === entityId)
|
|
547
|
+
continue;
|
|
548
|
+
entities.push({ id: group.id, bounds: group.bounds, isGroup: true });
|
|
549
|
+
}
|
|
550
|
+
// Ripple: push overlapping downstream entities
|
|
551
|
+
let totalShifted = 0;
|
|
552
|
+
const pushed = new Set(); // track already-pushed IDs
|
|
553
|
+
let waveBounds = [movedBounds]; // bounds that may cause ripple
|
|
554
|
+
for (let depth = 0; depth < maxDepth && waveBounds.length > 0; depth++) {
|
|
555
|
+
const nextWave = [];
|
|
556
|
+
for (const sourceBounds of waveBounds) {
|
|
557
|
+
for (const entity of entities) {
|
|
558
|
+
if (pushed.has(entity.id))
|
|
559
|
+
continue;
|
|
560
|
+
if (!isDownstream(sourceBounds, entity.bounds, flowDir))
|
|
561
|
+
continue;
|
|
562
|
+
if (!boundsOverlap(sourceBounds, entity.bounds))
|
|
563
|
+
continue;
|
|
564
|
+
const push = computePushVector(sourceBounds, entity.bounds, flowDir);
|
|
565
|
+
if (!push)
|
|
566
|
+
continue;
|
|
567
|
+
// Apply push
|
|
568
|
+
if (entity.isGroup) {
|
|
569
|
+
this.pushGroup(entity.id, push.dx, push.dy);
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
this.pushShape(entity.id, push.dx, push.dy);
|
|
573
|
+
}
|
|
574
|
+
pushed.add(entity.id);
|
|
575
|
+
totalShifted++;
|
|
576
|
+
// Update entity bounds for future iterations
|
|
577
|
+
entity.bounds = {
|
|
578
|
+
...entity.bounds,
|
|
579
|
+
x: entity.bounds.x + push.dx,
|
|
580
|
+
y: entity.bounds.y + push.dy,
|
|
581
|
+
};
|
|
582
|
+
nextWave.push(entity.bounds);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
waveBounds = nextWave;
|
|
586
|
+
}
|
|
587
|
+
return totalShifted;
|
|
588
|
+
}
|
|
589
|
+
pushShape(shapeId, dx, dy) {
|
|
590
|
+
const page = this.getActivePage();
|
|
591
|
+
const shape = page.shapes.get(shapeId);
|
|
592
|
+
if (!shape)
|
|
593
|
+
return;
|
|
594
|
+
const before = { bounds: { ...shape.bounds } };
|
|
595
|
+
shape.bounds = { ...shape.bounds, x: shape.bounds.x + dx, y: shape.bounds.y + dy };
|
|
596
|
+
shape.modifiedAt = nextSequence();
|
|
597
|
+
this.emit({
|
|
598
|
+
type: "shape_modified",
|
|
599
|
+
id: shapeId,
|
|
600
|
+
before,
|
|
601
|
+
after: { bounds: { ...shape.bounds } },
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
pushGroup(groupId, dx, dy) {
|
|
605
|
+
const page = this.getActivePage();
|
|
606
|
+
const group = page.groups.get(groupId);
|
|
607
|
+
if (!group)
|
|
608
|
+
return;
|
|
609
|
+
// Move all member shapes
|
|
610
|
+
for (const memberId of group.memberIds) {
|
|
611
|
+
this.pushShape(memberId, dx, dy);
|
|
612
|
+
}
|
|
613
|
+
// Recompute group bounds
|
|
614
|
+
this.recomputeGroupBounds(group, page);
|
|
615
|
+
}
|
|
616
|
+
// ── Layout application ──────────────────────────────────────
|
|
617
|
+
/**
|
|
618
|
+
* Apply an ELK layout result: update shape positions, edge waypoints, and recompute group bounds.
|
|
619
|
+
* Emits shape_modified/edge_modified events for undo support.
|
|
620
|
+
*/
|
|
621
|
+
applyLayout(result) {
|
|
622
|
+
const page = this.getActivePage();
|
|
623
|
+
let count = 0;
|
|
624
|
+
// Update shape positions
|
|
625
|
+
for (const [id, pos] of result.shapePositions) {
|
|
626
|
+
const shape = page.shapes.get(id);
|
|
627
|
+
if (shape) {
|
|
628
|
+
const before = { bounds: { ...shape.bounds } };
|
|
629
|
+
shape.bounds = { ...shape.bounds, x: pos.x, y: pos.y };
|
|
630
|
+
shape.modifiedAt = nextSequence();
|
|
631
|
+
this.emit({
|
|
632
|
+
type: "shape_modified",
|
|
633
|
+
id,
|
|
634
|
+
before,
|
|
635
|
+
after: { bounds: { ...shape.bounds } },
|
|
636
|
+
});
|
|
637
|
+
count++;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
// Update edge waypoints
|
|
641
|
+
for (const [id, waypoints] of result.edgeWaypoints) {
|
|
642
|
+
const edge = page.edges.get(id);
|
|
643
|
+
if (edge && waypoints.length > 0) {
|
|
644
|
+
const before = { waypoints: [...edge.waypoints] };
|
|
645
|
+
edge.waypoints = waypoints;
|
|
646
|
+
edge.modifiedAt = nextSequence();
|
|
647
|
+
this.emit({
|
|
648
|
+
type: "edge_modified",
|
|
649
|
+
id,
|
|
650
|
+
before,
|
|
651
|
+
after: { waypoints: [...waypoints] },
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
// Recompute group bounds
|
|
656
|
+
for (const [, group] of page.groups) {
|
|
657
|
+
this.recomputeGroupBounds(group, page);
|
|
658
|
+
}
|
|
659
|
+
this.rebuildRegistry();
|
|
660
|
+
return count;
|
|
661
|
+
}
|
|
662
|
+
// ── Position computation ─────────────────────────────────
|
|
663
|
+
computePosition(page, options, size) {
|
|
664
|
+
// Absolute position
|
|
665
|
+
if (options.at)
|
|
666
|
+
return options.at;
|
|
667
|
+
// Relative to another shape
|
|
668
|
+
if (options.near) {
|
|
669
|
+
const ref = page.shapes.get(options.near);
|
|
670
|
+
if (ref) {
|
|
671
|
+
return this.positionRelativeTo(ref.bounds, size, options.dir ?? "below");
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
// Relative to most recent shape
|
|
675
|
+
const recent = this.registry.getMostRecent(1);
|
|
676
|
+
if (recent.length > 0) {
|
|
677
|
+
return this.positionRelativeTo(recent[0].bounds, { width: size.width, height: size.height }, "below");
|
|
678
|
+
}
|
|
679
|
+
// First shape on empty page
|
|
680
|
+
return FIRST_SHAPE_POS;
|
|
681
|
+
}
|
|
682
|
+
positionRelativeTo(ref, size, dir) {
|
|
683
|
+
const gap = DEFAULT_GAP;
|
|
684
|
+
const refCx = ref.x + ref.width / 2;
|
|
685
|
+
const refCy = ref.y + ref.height / 2;
|
|
686
|
+
switch (dir) {
|
|
687
|
+
case "below":
|
|
688
|
+
return { x: refCx - size.width / 2, y: ref.y + ref.height + gap };
|
|
689
|
+
case "above":
|
|
690
|
+
return { x: refCx - size.width / 2, y: ref.y - gap - size.height };
|
|
691
|
+
case "right":
|
|
692
|
+
return { x: ref.x + ref.width + gap, y: refCy - size.height / 2 };
|
|
693
|
+
case "left":
|
|
694
|
+
return { x: ref.x - gap - size.width, y: refCy - size.height / 2 };
|
|
695
|
+
case "below-right":
|
|
696
|
+
return { x: ref.x + ref.width + gap, y: ref.y + ref.height + gap };
|
|
697
|
+
case "below-left":
|
|
698
|
+
return { x: ref.x - gap - size.width, y: ref.y + ref.height + gap };
|
|
699
|
+
case "above-right":
|
|
700
|
+
return { x: ref.x + ref.width + gap, y: ref.y - gap - size.height };
|
|
701
|
+
case "above-left":
|
|
702
|
+
return { x: ref.x - gap - size.width, y: ref.y - gap - size.height };
|
|
703
|
+
default:
|
|
704
|
+
return { x: refCx - size.width / 2, y: ref.y + ref.height + gap };
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
// ── Style building ───────────────────────────────────────
|
|
708
|
+
buildShapeStyle(type, theme, skipDefaultTheme) {
|
|
709
|
+
const style = createDefaultStyle();
|
|
710
|
+
const typeDef = NODE_TYPES[type];
|
|
711
|
+
if (typeDef && typeDef.baseStyle.includes("rounded=1")) {
|
|
712
|
+
style.rounded = true;
|
|
713
|
+
}
|
|
714
|
+
// When using a stencil with no explicit theme, skip applying default "blue" theme
|
|
715
|
+
// so stencil's embedded colors pass through
|
|
716
|
+
if (skipDefaultTheme && !theme) {
|
|
717
|
+
return style;
|
|
718
|
+
}
|
|
719
|
+
// Apply theme colors
|
|
720
|
+
const themeName = theme ?? "blue";
|
|
721
|
+
if (isThemeName(themeName)) {
|
|
722
|
+
const colors = THEMES[themeName];
|
|
723
|
+
style.fillColor = colors.fill;
|
|
724
|
+
style.strokeColor = colors.stroke;
|
|
725
|
+
if (colors.fontColor)
|
|
726
|
+
style.fontColor = colors.fontColor;
|
|
727
|
+
}
|
|
728
|
+
return style;
|
|
729
|
+
}
|
|
730
|
+
// ── Group bounds ─────────────────────────────────────────
|
|
731
|
+
recomputeGroupBounds(group, page) {
|
|
732
|
+
const paddingX = 40;
|
|
733
|
+
const paddingBottom = 35;
|
|
734
|
+
const paddingTop = 50; // room for bold group label
|
|
735
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
736
|
+
for (const id of group.memberIds) {
|
|
737
|
+
const shape = page.shapes.get(id);
|
|
738
|
+
if (!shape)
|
|
739
|
+
continue;
|
|
740
|
+
minX = Math.min(minX, shape.bounds.x);
|
|
741
|
+
minY = Math.min(minY, shape.bounds.y);
|
|
742
|
+
maxX = Math.max(maxX, shape.bounds.x + shape.bounds.width);
|
|
743
|
+
maxY = Math.max(maxY, shape.bounds.y + shape.bounds.height);
|
|
744
|
+
}
|
|
745
|
+
if (minX !== Infinity) {
|
|
746
|
+
let width = maxX - minX + paddingX * 2;
|
|
747
|
+
// Ensure minimum width so group label isn't truncated (~8px per char)
|
|
748
|
+
const minLabelWidth = group.name.length * 9 + paddingX * 2;
|
|
749
|
+
width = Math.max(width, minLabelWidth);
|
|
750
|
+
group.bounds = {
|
|
751
|
+
x: minX - paddingX,
|
|
752
|
+
y: minY - paddingTop,
|
|
753
|
+
width,
|
|
754
|
+
height: maxY - minY + paddingTop + paddingBottom,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
// ── Event handling ───────────────────────────────────────
|
|
759
|
+
emit(event) {
|
|
760
|
+
this.eventLog.append(event);
|
|
761
|
+
this.diagram.metadata.modified = new Date().toISOString();
|
|
762
|
+
}
|
|
763
|
+
reverseEvent(event) {
|
|
764
|
+
const page = this.getActivePage();
|
|
765
|
+
switch (event.type) {
|
|
766
|
+
case "shape_created":
|
|
767
|
+
page.shapes.delete(event.shape.id);
|
|
768
|
+
break;
|
|
769
|
+
case "shape_deleted":
|
|
770
|
+
page.shapes.set(event.shape.id, { ...event.shape });
|
|
771
|
+
break;
|
|
772
|
+
case "shape_modified": {
|
|
773
|
+
const shape = page.shapes.get(event.id);
|
|
774
|
+
if (shape)
|
|
775
|
+
Object.assign(shape, event.before);
|
|
776
|
+
break;
|
|
777
|
+
}
|
|
778
|
+
case "edge_created":
|
|
779
|
+
page.edges.delete(event.edge.id);
|
|
780
|
+
break;
|
|
781
|
+
case "edge_deleted":
|
|
782
|
+
page.edges.set(event.edge.id, { ...event.edge });
|
|
783
|
+
break;
|
|
784
|
+
case "edge_modified": {
|
|
785
|
+
const edge = page.edges.get(event.id);
|
|
786
|
+
if (edge)
|
|
787
|
+
Object.assign(edge, event.before);
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
case "group_created":
|
|
791
|
+
page.groups.delete(event.group.id);
|
|
792
|
+
for (const id of event.group.memberIds) {
|
|
793
|
+
const shape = page.shapes.get(id);
|
|
794
|
+
if (shape)
|
|
795
|
+
shape.parentGroup = null;
|
|
796
|
+
}
|
|
797
|
+
break;
|
|
798
|
+
case "group_dissolved":
|
|
799
|
+
page.groups.set(event.group.id, {
|
|
800
|
+
...event.group,
|
|
801
|
+
memberIds: new Set(event.group.memberIds),
|
|
802
|
+
});
|
|
803
|
+
for (const id of event.group.memberIds) {
|
|
804
|
+
const shape = page.shapes.get(id);
|
|
805
|
+
if (shape)
|
|
806
|
+
shape.parentGroup = event.group.id;
|
|
807
|
+
}
|
|
808
|
+
break;
|
|
809
|
+
case "page_added": {
|
|
810
|
+
const idx = this.diagram.pages.findIndex((p) => p.id === event.page.id);
|
|
811
|
+
if (idx !== -1)
|
|
812
|
+
this.diagram.pages.splice(idx, 1);
|
|
813
|
+
break;
|
|
814
|
+
}
|
|
815
|
+
case "page_removed":
|
|
816
|
+
this.diagram.pages.push(event.page);
|
|
817
|
+
break;
|
|
818
|
+
case "layer_created": {
|
|
819
|
+
const p = this.diagram.pages.find((pg) => pg.id === event.pageId);
|
|
820
|
+
if (p) {
|
|
821
|
+
const idx = p.layers.findIndex((l) => l.id === event.layer.id);
|
|
822
|
+
if (idx !== -1)
|
|
823
|
+
p.layers.splice(idx, 1);
|
|
824
|
+
}
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
case "layer_modified": {
|
|
828
|
+
const p = this.diagram.pages.find((pg) => pg.id === event.pageId);
|
|
829
|
+
if (p) {
|
|
830
|
+
const layer = p.layers.find((l) => l.id === event.layerId);
|
|
831
|
+
if (layer)
|
|
832
|
+
Object.assign(layer, event.before);
|
|
833
|
+
}
|
|
834
|
+
break;
|
|
835
|
+
}
|
|
836
|
+
case "flow_direction_changed": {
|
|
837
|
+
const p = this.diagram.pages.find((pg) => pg.id === event.pageId);
|
|
838
|
+
if (p)
|
|
839
|
+
p.flowDirection = event.before;
|
|
840
|
+
break;
|
|
841
|
+
}
|
|
842
|
+
case "title_changed":
|
|
843
|
+
this.diagram.title = event.before;
|
|
844
|
+
break;
|
|
845
|
+
case "checkpoint":
|
|
846
|
+
// No-op for undo
|
|
847
|
+
break;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
replayEvent(event) {
|
|
851
|
+
const page = this.getActivePage();
|
|
852
|
+
switch (event.type) {
|
|
853
|
+
case "shape_created":
|
|
854
|
+
page.shapes.set(event.shape.id, { ...event.shape });
|
|
855
|
+
break;
|
|
856
|
+
case "shape_deleted":
|
|
857
|
+
page.shapes.delete(event.shape.id);
|
|
858
|
+
break;
|
|
859
|
+
case "shape_modified": {
|
|
860
|
+
const shape = page.shapes.get(event.id);
|
|
861
|
+
if (shape)
|
|
862
|
+
Object.assign(shape, event.after);
|
|
863
|
+
break;
|
|
864
|
+
}
|
|
865
|
+
case "edge_created":
|
|
866
|
+
page.edges.set(event.edge.id, { ...event.edge });
|
|
867
|
+
break;
|
|
868
|
+
case "edge_deleted":
|
|
869
|
+
page.edges.delete(event.edge.id);
|
|
870
|
+
break;
|
|
871
|
+
case "edge_modified": {
|
|
872
|
+
const edge = page.edges.get(event.id);
|
|
873
|
+
if (edge)
|
|
874
|
+
Object.assign(edge, event.after);
|
|
875
|
+
break;
|
|
876
|
+
}
|
|
877
|
+
case "group_created":
|
|
878
|
+
page.groups.set(event.group.id, {
|
|
879
|
+
...event.group,
|
|
880
|
+
memberIds: new Set(event.group.memberIds),
|
|
881
|
+
});
|
|
882
|
+
for (const id of event.group.memberIds) {
|
|
883
|
+
const shape = page.shapes.get(id);
|
|
884
|
+
if (shape)
|
|
885
|
+
shape.parentGroup = event.group.id;
|
|
886
|
+
}
|
|
887
|
+
break;
|
|
888
|
+
case "group_dissolved":
|
|
889
|
+
page.groups.delete(event.group.id);
|
|
890
|
+
for (const id of event.group.memberIds) {
|
|
891
|
+
const shape = page.shapes.get(id);
|
|
892
|
+
if (shape)
|
|
893
|
+
shape.parentGroup = null;
|
|
894
|
+
}
|
|
895
|
+
break;
|
|
896
|
+
case "page_added":
|
|
897
|
+
this.diagram.pages.push(event.page);
|
|
898
|
+
break;
|
|
899
|
+
case "page_removed": {
|
|
900
|
+
const idx = this.diagram.pages.findIndex((p) => p.id === event.page.id);
|
|
901
|
+
if (idx !== -1)
|
|
902
|
+
this.diagram.pages.splice(idx, 1);
|
|
903
|
+
break;
|
|
904
|
+
}
|
|
905
|
+
case "layer_created": {
|
|
906
|
+
const p = this.diagram.pages.find((pg) => pg.id === event.pageId);
|
|
907
|
+
if (p)
|
|
908
|
+
p.layers.push({ ...event.layer });
|
|
909
|
+
break;
|
|
910
|
+
}
|
|
911
|
+
case "layer_modified": {
|
|
912
|
+
const p = this.diagram.pages.find((pg) => pg.id === event.pageId);
|
|
913
|
+
if (p) {
|
|
914
|
+
const layer = p.layers.find((l) => l.id === event.layerId);
|
|
915
|
+
if (layer)
|
|
916
|
+
Object.assign(layer, event.after);
|
|
917
|
+
}
|
|
918
|
+
break;
|
|
919
|
+
}
|
|
920
|
+
case "flow_direction_changed": {
|
|
921
|
+
const p = this.diagram.pages.find((pg) => pg.id === event.pageId);
|
|
922
|
+
if (p)
|
|
923
|
+
p.flowDirection = event.after;
|
|
924
|
+
break;
|
|
925
|
+
}
|
|
926
|
+
case "title_changed":
|
|
927
|
+
this.diagram.title = event.after;
|
|
928
|
+
break;
|
|
929
|
+
case "checkpoint":
|
|
930
|
+
break;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
// ── Registry rebuild ─────────────────────────────────────
|
|
934
|
+
rebuildRegistry() {
|
|
935
|
+
this.registry.rebuild(this.getActivePage());
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
//# sourceMappingURL=diagram-model.js.map
|