@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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -0
  3. package/dist/EntityNode.svelte +167 -0
  4. package/dist/EntityNode.svelte.d.ts +8 -0
  5. package/dist/EntityNode.svelte.d.ts.map +1 -0
  6. package/dist/IconButton.svelte +83 -0
  7. package/dist/IconButton.svelte.d.ts +13 -0
  8. package/dist/IconButton.svelte.d.ts.map +1 -0
  9. package/dist/SimpleDiagram/DiagramDebug.svelte +499 -0
  10. package/dist/SimpleDiagram/DiagramDebug.svelte.d.ts +4 -0
  11. package/dist/SimpleDiagram/DiagramDebug.svelte.d.ts.map +1 -0
  12. package/dist/SimpleDiagram/ModelDiagram.svelte +569 -0
  13. package/dist/SimpleDiagram/ModelDiagram.svelte.d.ts +18 -0
  14. package/dist/SimpleDiagram/ModelDiagram.svelte.d.ts.map +1 -0
  15. package/dist/SimpleDiagram/NOTE.md +14 -0
  16. package/dist/SimpleDiagram/SimpleDiagram.svelte +755 -0
  17. package/dist/SimpleDiagram/SimpleDiagram.svelte.d.ts +13 -0
  18. package/dist/SimpleDiagram/SimpleDiagram.svelte.d.ts.map +1 -0
  19. package/dist/SimpleDiagram/diagram-geometry.d.ts +33 -0
  20. package/dist/SimpleDiagram/diagram-geometry.d.ts.map +1 -0
  21. package/dist/SimpleDiagram/diagram-geometry.js +137 -0
  22. package/dist/SimpleDiagram/simple-layout.d.ts +27 -0
  23. package/dist/SimpleDiagram/simple-layout.d.ts.map +1 -0
  24. package/dist/SimpleDiagram/simple-layout.js +364 -0
  25. package/dist/WorkspaceMap/ModuleCard.svelte +165 -0
  26. package/dist/WorkspaceMap/ModuleCard.svelte.d.ts +8 -0
  27. package/dist/WorkspaceMap/ModuleCard.svelte.d.ts.map +1 -0
  28. package/dist/WorkspaceMap/WorkspaceMap.svelte +319 -0
  29. package/dist/WorkspaceMap/WorkspaceMap.svelte.d.ts +12 -0
  30. package/dist/WorkspaceMap/WorkspaceMap.svelte.d.ts.map +1 -0
  31. package/dist/WorkspaceMap/workspace-graph.d.ts +18 -0
  32. package/dist/WorkspaceMap/workspace-graph.d.ts.map +1 -0
  33. package/dist/WorkspaceMap/workspace-graph.js +168 -0
  34. package/dist/WorkspaceMap/workspace-types.d.ts +32 -0
  35. package/dist/WorkspaceMap/workspace-types.d.ts.map +1 -0
  36. package/dist/WorkspaceMap/workspace-types.js +1 -0
  37. package/dist/diagram-types.d.ts +47 -0
  38. package/dist/diagram-types.d.ts.map +1 -0
  39. package/dist/diagram-types.js +2 -0
  40. package/dist/edge-routing.d.ts +82 -0
  41. package/dist/edge-routing.d.ts.map +1 -0
  42. package/dist/edge-routing.js +388 -0
  43. package/dist/i18n.d.ts +13 -0
  44. package/dist/i18n.d.ts.map +1 -0
  45. package/dist/i18n.js +20 -0
  46. package/dist/index.d.ts +23 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +21 -0
  49. package/dist/local-storage-sync.d.ts +10 -0
  50. package/dist/local-storage-sync.d.ts.map +1 -0
  51. package/dist/local-storage-sync.js +39 -0
  52. package/dist/model-graph.d.ts +20 -0
  53. package/dist/model-graph.d.ts.map +1 -0
  54. package/dist/model-graph.js +100 -0
  55. package/dist/model-types.d.ts +55 -0
  56. package/dist/model-types.d.ts.map +1 -0
  57. package/dist/model-types.js +11 -0
  58. package/dist/pan-zoom.svelte.d.ts +34 -0
  59. package/dist/pan-zoom.svelte.d.ts.map +1 -0
  60. package/dist/pan-zoom.svelte.js +130 -0
  61. package/package.json +55 -0
