gitmaps 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +167 -0
- package/app/api/auth/favorites/route.ts +56 -0
- package/app/api/auth/github/callback/route.ts +103 -0
- package/app/api/auth/github/route.ts +32 -0
- package/app/api/auth/me/route.ts +52 -0
- package/app/api/auth/positions/route.ts +50 -0
- package/app/api/chat/route.ts +101 -0
- package/app/api/connections/route.ts +72 -0
- package/app/api/github/repos/route.ts +111 -0
- package/app/api/positions/route.ts +80 -0
- package/app/api/repo/branch-diff/route.ts +201 -0
- package/app/api/repo/branches/route.ts +53 -0
- package/app/api/repo/browse/route.ts +55 -0
- package/app/api/repo/clone/route.ts +78 -0
- package/app/api/repo/clone-stream/route.ts +131 -0
- package/app/api/repo/file-content/route.ts +28 -0
- package/app/api/repo/file-delete/route.ts +62 -0
- package/app/api/repo/file-history/route.ts +45 -0
- package/app/api/repo/file-rename/route.ts +83 -0
- package/app/api/repo/file-save/route.ts +45 -0
- package/app/api/repo/files/route.ts +169 -0
- package/app/api/repo/git-blame/route.ts +86 -0
- package/app/api/repo/git-commit/route.ts +40 -0
- package/app/api/repo/git-heatmap/route.ts +55 -0
- package/app/api/repo/imports/route.ts +154 -0
- package/app/api/repo/load/route.ts +56 -0
- package/app/api/repo/mode/route.ts +14 -0
- package/app/api/repo/search/route.ts +127 -0
- package/app/api/repo/tree/route.ts +104 -0
- package/app/api/repo/upload/route.ts +53 -0
- package/app/api/repo/validate-path.ts +53 -0
- package/app/canvas_users.db +0 -0
- package/app/canvas_users.db-shm +0 -0
- package/app/canvas_users.db-wal +0 -0
- package/app/globals.css +7899 -0
- package/app/layout.tsx +493 -0
- package/app/lib/auth.ts +193 -0
- package/app/lib/auto-save.ts +137 -0
- package/app/lib/branch-compare.ts +443 -0
- package/app/lib/breadcrumbs.ts +170 -0
- package/app/lib/canvas-export.ts +358 -0
- package/app/lib/canvas-text.ts +912 -0
- package/app/lib/canvas.ts +564 -0
- package/app/lib/card-arrangement.ts +188 -0
- package/app/lib/card-context-menu.tsx +453 -0
- package/app/lib/card-diff-markers.ts +270 -0
- package/app/lib/card-expand.ts +189 -0
- package/app/lib/card-groups.ts +246 -0
- package/app/lib/cards.tsx +914 -0
- package/app/lib/chat.tsx +308 -0
- package/app/lib/code-editor.ts +508 -0
- package/app/lib/command-palette.ts +262 -0
- package/app/lib/connections.tsx +1037 -0
- package/app/lib/context.ts +94 -0
- package/app/lib/cursor-sharing.ts +281 -0
- package/app/lib/dependency-graph.ts +438 -0
- package/app/lib/events.tsx +1747 -0
- package/app/lib/file-card-plugin.ts +134 -0
- package/app/lib/file-modal.tsx +849 -0
- package/app/lib/file-preview.ts +400 -0
- package/app/lib/file-tabs.ts +318 -0
- package/app/lib/galaxydraw-bridge.ts +477 -0
- package/app/lib/galaxydraw.test.ts +229 -0
- package/app/lib/global-search.ts +264 -0
- package/app/lib/goto-definition.ts +224 -0
- package/app/lib/heatmap.ts +178 -0
- package/app/lib/hidden-files.tsx +222 -0
- package/app/lib/layers.ts +0 -0
- package/app/lib/layers.tsx +365 -0
- package/app/lib/loading.tsx +45 -0
- package/app/lib/multi-repo.ts +286 -0
- package/app/lib/new-file-dialog.tsx +230 -0
- package/app/lib/onboarding.tsx +213 -0
- package/app/lib/perf-overlay.ts +360 -0
- package/app/lib/positions.ts +176 -0
- package/app/lib/pr-review.ts +374 -0
- package/app/lib/production-mode.ts +47 -0
- package/app/lib/repo.tsx +977 -0
- package/app/lib/settings-modal.tsx +374 -0
- package/app/lib/settings.ts +97 -0
- package/app/lib/shortcuts-panel.ts +141 -0
- package/app/lib/status-bar.ts +128 -0
- package/app/lib/symbol-outline.ts +212 -0
- package/app/lib/syntax.ts +177 -0
- package/app/lib/tab-diff.ts +238 -0
- package/app/lib/user.tsx +133 -0
- package/app/lib/utils.ts +78 -0
- package/app/lib/viewport-culling.ts +728 -0
- package/app/page.client.tsx +215 -0
- package/app/page.tsx +291 -0
- package/app/state/machine.js +196 -0
- package/app/styles/main.css +2168 -0
- package/banner.png +0 -0
- package/cli.ts +44 -0
- package/package.json +75 -0
- package/packages/galaxydraw/README.md +296 -0
- package/packages/galaxydraw/banner.png +0 -0
- package/packages/galaxydraw/demo/build-static.ts +100 -0
- package/packages/galaxydraw/demo/client.ts +154 -0
- package/packages/galaxydraw/demo/dist/client.js +8 -0
- package/packages/galaxydraw/demo/index.html +256 -0
- package/packages/galaxydraw/demo/server.ts +96 -0
- package/packages/galaxydraw/dist/index.js +984 -0
- package/packages/galaxydraw/dist/index.js.map +16 -0
- package/packages/galaxydraw/node_modules/.bin/tsc.bunx +0 -0
- package/packages/galaxydraw/node_modules/.bin/tsc.exe +0 -0
- package/packages/galaxydraw/node_modules/.bin/tsserver.bunx +0 -0
- package/packages/galaxydraw/node_modules/.bin/tsserver.exe +0 -0
- package/packages/galaxydraw/package.json +49 -0
- package/packages/galaxydraw/perf.test.ts +284 -0
- package/packages/galaxydraw/src/core/cards.ts +435 -0
- package/packages/galaxydraw/src/core/engine.ts +339 -0
- package/packages/galaxydraw/src/core/events.ts +81 -0
- package/packages/galaxydraw/src/core/layout.ts +136 -0
- package/packages/galaxydraw/src/core/minimap.ts +216 -0
- package/packages/galaxydraw/src/core/state.ts +177 -0
- package/packages/galaxydraw/src/core/viewport.ts +106 -0
- package/packages/galaxydraw/src/galaxydraw.css +166 -0
- package/packages/galaxydraw/src/index.ts +40 -0
- package/packages/galaxydraw/tsconfig.json +30 -0
- package/server.ts +62 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Dependency Graph View — force-directed layout of import relationships.
|
|
4
|
+
*
|
|
5
|
+
* Flow:
|
|
6
|
+
* 1. Fetch import edges from /api/repo/imports API
|
|
7
|
+
* 2. Build adjacency graph from file cards on canvas
|
|
8
|
+
* 3. Run force-directed simulation (spring-charge model)
|
|
9
|
+
* 4. Smoothly animate cards to new positions
|
|
10
|
+
* 5. Draw dependency lines as SVG arrows on the overlay
|
|
11
|
+
*
|
|
12
|
+
* Toggle between spatial ↔ dependency layout with Ctrl+G or toolbar button.
|
|
13
|
+
*/
|
|
14
|
+
import { measure } from 'measure-fn';
|
|
15
|
+
import type { CanvasContext } from './context';
|
|
16
|
+
import { savePosition } from './positions';
|
|
17
|
+
import { updateMinimap } from './canvas';
|
|
18
|
+
import { renderConnections } from './connections';
|
|
19
|
+
import { showToast } from './utils';
|
|
20
|
+
|
|
21
|
+
// ─── State ──────────────────────────────────────────────
|
|
22
|
+
let _isGraphMode = false;
|
|
23
|
+
let _savedPositions: Map<string, { x: number; y: number }> = new Map();
|
|
24
|
+
let _graphEdges: { source: string; target: string }[] = [];
|
|
25
|
+
let _graphSvg: SVGGElement | null = null;
|
|
26
|
+
|
|
27
|
+
export function isGraphMode(): boolean { return _isGraphMode; }
|
|
28
|
+
|
|
29
|
+
// ─── Types ──────────────────────────────────────────────
|
|
30
|
+
interface Node {
|
|
31
|
+
path: string;
|
|
32
|
+
x: number;
|
|
33
|
+
y: number;
|
|
34
|
+
vx: number;
|
|
35
|
+
vy: number;
|
|
36
|
+
w: number;
|
|
37
|
+
h: number;
|
|
38
|
+
pinned: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── Force-Directed Layout ──────────────────────────────
|
|
42
|
+
function forceDirectedLayout(
|
|
43
|
+
nodes: Node[],
|
|
44
|
+
edges: { source: string; target: string }[],
|
|
45
|
+
iterations = 150,
|
|
46
|
+
): void {
|
|
47
|
+
const nodeMap = new Map(nodes.map(n => [n.path, n]));
|
|
48
|
+
|
|
49
|
+
// Scale forces based on number of nodes
|
|
50
|
+
const N = nodes.length;
|
|
51
|
+
const REPULSION = N > 50 ? 1_200_000 : 800_000;
|
|
52
|
+
const SPRING_K = 0.006;
|
|
53
|
+
const IDEAL_LEN = N > 80 ? 600 : N > 40 ? 500 : 400;
|
|
54
|
+
const DAMPING = 0.88;
|
|
55
|
+
const MAX_FORCE = 150;
|
|
56
|
+
|
|
57
|
+
// ── Scatter initial positions around centroid ──
|
|
58
|
+
// Starting from actual canvas positions (which may be spread over 20,000+ px)
|
|
59
|
+
// means repulsion is negligible. Cluster nodes tightly first so forces work.
|
|
60
|
+
const cx = nodes.reduce((s, n) => s + n.x, 0) / N;
|
|
61
|
+
const cy = nodes.reduce((s, n) => s + n.y, 0) / N;
|
|
62
|
+
const spread = Math.sqrt(N) * 80; // Scale cluster size with node count
|
|
63
|
+
|
|
64
|
+
for (const node of nodes) {
|
|
65
|
+
node.x = cx + (Math.random() - 0.5) * spread;
|
|
66
|
+
node.y = cy + (Math.random() - 0.5) * spread;
|
|
67
|
+
node.vx = 0;
|
|
68
|
+
node.vy = 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Build adjacency for hub detection ──
|
|
72
|
+
const degree = new Map<string, number>();
|
|
73
|
+
for (const e of edges) {
|
|
74
|
+
degree.set(e.source, (degree.get(e.source) || 0) + 1);
|
|
75
|
+
degree.set(e.target, (degree.get(e.target) || 0) + 1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (let iter = 0; iter < iterations; iter++) {
|
|
79
|
+
const temp = 1 - (iter / iterations) * 0.7; // Slower cooling
|
|
80
|
+
|
|
81
|
+
// ── Repulsion (all pairs) ──
|
|
82
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
83
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
84
|
+
const a = nodes[i]!, b = nodes[j]!;
|
|
85
|
+
let dx = b.x - a.x;
|
|
86
|
+
let dy = b.y - a.y;
|
|
87
|
+
const dist = Math.max(Math.sqrt(dx * dx + dy * dy), 30);
|
|
88
|
+
const force = REPULSION / (dist * dist);
|
|
89
|
+
const fx = Math.min(Math.max((dx / dist) * force, -MAX_FORCE), MAX_FORCE);
|
|
90
|
+
const fy = Math.min(Math.max((dy / dist) * force, -MAX_FORCE), MAX_FORCE);
|
|
91
|
+
a.vx -= fx * temp;
|
|
92
|
+
a.vy -= fy * temp;
|
|
93
|
+
b.vx += fx * temp;
|
|
94
|
+
b.vy += fy * temp;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── Attraction (edges only) ──
|
|
99
|
+
for (const edge of edges) {
|
|
100
|
+
const a = nodeMap.get(edge.source);
|
|
101
|
+
const b = nodeMap.get(edge.target);
|
|
102
|
+
if (!a || !b) continue;
|
|
103
|
+
const dx = b.x - a.x;
|
|
104
|
+
const dy = b.y - a.y;
|
|
105
|
+
const dist = Math.max(Math.sqrt(dx * dx + dy * dy), 1);
|
|
106
|
+
const displacement = dist - IDEAL_LEN;
|
|
107
|
+
const fx = Math.min(Math.max(SPRING_K * displacement * (dx / dist), -MAX_FORCE), MAX_FORCE);
|
|
108
|
+
const fy = Math.min(Math.max(SPRING_K * displacement * (dy / dist), -MAX_FORCE), MAX_FORCE);
|
|
109
|
+
a.vx += fx * temp;
|
|
110
|
+
a.vy += fy * temp;
|
|
111
|
+
b.vx -= fx * temp;
|
|
112
|
+
b.vy -= fy * temp;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── Gravity toward center to prevent drift ──
|
|
116
|
+
const gcx = nodes.reduce((s, n) => s + n.x, 0) / N;
|
|
117
|
+
const gcy = nodes.reduce((s, n) => s + n.y, 0) / N;
|
|
118
|
+
for (const node of nodes) {
|
|
119
|
+
node.vx += (gcx - node.x) * 0.0005 * temp;
|
|
120
|
+
node.vy += (gcy - node.y) * 0.0005 * temp;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Apply velocities ──
|
|
124
|
+
for (const node of nodes) {
|
|
125
|
+
if (node.pinned) continue;
|
|
126
|
+
node.vx *= DAMPING;
|
|
127
|
+
node.vy *= DAMPING;
|
|
128
|
+
node.x += node.vx;
|
|
129
|
+
node.y += node.vy;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ─── Animate cards to target positions ──────────────────
|
|
135
|
+
function animateToPositions(
|
|
136
|
+
ctx: CanvasContext,
|
|
137
|
+
targets: Map<string, { x: number; y: number }>,
|
|
138
|
+
durationMs = 600,
|
|
139
|
+
) {
|
|
140
|
+
const starts = new Map<string, { x: number; y: number }>();
|
|
141
|
+
for (const [path, target] of targets) {
|
|
142
|
+
const card = ctx.fileCards.get(path);
|
|
143
|
+
if (card) {
|
|
144
|
+
starts.set(path, {
|
|
145
|
+
x: parseFloat(card.style.left) || 0,
|
|
146
|
+
y: parseFloat(card.style.top) || 0,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
// Also animate deferred cards
|
|
150
|
+
const deferred = ctx.deferredCards?.get(path);
|
|
151
|
+
if (deferred && !starts.has(path)) {
|
|
152
|
+
starts.set(path, { x: deferred.x, y: deferred.y });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const t0 = performance.now();
|
|
157
|
+
const ease = (t: number) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; // easeInOutCubic
|
|
158
|
+
|
|
159
|
+
function frame() {
|
|
160
|
+
const elapsed = performance.now() - t0;
|
|
161
|
+
const progress = Math.min(elapsed / durationMs, 1);
|
|
162
|
+
const eased = ease(progress);
|
|
163
|
+
const commitHash = ctx.snap().context.currentCommitHash || 'allfiles';
|
|
164
|
+
|
|
165
|
+
for (const [path, target] of targets) {
|
|
166
|
+
const start = starts.get(path);
|
|
167
|
+
if (!start) continue;
|
|
168
|
+
const x = start.x + (target.x - start.x) * eased;
|
|
169
|
+
const y = start.y + (target.y - start.y) * eased;
|
|
170
|
+
|
|
171
|
+
// Update DOM card
|
|
172
|
+
const card = ctx.fileCards.get(path);
|
|
173
|
+
if (card) {
|
|
174
|
+
card.style.left = `${x}px`;
|
|
175
|
+
card.style.top = `${y}px`;
|
|
176
|
+
}
|
|
177
|
+
// Update deferred entry
|
|
178
|
+
const deferred = ctx.deferredCards?.get(path);
|
|
179
|
+
if (deferred) { deferred.x = x; deferred.y = y; }
|
|
180
|
+
// Update pill
|
|
181
|
+
const pill = document.querySelector(`.file-card-pill[data-path="${CSS.escape(path)}"]`) as HTMLElement;
|
|
182
|
+
if (pill) { pill.style.left = `${x}px`; pill.style.top = `${y}px`; }
|
|
183
|
+
|
|
184
|
+
if (progress >= 1) {
|
|
185
|
+
savePosition(ctx, commitHash, path, target.x, target.y);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
renderConnections(ctx);
|
|
190
|
+
|
|
191
|
+
if (progress < 1) {
|
|
192
|
+
requestAnimationFrame(frame);
|
|
193
|
+
} else {
|
|
194
|
+
updateMinimap(ctx);
|
|
195
|
+
renderGraphEdges(ctx);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
requestAnimationFrame(frame);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ─── Render graph edges as SVG arrows ───────────────────
|
|
203
|
+
function renderGraphEdges(ctx: CanvasContext) {
|
|
204
|
+
if (!ctx.svgOverlay) return;
|
|
205
|
+
|
|
206
|
+
// Remove old graph edges
|
|
207
|
+
if (_graphSvg) { _graphSvg.remove(); _graphSvg = null; }
|
|
208
|
+
if (!_isGraphMode || _graphEdges.length === 0) return;
|
|
209
|
+
|
|
210
|
+
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
211
|
+
g.setAttribute('class', 'dependency-graph-edges');
|
|
212
|
+
|
|
213
|
+
// Ensure arrowhead marker exists
|
|
214
|
+
let defs = ctx.svgOverlay.querySelector('defs');
|
|
215
|
+
if (!defs) {
|
|
216
|
+
defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
|
|
217
|
+
ctx.svgOverlay.prepend(defs);
|
|
218
|
+
}
|
|
219
|
+
if (!defs.querySelector('#dep-arrow')) {
|
|
220
|
+
const marker = document.createElementNS('http://www.w3.org/2000/svg', 'marker');
|
|
221
|
+
marker.setAttribute('id', 'dep-arrow');
|
|
222
|
+
marker.setAttribute('viewBox', '0 0 10 10');
|
|
223
|
+
marker.setAttribute('refX', '10');
|
|
224
|
+
marker.setAttribute('refY', '5');
|
|
225
|
+
marker.setAttribute('markerWidth', '8');
|
|
226
|
+
marker.setAttribute('markerHeight', '8');
|
|
227
|
+
marker.setAttribute('orient', 'auto-start-reverse');
|
|
228
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
229
|
+
path.setAttribute('d', 'M 0 0 L 10 5 L 0 10 z');
|
|
230
|
+
path.setAttribute('fill', 'rgba(168, 130, 255, 0.7)');
|
|
231
|
+
marker.appendChild(path);
|
|
232
|
+
defs.appendChild(marker);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Build lookup for inbound edge count per file to colorize
|
|
236
|
+
const inbound = new Map<string, number>();
|
|
237
|
+
for (const edge of _graphEdges) {
|
|
238
|
+
inbound.set(edge.target, (inbound.get(edge.target) || 0) + 1);
|
|
239
|
+
}
|
|
240
|
+
const maxInbound = Math.max(...inbound.values(), 1);
|
|
241
|
+
|
|
242
|
+
for (const edge of _graphEdges) {
|
|
243
|
+
const srcCard = ctx.fileCards.get(edge.source);
|
|
244
|
+
const tgtCard = ctx.fileCards.get(edge.target);
|
|
245
|
+
// Fall back to deferred card positions
|
|
246
|
+
const srcDeferred = ctx.deferredCards?.get(edge.source);
|
|
247
|
+
const tgtDeferred = ctx.deferredCards?.get(edge.target);
|
|
248
|
+
|
|
249
|
+
let sx: number, sy: number, tx: number, ty: number;
|
|
250
|
+
if (srcCard) {
|
|
251
|
+
sx = parseFloat(srcCard.style.left) + (srcCard.offsetWidth || 580) / 2;
|
|
252
|
+
sy = parseFloat(srcCard.style.top) + (srcCard.offsetHeight || 400) / 2;
|
|
253
|
+
} else if (srcDeferred) {
|
|
254
|
+
sx = srcDeferred.x + (srcDeferred.size?.width || 580) / 2;
|
|
255
|
+
sy = srcDeferred.y + (srcDeferred.size?.height || 400) / 2;
|
|
256
|
+
} else continue;
|
|
257
|
+
|
|
258
|
+
if (tgtCard) {
|
|
259
|
+
tx = parseFloat(tgtCard.style.left) + (tgtCard.offsetWidth || 580) / 2;
|
|
260
|
+
ty = parseFloat(tgtCard.style.top) + (tgtCard.offsetHeight || 400) / 2;
|
|
261
|
+
} else if (tgtDeferred) {
|
|
262
|
+
tx = tgtDeferred.x + (tgtDeferred.size?.width || 580) / 2;
|
|
263
|
+
ty = tgtDeferred.y + (tgtDeferred.size?.height || 400) / 2;
|
|
264
|
+
} else continue;
|
|
265
|
+
|
|
266
|
+
// Use curved path for better visual clarity
|
|
267
|
+
const dx = tx - sx;
|
|
268
|
+
const dy = ty - sy;
|
|
269
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
270
|
+
const curvature = Math.min(dist * 0.15, 80);
|
|
271
|
+
const mx = (sx + tx) / 2 - (dy / dist) * curvature;
|
|
272
|
+
const my = (sy + ty) / 2 + (dx / dist) * curvature;
|
|
273
|
+
|
|
274
|
+
const pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
275
|
+
pathEl.setAttribute('d', `M ${sx} ${sy} Q ${mx} ${my} ${tx} ${ty}`);
|
|
276
|
+
|
|
277
|
+
// Color intensity based on how many things import the target
|
|
278
|
+
const intensity = Math.min((inbound.get(edge.target) || 1) / maxInbound + 0.3, 1);
|
|
279
|
+
pathEl.setAttribute('stroke', `rgba(168, 130, 255, ${(0.2 + intensity * 0.4).toFixed(2)})`);
|
|
280
|
+
pathEl.setAttribute('stroke-width', '2');
|
|
281
|
+
pathEl.setAttribute('fill', 'none');
|
|
282
|
+
pathEl.setAttribute('stroke-dasharray', '8,4');
|
|
283
|
+
pathEl.setAttribute('marker-end', 'url(#dep-arrow)');
|
|
284
|
+
g.appendChild(pathEl);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
ctx.svgOverlay.appendChild(g);
|
|
288
|
+
_graphSvg = g;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ─── Toggle Graph Mode ──────────────────────────────────
|
|
292
|
+
export async function toggleDependencyGraph(ctx: CanvasContext) {
|
|
293
|
+
if (_isGraphMode) {
|
|
294
|
+
// Restore original positions
|
|
295
|
+
_isGraphMode = false;
|
|
296
|
+
document.getElementById('dep-graph-btn')?.classList.remove('active');
|
|
297
|
+
|
|
298
|
+
if (_savedPositions.size > 0) {
|
|
299
|
+
showToast('Restoring spatial layout...', 'info');
|
|
300
|
+
animateToPositions(ctx, _savedPositions, 500);
|
|
301
|
+
_savedPositions.clear();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Remove graph edges
|
|
305
|
+
if (_graphSvg) { _graphSvg.remove(); _graphSvg = null; }
|
|
306
|
+
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Enter graph mode
|
|
311
|
+
_isGraphMode = true;
|
|
312
|
+
document.getElementById('dep-graph-btn')?.classList.add('active');
|
|
313
|
+
showToast('Building dependency graph...', 'info');
|
|
314
|
+
|
|
315
|
+
await measure('depGraph:layout', async () => {
|
|
316
|
+
const state = ctx.snap().context;
|
|
317
|
+
const repoPath = state.repoPath;
|
|
318
|
+
const commit = state.currentCommitHash || 'HEAD';
|
|
319
|
+
|
|
320
|
+
if (!repoPath) {
|
|
321
|
+
_isGraphMode = false;
|
|
322
|
+
document.getElementById('dep-graph-btn')?.classList.remove('active');
|
|
323
|
+
showToast('Load a repository first', 'error');
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ── 1. Save current positions ──
|
|
328
|
+
_savedPositions.clear();
|
|
329
|
+
for (const [path, card] of ctx.fileCards) {
|
|
330
|
+
_savedPositions.set(path, {
|
|
331
|
+
x: parseFloat(card.style.left) || 0,
|
|
332
|
+
y: parseFloat(card.style.top) || 0,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
if (ctx.deferredCards) {
|
|
336
|
+
for (const [path, entry] of ctx.deferredCards) {
|
|
337
|
+
if (!_savedPositions.has(path)) {
|
|
338
|
+
_savedPositions.set(path, { x: entry.x, y: entry.y });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ── 2. Fetch import edges ──
|
|
344
|
+
try {
|
|
345
|
+
const res = await fetch('/api/repo/imports', {
|
|
346
|
+
method: 'POST',
|
|
347
|
+
headers: { 'Content-Type': 'application/json' },
|
|
348
|
+
body: JSON.stringify({ path: repoPath, commit }),
|
|
349
|
+
});
|
|
350
|
+
if (!res.ok) {
|
|
351
|
+
_isGraphMode = false;
|
|
352
|
+
document.getElementById('dep-graph-btn')?.classList.remove('active');
|
|
353
|
+
showToast('Failed to fetch import data', 'error');
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const data = await res.json();
|
|
357
|
+
const edges = (data.edges || []) as { source: string; target: string; line: number }[];
|
|
358
|
+
|
|
359
|
+
// Filter to edges where both files are on the canvas
|
|
360
|
+
const canvasFiles = new Set([...ctx.fileCards.keys()]);
|
|
361
|
+
if (ctx.deferredCards) {
|
|
362
|
+
for (const path of ctx.deferredCards.keys()) canvasFiles.add(path);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
_graphEdges = edges.filter(e => canvasFiles.has(e.source) && canvasFiles.has(e.target));
|
|
366
|
+
|
|
367
|
+
if (_graphEdges.length === 0) {
|
|
368
|
+
_isGraphMode = false;
|
|
369
|
+
document.getElementById('dep-graph-btn')?.classList.remove('active');
|
|
370
|
+
showToast(`No import relationships found (${data.filesScanned} files scanned)`, 'info');
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
console.log(`[dep-graph] ${_graphEdges.length} edges across ${canvasFiles.size} canvas files`);
|
|
375
|
+
} catch (err) {
|
|
376
|
+
_isGraphMode = false;
|
|
377
|
+
document.getElementById('dep-graph-btn')?.classList.remove('active');
|
|
378
|
+
showToast('Error scanning imports', 'error');
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ── 3. Build nodes from canvas cards ──
|
|
383
|
+
const connectedFiles = new Set<string>();
|
|
384
|
+
for (const e of _graphEdges) { connectedFiles.add(e.source); connectedFiles.add(e.target); }
|
|
385
|
+
|
|
386
|
+
// Center of current viewport
|
|
387
|
+
const centerX = _savedPositions.size > 0
|
|
388
|
+
? [..._savedPositions.values()].reduce((s, p) => s + p.x, 0) / _savedPositions.size
|
|
389
|
+
: 2000;
|
|
390
|
+
const centerY = _savedPositions.size > 0
|
|
391
|
+
? [..._savedPositions.values()].reduce((s, p) => s + p.y, 0) / _savedPositions.size
|
|
392
|
+
: 2000;
|
|
393
|
+
|
|
394
|
+
const nodes: Node[] = [];
|
|
395
|
+
for (const path of connectedFiles) {
|
|
396
|
+
const saved = _savedPositions.get(path);
|
|
397
|
+
const card = ctx.fileCards.get(path);
|
|
398
|
+
nodes.push({
|
|
399
|
+
path,
|
|
400
|
+
x: saved?.x ?? centerX + (Math.random() - 0.5) * 1000,
|
|
401
|
+
y: saved?.y ?? centerY + (Math.random() - 0.5) * 1000,
|
|
402
|
+
vx: 0,
|
|
403
|
+
vy: 0,
|
|
404
|
+
w: card?.offsetWidth || 580,
|
|
405
|
+
h: card?.offsetHeight || 400,
|
|
406
|
+
pinned: false,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ── 4. Run force simulation ──
|
|
411
|
+
forceDirectedLayout(nodes, _graphEdges, 150);
|
|
412
|
+
|
|
413
|
+
// ── 5. Center the result around the original centroid ──
|
|
414
|
+
const graphCenterX = nodes.reduce((s, n) => s + n.x, 0) / nodes.length;
|
|
415
|
+
const graphCenterY = nodes.reduce((s, n) => s + n.y, 0) / nodes.length;
|
|
416
|
+
const offsetX = centerX - graphCenterX;
|
|
417
|
+
const offsetY = centerY - graphCenterY;
|
|
418
|
+
|
|
419
|
+
const targets = new Map<string, { x: number; y: number }>();
|
|
420
|
+
for (const node of nodes) {
|
|
421
|
+
targets.set(node.path, { x: node.x + offsetX, y: node.y + offsetY });
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ── 6. Animate to new positions ──
|
|
425
|
+
showToast(`📊 ${connectedFiles.size} files, ${_graphEdges.length} dependencies`, 'success');
|
|
426
|
+
animateToPositions(ctx, targets, 800);
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ─── Keyboard shortcut registration ─────────────────────
|
|
431
|
+
export function setupDependencyGraphShortcut(ctx: CanvasContext) {
|
|
432
|
+
document.addEventListener('keydown', (e) => {
|
|
433
|
+
if (e.ctrlKey && e.key === 'g' && !e.shiftKey && !e.altKey) {
|
|
434
|
+
e.preventDefault();
|
|
435
|
+
toggleDependencyGraph(ctx);
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
}
|