@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,13 @@
1
+ import type { DiagramNode, ResolvedEdge } from '../diagram-types';
2
+ interface Props {
3
+ nodes: DiagramNode[];
4
+ edges: ResolvedEdge[];
5
+ storageKey?: string;
6
+ minZoom?: number;
7
+ maxZoom?: number;
8
+ onNodeDblClick?: (nodeId: string) => void;
9
+ }
10
+ declare const SimpleDiagram: import("svelte").Component<Props, {}, "">;
11
+ type SimpleDiagram = ReturnType<typeof SimpleDiagram>;
12
+ export default SimpleDiagram;
13
+ //# sourceMappingURL=SimpleDiagram.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SimpleDiagram.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/SimpleDiagram/SimpleDiagram.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAS,MAAM,kBAAkB,CAAC;AAUxE,UAAU,KAAK;IACd,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CAC1C;AAohBF,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
@@ -0,0 +1,33 @@
1
+ import type { Point } from '../diagram-types';
2
+ /** Build an SVG path string from a sequence of points, with rounded corners. */
3
+ export declare function buildSvgPath(points: Point[]): string;
4
+ /** Get the label position for an edge (midpoint of the path, offset above). */
5
+ export declare function edgeLabelPos(points: Point[]): {
6
+ x: number;
7
+ y: number;
8
+ };
9
+ /** Insert clamp points before drag to prevent diagonals. */
10
+ export declare function prepareDragPoints(pts: Point[], segIndex: number, orientation: 'H' | 'V'): {
11
+ points: Point[];
12
+ segIndex: number;
13
+ };
14
+ /** Remove zero-length segments and collinear midpoints. */
15
+ export declare function consolidatePoints(pts: Point[]): Point[];
16
+ /** Insert a rectangular jog at a segment, returns new points + segment to drag. */
17
+ export declare function insertJog(pts: Point[], segIndex: number, orientation: 'H' | 'V'): {
18
+ points: Point[];
19
+ segIndex: number;
20
+ orientation: 'H' | 'V';
21
+ };
22
+ export interface SegHandle {
23
+ x: number;
24
+ y: number;
25
+ segIndex: number;
26
+ orientation: 'H' | 'V';
27
+ draggable: boolean;
28
+ }
29
+ /** Compute drag/split handles for each segment in a point array. */
30
+ export declare function getSegmentHandles(points: Point[]): SegHandle[];
31
+ /** Apply a segment drag: move both endpoints of the segment in the drag direction. */
32
+ export declare function applySegmentDrag(pts: Point[], segIndex: number, orientation: 'H' | 'V', pos: Point): Point[];
33
+ //# sourceMappingURL=diagram-geometry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagram-geometry.d.ts","sourceRoot":"","sources":["../../src/lib/SimpleDiagram/diagram-geometry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AA+B9C,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,CAQpD;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAGtE;AAID,4DAA4D;AAC5D,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,GAAG,GAAG,GAAG;IAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAyB/H;AAED,2DAA2D;AAC3D,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,KAAK,EAAE,CAiBvD;AAED,mFAAmF;AACnF,wBAAgB,SAAS,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,GAAG,GAAG,GAAG;IAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,GAAG,GAAG,GAAG,CAAA;CAAE,CAa/I;AAID,MAAM,WAAW,SAAS;IACzB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,GAAG,GAAG,GAAG,CAAC;IACvB,SAAS,EAAE,OAAO,CAAC;CACnB;AAED,oEAAoE;AACpE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,SAAS,EAAE,CAgB9D;AAED,sFAAsF;AACtF,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,GAAG,GAAG,EAAE,GAAG,EAAE,KAAK,GAAG,KAAK,EAAE,CAU5G"}
@@ -0,0 +1,137 @@
1
+ const BEND_RADIUS = 8;
2
+ const SNAP = 3;
3
+ // --- SVG path building ---
4
+ /** Build a quadratic Bezier corner between two orthogonal segments. */
5
+ function roundedCorner(from, corner, to, r) {
6
+ const d1 = Math.max(Math.abs(corner.x - from.x), Math.abs(corner.y - from.y));
7
+ const d2 = Math.max(Math.abs(to.x - corner.x), Math.abs(to.y - corner.y));
8
+ const cr = Math.min(r, d1 / 2, d2 / 2);
9
+ if (cr < 1)
10
+ return `L ${corner.x} ${corner.y}`;
11
+ const d1x = corner.x - from.x, d1y = corner.y - from.y;
12
+ const d2x = to.x - corner.x, d2y = to.y - corner.y;
13
+ const h1 = Math.abs(d1x) >= Math.abs(d1y);
14
+ const dx1 = h1 ? Math.sign(d1x) : 0;
15
+ const dy1 = h1 ? 0 : Math.sign(d1y);
16
+ const h2 = Math.abs(d2x) >= Math.abs(d2y);
17
+ const dx2 = h2 ? Math.sign(d2x) : 0;
18
+ const dy2 = h2 ? 0 : Math.sign(d2y);
19
+ const ax = corner.x - dx1 * cr;
20
+ const ay = corner.y - dy1 * cr;
21
+ const bx = corner.x + dx2 * cr;
22
+ const by = corner.y + dy2 * cr;
23
+ return `L ${ax} ${ay} Q ${corner.x} ${corner.y} ${bx} ${by}`;
24
+ }
25
+ /** Build an SVG path string from a sequence of points, with rounded corners. */
26
+ export function buildSvgPath(points) {
27
+ if (points.length < 2)
28
+ return '';
29
+ let d = `M ${points[0].x} ${points[0].y}`;
30
+ for (let i = 1; i < points.length - 1; i++) {
31
+ d += ` ${roundedCorner(points[i - 1], points[i], points[i + 1], BEND_RADIUS)}`;
32
+ }
33
+ d += ` L ${points[points.length - 1].x} ${points[points.length - 1].y}`;
34
+ return d;
35
+ }
36
+ /** Get the label position for an edge (midpoint of the path, offset above). */
37
+ export function edgeLabelPos(points) {
38
+ const mid = points[Math.floor(points.length / 2)];
39
+ return { x: mid.x, y: mid.y - 6 };
40
+ }
41
+ // --- Edge segment editing ---
42
+ /** Insert clamp points before drag to prevent diagonals. */
43
+ export function prepareDragPoints(pts, segIndex, orientation) {
44
+ const result = pts.map((p) => ({ ...p }));
45
+ let si = segIndex;
46
+ if (orientation === 'H') {
47
+ const y = result[si].y;
48
+ if (si > 0 && Math.abs(result[si - 1].y - y) < SNAP && Math.abs(result[si - 1].x - result[si].x) > SNAP) {
49
+ result.splice(si, 0, { ...result[si] });
50
+ si++;
51
+ }
52
+ if (si + 2 < result.length && Math.abs(result[si + 2].y - y) < SNAP && Math.abs(result[si + 1].x - result[si + 2].x) > SNAP) {
53
+ result.splice(si + 2, 0, { ...result[si + 1] });
54
+ }
55
+ }
56
+ else {
57
+ const x = result[si].x;
58
+ if (si > 0 && Math.abs(result[si - 1].x - x) < SNAP && Math.abs(result[si - 1].y - result[si].y) > SNAP) {
59
+ result.splice(si, 0, { ...result[si] });
60
+ si++;
61
+ }
62
+ if (si + 2 < result.length && Math.abs(result[si + 2].x - x) < SNAP && Math.abs(result[si + 1].y - result[si + 2].y) > SNAP) {
63
+ result.splice(si + 2, 0, { ...result[si + 1] });
64
+ }
65
+ }
66
+ return { points: result, segIndex: si };
67
+ }
68
+ /** Remove zero-length segments and collinear midpoints. */
69
+ export function consolidatePoints(pts) {
70
+ if (pts.length < 3)
71
+ return pts;
72
+ const result = [pts[0]];
73
+ for (let i = 1; i < pts.length; i++) {
74
+ const prev = result[result.length - 1];
75
+ const curr = pts[i];
76
+ if (Math.abs(prev.x - curr.x) < SNAP && Math.abs(prev.y - curr.y) < SNAP)
77
+ continue;
78
+ if (i < pts.length - 1) {
79
+ const next = pts[i + 1];
80
+ const sameX = Math.abs(prev.x - curr.x) < SNAP && Math.abs(curr.x - next.x) < SNAP;
81
+ const sameY = Math.abs(prev.y - curr.y) < SNAP && Math.abs(curr.y - next.y) < SNAP;
82
+ if (sameX && curr.y >= Math.min(prev.y, next.y) && curr.y <= Math.max(prev.y, next.y))
83
+ continue;
84
+ if (sameY && curr.x >= Math.min(prev.x, next.x) && curr.x <= Math.max(prev.x, next.x))
85
+ continue;
86
+ }
87
+ result.push(curr);
88
+ }
89
+ return result;
90
+ }
91
+ /** Insert a rectangular jog at a segment, returns new points + segment to drag. */
92
+ export function insertJog(pts, segIndex, orientation) {
93
+ const result = pts.map((p) => ({ ...p }));
94
+ const a = result[segIndex], b = result[segIndex + 1];
95
+ if (orientation === 'H') {
96
+ const mx = (a.x + b.x) / 2;
97
+ result.splice(segIndex + 1, 0, { x: mx, y: a.y }, { x: mx, y: a.y });
98
+ return { points: result, segIndex: segIndex + 1, orientation: 'V' };
99
+ }
100
+ else {
101
+ const my = (a.y + b.y) / 2;
102
+ result.splice(segIndex + 1, 0, { x: a.x, y: my }, { x: a.x, y: my });
103
+ return { points: result, segIndex: segIndex + 1, orientation: 'H' };
104
+ }
105
+ }
106
+ /** Compute drag/split handles for each segment in a point array. */
107
+ export function getSegmentHandles(points) {
108
+ if (points.length < 2)
109
+ return [];
110
+ const handles = [];
111
+ const lastSeg = points.length - 2;
112
+ for (let i = 0; i < points.length - 1; i++) {
113
+ const a = points[i], b = points[i + 1];
114
+ const isH = Math.abs(a.y - b.y) < SNAP;
115
+ handles.push({
116
+ x: (a.x + b.x) / 2,
117
+ y: (a.y + b.y) / 2,
118
+ segIndex: i,
119
+ orientation: isH ? 'H' : 'V',
120
+ draggable: i > 0 && i < lastSeg,
121
+ });
122
+ }
123
+ return handles;
124
+ }
125
+ /** Apply a segment drag: move both endpoints of the segment in the drag direction. */
126
+ export function applySegmentDrag(pts, segIndex, orientation, pos) {
127
+ const result = pts.map((p) => ({ ...p }));
128
+ if (orientation === 'H') {
129
+ result[segIndex] = { ...result[segIndex], y: pos.y };
130
+ result[segIndex + 1] = { ...result[segIndex + 1], y: pos.y };
131
+ }
132
+ else {
133
+ result[segIndex] = { ...result[segIndex], x: pos.x };
134
+ result[segIndex + 1] = { ...result[segIndex + 1], x: pos.x };
135
+ }
136
+ return result;
137
+ }
@@ -0,0 +1,27 @@
1
+ import type { DiagramNode, DiagramEdge, ResolvedEdge } from '../diagram-types';
2
+ export type NodeLookup = (id: string) => DiagramNode | undefined;
3
+ /**
4
+ * Compute node positions using topological layered placement.
5
+ */
6
+ export declare function simpleLayout(nodes: DiagramNode[], edges: DiagramEdge[]): {
7
+ nodes: DiagramNode[];
8
+ edges: ResolvedEdge[];
9
+ };
10
+ /**
11
+ * Layout with module as the primary grouping axis.
12
+ * Each module's entities are arranged in a compact rectangle (matrix grid).
13
+ * Module blocks are laid out left-to-right, wrapping to avoid excessive width.
14
+ */
15
+ export declare function moduleLayout(nodes: DiagramNode[], edges: DiagramEdge[]): {
16
+ nodes: DiagramNode[];
17
+ edges: ResolvedEdge[];
18
+ };
19
+ /**
20
+ * Route a single edge: compute the fully resolved point array.
21
+ */
22
+ export declare function routeEdge(edge: DiagramEdge, lookup: NodeLookup): ResolvedEdge;
23
+ /**
24
+ * Route all edges given the current node positions.
25
+ */
26
+ export declare function routeAllEdges(nodes: DiagramNode[], edges: DiagramEdge[]): ResolvedEdge[];
27
+ //# sourceMappingURL=simple-layout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"simple-layout.d.ts","sourceRoot":"","sources":["../../src/lib/SimpleDiagram/simple-layout.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,YAAY,EAAS,MAAM,kBAAkB,CAAC;AActF,MAAM,MAAM,UAAU,GAAG,CAAC,EAAE,EAAE,MAAM,KAAK,WAAW,GAAG,SAAS,CAAC;AA0HjE;;GAEG;AACH,wBAAgB,YAAY,CAC3B,KAAK,EAAE,WAAW,EAAE,EACpB,KAAK,EAAE,WAAW,EAAE,GAClB;IAAE,KAAK,EAAE,WAAW,EAAE,CAAC;IAAC,KAAK,EAAE,YAAY,EAAE,CAAA;CAAE,CAejD;AAqFD;;;;GAIG;AACH,wBAAgB,YAAY,CAC3B,KAAK,EAAE,WAAW,EAAE,EACpB,KAAK,EAAE,WAAW,EAAE,GAClB;IAAE,KAAK,EAAE,WAAW,EAAE,CAAC;IAAC,KAAK,EAAE,YAAY,EAAE,CAAA;CAAE,CAmCjD;AA0GD;;GAEG;AACH,wBAAgB,SAAS,CACxB,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,UAAU,GAChB,YAAY,CAcd;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC5B,KAAK,EAAE,WAAW,EAAE,EACpB,KAAK,EAAE,WAAW,EAAE,GAClB,YAAY,EAAE,CAIhB"}
@@ -0,0 +1,364 @@
1
+ const NODE_WIDTH = 260;
2
+ const NODE_GAP = 40;
3
+ const LAYER_GAP = 100;
4
+ const STUB_LENGTH = 24;
5
+ const LOOP_OFFSET = 40;
6
+ const HEADER_HEIGHT = 30;
7
+ const ROW_HEIGHT = 22;
8
+ const MODULE_BLOCK_GAP = 80;
9
+ const OVERLAP_MARGIN = 30;
10
+ /** Group items by a key function into a Map of arrays. */
11
+ function groupBy(items, key) {
12
+ const map = new Map();
13
+ for (const item of items) {
14
+ const k = key(item);
15
+ let group = map.get(k);
16
+ if (!group) {
17
+ group = [];
18
+ map.set(k, group);
19
+ }
20
+ group.push(item);
21
+ }
22
+ return map;
23
+ }
24
+ /** Build adjacency lists from a set of node IDs and edges (skipping self-edges and external nodes). */
25
+ function buildAdjacency(nodeIds, edges) {
26
+ const inDeg = new Map();
27
+ const outList = new Map();
28
+ for (const id of nodeIds) {
29
+ inDeg.set(id, 0);
30
+ outList.set(id, []);
31
+ }
32
+ for (const e of edges) {
33
+ if (e.source === e.target || !nodeIds.has(e.source) || !nodeIds.has(e.target))
34
+ continue;
35
+ inDeg.set(e.target, (inDeg.get(e.target) ?? 0) + 1);
36
+ outList.get(e.source).push(e.target);
37
+ }
38
+ return { inDeg, outList };
39
+ }
40
+ /** Compute a row's Y center position on a node by predicate. */
41
+ function rowY(node, predicate) {
42
+ const idx = Math.max(0, node.data.columns.findIndex(predicate));
43
+ return node.y + HEADER_HEIGHT + idx * ROW_HEIGHT + ROW_HEIGHT / 2;
44
+ }
45
+ // --- Topological layered layout ---
46
+ /**
47
+ * Assign layers via BFS from root nodes (no incoming edges).
48
+ * Returns Map<nodeId, layer>.
49
+ */
50
+ function assignLayers(nodes, edges) {
51
+ const nodeIds = new Set(nodes.map((n) => n.id));
52
+ const { inDeg, outList } = buildAdjacency(nodeIds, edges);
53
+ const layers = new Map();
54
+ const queue = [];
55
+ // Roots: nodes with no incoming edges
56
+ for (const n of nodes) {
57
+ if (inDeg.get(n.id) === 0) {
58
+ layers.set(n.id, 0);
59
+ queue.push(n.id);
60
+ }
61
+ }
62
+ // If all nodes have incoming edges (cycle), pick the first as root
63
+ if (queue.length === 0 && nodes.length > 0) {
64
+ layers.set(nodes[0].id, 0);
65
+ queue.push(nodes[0].id);
66
+ }
67
+ // BFS — push each target to max(currentLayer + 1, existing).
68
+ // Cap re-visits per node to handle cycles in FK relationships.
69
+ const visits = new Map();
70
+ const maxVisits = nodes.length;
71
+ let head = 0;
72
+ while (head < queue.length) {
73
+ const nodeId = queue[head++];
74
+ const layer = layers.get(nodeId);
75
+ for (const tgt of outList.get(nodeId) ?? []) {
76
+ const prev = layers.get(tgt);
77
+ const next = layer + 1;
78
+ if (prev === undefined || next > prev) {
79
+ const vc = (visits.get(tgt) ?? 0) + 1;
80
+ if (vc > maxVisits)
81
+ continue; // cycle — stop propagating
82
+ visits.set(tgt, vc);
83
+ layers.set(tgt, next);
84
+ queue.push(tgt);
85
+ }
86
+ }
87
+ }
88
+ // Assign any remaining unvisited nodes (disconnected)
89
+ for (const n of nodes) {
90
+ if (!layers.has(n.id)) {
91
+ layers.set(n.id, 0);
92
+ }
93
+ }
94
+ return layers;
95
+ }
96
+ /**
97
+ * Position nodes in columns and center vertically.
98
+ */
99
+ function positionColumns(columns) {
100
+ const colHeights = columns.map((col) => {
101
+ let h = 0;
102
+ for (let i = 0; i < col.length; i++) {
103
+ h += col[i].height;
104
+ if (i < col.length - 1)
105
+ h += NODE_GAP;
106
+ }
107
+ return h;
108
+ });
109
+ const maxH = Math.max(...colHeights, 0);
110
+ const positioned = [];
111
+ for (let ci = 0; ci < columns.length; ci++) {
112
+ const col = columns[ci];
113
+ const x = ci * (NODE_WIDTH + LAYER_GAP);
114
+ const offsetY = (maxH - colHeights[ci]) / 2;
115
+ let y = offsetY;
116
+ for (const n of col) {
117
+ positioned.push({ ...n, x, y });
118
+ y += n.height + NODE_GAP;
119
+ }
120
+ }
121
+ return positioned;
122
+ }
123
+ /**
124
+ * Compute node positions using topological layered placement.
125
+ */
126
+ export function simpleLayout(nodes, edges) {
127
+ if (nodes.length === 0)
128
+ return { nodes, edges: [] };
129
+ const layers = assignLayers(nodes, edges);
130
+ const layerGroups = groupBy(nodes, (n) => layers.get(n.id) ?? 0);
131
+ const maxLayer = Math.max(...layers.values());
132
+ const columns = [];
133
+ for (let i = 0; i <= maxLayer; i++) {
134
+ columns.push(layerGroups.get(i) ?? []);
135
+ }
136
+ const positioned = positionColumns(columns);
137
+ const resolved = routeAllEdges(positioned, edges);
138
+ return { nodes: positioned, edges: resolved };
139
+ }
140
+ /**
141
+ * Sort nodes topologically within a group using intra-group FK edges.
142
+ */
143
+ function topoSort(group, edges) {
144
+ const nodeIds = new Set(group.map((n) => n.id));
145
+ const { inDeg, outList } = buildAdjacency(nodeIds, edges);
146
+ const sorted = [];
147
+ const visited = new Set();
148
+ const queue = [];
149
+ for (const n of group) {
150
+ if (inDeg.get(n.id) === 0)
151
+ queue.push(n.id);
152
+ }
153
+ const nodeMap = new Map(group.map((n) => [n.id, n]));
154
+ while (queue.length > 0) {
155
+ const id = queue.shift();
156
+ sorted.push(nodeMap.get(id));
157
+ visited.add(id);
158
+ for (const tgt of outList.get(id) ?? []) {
159
+ const d = inDeg.get(tgt) - 1;
160
+ inDeg.set(tgt, d);
161
+ if (d === 0)
162
+ queue.push(tgt);
163
+ }
164
+ }
165
+ // Append any remaining (cycles)
166
+ for (const n of group) {
167
+ if (!visited.has(n.id))
168
+ sorted.push(n);
169
+ }
170
+ return sorted;
171
+ }
172
+ /** Compute grid block dimensions without positioning. */
173
+ function gridBlockSize(group) {
174
+ if (group.length === 0)
175
+ return { cols: 0, rows: 0, rowMaxH: [], width: 0, height: 0 };
176
+ const cols = Math.ceil(Math.sqrt(group.length));
177
+ const rows = Math.ceil(group.length / cols);
178
+ const rowMaxH = new Array(rows).fill(0);
179
+ for (let i = 0; i < group.length; i++) {
180
+ const r = Math.floor(i / cols);
181
+ rowMaxH[r] = Math.max(rowMaxH[r], group[i].height);
182
+ }
183
+ const width = cols * NODE_WIDTH + (cols - 1) * LAYER_GAP;
184
+ let height = 0;
185
+ for (let r = 0; r < rows; r++) {
186
+ height += rowMaxH[r];
187
+ if (r < rows - 1)
188
+ height += NODE_GAP;
189
+ }
190
+ return { cols, rows, rowMaxH, width, height };
191
+ }
192
+ /** Position nodes in a grid block at the given origin, using pre-computed size. */
193
+ function gridBlock(group, originX, originY, size) {
194
+ if (group.length === 0)
195
+ return [];
196
+ const positioned = [];
197
+ for (let i = 0; i < group.length; i++) {
198
+ const col = i % size.cols;
199
+ const row = Math.floor(i / size.cols);
200
+ let y = originY;
201
+ for (let r = 0; r < row; r++)
202
+ y += size.rowMaxH[r] + NODE_GAP;
203
+ positioned.push({
204
+ ...group[i],
205
+ x: originX + col * (NODE_WIDTH + LAYER_GAP),
206
+ y,
207
+ });
208
+ }
209
+ return positioned;
210
+ }
211
+ /**
212
+ * Layout with module as the primary grouping axis.
213
+ * Each module's entities are arranged in a compact rectangle (matrix grid).
214
+ * Module blocks are laid out left-to-right, wrapping to avoid excessive width.
215
+ */
216
+ export function moduleLayout(nodes, edges) {
217
+ if (nodes.length === 0)
218
+ return { nodes, edges: [] };
219
+ // Group nodes by module, sort each topologically
220
+ const moduleGroups = groupBy(nodes, (n) => n.data.moduleCd);
221
+ for (const [mod, group] of moduleGroups) {
222
+ moduleGroups.set(mod, topoSort(group, edges));
223
+ }
224
+ // Lay out module blocks left-to-right, wrapping when needed
225
+ const positioned = [];
226
+ let cursorX = 0;
227
+ let cursorY = 0;
228
+ let rowHeight = 0;
229
+ // Target ~3 module blocks per row
230
+ const maxRowWidth = 3 * (3 * (NODE_WIDTH + LAYER_GAP)) + 2 * MODULE_BLOCK_GAP;
231
+ for (const group of moduleGroups.values()) {
232
+ const size = gridBlockSize(group);
233
+ // Wrap to next row if this block exceeds max width (unless first in row)
234
+ if (cursorX > 0 && cursorX + size.width > maxRowWidth) {
235
+ cursorX = 0;
236
+ cursorY += rowHeight + MODULE_BLOCK_GAP;
237
+ rowHeight = 0;
238
+ }
239
+ positioned.push(...gridBlock(group, cursorX, cursorY, size));
240
+ cursorX += size.width + MODULE_BLOCK_GAP;
241
+ rowHeight = Math.max(rowHeight, size.height);
242
+ }
243
+ const resolved = routeAllEdges(positioned, edges);
244
+ return { nodes: positioned, edges: resolved };
245
+ }
246
+ // --- Edge routing ---
247
+ /** Route a self-referencing edge as a U-loop on the right side. */
248
+ function routeSelfEdge(edge, node, srcY, tgtY) {
249
+ const rightEdge = node.x + node.width;
250
+ const src = { x: rightEdge, y: srcY };
251
+ const tgt = { x: rightEdge, y: tgtY };
252
+ const rightX = rightEdge + LOOP_OFFSET;
253
+ if (Math.abs(srcY - tgtY) < ROW_HEIGHT) {
254
+ // Same row: rectangle loop above
255
+ const topY = Math.min(srcY, tgtY) - LOOP_OFFSET;
256
+ return { ...edge, points: [
257
+ src,
258
+ { x: rightX, y: src.y },
259
+ { x: rightX, y: topY },
260
+ { x: rightEdge, y: topY },
261
+ tgt,
262
+ ] };
263
+ }
264
+ // Different rows: simple U-shape on the right
265
+ return { ...edge, points: [
266
+ src,
267
+ { x: rightX, y: src.y },
268
+ { x: rightX, y: tgt.y },
269
+ tgt,
270
+ ] };
271
+ }
272
+ /** Route stubs that avoid crossing through the connected nodes. */
273
+ function stubRoute(src, tgt, dir, srcNode, tgtNode) {
274
+ const stubSrc = src.x + dir * STUB_LENGTH;
275
+ const stubTgt = tgt.x - dir * STUB_LENGTH;
276
+ // Find midY that doesn't cross through either node
277
+ const srcBottom = srcNode.y + srcNode.height;
278
+ const tgtTop = tgtNode.y;
279
+ const srcTop = srcNode.y;
280
+ const tgtBottom = tgtNode.y + tgtNode.height;
281
+ let midY;
282
+ if (srcBottom + OVERLAP_MARGIN < tgtTop) {
283
+ // Gap between nodes — route through it
284
+ midY = (srcBottom + tgtTop) / 2;
285
+ }
286
+ else if (tgtBottom + OVERLAP_MARGIN < srcTop) {
287
+ midY = (tgtBottom + srcTop) / 2;
288
+ }
289
+ else {
290
+ // Nodes overlap vertically — route above both
291
+ midY = Math.min(srcTop, tgtTop) - OVERLAP_MARGIN;
292
+ }
293
+ return [
294
+ src,
295
+ { x: stubSrc, y: src.y },
296
+ { x: stubSrc, y: midY },
297
+ { x: stubTgt, y: midY },
298
+ { x: stubTgt, y: tgt.y },
299
+ tgt,
300
+ ];
301
+ }
302
+ /** Route a normal (non-self) edge with orthogonal H→V→H path. */
303
+ function routeNormalEdge(edge, srcNode, tgtNode, srcY, tgtY) {
304
+ const srcCenterX = srcNode.x + srcNode.width / 2;
305
+ const tgtCenterX = tgtNode.x + tgtNode.width / 2;
306
+ const targetIsRight = tgtCenterX >= srcCenterX;
307
+ const dir = targetIsRight ? 1 : -1;
308
+ const src = {
309
+ x: targetIsRight ? srcNode.x + srcNode.width : srcNode.x,
310
+ y: srcY,
311
+ };
312
+ const tgt = {
313
+ x: targetIsRight ? tgtNode.x : tgtNode.x + tgtNode.width,
314
+ y: tgtY,
315
+ };
316
+ const gap = (tgt.x - src.x) * dir;
317
+ let points;
318
+ if (gap > STUB_LENGTH * 2) {
319
+ const midX = (src.x + tgt.x) / 2;
320
+ // Check if the vertical segment at midX would pass through either node
321
+ const midXInSrc = midX > srcNode.x && midX < srcNode.x + srcNode.width;
322
+ const midXInTgt = midX > tgtNode.x && midX < tgtNode.x + tgtNode.width;
323
+ if (!midXInSrc && !midXInTgt) {
324
+ points = [
325
+ src,
326
+ { x: midX, y: src.y },
327
+ { x: midX, y: tgt.y },
328
+ tgt,
329
+ ];
330
+ }
331
+ else {
332
+ // midX overlaps a node — use stubs to route around
333
+ points = stubRoute(src, tgt, dir, srcNode, tgtNode);
334
+ }
335
+ }
336
+ else {
337
+ points = stubRoute(src, tgt, dir, srcNode, tgtNode);
338
+ }
339
+ return { ...edge, points };
340
+ }
341
+ /**
342
+ * Route a single edge: compute the fully resolved point array.
343
+ */
344
+ export function routeEdge(edge, lookup) {
345
+ const srcNode = lookup(edge.source);
346
+ const tgtNode = lookup(edge.target);
347
+ if (!srcNode || !tgtNode) {
348
+ return { ...edge, points: [] };
349
+ }
350
+ const srcY = rowY(srcNode, (c) => c.columnCd === edge.sourceHandle);
351
+ const tgtY = rowY(tgtNode, (c) => c.isPk);
352
+ if (edge.source === edge.target) {
353
+ return routeSelfEdge(edge, srcNode, srcY, tgtY);
354
+ }
355
+ return routeNormalEdge(edge, srcNode, tgtNode, srcY, tgtY);
356
+ }
357
+ /**
358
+ * Route all edges given the current node positions.
359
+ */
360
+ export function routeAllEdges(nodes, edges) {
361
+ const nodeMap = new Map(nodes.map((n) => [n.id, n]));
362
+ const lookup = (id) => nodeMap.get(id);
363
+ return edges.map((e) => routeEdge(e, lookup));
364
+ }