@@ -0,0 +1,165 @@
1
+ <script lang="ts">
2
+ import type { WorkspaceModule } from './workspace-types';
3
+ import { INDENT_WIDTH, CARD_PADDING_LEFT } from './workspace-graph';
4
+
5
+ interface Props {
6
+ module: WorkspaceModule;
7
+ }
8
+
9
+ let { module: mod }: Props = $props();
10
+
11
+ // Tree connector lines. Both SVG and pills use entity.ry (card-relative),
12
+ // so they share the exact same coordinate system — no drift.
13
+ const connectors = $derived.by(() => {
14
+
15
+ type Line = { x1: number; y1: number; x2: number; y2: number };
16
+ const lines: Line[] = [];
17
+ const entities = mod.entities;
18
+
19
+ for (let i = 0; i < entities.length; i++) {
20
+ const ent = entities[i];
21
+ if (ent.depth === 0) continue;
22
+
23
+ const cy = ent.ry + ent.height / 2;
24
+ const pillLeft = ent.rx;
25
+ // Branch line sits at parent's indent level + half indent
26
+ const bx = CARD_PADDING_LEFT + (ent.depth - 1) * INDENT_WIDTH + INDENT_WIDTH / 2;
27
+
28
+ // Horizontal: branch → pill left edge
29
+ lines.push({ x1: bx, y1: cy, x2: pillLeft, y2: cy });
30
+
31
+ // Vertical: from previous sibling or parent down to this entity
32
+ for (let j = i - 1; j >= 0; j--) {
33
+ if (entities[j].depth <= ent.depth) {
34
+ const fromY = entities[j].ry + entities[j].height / 2;
35
+ lines.push({ x1: bx, y1: fromY, x2: bx, y2: cy });
36
+ break;
37
+ }
38
+ }
39
+ }
40
+
41
+ return lines;
42
+ });
43
+ </script>
44
+
45
+ <div class="ws-module-card" style:--mod-color={mod.color} style:height="{mod.height}px">
46
+ <!-- Tree connector SVG — covers entire card, same coords as pills -->
47
+ <svg class="ws-tree-lines" xmlns="http://www.w3.org/2000/svg">
48
+ {#each connectors as c}
49
+ <line
50
+ x1={c.x1} y1={c.y1}
51
+ x2={c.x2} y2={c.y2}
52
+ stroke={mod.color}
53
+ stroke-width="1.5"
54
+ stroke-opacity="0.35"
55
+ />
56
+ {/each}
57
+ </svg>
58
+
59
+ <div class="ws-module-header">
60
+ <span class="ws-module-dot"></span>
61
+ <span class="ws-module-label">{mod.label}</span>
62
+ <span class="ws-module-count">{mod.entities.length}</span>
63
+ </div>
64
+
65
+ {#each mod.entities as entity}
66
+ <div
67
+ class="ws-entity-pill"
68
+ style:top="{entity.ry}px"
69
+ style:left="{entity.rx}px"
70
+ style:width="{entity.width}px"
71
+ style:height="{entity.height}px"
72
+ >
73
+ <span class="ws-pill-label">{entity.label}</span>
74
+ </div>
75
+ {/each}
76
+ </div>
77
+
78
+ <style>
79
+ .ws-module-card {
80
+ width: 100%;
81
+ border-radius: 10px;
82
+ border: 1.5px solid color-mix(in srgb, var(--mod-color) 40%, var(--bs-border-color, #dee2e6));
83
+ /* background: var(--bs-body-bg, #fff); */
84
+ overflow: hidden;
85
+ font-family: var(--bs-body-font-family, system-ui, sans-serif);
86
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
87
+ position: relative;
88
+ }
89
+
90
+ .ws-tree-lines {
91
+ position: absolute;
92
+ inset: 0;
93
+ width: 100%;
94
+ height: 100%;
95
+ pointer-events: none;
96
+ z-index: 0;
97
+ }
98
+
99
+ .ws-module-header {
100
+ display: flex;
101
+ align-items: center;
102
+ gap: 6px;
103
+ padding: 8px 12px;
104
+ background: color-mix(in srgb, var(--mod-color) 10%, var(--bs-body-bg, #fff));
105
+ border-bottom: 1px solid color-mix(in srgb, var(--mod-color) 20%, var(--bs-border-color, #dee2e6));
106
+ position: relative;
107
+ z-index: 1;
108
+ }
109
+
110
+ .ws-module-dot {
111
+ width: 10px;
112
+ height: 10px;
113
+ border-radius: 50%;
114
+ background-color: var(--mod-color);
115
+ flex-shrink: 0;
116
+ }
117
+
118
+ .ws-module-label {
119
+ font-size: 12px;
120
+ font-weight: 600;
121
+ color: var(--bs-body-color, #212529);
122
+ flex: 1;
123
+ overflow: hidden;
124
+ text-overflow: ellipsis;
125
+ white-space: nowrap;
126
+ }
127
+
128
+ .ws-module-count {
129
+ font-size: 10px;
130
+ color: var(--bs-secondary-color, #6c757d);
131
+ background: var(--bs-tertiary-bg, #f8f9fa);
132
+ padding: 1px 6px;
133
+ border-radius: 999px;
134
+ flex-shrink: 0;
135
+ }
136
+
137
+ .ws-entity-pill {
138
+ position: absolute;
139
+ box-sizing: border-box;
140
+ display: flex;
141
+ align-items: center;
142
+ padding: 0 10px;
143
+ border-radius: 6px;
144
+ background: color-mix(in srgb, var(--mod-color) 8%, var(--bs-body-bg, #fff));
145
+ border: 1px solid color-mix(in srgb, var(--mod-color) 25%, var(--bs-border-color, #dee2e6));
146
+ font-size: 11px;
147
+ color: var(--bs-body-color, #212529);
148
+ white-space: nowrap;
149
+ overflow: hidden;
150
+ text-overflow: ellipsis;
151
+ transition: background 0.15s;
152
+ z-index: 1;
153
+ }
154
+
155
+ .ws-entity-pill:hover {
156
+ background: color-mix(in srgb, var(--mod-color) 18%, var(--bs-body-bg, #fff));
157
+ }
158
+
159
+ .ws-pill-label {
160
+ pointer-events: none;
161
+ user-select: none;
162
+ overflow: hidden;
163
+ text-overflow: ellipsis;
164
+ }
165
+ </style>
@@ -0,0 +1,8 @@
1
+ import type { WorkspaceModule } from './workspace-types';
2
+ interface Props {
3
+ module: WorkspaceModule;
4
+ }
5
+ declare const ModuleCard: import("svelte").Component<Props, {}, "">;
6
+ type ModuleCard = ReturnType<typeof ModuleCard>;
7
+ export default ModuleCard;
8
+ //# sourceMappingURL=ModuleCard.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ModuleCard.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/WorkspaceMap/ModuleCard.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAIxD,UAAU,KAAK;IACd,MAAM,EAAE,eAAe,CAAC;CACxB;AAmEF,QAAA,MAAM,UAAU,2CAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
@@ -0,0 +1,319 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import IconButton from '../IconButton.svelte';
4
+ import type { WorkspaceModule, WorkspaceEdge } from './workspace-types';
5
+ import type { ViewTransform } from '../diagram-types';
6
+ import { getDiagramTranslations } from '../i18n';
7
+ import { createPanZoom } from '../pan-zoom.svelte';
8
+ import { createLocalStorageSync } from '../local-storage-sync';
9
+ import ModuleCard from './ModuleCard.svelte';
10
+
11
+ const t = getDiagramTranslations();
12
+
13
+ interface Props {
14
+ modules: WorkspaceModule[];
15
+ edges: WorkspaceEdge[];
16
+ storageKey?: string;
17
+ minZoom?: number;
18
+ maxZoom?: number;
19
+ }
20
+
21
+ let { modules, edges, storageKey, minZoom = 0.1, maxZoom = 2 }: Props = $props();
22
+
23
+ let svgEl: SVGSVGElement;
24
+ let containerEl: HTMLDivElement;
25
+ let dragOffsets = $state<Record<string, { dx: number; dy: number }>>({});
26
+ let dragging = $state<{ moduleId: string; offsetX: number; offsetY: number } | null>(null);
27
+ let patternId = `ws-grid-${Math.random().toString(36).slice(2, 8)}`;
28
+
29
+ const pz = createPanZoom({
30
+ get minZoom() { return minZoom; },
31
+ get maxZoom() { return maxZoom; },
32
+ bgClass: 'ws-bg',
33
+ getSvgEl: () => svgEl,
34
+ getContainerEl: () => containerEl,
35
+ });
36
+
37
+ // --- localStorage persistence ---
38
+
39
+ interface WsState {
40
+ dragOffsets: Record<string, { dx: number; dy: number }>;
41
+ transform: ViewTransform;
42
+ }
43
+
44
+ const storage = createLocalStorageSync<WsState>(() => storageKey);
45
+ let restoredTransform = false;
46
+
47
+ $effect(() => {
48
+ // Track both values for auto-save
49
+ const state: WsState = { dragOffsets, transform: pz.transform };
50
+ storage.save(state);
51
+ });
52
+
53
+ // Reset when module set changes
54
+ const moduleIds = $derived(modules.map((m) => m.id).join(','));
55
+ $effect(() => {
56
+ moduleIds; // track
57
+ const saved = storage.load();
58
+ if (saved && Object.keys(saved.dragOffsets).length > 0) {
59
+ dragOffsets = saved.dragOffsets;
60
+ pz.setTransform(saved.transform);
61
+ restoredTransform = true;
62
+ } else if (saved) {
63
+ dragOffsets = {};
64
+ pz.setTransform(saved.transform);
65
+ restoredTransform = true;
66
+ } else {
67
+ dragOffsets = {};
68
+ restoredTransform = false;
69
+ }
70
+ });
71
+
72
+ // Derive module positions with drag offsets
73
+ const modulePositions = $derived.by(() => {
74
+ const pos: Record<string, { x: number; y: number }> = {};
75
+ for (const mod of modules) {
76
+ const off = dragOffsets[mod.id];
77
+ pos[mod.id] = off
78
+ ? { x: mod.x + off.dx, y: mod.y + off.dy }
79
+ : { x: mod.x, y: mod.y };
80
+ }
81
+ return pos;
82
+ });
83
+
84
+ // Entity position lookup: absolute position of each entity pill
85
+ const entityPositions = $derived.by(() => {
86
+ const pos = new Map<number, { x: number; y: number; width: number; height: number; moduleId: string }>();
87
+ for (const mod of modules) {
88
+ const modPos = modulePositions[mod.id];
89
+ if (!modPos) continue;
90
+ for (const ent of mod.entities) {
91
+ pos.set(ent.entityId, {
92
+ x: modPos.x + ent.rx,
93
+ y: modPos.y + ent.ry,
94
+ width: ent.width,
95
+ height: ent.height,
96
+ moduleId: mod.id,
97
+ });
98
+ }
99
+ }
100
+ return pos;
101
+ });
102
+
103
+ // Cross-module edges only (intra-module hierarchy shown via tree indentation)
104
+ const crossEdges = $derived(
105
+ edges.filter((e) => e.sourceModuleId !== e.targetModuleId)
106
+ );
107
+
108
+ // Compute cross-module edge paths
109
+ const crossEdgePaths = $derived.by(() => {
110
+ const paths: { id: string; d: string; color: string }[] = [];
111
+
112
+ for (const edge of crossEdges) {
113
+ const src = entityPositions.get(edge.sourceEntityId);
114
+ const tgt = entityPositions.get(edge.targetEntityId);
115
+ if (!src || !tgt) continue;
116
+
117
+ // Source: right side of pill, Target: left side of pill
118
+ const srcX = src.x + src.width;
119
+ const srcY = src.y + src.height / 2;
120
+ const tgtX = tgt.x;
121
+ const tgtY = tgt.y + tgt.height / 2;
122
+
123
+ // Simple orthogonal H→V→H routing
124
+ const midX = (srcX + tgtX) / 2;
125
+ const d = `M ${srcX} ${srcY} H ${midX} V ${tgtY} H ${tgtX}`;
126
+
127
+ paths.push({ id: edge.id, d, color: edge.color });
128
+ }
129
+
130
+ return paths;
131
+ });
132
+
133
+ // --- Event handlers ---
134
+
135
+ function handleBackgroundPointerDown(e: PointerEvent) {
136
+ pz.startPan(e);
137
+ }
138
+
139
+ function handlePointerMove(e: PointerEvent) {
140
+ if (pz.onPointerMove(e)) return;
141
+ if (dragging) {
142
+ const pos = pz.clientToSvg(e.clientX, e.clientY);
143
+ const mod = modules.find((m) => m.id === dragging!.moduleId);
144
+ if (mod) {
145
+ dragOffsets = {
146
+ ...dragOffsets,
147
+ [mod.id]: {
148
+ dx: pos.x - dragging.offsetX - mod.x,
149
+ dy: pos.y - dragging.offsetY - mod.y,
150
+ },
151
+ };
152
+ }
153
+ }
154
+ }
155
+
156
+ function handlePointerUp(e: PointerEvent) {
157
+ pz.endPan(e);
158
+ dragging = null;
159
+ }
160
+
161
+ function handleModulePointerDown(e: PointerEvent, moduleId: string) {
162
+ e.stopPropagation();
163
+ const pos = pz.clientToSvg(e.clientX, e.clientY);
164
+ const modPos = modulePositions[moduleId];
165
+ if (!modPos) return;
166
+ dragging = {
167
+ moduleId,
168
+ offsetX: pos.x - modPos.x,
169
+ offsetY: pos.y - modPos.y,
170
+ };
171
+ }
172
+
173
+ // --- Fit / reset ---
174
+
175
+ function computeBounds() {
176
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
177
+ for (const mod of modules) {
178
+ const pos = modulePositions[mod.id];
179
+ if (!pos) continue;
180
+ minX = Math.min(minX, pos.x);
181
+ minY = Math.min(minY, pos.y);
182
+ maxX = Math.max(maxX, pos.x + mod.width);
183
+ maxY = Math.max(maxY, pos.y + mod.height);
184
+ }
185
+ return { minX, minY, maxX, maxY };
186
+ }
187
+
188
+ function fitView() {
189
+ if (modules.length === 0) return;
190
+ pz.fitView(computeBounds());
191
+ }
192
+
193
+ function resetLayout() {
194
+ dragOffsets = {};
195
+ requestAnimationFrame(() => fitView());
196
+ }
197
+
198
+ onMount(() => {
199
+ svgEl.addEventListener('wheel', pz.handleWheel, { passive: false });
200
+ if (!restoredTransform) {
201
+ requestAnimationFrame(() => fitView());
202
+ }
203
+ return () => {
204
+ svgEl.removeEventListener('wheel', pz.handleWheel);
205
+ storage.clear();
206
+ };
207
+ });
208
+ </script>
209
+
210
+ <div class="ws-map" bind:this={containerEl}>
211
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
212
+ <svg
213
+ bind:this={svgEl}
214
+ role="application"
215
+ onpointerdown={handleBackgroundPointerDown}
216
+ onpointermove={handlePointerMove}
217
+ onpointerup={handlePointerUp}
218
+ >
219
+ <defs>
220
+ <pattern id={patternId} width="20" height="20" patternUnits="userSpaceOnUse"
221
+ patternTransform="translate({pz.transform.x},{pz.transform.y}) scale({pz.transform.k})"
222
+ >
223
+ <circle cx="1" cy="1" r="1" fill="var(--bs-border-color, #dee2e6)" />
224
+ </pattern>
225
+ </defs>
226
+
227
+ <!-- Background grid -->
228
+ <rect class="ws-bg" width="100%" height="100%" fill="url(#{patternId})" />
229
+
230
+ <!-- Transformed content -->
231
+ <g transform="translate({pz.transform.x},{pz.transform.y}) scale({pz.transform.k})">
232
+ <!-- Cross-module edges (behind cards) -->
233
+ {#each crossEdgePaths as path (path.id)}
234
+ <path
235
+ d={path.d}
236
+ fill="none"
237
+ stroke={path.color}
238
+ stroke-width="1.5"
239
+ stroke-opacity="0.4"
240
+ stroke-dasharray="6 3"
241
+ class="ws-cross-edge"
242
+ />
243
+ {/each}
244
+
245
+ <!-- Module cards -->
246
+ {#each modules as mod (mod.id)}
247
+ {@const pos = modulePositions[mod.id]}
248
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
249
+ <foreignObject
250
+ x={pos.x}
251
+ y={pos.y}
252
+ width={mod.width}
253
+ height={mod.height}
254
+ onpointerdown={(e: PointerEvent) => handleModulePointerDown(e, mod.id)}
255
+ class="ws-module-fo"
256
+ >
257
+ <div xmlns="http://www.w3.org/1999/xhtml">
258
+ <ModuleCard module={mod} />
259
+ </div>
260
+ </foreignObject>
261
+ {/each}
262
+ </g>
263
+ </svg>
264
+
265
+ <!-- Controls -->
266
+ <div class="ws-controls">
267
+ <IconButton icon="bi-plus-lg" title={t.zoom_in} size="sm" onclick={() => pz.zoomBy(1.3)} />
268
+ <IconButton icon="bi-dash-lg" title={t.zoom_out} size="sm" onclick={() => pz.zoomBy(1 / 1.3)} />
269
+ <IconButton icon="bi-fullscreen" title={t.fit_view} size="sm" onclick={fitView} />
270
+ <IconButton icon="bi-arrow-counterclockwise" title={t.reset_layout} size="sm" onclick={resetLayout} />
271
+ </div>
272
+ </div>
273
+
274
+ <style>
275
+ .ws-map {
276
+ width: 100%;
277
+ height: 100%;
278
+ position: relative;
279
+ overflow: hidden;
280
+ }
281
+
282
+ .ws-map svg {
283
+ width: 100%;
284
+ height: 100%;
285
+ cursor: grab;
286
+ display: block;
287
+ }
288
+
289
+ .ws-map svg:active {
290
+ cursor: grabbing;
291
+ }
292
+
293
+ .ws-module-fo {
294
+ cursor: grab;
295
+ overflow: visible;
296
+ }
297
+
298
+ .ws-module-fo:active {
299
+ cursor: grabbing;
300
+ }
301
+
302
+ .ws-cross-edge {
303
+ pointer-events: none;
304
+ }
305
+
306
+ .ws-controls {
307
+ position: absolute;
308
+ bottom: 12px;
309
+ left: 12px;
310
+ display: flex;
311
+ flex-direction: column;
312
+ gap: 2px;
313
+ background: var(--bs-body-bg, #fff);
314
+ border: 1px solid var(--bs-border-color, #dee2e6);
315
+ border-radius: 6px;
316
+ padding: 4px;
317
+ box-shadow: 0 2px 6px var(--df-shadow-color);
318
+ }
319
+ </style>
@@ -0,0 +1,12 @@
1
+ import type { WorkspaceModule, WorkspaceEdge } from './workspace-types';
2
+ interface Props {
3
+ modules: WorkspaceModule[];
4
+ edges: WorkspaceEdge[];
5
+ storageKey?: string;
6
+ minZoom?: number;
7
+ maxZoom?: number;
8
+ }
9
+ declare const WorkspaceMap: import("svelte").Component<Props, {}, "">;
10
+ type WorkspaceMap = ReturnType<typeof WorkspaceMap>;
11
+ export default WorkspaceMap;
12
+ //# sourceMappingURL=WorkspaceMap.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WorkspaceMap.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/WorkspaceMap/WorkspaceMap.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAQvE,UAAU,KAAK;IACd,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAyPF,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -0,0 +1,18 @@
1
+ import type { ModelModule } from '../model-types';
2
+ import type { WorkspaceModule, WorkspaceEdge } from './workspace-types';
3
+ export declare const PILL_HEIGHT = 28;
4
+ export declare const PILL_GAP = 5;
5
+ export declare const INDENT_WIDTH = 18;
6
+ export declare const CARD_PADDING_TOP = 40;
7
+ export declare const CARD_PADDING_BOTTOM = 12;
8
+ export declare const CARD_PADDING_LEFT = 14;
9
+ export declare const CARD_PADDING_RIGHT = 14;
10
+ export declare const BASE_PILL_WIDTH = 150;
11
+ /**
12
+ * Build the workspace graph from model modules.
13
+ */
14
+ export declare function buildWorkspaceGraph(modules: ModelModule[]): {
15
+ modules: WorkspaceModule[];
16
+ edges: WorkspaceEdge[];
17
+ };
18
+ //# sourceMappingURL=workspace-graph.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace-graph.d.ts","sourceRoot":"","sources":["../../src/lib/WorkspaceMap/workspace-graph.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAe,MAAM,gBAAgB,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAmB,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAGzF,eAAO,MAAM,WAAW,KAAK,CAAC;AAC9B,eAAO,MAAM,QAAQ,IAAI,CAAC;AAC1B,eAAO,MAAM,YAAY,KAAK,CAAC;AAC/B,eAAO,MAAM,gBAAgB,KAAK,CAAC;AACnC,eAAO,MAAM,mBAAmB,KAAK,CAAC;AACtC,eAAO,MAAM,iBAAiB,KAAK,CAAC;AACpC,eAAO,MAAM,kBAAkB,KAAK,CAAC;AACrC,eAAO,MAAM,eAAe,MAAM,CAAC;AAyFnC;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG;IAC5D,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,KAAK,EAAE,aAAa,EAAE,CAAC;CACvB,CAgFA"}
@@ -0,0 +1,168 @@
1
+ import { getModuleColor } from '../model-graph';
2
+ export const PILL_HEIGHT = 28;
3
+ export const PILL_GAP = 5;
4
+ export const INDENT_WIDTH = 18;
5
+ export const CARD_PADDING_TOP = 40; // header height
6
+ export const CARD_PADDING_BOTTOM = 12;
7
+ export const CARD_PADDING_LEFT = 14;
8
+ export const CARD_PADDING_RIGHT = 14;
9
+ export const BASE_PILL_WIDTH = 150;
10
+ const GRID_GAP_X = 80;
11
+ const GRID_GAP_Y = 60;
12
+ const MODULES_PER_ROW = 3;
13
+ /**
14
+ * Build a tree from intra-module FK relationships.
15
+ * Each entity picks its first intra-module FK target as its parent.
16
+ * Entities with no intra-module parent are roots.
17
+ */
18
+ function buildEntityTree(entities, entityIdsInModule) {
19
+ // For each entity, find its "primary parent" (first intra-module FK)
20
+ const parentMap = new Map(); // childId → parentId
21
+ const childrenMap = new Map(); // parentId → childIds
22
+ for (const ent of entities) {
23
+ childrenMap.set(ent.entityId, []);
24
+ }
25
+ for (const ent of entities) {
26
+ for (const col of ent.columns) {
27
+ if (col.columnType === 'R' &&
28
+ col.refEntityId &&
29
+ entityIdsInModule.has(col.refEntityId) &&
30
+ col.refEntityId !== ent.entityId &&
31
+ !parentMap.has(ent.entityId) // only first FK becomes primary parent
32
+ ) {
33
+ parentMap.set(ent.entityId, col.refEntityId);
34
+ childrenMap.get(col.refEntityId).push(ent.entityId);
35
+ }
36
+ }
37
+ }
38
+ const entityById = new Map(entities.map((e) => [e.entityId, e]));
39
+ function buildNode(entityId, visited) {
40
+ visited.add(entityId);
41
+ const children = (childrenMap.get(entityId) ?? [])
42
+ .filter((id) => !visited.has(id))
43
+ .map((id) => buildNode(id, visited));
44
+ return { entity: entityById.get(entityId), children };
45
+ }
46
+ // Roots: entities with no intra-module parent
47
+ const roots = [];
48
+ const visited = new Set();
49
+ for (const ent of entities) {
50
+ if (!parentMap.has(ent.entityId) && !visited.has(ent.entityId)) {
51
+ roots.push(buildNode(ent.entityId, visited));
52
+ }
53
+ }
54
+ // Append orphans (from cycles)
55
+ for (const ent of entities) {
56
+ if (!visited.has(ent.entityId)) {
57
+ roots.push(buildNode(ent.entityId, visited));
58
+ }
59
+ }
60
+ return roots;
61
+ }
62
+ /**
63
+ * Flatten tree to ordered list with depth info.
64
+ */
65
+ function flattenTree(roots) {
66
+ const result = [];
67
+ function walk(node, depth) {
68
+ result.push({ entity: node.entity, depth });
69
+ for (const child of node.children) {
70
+ walk(child, depth + 1);
71
+ }
72
+ }
73
+ for (const root of roots) {
74
+ walk(root, 0);
75
+ }
76
+ return result;
77
+ }
78
+ /**
79
+ * Build the workspace graph from model modules.
80
+ */
81
+ export function buildWorkspaceGraph(modules) {
82
+ // Map entityId → moduleCd for cross-module edge resolution
83
+ const entityToModule = new Map();
84
+ for (const mod of modules) {
85
+ for (const ent of mod.entities) {
86
+ entityToModule.set(ent.entityId, mod.moduleCd);
87
+ }
88
+ }
89
+ const wsModules = [];
90
+ const wsEdges = [];
91
+ for (let mi = 0; mi < modules.length; mi++) {
92
+ const mod = modules[mi];
93
+ const color = getModuleColor(mi);
94
+ const moduleId = `mod-${mod.moduleCd}`;
95
+ const entityIdsInModule = new Set(mod.entities.map((e) => e.entityId));
96
+ const tree = buildEntityTree(mod.entities, entityIdsInModule);
97
+ const flat = flattenTree(tree);
98
+ // Compute max depth to determine card width
99
+ const maxDepth = flat.reduce((max, f) => Math.max(max, f.depth), 0);
100
+ const cardWidth = CARD_PADDING_LEFT + INDENT_WIDTH * maxDepth + BASE_PILL_WIDTH + CARD_PADDING_RIGHT;
101
+ const entities = flat.map((f, idx) => ({
102
+ entityId: f.entity.entityId,
103
+ entityCd: f.entity.entityCd,
104
+ label: f.entity.resLabel || f.entity.entityCd,
105
+ depth: f.depth,
106
+ rx: CARD_PADDING_LEFT + f.depth * INDENT_WIDTH,
107
+ ry: CARD_PADDING_TOP + idx * (PILL_HEIGHT + PILL_GAP),
108
+ width: BASE_PILL_WIDTH,
109
+ height: PILL_HEIGHT,
110
+ }));
111
+ const cardHeight = CARD_PADDING_TOP +
112
+ entities.length * PILL_HEIGHT +
113
+ Math.max(0, entities.length - 1) * PILL_GAP +
114
+ CARD_PADDING_BOTTOM;
115
+ wsModules.push({
116
+ id: moduleId,
117
+ moduleCd: mod.moduleCd,
118
+ label: mod.description || mod.moduleCd,
119
+ color,
120
+ x: 0,
121
+ y: 0,
122
+ width: cardWidth,
123
+ height: cardHeight,
124
+ entities,
125
+ });
126
+ // Build edges from FK columns (cross-module only, intra handled by tree)
127
+ for (const ent of mod.entities) {
128
+ for (const col of ent.columns) {
129
+ if (col.columnType === 'R' && col.refEntityId) {
130
+ const targetModuleCd = entityToModule.get(col.refEntityId);
131
+ if (!targetModuleCd)
132
+ continue;
133
+ const targetModuleId = `mod-${targetModuleCd}`;
134
+ wsEdges.push({
135
+ id: `ws-edge-${ent.entityId}-${col.columnCd}`,
136
+ sourceModuleId: moduleId,
137
+ sourceEntityId: ent.entityId,
138
+ targetModuleId,
139
+ targetEntityId: col.refEntityId,
140
+ color,
141
+ });
142
+ }
143
+ }
144
+ }
145
+ }
146
+ // Layout modules in a grid
147
+ layoutModuleGrid(wsModules);
148
+ return { modules: wsModules, edges: wsEdges };
149
+ }
150
+ /**
151
+ * Arrange modules in a grid layout.
152
+ */
153
+ function layoutModuleGrid(modules) {
154
+ let curX = 0;
155
+ let curY = 0;
156
+ let rowMaxHeight = 0;
157
+ for (let i = 0; i < modules.length; i++) {
158
+ if (i > 0 && i % MODULES_PER_ROW === 0) {
159
+ curX = 0;
160
+ curY += rowMaxHeight + GRID_GAP_Y;
161
+ rowMaxHeight = 0;
162
+ }
163
+ modules[i].x = curX;
164
+ modules[i].y = curY;
165
+ curX += modules[i].width + GRID_GAP_X;
166
+ rowMaxHeight = Math.max(rowMaxHeight, modules[i].height);
167
+ }
168
+ }