@dforge-core/diagram 0.0.1
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 +45 -0
- package/dist/EntityNode.svelte +167 -0
- package/dist/EntityNode.svelte.d.ts +8 -0
- package/dist/EntityNode.svelte.d.ts.map +1 -0
- package/dist/IconButton.svelte +83 -0
- package/dist/IconButton.svelte.d.ts +13 -0
- package/dist/IconButton.svelte.d.ts.map +1 -0
- package/dist/SimpleDiagram/DiagramDebug.svelte +499 -0
- package/dist/SimpleDiagram/DiagramDebug.svelte.d.ts +4 -0
- package/dist/SimpleDiagram/DiagramDebug.svelte.d.ts.map +1 -0
- package/dist/SimpleDiagram/ModelDiagram.svelte +569 -0
- package/dist/SimpleDiagram/ModelDiagram.svelte.d.ts +18 -0
- package/dist/SimpleDiagram/ModelDiagram.svelte.d.ts.map +1 -0
- package/dist/SimpleDiagram/NOTE.md +14 -0
- package/dist/SimpleDiagram/SimpleDiagram.svelte +755 -0
- package/dist/SimpleDiagram/SimpleDiagram.svelte.d.ts +13 -0
- package/dist/SimpleDiagram/SimpleDiagram.svelte.d.ts.map +1 -0
- package/dist/SimpleDiagram/diagram-geometry.d.ts +33 -0
- package/dist/SimpleDiagram/diagram-geometry.d.ts.map +1 -0
- package/dist/SimpleDiagram/diagram-geometry.js +137 -0
- package/dist/SimpleDiagram/simple-layout.d.ts +27 -0
- package/dist/SimpleDiagram/simple-layout.d.ts.map +1 -0
- package/dist/SimpleDiagram/simple-layout.js +364 -0
- package/dist/WorkspaceMap/ModuleCard.svelte +165 -0
- package/dist/WorkspaceMap/ModuleCard.svelte.d.ts +8 -0
- package/dist/WorkspaceMap/ModuleCard.svelte.d.ts.map +1 -0
- package/dist/WorkspaceMap/WorkspaceMap.svelte +319 -0
- package/dist/WorkspaceMap/WorkspaceMap.svelte.d.ts +12 -0
- package/dist/WorkspaceMap/WorkspaceMap.svelte.d.ts.map +1 -0
- package/dist/WorkspaceMap/workspace-graph.d.ts +18 -0
- package/dist/WorkspaceMap/workspace-graph.d.ts.map +1 -0
- package/dist/WorkspaceMap/workspace-graph.js +168 -0
- package/dist/WorkspaceMap/workspace-types.d.ts +32 -0
- package/dist/WorkspaceMap/workspace-types.d.ts.map +1 -0
- package/dist/WorkspaceMap/workspace-types.js +1 -0
- package/dist/diagram-types.d.ts +47 -0
- package/dist/diagram-types.d.ts.map +1 -0
- package/dist/diagram-types.js +2 -0
- package/dist/edge-routing.d.ts +82 -0
- package/dist/edge-routing.d.ts.map +1 -0
- package/dist/edge-routing.js +388 -0
- package/dist/i18n.d.ts +13 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +20 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/local-storage-sync.d.ts +10 -0
- package/dist/local-storage-sync.d.ts.map +1 -0
- package/dist/local-storage-sync.js +39 -0
- package/dist/model-graph.d.ts +20 -0
- package/dist/model-graph.d.ts.map +1 -0
- package/dist/model-graph.js +100 -0
- package/dist/model-types.d.ts +55 -0
- package/dist/model-types.d.ts.map +1 -0
- package/dist/model-types.js +11 -0
- package/dist/pan-zoom.svelte.d.ts +34 -0
- package/dist/pan-zoom.svelte.d.ts.map +1 -0
- package/dist/pan-zoom.svelte.js +130 -0
- package/package.json +55 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import IconButton from '../IconButton.svelte';
|
|
4
|
+
import type { DiagramNode, DiagramEdge, Point, DragSegment } from '../diagram-types';
|
|
5
|
+
import { computeEdgePath, extractBendPoints, getEdgeHandles, prepareDrag, dragSegment, splitSegment, consolidateBends, PORT_DOT_RADIUS, type EdgePath, type SegmentHandle } from '../edge-routing';
|
|
6
|
+
import { getDiagramTranslations } from '../i18n';
|
|
7
|
+
import { createPanZoom } from '../pan-zoom.svelte';
|
|
8
|
+
import EntityNode from '../EntityNode.svelte';
|
|
9
|
+
|
|
10
|
+
const t = getDiagramTranslations();
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
nodes: DiagramNode[];
|
|
14
|
+
edges: DiagramEdge[];
|
|
15
|
+
initialBendPoints?: Record<string, Point[]>;
|
|
16
|
+
minZoom?: number;
|
|
17
|
+
maxZoom?: number;
|
|
18
|
+
onBendPointsChange?: (bends: Record<string, Point[]>) => void;
|
|
19
|
+
onEdgePathsChange?: (paths: { edge: DiagramEdge; path: EdgePath }[]) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let { nodes, edges, initialBendPoints, minZoom = 0.1, maxZoom = 2, onBendPointsChange, onEdgePathsChange }: Props = $props();
|
|
23
|
+
|
|
24
|
+
let svgEl: SVGSVGElement;
|
|
25
|
+
let containerEl: HTMLDivElement;
|
|
26
|
+
// Drag offsets: only contains entries for nodes that have been dragged.
|
|
27
|
+
// Reset when node set changes (new layout loaded).
|
|
28
|
+
let dragOffsets = $state<Record<string, { dx: number; dy: number }>>({});
|
|
29
|
+
let selectedEdgeId = $state<string | null>(null);
|
|
30
|
+
let edgeBendPoints = $state<Record<string, Point[]>>({});
|
|
31
|
+
let draggingSeg = $state<DragSegment | null>(null);
|
|
32
|
+
let dragging = $state<{ nodeId: string; offsetX: number; offsetY: number } | null>(null);
|
|
33
|
+
let patternId = `grid-${Math.random().toString(36).slice(2, 8)}`;
|
|
34
|
+
|
|
35
|
+
const pz = createPanZoom({
|
|
36
|
+
get minZoom() { return minZoom; },
|
|
37
|
+
get maxZoom() { return maxZoom; },
|
|
38
|
+
getSvgEl: () => svgEl,
|
|
39
|
+
getContainerEl: () => containerEl,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const nodeIds = $derived(nodes.map((n) => n.id).join(','));
|
|
43
|
+
$effect(() => {
|
|
44
|
+
nodeIds; // track
|
|
45
|
+
dragOffsets = {};
|
|
46
|
+
selectedEdgeId = null;
|
|
47
|
+
if (initialBendPoints) {
|
|
48
|
+
// Consolidate initial bends (remove collinear/duplicate points)
|
|
49
|
+
const nodeMap: Record<string, DiagramNode> = {};
|
|
50
|
+
for (const n of nodes) nodeMap[n.id] = n;
|
|
51
|
+
const cleaned: Record<string, Point[]> = {};
|
|
52
|
+
for (const [edgeId, bends] of Object.entries(initialBendPoints)) {
|
|
53
|
+
const edge = edges.find((e) => e.id === edgeId);
|
|
54
|
+
cleaned[edgeId] = edge ? consolidateBends(edge, nodeMap, bends) : bends;
|
|
55
|
+
}
|
|
56
|
+
edgeBendPoints = cleaned;
|
|
57
|
+
} else {
|
|
58
|
+
edgeBendPoints = {};
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
$effect(() => {
|
|
62
|
+
onBendPointsChange?.(edgeBendPoints);
|
|
63
|
+
});
|
|
64
|
+
$effect(() => {
|
|
65
|
+
onEdgePathsChange?.(edgePaths);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Derive node positions: base from props + drag offsets
|
|
69
|
+
const nodePositions = $derived.by(() => {
|
|
70
|
+
const pos: Record<string, { x: number; y: number }> = {};
|
|
71
|
+
for (const n of nodes) {
|
|
72
|
+
const off = dragOffsets[n.id];
|
|
73
|
+
pos[n.id] = off ? { x: n.x + off.dx, y: n.y + off.dy } : { x: n.x, y: n.y };
|
|
74
|
+
}
|
|
75
|
+
return pos;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Build a node lookup (with updated positions) for edge routing
|
|
79
|
+
const resolvedNodes = $derived.by(() => {
|
|
80
|
+
const map: Record<string, DiagramNode> = {};
|
|
81
|
+
for (const n of nodes) {
|
|
82
|
+
const pos = nodePositions[n.id];
|
|
83
|
+
map[n.id] = pos ? { ...n, x: pos.x, y: pos.y } : n;
|
|
84
|
+
}
|
|
85
|
+
return map;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Compute edge paths reactively
|
|
89
|
+
const edgePaths = $derived.by(() => {
|
|
90
|
+
const result: { edge: DiagramEdge; path: EdgePath }[] = [];
|
|
91
|
+
for (const e of edges) {
|
|
92
|
+
const p = computeEdgePath(e, resolvedNodes, edgeBendPoints[e.id]);
|
|
93
|
+
if (p) result.push({ edge: e, path: p });
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Compute handles for the selected edge
|
|
99
|
+
const selectedEdgeHandles = $derived.by(() => {
|
|
100
|
+
if (!selectedEdgeId) return null;
|
|
101
|
+
const edge = edges.find((e) => e.id === selectedEdgeId);
|
|
102
|
+
if (!edge) return null;
|
|
103
|
+
return getEdgeHandles(edge, resolvedNodes, edgeBendPoints[selectedEdgeId]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// --- Event handlers ---
|
|
107
|
+
|
|
108
|
+
function handleBackgroundPointerDown(e: PointerEvent) {
|
|
109
|
+
// Only start pan from background (svg itself or the grid rect)
|
|
110
|
+
const target = e.target as Element;
|
|
111
|
+
if (target !== svgEl && !target.classList.contains('diagram-bg')) return;
|
|
112
|
+
selectedEdgeId = null;
|
|
113
|
+
pz.startPan(e);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function handlePointerMove(e: PointerEvent) {
|
|
117
|
+
if (pz.onPointerMove(e)) return;
|
|
118
|
+
if (draggingSeg) {
|
|
119
|
+
const pos = pz.clientToSvg(e.clientX, e.clientY);
|
|
120
|
+
const bends = edgeBendPoints[draggingSeg.edgeId] ?? [];
|
|
121
|
+
edgeBendPoints = {
|
|
122
|
+
...edgeBendPoints,
|
|
123
|
+
[draggingSeg.edgeId]: dragSegment(bends, draggingSeg.segIndex, draggingSeg.orientation, pos),
|
|
124
|
+
};
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (dragging) {
|
|
128
|
+
// Clear stored bend points for edges connected to dragged node —
|
|
129
|
+
// absolute bends become invalid when port positions change.
|
|
130
|
+
const nid = dragging.nodeId;
|
|
131
|
+
const hasBends = edges.some((ed) => (ed.source === nid || ed.target === nid) && edgeBendPoints[ed.id]);
|
|
132
|
+
if (hasBends) {
|
|
133
|
+
const cleaned = { ...edgeBendPoints };
|
|
134
|
+
for (const ed of edges) {
|
|
135
|
+
if (ed.source === nid || ed.target === nid) delete cleaned[ed.id];
|
|
136
|
+
}
|
|
137
|
+
edgeBendPoints = cleaned;
|
|
138
|
+
}
|
|
139
|
+
const pos = pz.clientToSvg(e.clientX, e.clientY);
|
|
140
|
+
const node = nodes.find((n) => n.id === dragging!.nodeId);
|
|
141
|
+
if (node) {
|
|
142
|
+
dragOffsets = {
|
|
143
|
+
...dragOffsets,
|
|
144
|
+
[dragging.nodeId]: {
|
|
145
|
+
dx: pos.x - dragging.offsetX - node.x,
|
|
146
|
+
dy: pos.y - dragging.offsetY - node.y,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function handlePointerUp(e: PointerEvent) {
|
|
154
|
+
pz.endPan(e);
|
|
155
|
+
// Consolidate bends after segment drag (remove collinear/duplicate points)
|
|
156
|
+
if (draggingSeg) {
|
|
157
|
+
const edgeId = draggingSeg.edgeId;
|
|
158
|
+
const bends = edgeBendPoints[edgeId];
|
|
159
|
+
if (bends) {
|
|
160
|
+
const edge = edges.find((ed) => ed.id === edgeId);
|
|
161
|
+
if (edge) {
|
|
162
|
+
edgeBendPoints = { ...edgeBendPoints, [edgeId]: consolidateBends(edge, resolvedNodes, bends) };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
dragging = null;
|
|
167
|
+
draggingSeg = null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// --- Node drag ---
|
|
171
|
+
function handleNodePointerDown(e: PointerEvent, nodeId: string) {
|
|
172
|
+
e.stopPropagation();
|
|
173
|
+
const pos = pz.clientToSvg(e.clientX, e.clientY);
|
|
174
|
+
const nodePos = nodePositions[nodeId];
|
|
175
|
+
if (!nodePos) return;
|
|
176
|
+
dragging = {
|
|
177
|
+
nodeId,
|
|
178
|
+
offsetX: pos.x - nodePos.x,
|
|
179
|
+
offsetY: pos.y - nodePos.y,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// --- Edge selection ---
|
|
184
|
+
function handleEdgePointerDown(e: PointerEvent, edgeId: string) {
|
|
185
|
+
e.stopPropagation();
|
|
186
|
+
selectedEdgeId = edgeId;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function handleEdgeReset(edgeId: string) {
|
|
190
|
+
const { [edgeId]: _, ...rest } = edgeBendPoints;
|
|
191
|
+
edgeBendPoints = rest;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function handleEdgeDblClick(e: MouseEvent, edgeId: string) {
|
|
195
|
+
e.stopPropagation();
|
|
196
|
+
handleEdgeReset(edgeId);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// --- Segment handle interaction (move existing segment) ---
|
|
200
|
+
function handleSegmentHandlePointerDown(e: PointerEvent, edgeId: string, segIndex: number, orientation: 'H' | 'V') {
|
|
201
|
+
e.stopPropagation();
|
|
202
|
+
const edge = edges.find((ed) => ed.id === edgeId);
|
|
203
|
+
if (!edge) return;
|
|
204
|
+
|
|
205
|
+
// Initialize from auto-routed bend points if none stored
|
|
206
|
+
if (!edgeBendPoints[edgeId]) {
|
|
207
|
+
edgeBendPoints = { ...edgeBendPoints, [edgeId]: extractBendPoints(edge, resolvedNodes) };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Insert clamp bends where adjacent segments are collinear,
|
|
211
|
+
// so dragging won't create diagonal lines
|
|
212
|
+
const prepared = prepareDrag(edge, resolvedNodes, edgeBendPoints[edgeId]!, segIndex, orientation);
|
|
213
|
+
edgeBendPoints = { ...edgeBendPoints, [edgeId]: prepared.bends };
|
|
214
|
+
draggingSeg = { edgeId, segIndex: prepared.segIndex, orientation };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// --- Split handle interaction (split segment creating a rectangular jog) ---
|
|
218
|
+
function handleSplitPointerDown(e: PointerEvent, edgeId: string, handle: SegmentHandle) {
|
|
219
|
+
e.stopPropagation();
|
|
220
|
+
const edge = edges.find((ed) => ed.id === edgeId);
|
|
221
|
+
if (!edge) return;
|
|
222
|
+
|
|
223
|
+
// Initialize bends if not stored
|
|
224
|
+
if (!edgeBendPoints[edgeId]) {
|
|
225
|
+
edgeBendPoints = { ...edgeBendPoints, [edgeId]: extractBendPoints(edge, resolvedNodes) };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const result = splitSegment(edge, resolvedNodes, edgeBendPoints[edgeId]!, handle);
|
|
229
|
+
// Insert clamp bends so dragging the new segment stays orthogonal
|
|
230
|
+
const prepared = prepareDrag(edge, resolvedNodes, result.bends, result.segIndex, result.orientation);
|
|
231
|
+
edgeBendPoints = { ...edgeBendPoints, [edgeId]: prepared.bends };
|
|
232
|
+
draggingSeg = { edgeId, segIndex: prepared.segIndex, orientation: result.orientation };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// --- Fit / reset ---
|
|
236
|
+
|
|
237
|
+
function computeBounds() {
|
|
238
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
239
|
+
for (const n of nodes) {
|
|
240
|
+
const pos = nodePositions[n.id] ?? { x: n.x, y: n.y };
|
|
241
|
+
minX = Math.min(minX, pos.x);
|
|
242
|
+
minY = Math.min(minY, pos.y);
|
|
243
|
+
maxX = Math.max(maxX, pos.x + n.width);
|
|
244
|
+
maxY = Math.max(maxY, pos.y + n.height);
|
|
245
|
+
}
|
|
246
|
+
return { minX, minY, maxX, maxY };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function fitView() {
|
|
250
|
+
if (nodes.length === 0) return;
|
|
251
|
+
pz.fitView(computeBounds());
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
onMount(() => {
|
|
255
|
+
// Passive: false required to preventDefault on wheel
|
|
256
|
+
svgEl.addEventListener('wheel', pz.handleWheel, { passive: false });
|
|
257
|
+
|
|
258
|
+
// Fit view once positions are available
|
|
259
|
+
requestAnimationFrame(() => fitView());
|
|
260
|
+
|
|
261
|
+
return () => {
|
|
262
|
+
svgEl.removeEventListener('wheel', pz.handleWheel);
|
|
263
|
+
};
|
|
264
|
+
});
|
|
265
|
+
</script>
|
|
266
|
+
|
|
267
|
+
<div class="model-diagram" bind:this={containerEl}>
|
|
268
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
269
|
+
<svg
|
|
270
|
+
bind:this={svgEl}
|
|
271
|
+
role="application"
|
|
272
|
+
onpointerdown={handleBackgroundPointerDown}
|
|
273
|
+
onpointermove={handlePointerMove}
|
|
274
|
+
onpointerup={handlePointerUp}
|
|
275
|
+
>
|
|
276
|
+
<defs>
|
|
277
|
+
<pattern id={patternId} width="20" height="20" patternUnits="userSpaceOnUse"
|
|
278
|
+
patternTransform="translate({pz.transform.x},{pz.transform.y}) scale({pz.transform.k})"
|
|
279
|
+
>
|
|
280
|
+
<circle cx="1" cy="1" r="1" fill="var(--bs-border-color, #dee2e6)" />
|
|
281
|
+
</pattern>
|
|
282
|
+
</defs>
|
|
283
|
+
|
|
284
|
+
<!-- Background grid -->
|
|
285
|
+
<rect class="diagram-bg" width="100%" height="100%" fill="url(#{patternId})" />
|
|
286
|
+
|
|
287
|
+
<!-- Transformed content -->
|
|
288
|
+
<g transform="translate({pz.transform.x},{pz.transform.y}) scale({pz.transform.k})">
|
|
289
|
+
<!-- Edges -->
|
|
290
|
+
{#each edgePaths as { edge, path } (edge.id)}
|
|
291
|
+
<!-- Invisible wider hit area for easier clicking -->
|
|
292
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
293
|
+
<path
|
|
294
|
+
d={path.d}
|
|
295
|
+
fill="none"
|
|
296
|
+
class="diagram-edge-hit"
|
|
297
|
+
onpointerdown={(e: PointerEvent) => handleEdgePointerDown(e, edge.id)}
|
|
298
|
+
ondblclick={(e: MouseEvent) => handleEdgeDblClick(e, edge.id)}
|
|
299
|
+
/>
|
|
300
|
+
<!-- Visible edge -->
|
|
301
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
302
|
+
<path
|
|
303
|
+
d={path.d}
|
|
304
|
+
fill="none"
|
|
305
|
+
stroke={edge.color}
|
|
306
|
+
stroke-width={selectedEdgeId === edge.id ? 3 : 1.5}
|
|
307
|
+
class="diagram-edge"
|
|
308
|
+
class:selected={selectedEdgeId === edge.id}
|
|
309
|
+
onpointerdown={(e: PointerEvent) => handleEdgePointerDown(e, edge.id)}
|
|
310
|
+
ondblclick={(e: MouseEvent) => handleEdgeDblClick(e, edge.id)}
|
|
311
|
+
/>
|
|
312
|
+
{/each}
|
|
313
|
+
|
|
314
|
+
<!-- Edge labels -->
|
|
315
|
+
{#each edgePaths as { edge, path } (`label-${edge.id}`)}
|
|
316
|
+
{#if edge.label}
|
|
317
|
+
<text
|
|
318
|
+
x={path.labelX}
|
|
319
|
+
y={path.labelY}
|
|
320
|
+
text-anchor="middle"
|
|
321
|
+
class="diagram-edge-label"
|
|
322
|
+
fill="var(--bs-secondary-color, #6c757d)"
|
|
323
|
+
>{edge.label}</text>
|
|
324
|
+
{/if}
|
|
325
|
+
{/each}
|
|
326
|
+
|
|
327
|
+
<!-- Port dots & cardinality labels -->
|
|
328
|
+
{#each edgePaths as { edge, path } (`ports-${edge.id}`)}
|
|
329
|
+
<!-- Source port dot (FK/many side) -->
|
|
330
|
+
<circle cx={path.srcPort.x} cy={path.srcPort.y} r={PORT_DOT_RADIUS}
|
|
331
|
+
fill={edge.color} class="port-dot" />
|
|
332
|
+
<!-- Target port dot (PK/one side) -->
|
|
333
|
+
<circle cx={path.tgtPort.x} cy={path.tgtPort.y} r={PORT_DOT_RADIUS}
|
|
334
|
+
fill={edge.color} class="port-dot" />
|
|
335
|
+
<!-- Source cardinality -->
|
|
336
|
+
<text x={path.srcPort.x + 8} y={path.srcPort.y - 6}
|
|
337
|
+
class="cardinality-label" fill={edge.color}
|
|
338
|
+
>{edge.isNullable ? '0..N' : 'N'}</text>
|
|
339
|
+
<!-- Target cardinality -->
|
|
340
|
+
<text x={path.tgtPort.x - 8} y={path.tgtPort.y - 6}
|
|
341
|
+
text-anchor="end" class="cardinality-label" fill={edge.color}
|
|
342
|
+
>1</text>
|
|
343
|
+
{/each}
|
|
344
|
+
|
|
345
|
+
<!-- Handles for selected edge -->
|
|
346
|
+
{#if selectedEdgeHandles}
|
|
347
|
+
{#each selectedEdgeHandles.handles as handle}
|
|
348
|
+
{#if handle.draggable}
|
|
349
|
+
<!-- Internal segment: filled square — drag to move -->
|
|
350
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
351
|
+
<rect
|
|
352
|
+
x={handle.x - 3.5} y={handle.y - 3.5} width="7" height="7"
|
|
353
|
+
fill="var(--bs-primary, #0d6efd)"
|
|
354
|
+
stroke="white" stroke-width="1"
|
|
355
|
+
class="seg-handle"
|
|
356
|
+
class:seg-handle-h={handle.orientation === 'H'}
|
|
357
|
+
class:seg-handle-v={handle.orientation === 'V'}
|
|
358
|
+
onpointerdown={(e: PointerEvent) => handleSegmentHandlePointerDown(e, selectedEdgeId!, handle.segIndex, handle.orientation)}
|
|
359
|
+
/>
|
|
360
|
+
{:else}
|
|
361
|
+
<!-- First/last segment: hollow circle — drag to split -->
|
|
362
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
363
|
+
<circle
|
|
364
|
+
cx={handle.x} cy={handle.y} r="5"
|
|
365
|
+
fill="var(--bs-body-bg, #fff)"
|
|
366
|
+
stroke="var(--bs-primary, #0d6efd)" stroke-width="1.5"
|
|
367
|
+
class="split-handle"
|
|
368
|
+
class:split-handle-h={handle.orientation === 'H'}
|
|
369
|
+
class:split-handle-v={handle.orientation === 'V'}
|
|
370
|
+
onpointerdown={(e: PointerEvent) => handleSplitPointerDown(e, selectedEdgeId!, handle)}
|
|
371
|
+
/>
|
|
372
|
+
{/if}
|
|
373
|
+
{/each}
|
|
374
|
+
{/if}
|
|
375
|
+
|
|
376
|
+
<!-- Reset button for selected edge with stored bend points -->
|
|
377
|
+
{#if selectedEdgeId && edgeBendPoints[selectedEdgeId]}
|
|
378
|
+
{@const ep = edgePaths.find((p) => p.edge.id === selectedEdgeId)}
|
|
379
|
+
{#if ep}
|
|
380
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
381
|
+
<g
|
|
382
|
+
class="edge-reset-btn"
|
|
383
|
+
onpointerdown={(ev: PointerEvent) => { ev.stopPropagation(); handleEdgeReset(selectedEdgeId!); }}
|
|
384
|
+
>
|
|
385
|
+
<rect
|
|
386
|
+
x={ep.path.labelX - 11} y={ep.path.labelY - 24}
|
|
387
|
+
width="22" height="20" rx="4"
|
|
388
|
+
/>
|
|
389
|
+
<text
|
|
390
|
+
x={ep.path.labelX} y={ep.path.labelY - 14}
|
|
391
|
+
text-anchor="middle"
|
|
392
|
+
dominant-baseline="central"
|
|
393
|
+
>↺</text>
|
|
394
|
+
</g>
|
|
395
|
+
{/if}
|
|
396
|
+
{/if}
|
|
397
|
+
|
|
398
|
+
<!-- Nodes -->
|
|
399
|
+
{#each nodes as node (node.id)}
|
|
400
|
+
{@const pos = nodePositions[node.id] ?? { x: node.x, y: node.y }}
|
|
401
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
402
|
+
<foreignObject
|
|
403
|
+
x={pos.x}
|
|
404
|
+
y={pos.y}
|
|
405
|
+
width={node.width}
|
|
406
|
+
height={node.height}
|
|
407
|
+
onpointerdown={(e: PointerEvent) => handleNodePointerDown(e, node.id)}
|
|
408
|
+
class="diagram-node-fo"
|
|
409
|
+
>
|
|
410
|
+
<div xmlns="http://www.w3.org/1999/xhtml">
|
|
411
|
+
<EntityNode data={node.data} />
|
|
412
|
+
</div>
|
|
413
|
+
</foreignObject>
|
|
414
|
+
{/each}
|
|
415
|
+
</g>
|
|
416
|
+
</svg>
|
|
417
|
+
|
|
418
|
+
<!-- Controls -->
|
|
419
|
+
<div class="diagram-controls">
|
|
420
|
+
<IconButton icon="bi-plus-lg" title={t.zoom_in} size="sm" onclick={() => pz.zoomBy(1.3)} />
|
|
421
|
+
<IconButton icon="bi-dash-lg" title={t.zoom_out} size="sm" onclick={() => pz.zoomBy(1 / 1.3)} />
|
|
422
|
+
<IconButton icon="bi-fullscreen" title={t.fit_view} size="sm" onclick={fitView} />
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
|
|
426
|
+
<style>
|
|
427
|
+
.model-diagram {
|
|
428
|
+
width: 100%;
|
|
429
|
+
height: 100%;
|
|
430
|
+
position: relative;
|
|
431
|
+
overflow: hidden;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.model-diagram svg {
|
|
435
|
+
width: 100%;
|
|
436
|
+
height: 100%;
|
|
437
|
+
cursor: grab;
|
|
438
|
+
display: block;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.model-diagram svg:active {
|
|
442
|
+
cursor: grabbing;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.diagram-node-fo {
|
|
446
|
+
cursor: grab;
|
|
447
|
+
overflow: visible;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.diagram-node-fo:active {
|
|
451
|
+
cursor: grabbing;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.diagram-edge {
|
|
455
|
+
stroke-dasharray: 6 3;
|
|
456
|
+
animation: dash-flow 0.5s linear infinite;
|
|
457
|
+
pointer-events: none;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
.diagram-edge.selected {
|
|
461
|
+
stroke-width: 3;
|
|
462
|
+
stroke-dasharray: none;
|
|
463
|
+
animation: none;
|
|
464
|
+
filter: brightness(1.3);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.diagram-edge-hit {
|
|
468
|
+
stroke: transparent;
|
|
469
|
+
stroke-width: 12;
|
|
470
|
+
pointer-events: stroke;
|
|
471
|
+
cursor: pointer;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.diagram-edge-label {
|
|
475
|
+
font-size: 9px;
|
|
476
|
+
pointer-events: none;
|
|
477
|
+
user-select: none;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.port-dot {
|
|
481
|
+
pointer-events: none;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
.cardinality-label {
|
|
485
|
+
font-size: 8px;
|
|
486
|
+
font-weight: 600;
|
|
487
|
+
pointer-events: none;
|
|
488
|
+
user-select: none;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
@keyframes dash-flow {
|
|
492
|
+
to {
|
|
493
|
+
stroke-dashoffset: -9;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.split-handle {
|
|
498
|
+
pointer-events: auto;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.split-handle-h {
|
|
502
|
+
cursor: ns-resize;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.split-handle-v {
|
|
506
|
+
cursor: ew-resize;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.split-handle:hover {
|
|
510
|
+
fill: color-mix(in srgb, var(--bs-primary, #0d6efd) 15%, var(--bs-body-bg, #fff));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.seg-handle {
|
|
514
|
+
pointer-events: auto;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
.seg-handle-h {
|
|
518
|
+
cursor: ns-resize;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.seg-handle-v {
|
|
522
|
+
cursor: ew-resize;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.seg-handle:hover {
|
|
526
|
+
filter: brightness(1.2);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.edge-reset-btn {
|
|
530
|
+
cursor: pointer;
|
|
531
|
+
pointer-events: auto;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.edge-reset-btn rect {
|
|
535
|
+
fill: var(--bs-body-bg, #fff);
|
|
536
|
+
stroke: var(--bs-border-color, #dee2e6);
|
|
537
|
+
stroke-width: 1;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.edge-reset-btn text {
|
|
541
|
+
font-size: 13px;
|
|
542
|
+
fill: var(--bs-secondary-color, #6c757d);
|
|
543
|
+
pointer-events: none;
|
|
544
|
+
user-select: none;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.edge-reset-btn:hover rect {
|
|
548
|
+
fill: var(--bs-primary, #0d6efd);
|
|
549
|
+
stroke: var(--bs-primary, #0d6efd);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
.edge-reset-btn:hover text {
|
|
553
|
+
fill: white;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.diagram-controls {
|
|
557
|
+
position: absolute;
|
|
558
|
+
bottom: 12px;
|
|
559
|
+
left: 12px;
|
|
560
|
+
display: flex;
|
|
561
|
+
flex-direction: column;
|
|
562
|
+
gap: 2px;
|
|
563
|
+
background: var(--bs-body-bg, #fff);
|
|
564
|
+
border: 1px solid var(--bs-border-color, #dee2e6);
|
|
565
|
+
border-radius: 6px;
|
|
566
|
+
padding: 4px;
|
|
567
|
+
box-shadow: 0 2px 6px var(--df-shadow-color);
|
|
568
|
+
}
|
|
569
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DiagramNode, DiagramEdge, Point } from '../diagram-types';
|
|
2
|
+
import { type EdgePath } from '../edge-routing';
|
|
3
|
+
interface Props {
|
|
4
|
+
nodes: DiagramNode[];
|
|
5
|
+
edges: DiagramEdge[];
|
|
6
|
+
initialBendPoints?: Record<string, Point[]>;
|
|
7
|
+
minZoom?: number;
|
|
8
|
+
maxZoom?: number;
|
|
9
|
+
onBendPointsChange?: (bends: Record<string, Point[]>) => void;
|
|
10
|
+
onEdgePathsChange?: (paths: {
|
|
11
|
+
edge: DiagramEdge;
|
|
12
|
+
path: EdgePath;
|
|
13
|
+
}[]) => void;
|
|
14
|
+
}
|
|
15
|
+
declare const ModelDiagram: import("svelte").Component<Props, {}, "">;
|
|
16
|
+
type ModelDiagram = ReturnType<typeof ModelDiagram>;
|
|
17
|
+
export default ModelDiagram;
|
|
18
|
+
//# sourceMappingURL=ModelDiagram.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ModelDiagram.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/SimpleDiagram/ModelDiagram.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,KAAK,EAAe,MAAM,kBAAkB,CAAC;AACrF,OAAO,EAAiI,KAAK,QAAQ,EAAsB,MAAM,iBAAiB,CAAC;AAMlM,UAAU,KAAK;IACd,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IAC9D,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,WAAW,CAAC;QAAC,IAAI,EAAE,QAAQ,CAAA;KAAE,EAAE,KAAK,IAAI,CAAC;CAC7E;AAiWF,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# DiagramDebug
|
|
2
|
+
|
|
3
|
+
`DiagramDebug.svelte` is a standalone debug/test harness for the diagram components.
|
|
4
|
+
It was moved here from `clients/apps/dforge/src/routes/(app)/model/debug/` so it lives
|
|
5
|
+
alongside the code it tests.
|
|
6
|
+
|
|
7
|
+
To use it, create a route in the app and render:
|
|
8
|
+
|
|
9
|
+
```svelte
|
|
10
|
+
<script>
|
|
11
|
+
import { DiagramDebug } from '@dforge/diagram';
|
|
12
|
+
</script>
|
|
13
|
+
<DiagramDebug />
|
|
14
|
+
```
|