@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,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
|
+
}
|