gitmaps 1.1.0 → 1.1.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/README.md +267 -118
- package/app/[...slug]/page.client.tsx +1 -0
- package/app/[...slug]/page.tsx +6 -0
- package/app/analytics.db +0 -0
- package/app/api/analytics/route.ts +64 -0
- package/app/api/auth/positions/route.ts +95 -33
- package/app/api/build-info/route.ts +19 -0
- package/app/api/chat/route.ts +13 -2
- package/app/api/og-image/route.ts +14 -0
- package/app/api/repo/file-content/route.ts +73 -20
- package/app/api/repo/load/route.test.ts +62 -0
- package/app/api/repo/load/route.ts +41 -1
- package/app/api/repo/pdf-thumb/route.ts +127 -0
- package/app/api/repo/resolve-slug/route.ts +51 -0
- package/app/api/repo/tree/route.ts +188 -104
- package/app/api/version/route.ts +26 -0
- package/app/globals.css +5706 -4938
- package/app/layout.tsx +1279 -490
- package/app/lib/auto-arrange.test.ts +158 -0
- package/app/lib/auto-arrange.ts +147 -0
- package/app/lib/canvas-export.ts +358 -358
- package/app/lib/canvas.ts +625 -564
- package/app/lib/cards.tsx +1361 -916
- package/app/lib/chat.tsx +65 -9
- package/app/lib/code-editor.ts +86 -2
- package/app/lib/context.test.ts +32 -0
- package/app/lib/context.ts +19 -3
- package/app/lib/cursor-sharing.ts +34 -0
- package/app/lib/events.tsx +71 -93
- package/app/lib/export-canvas.ts +287 -0
- package/app/lib/file-card-plugin.ts +148 -148
- package/app/lib/file-modal.tsx +49 -0
- package/app/lib/file-preview.ts +486 -427
- package/app/lib/github-import.test.ts +424 -0
- package/app/lib/initial-route-hydration.test.ts +283 -0
- package/app/lib/initial-route-hydration.ts +202 -0
- package/app/lib/landing-reset.test.ts +99 -0
- package/app/lib/landing-reset.ts +106 -0
- package/app/lib/landing-shell.test.ts +75 -0
- package/app/lib/large-repo-optimization.ts +37 -0
- package/app/lib/layout-snapshots.ts +320 -0
- package/app/lib/loading.test.ts +69 -0
- package/app/lib/loading.tsx +160 -45
- package/app/lib/mount-cleanup.test.ts +52 -0
- package/app/lib/mount-cleanup.ts +34 -0
- package/app/lib/mount-init.test.ts +123 -0
- package/app/lib/mount-init.ts +107 -0
- package/app/lib/mount-lifecycle.test.ts +39 -0
- package/app/lib/mount-lifecycle.ts +12 -0
- package/app/lib/mount-route-wiring.test.ts +87 -0
- package/app/lib/mount-route-wiring.ts +84 -0
- package/app/lib/multi-repo.ts +14 -0
- package/app/lib/onboarding-tutorial.ts +278 -0
- package/app/lib/positions.ts +190 -121
- package/app/lib/recent-commits.test.ts +869 -0
- package/app/lib/recent-commits.ts +227 -0
- package/app/lib/repo-handoff.test.ts +23 -0
- package/app/lib/repo-handoff.ts +16 -0
- package/app/lib/repo-progressive.ts +119 -0
- package/app/lib/repo-select.test.ts +61 -0
- package/app/lib/repo-select.ts +74 -0
- package/app/lib/repo.tsx +1383 -987
- package/app/lib/role.ts +228 -0
- package/app/lib/route-catchall.test.ts +27 -0
- package/app/lib/route-repo-entry.test.ts +95 -0
- package/app/lib/route-repo-entry.ts +36 -0
- package/app/lib/router-contract.test.ts +22 -0
- package/app/lib/router-contract.ts +19 -0
- package/app/lib/shared-layout.test.ts +86 -0
- package/app/lib/shared-layout.ts +82 -0
- package/app/lib/status-bar.test.ts +118 -0
- package/app/lib/status-bar.ts +365 -128
- package/app/lib/sync-controls.test.ts +43 -0
- package/app/lib/sync-controls.tsx +303 -0
- package/app/lib/test-dom.ts +145 -0
- package/app/lib/test-fixtures/router-contract/[...slug]/page.tsx +3 -0
- package/app/lib/test-fixtures/router-contract/api/health/route.ts +3 -0
- package/app/lib/test-fixtures/router-contract/api/version/route.ts +3 -0
- package/app/lib/test-fixtures/router-contract/galaxy-canvas/page.tsx +3 -0
- package/app/lib/test-fixtures/router-contract/page.tsx +3 -0
- package/app/lib/transclusion-smoke.test.ts +163 -0
- package/app/lib/tutorial.ts +301 -0
- package/app/lib/version.ts +93 -0
- package/app/lib/viewport-culling.ts +740 -735
- package/app/lib/virtual-files.ts +456 -0
- package/app/lib/webgl-text.ts +189 -0
- package/app/lib/{galaxydraw-bridge.ts → xydraw-bridge.ts} +485 -482
- package/app/lib/{galaxydraw.test.ts → xydraw.test.ts} +228 -229
- package/app/og-image.png +0 -0
- package/app/page.client.tsx +70 -269
- package/app/page.tsx +15 -16
- package/app/state/machine.js +13 -0
- package/package.json +16 -7
- package/server.ts +10 -0
- package/app/[owner]/[repo]/page.tsx +0 -6
- package/app/[slug]/page.tsx +0 -6
- package/packages/galaxydraw/README.md +0 -296
- package/packages/galaxydraw/banner.png +0 -0
- package/packages/galaxydraw/demo/build-static.ts +0 -100
- package/packages/galaxydraw/demo/client.ts +0 -154
- package/packages/galaxydraw/demo/dist/client.js +0 -8
- package/packages/galaxydraw/demo/index.html +0 -256
- package/packages/galaxydraw/demo/server.ts +0 -96
- package/packages/galaxydraw/dist/index.js +0 -984
- package/packages/galaxydraw/dist/index.js.map +0 -16
- 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 +0 -49
- package/packages/galaxydraw/perf.test.ts +0 -284
- package/packages/galaxydraw/src/core/cards.ts +0 -435
- package/packages/galaxydraw/src/core/engine.ts +0 -339
- package/packages/galaxydraw/src/core/events.ts +0 -81
- package/packages/galaxydraw/src/core/layout.ts +0 -136
- package/packages/galaxydraw/src/core/minimap.ts +0 -216
- package/packages/galaxydraw/src/core/state.ts +0 -177
- package/packages/galaxydraw/src/core/viewport.ts +0 -106
- package/packages/galaxydraw/src/galaxydraw.css +0 -166
- package/packages/galaxydraw/src/index.ts +0 -40
- package/packages/galaxydraw/tsconfig.json +0 -30
package/app/lib/canvas.ts
CHANGED
|
@@ -1,564 +1,625 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
/**
|
|
3
|
-
* Canvas transform, zoom, minimap, fit-all.
|
|
4
|
-
*/
|
|
5
|
-
import { measure } from 'measure-fn';
|
|
6
|
-
import { updateStatusBarZoom } from './status-bar';
|
|
7
|
-
import type { CanvasContext } from './context';
|
|
8
|
-
import { scheduleViewportCulling,
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
viewport
|
|
198
|
-
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
return;
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
//
|
|
282
|
-
const
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
);
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
const
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
}
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Canvas transform, zoom, minimap, fit-all.
|
|
4
|
+
*/
|
|
5
|
+
import { measure } from 'measure-fn';
|
|
6
|
+
import { updateStatusBarZoom } from './status-bar';
|
|
7
|
+
import type { CanvasContext } from './context';
|
|
8
|
+
import { scheduleViewportCulling, markTransformActive, clearAllPills } from './viewport-culling';
|
|
9
|
+
import { clearVirtualCards } from './virtual-files';
|
|
10
|
+
import { getGalaxyDrawState } from './xydraw-bridge';
|
|
11
|
+
|
|
12
|
+
// ─── Minimap cached state (avoids full rebuild on every pan/zoom) ──
|
|
13
|
+
let _mmCache: {
|
|
14
|
+
minX: number; minY: number; maxX: number; maxY: number;
|
|
15
|
+
scale: number; mmW: number; mmH: number;
|
|
16
|
+
dotEls: Map<string, { dot: HTMLElement; label: HTMLElement }>;
|
|
17
|
+
} | null = null;
|
|
18
|
+
let _mmRebuildTimer: any = null;
|
|
19
|
+
|
|
20
|
+
export function restoreViewport(ctx: CanvasContext) {
|
|
21
|
+
const state = ctx.snap().context;
|
|
22
|
+
if (!state.repoPath) return;
|
|
23
|
+
|
|
24
|
+
// Priority 0: ?layout= query param (full shared layout)
|
|
25
|
+
const layoutParam = new URLSearchParams(window.location.search).get('layout');
|
|
26
|
+
if (layoutParam) {
|
|
27
|
+
try {
|
|
28
|
+
const layout = JSON.parse(atob(layoutParam));
|
|
29
|
+
if (layout.zoom) ctx.actor.send({ type: 'SET_ZOOM', zoom: layout.zoom });
|
|
30
|
+
if (layout.offsetX !== undefined && layout.offsetY !== undefined) {
|
|
31
|
+
ctx.actor.send({ type: 'SET_OFFSET', x: layout.offsetX, y: layout.offsetY });
|
|
32
|
+
}
|
|
33
|
+
// Restore card positions
|
|
34
|
+
if (layout.positions) {
|
|
35
|
+
for (const [path, pos] of Object.entries(layout.positions)) {
|
|
36
|
+
ctx.positions.set(path, pos as any);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Restore hidden files
|
|
40
|
+
if (layout.hiddenFiles) {
|
|
41
|
+
for (const f of layout.hiddenFiles) ctx.hiddenFiles.add(f);
|
|
42
|
+
}
|
|
43
|
+
// Clean URL
|
|
44
|
+
const cleanUrl = window.location.pathname;
|
|
45
|
+
history.replaceState(null, '', cleanUrl);
|
|
46
|
+
return;
|
|
47
|
+
} catch { }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Priority 1: URL hash (viewport-only shared link)
|
|
51
|
+
const hashVp = parseViewportHash();
|
|
52
|
+
if (hashVp) {
|
|
53
|
+
if (hashVp.z) ctx.actor.send({ type: 'SET_ZOOM', zoom: hashVp.z });
|
|
54
|
+
if (hashVp.x !== undefined && hashVp.y !== undefined) ctx.actor.send({ type: 'SET_OFFSET', x: hashVp.x, y: hashVp.y });
|
|
55
|
+
history.replaceState(null, '', window.location.pathname + window.location.search);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Priority 2: localStorage (returning user)
|
|
60
|
+
try {
|
|
61
|
+
const saved = localStorage.getItem(`gitcanvas:viewport:${state.repoPath}`);
|
|
62
|
+
if (saved) {
|
|
63
|
+
const vp = JSON.parse(saved);
|
|
64
|
+
if (vp.zoom) ctx.actor.send({ type: 'SET_ZOOM', zoom: vp.zoom });
|
|
65
|
+
if (vp.x !== undefined && vp.y !== undefined) ctx.actor.send({ type: 'SET_OFFSET', x: vp.x, y: vp.y });
|
|
66
|
+
}
|
|
67
|
+
} catch (e) { }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Parse viewport state from URL hash: #z=0.5&x=-1000&y=-500 */
|
|
71
|
+
function parseViewportHash(): { z?: number; x?: number; y?: number } | null {
|
|
72
|
+
const hash = window.location.hash.slice(1);
|
|
73
|
+
if (!hash) return null;
|
|
74
|
+
const params = new URLSearchParams(hash);
|
|
75
|
+
const z = params.get('z');
|
|
76
|
+
const x = params.get('x');
|
|
77
|
+
const y = params.get('y');
|
|
78
|
+
if (!z && !x && !y) return null;
|
|
79
|
+
return {
|
|
80
|
+
z: z ? parseFloat(z) : undefined,
|
|
81
|
+
x: x ? parseFloat(x) : undefined,
|
|
82
|
+
y: y ? parseFloat(y) : undefined,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Get a shareable URL with current viewport encoded in hash */
|
|
87
|
+
export function getShareableLink(ctx: CanvasContext): string {
|
|
88
|
+
const state = ctx.snap().context;
|
|
89
|
+
const z = state.zoom.toFixed(3);
|
|
90
|
+
const x = Math.round(state.offsetX);
|
|
91
|
+
const y = Math.round(state.offsetY);
|
|
92
|
+
return `${window.location.origin}${window.location.pathname}#z=${z}&x=${x}&y=${y}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── Update canvas CSS transform from state ─────────────
|
|
96
|
+
export function updateCanvasTransform(ctx: CanvasContext) {
|
|
97
|
+
if (!ctx.canvas) return;
|
|
98
|
+
markTransformActive(); // Signal that user is actively panning/zooming
|
|
99
|
+
const state = ctx.snap().context;
|
|
100
|
+
|
|
101
|
+
// Phase 2: delegate to GalaxyDraw state engine if available
|
|
102
|
+
const gdState = getGalaxyDrawState();
|
|
103
|
+
if (gdState) {
|
|
104
|
+
// Sync XState → GalaxyDraw
|
|
105
|
+
gdState.zoom = state.zoom;
|
|
106
|
+
gdState.offsetX = state.offsetX;
|
|
107
|
+
gdState.offsetY = state.offsetY;
|
|
108
|
+
gdState.applyTransform();
|
|
109
|
+
} else {
|
|
110
|
+
// Fallback: manual transform (pre-bridge init)
|
|
111
|
+
ctx.canvas.style.transform = `translate(${Math.round(state.offsetX)}px, ${Math.round(state.offsetY)}px) scale(${state.zoom})`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Cheap: only move the viewport rect using cached bounds
|
|
115
|
+
updateMinimapViewport(ctx);
|
|
116
|
+
// Schedule viewport culling (debounced to next rAF)
|
|
117
|
+
scheduleViewportCulling(ctx);
|
|
118
|
+
|
|
119
|
+
if (state.repoPath) {
|
|
120
|
+
if ((window as any)._saveViewportTimer) clearTimeout((window as any)._saveViewportTimer);
|
|
121
|
+
(window as any)._saveViewportTimer = setTimeout(() => {
|
|
122
|
+
localStorage.setItem(`gitcanvas:viewport:${state.repoPath}`, JSON.stringify({
|
|
123
|
+
zoom: state.zoom,
|
|
124
|
+
x: state.offsetX,
|
|
125
|
+
y: state.offsetY
|
|
126
|
+
}));
|
|
127
|
+
}, 300);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Notify cursor sharing of viewport change
|
|
131
|
+
window.dispatchEvent(new Event('gitcanvas:viewport-changed'));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ─── Update zoom slider UI ──────────────────────────────
|
|
135
|
+
export function updateZoomUI(ctx: CanvasContext) {
|
|
136
|
+
const state = ctx.snap().context;
|
|
137
|
+
const zoomPct = `${Math.round(state.zoom * 100)}%`;
|
|
138
|
+
|
|
139
|
+
// Sidebar slider
|
|
140
|
+
const slider = document.getElementById('zoomSlider') as HTMLInputElement;
|
|
141
|
+
const value = document.getElementById('zoomValue');
|
|
142
|
+
if (slider) slider.value = state.zoom;
|
|
143
|
+
if (value) value.textContent = zoomPct;
|
|
144
|
+
|
|
145
|
+
// Sticky zoom pill
|
|
146
|
+
const stickySlider = document.getElementById('stickyZoomSlider') as HTMLInputElement;
|
|
147
|
+
const stickyValue = document.getElementById('stickyZoomValue');
|
|
148
|
+
if (stickySlider) stickySlider.value = String(state.zoom);
|
|
149
|
+
if (stickyValue) stickyValue.textContent = zoomPct;
|
|
150
|
+
|
|
151
|
+
updateStatusBarZoom(state.zoom);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ─── Cheap viewport-only minimap update ─────────────────
|
|
155
|
+
function updateMinimapViewport(ctx: CanvasContext) {
|
|
156
|
+
const viewport = document.getElementById('minimapViewport');
|
|
157
|
+
if (!viewport || !_mmCache || !ctx.canvasViewport) return;
|
|
158
|
+
|
|
159
|
+
const state = ctx.snap().context;
|
|
160
|
+
const canvasRect = ctx.canvasViewport.getBoundingClientRect();
|
|
161
|
+
const { scale, minX, minY } = _mmCache;
|
|
162
|
+
|
|
163
|
+
const vpWorldW = canvasRect.width / state.zoom;
|
|
164
|
+
const vpWorldH = canvasRect.height / state.zoom;
|
|
165
|
+
const vpWorldX = -state.offsetX / state.zoom;
|
|
166
|
+
const vpWorldY = -state.offsetY / state.zoom;
|
|
167
|
+
|
|
168
|
+
viewport.style.width = `${vpWorldW * scale}px`;
|
|
169
|
+
viewport.style.height = `${vpWorldH * scale}px`;
|
|
170
|
+
viewport.style.left = `${(vpWorldX - minX) * scale}px`;
|
|
171
|
+
viewport.style.top = `${(vpWorldY - minY) * scale}px`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ─── Full minimap rebuild (debounced, expensive) ────────
|
|
175
|
+
export function updateMinimap(ctx: CanvasContext) {
|
|
176
|
+
// Debounce full rebuilds to max once per 120ms
|
|
177
|
+
if (_mmRebuildTimer) clearTimeout(_mmRebuildTimer);
|
|
178
|
+
_mmRebuildTimer = setTimeout(() => {
|
|
179
|
+
_mmRebuildTimer = null;
|
|
180
|
+
_rebuildMinimap(ctx);
|
|
181
|
+
}, 120);
|
|
182
|
+
// Always do cheap viewport update immediately
|
|
183
|
+
updateMinimapViewport(ctx);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Force an immediate full minimap rebuild (skip debounce). */
|
|
187
|
+
export function forceMinimapRebuild(ctx: CanvasContext) {
|
|
188
|
+
if (_mmRebuildTimer) { clearTimeout(_mmRebuildTimer); _mmRebuildTimer = null; }
|
|
189
|
+
_rebuildMinimap(ctx);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function _rebuildMinimap(ctx: CanvasContext) {
|
|
193
|
+
const minimap = document.getElementById('minimap');
|
|
194
|
+
const viewport = document.getElementById('minimapViewport');
|
|
195
|
+
const state = ctx.snap().context;
|
|
196
|
+
|
|
197
|
+
if (!minimap || !viewport) return;
|
|
198
|
+
|
|
199
|
+
// Remove old labels/dots
|
|
200
|
+
if (_mmCache) {
|
|
201
|
+
_mmCache.dotEls.forEach(({ dot, label }) => { dot.remove(); label.remove(); });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Calculate actual bounding box from all file cards (DOM + deferred)
|
|
205
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
206
|
+
const cardInfos: { x: number; y: number; w: number; h: number; name: string; status: string; path: string; changed: boolean; displayPath?: string }[] = [];
|
|
207
|
+
|
|
208
|
+
ctx.fileCards.forEach((card, path) => {
|
|
209
|
+
const x = parseFloat(card.style.left);
|
|
210
|
+
const y = parseFloat(card.style.top);
|
|
211
|
+
// Skip cards with invalid positions (NaN poisons Math.min/max)
|
|
212
|
+
if (isNaN(x) || isNaN(y)) return;
|
|
213
|
+
const w = card.offsetWidth || 580;
|
|
214
|
+
const h = card.offsetHeight || 700;
|
|
215
|
+
const name = path.split('/').pop() || path;
|
|
216
|
+
const parts = path.split('/');
|
|
217
|
+
const displayPath = parts.length > 1 ? parts.slice(-2).join('/') : name;
|
|
218
|
+
const status = card.dataset.status || card.className.match(/file-card--(\w+)/)?.[1] || 'default';
|
|
219
|
+
const changed = card.dataset.changed === 'true';
|
|
220
|
+
|
|
221
|
+
minX = Math.min(minX, x);
|
|
222
|
+
minY = Math.min(minY, y);
|
|
223
|
+
maxX = Math.max(maxX, x + w);
|
|
224
|
+
maxY = Math.max(maxY, y + h);
|
|
225
|
+
|
|
226
|
+
cardInfos.push({ x, y, w, h, name, displayPath, status, path, changed });
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Also include deferred cards (not yet in DOM but positioned on canvas)
|
|
230
|
+
if (ctx.deferredCards) {
|
|
231
|
+
ctx.deferredCards.forEach((entry, path) => {
|
|
232
|
+
// Skip if already in fileCards (shouldn't happen, but safety)
|
|
233
|
+
if (ctx.fileCards.has(path)) return;
|
|
234
|
+
const x = entry.x;
|
|
235
|
+
const y = entry.y;
|
|
236
|
+
// Skip cards with invalid positions
|
|
237
|
+
if (isNaN(x) || isNaN(y)) return;
|
|
238
|
+
const w = entry.size?.width || 580;
|
|
239
|
+
const h = entry.size?.height || 700;
|
|
240
|
+
const name = path.split('/').pop() || path;
|
|
241
|
+
const parts = path.split('/');
|
|
242
|
+
const displayPath = parts.length > 1 ? parts.slice(-2).join('/') : name;
|
|
243
|
+
const changed = !!entry.isChanged;
|
|
244
|
+
|
|
245
|
+
minX = Math.min(minX, x);
|
|
246
|
+
minY = Math.min(minY, y);
|
|
247
|
+
maxX = Math.max(maxX, x + w);
|
|
248
|
+
maxY = Math.max(maxY, y + h);
|
|
249
|
+
|
|
250
|
+
cardInfos.push({ x, y, w, h, name, displayPath, status: changed ? 'modified' : 'default', path, changed });
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// If no cards, just hide viewport
|
|
255
|
+
if (cardInfos.length === 0) {
|
|
256
|
+
viewport.style.display = 'none';
|
|
257
|
+
_mmCache = null;
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
viewport.style.display = '';
|
|
261
|
+
|
|
262
|
+
// Add padding around content
|
|
263
|
+
const pad = 200;
|
|
264
|
+
minX -= pad; minY -= pad;
|
|
265
|
+
maxX += pad; maxY += pad;
|
|
266
|
+
|
|
267
|
+
const contentW = maxX - minX;
|
|
268
|
+
const contentH = maxY - minY;
|
|
269
|
+
const mmW = minimap.offsetWidth;
|
|
270
|
+
const mmH = minimap.offsetHeight;
|
|
271
|
+
|
|
272
|
+
// Guard: if minimap hasn't been laid out yet, defer rebuild
|
|
273
|
+
if (mmW === 0 || mmH === 0 || contentW <= 0 || contentH <= 0) {
|
|
274
|
+
requestAnimationFrame(() => _rebuildMinimap(ctx));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Scale to fit content in minimap
|
|
279
|
+
const scale = Math.min(mmW / contentW, mmH / contentH);
|
|
280
|
+
|
|
281
|
+
// Build DOM in a fragment for one reflow
|
|
282
|
+
const frag = document.createDocumentFragment();
|
|
283
|
+
const dotEls = new Map<string, { dot: HTMLElement; label: HTMLElement }>();
|
|
284
|
+
|
|
285
|
+
cardInfos.forEach((info, idx) => {
|
|
286
|
+
const dotX = (info.x - minX) * scale;
|
|
287
|
+
const dotY = (info.y - minY) * scale;
|
|
288
|
+
const dotW = Math.max(2, info.w * scale);
|
|
289
|
+
const dotH = Math.max(1, info.h * scale);
|
|
290
|
+
|
|
291
|
+
// Colored dot for file
|
|
292
|
+
const dot = document.createElement('div');
|
|
293
|
+
const statusClass = ['added', 'modified', 'deleted', 'renamed', 'copied'].includes(info.status) ? info.status : 'default';
|
|
294
|
+
dot.className = `minimap-dot minimap-dot--${statusClass}`;
|
|
295
|
+
// In all-files mode, highlight changed files
|
|
296
|
+
if (info.changed) {
|
|
297
|
+
dot.classList.add('minimap-dot--changed');
|
|
298
|
+
}
|
|
299
|
+
dot.dataset.path = info.name;
|
|
300
|
+
dot.style.cssText = `left:${dotX}px;top:${dotY}px;width:${dotW}px;height:${dotH}px`;
|
|
301
|
+
frag.appendChild(dot);
|
|
302
|
+
|
|
303
|
+
// File name label
|
|
304
|
+
const label = document.createElement('div');
|
|
305
|
+
label.className = 'minimap-label';
|
|
306
|
+
label.textContent = info.name;
|
|
307
|
+
label.style.cssText = `left:${dotX}px;top:${dotY}px;width:${dotW}px;height:${dotH}px`;
|
|
308
|
+
|
|
309
|
+
if (dotH > dotW * 1.5) {
|
|
310
|
+
const fontSize = Math.max(3, Math.min(dotW * 0.7, 7));
|
|
311
|
+
label.style.fontSize = `${fontSize}px`;
|
|
312
|
+
label.style.writingMode = 'vertical-rl';
|
|
313
|
+
label.style.textOrientation = 'mixed';
|
|
314
|
+
label.style.whiteSpace = 'nowrap';
|
|
315
|
+
} else {
|
|
316
|
+
const fontSize = Math.max(3, Math.min(dotH * 0.6, dotW * 0.15, 7));
|
|
317
|
+
label.style.fontSize = `${fontSize}px`;
|
|
318
|
+
label.style.whiteSpace = 'nowrap';
|
|
319
|
+
}
|
|
320
|
+
frag.appendChild(label);
|
|
321
|
+
dotEls.set(info.path, { dot, label });
|
|
322
|
+
|
|
323
|
+
// Hover tooltip: show enlarged file name
|
|
324
|
+
dot.addEventListener('mouseenter', () => {
|
|
325
|
+
// Remove any existing tooltip
|
|
326
|
+
minimap.querySelector('.minimap-tooltip')?.remove();
|
|
327
|
+
const tooltip = document.createElement('div');
|
|
328
|
+
tooltip.className = 'minimap-tooltip';
|
|
329
|
+
tooltip.textContent = info.displayPath;
|
|
330
|
+
tooltip.style.left = `${dotX + dotW / 2}px`;
|
|
331
|
+
tooltip.style.top = `${dotY}px`;
|
|
332
|
+
minimap.appendChild(tooltip);
|
|
333
|
+
});
|
|
334
|
+
dot.addEventListener('mouseleave', () => {
|
|
335
|
+
minimap.querySelector('.minimap-tooltip')?.remove();
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
minimap.appendChild(frag);
|
|
340
|
+
|
|
341
|
+
// Cache bounds + scale + elements for cheap viewport-only updates
|
|
342
|
+
_mmCache = { minX, minY, maxX, maxY, scale, mmW, mmH, dotEls };
|
|
343
|
+
|
|
344
|
+
// Viewport rectangle (immediate)
|
|
345
|
+
const canvasRect = ctx.canvasViewport.getBoundingClientRect();
|
|
346
|
+
const vpWorldW = canvasRect.width / state.zoom;
|
|
347
|
+
const vpWorldH = canvasRect.height / state.zoom;
|
|
348
|
+
const vpWorldX = -state.offsetX / state.zoom;
|
|
349
|
+
const vpWorldY = -state.offsetY / state.zoom;
|
|
350
|
+
|
|
351
|
+
viewport.style.width = `${vpWorldW * scale}px`;
|
|
352
|
+
viewport.style.height = `${vpWorldH * scale}px`;
|
|
353
|
+
viewport.style.left = `${(vpWorldX - minX) * scale}px`;
|
|
354
|
+
viewport.style.top = `${(vpWorldY - minY) * scale}px`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ─── Jump to a specific file on the canvas ──────────────
|
|
358
|
+
export function jumpToFile(ctx: CanvasContext, filePath: string) {
|
|
359
|
+
measure('canvas:jumpToFile', () => {
|
|
360
|
+
let card = ctx.fileCards.get(filePath);
|
|
361
|
+
let cardX: number, cardY: number, cardW: number, cardH: number;
|
|
362
|
+
|
|
363
|
+
if (card) {
|
|
364
|
+
cardX = parseFloat(card.style.left) || 0;
|
|
365
|
+
cardY = parseFloat(card.style.top) || 0;
|
|
366
|
+
cardW = card.offsetWidth || 580;
|
|
367
|
+
cardH = card.offsetHeight || 200;
|
|
368
|
+
} else if (ctx.deferredCards?.has(filePath)) {
|
|
369
|
+
// Card is deferred (not yet in DOM) — get position from deferred data
|
|
370
|
+
const entry = ctx.deferredCards.get(filePath)!;
|
|
371
|
+
cardX = entry.x;
|
|
372
|
+
cardY = entry.y;
|
|
373
|
+
cardW = entry.size?.width || 580;
|
|
374
|
+
cardH = entry.size?.height || 700;
|
|
375
|
+
} else {
|
|
376
|
+
// File not on current layer — try switching to its layer
|
|
377
|
+
import('./layers').then(({ navigateToFileInLayer }) => {
|
|
378
|
+
const switched = navigateToFileInLayer(ctx, filePath);
|
|
379
|
+
if (switched) {
|
|
380
|
+
// Layer switched and canvas re-rendered — retry jump after re-render settles
|
|
381
|
+
setTimeout(() => jumpToFile(ctx, filePath), 500);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const vpRect = ctx.canvasViewport.getBoundingClientRect();
|
|
388
|
+
const state = ctx.snap().context;
|
|
389
|
+
|
|
390
|
+
// Target zoom: bring to readable level (at least 0.6, or current if already zoomed in)
|
|
391
|
+
const targetZoom = Math.max(0.6, Math.min(state.zoom, 1));
|
|
392
|
+
const newOffsetX = vpRect.width / 2 - (cardX + cardW / 2) * targetZoom;
|
|
393
|
+
const newOffsetY = vpRect.height / 2 - (cardY + cardH / 2) * targetZoom;
|
|
394
|
+
|
|
395
|
+
// Animate using CSS transition on the canvas element
|
|
396
|
+
const canvasEl = ctx.canvas;
|
|
397
|
+
if (canvasEl) {
|
|
398
|
+
canvasEl.style.transition = 'transform 400ms cubic-bezier(0.25, 0.46, 0.45, 0.94)';
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
ctx.actor.send({ type: 'SET_ZOOM', zoom: targetZoom });
|
|
402
|
+
ctx.actor.send({ type: 'SET_OFFSET', x: newOffsetX, y: newOffsetY });
|
|
403
|
+
updateCanvasTransform(ctx);
|
|
404
|
+
updateZoomUI(ctx);
|
|
405
|
+
updateMinimap(ctx);
|
|
406
|
+
|
|
407
|
+
// Clean up transition after animation completes
|
|
408
|
+
setTimeout(() => {
|
|
409
|
+
if (canvasEl) {
|
|
410
|
+
canvasEl.style.transition = '';
|
|
411
|
+
}
|
|
412
|
+
// Re-cull after animation settles (may need to materialize the card)
|
|
413
|
+
scheduleViewportCulling(ctx);
|
|
414
|
+
|
|
415
|
+
// Flash highlight on the card (may have been materialized by culling)
|
|
416
|
+
const finalCard = ctx.fileCards.get(filePath);
|
|
417
|
+
if (finalCard) {
|
|
418
|
+
finalCard.style.outline = '2px solid var(--accent-primary)';
|
|
419
|
+
finalCard.style.outlineOffset = '4px';
|
|
420
|
+
setTimeout(() => {
|
|
421
|
+
finalCard.style.outline = '';
|
|
422
|
+
finalCard.style.outlineOffset = '';
|
|
423
|
+
}, 1500);
|
|
424
|
+
}
|
|
425
|
+
}, 420);
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ─── Fit all files in viewport ──────────────────────────
|
|
430
|
+
export function fitAllFiles(ctx: CanvasContext) {
|
|
431
|
+
measure('canvas:fitAll', () => {
|
|
432
|
+
if (ctx.fileCards.size === 0 && (!ctx.deferredCards || ctx.deferredCards.size === 0)) {
|
|
433
|
+
if (!ctx.canvasViewport) return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
437
|
+
ctx.fileCards.forEach(card => {
|
|
438
|
+
const x = parseInt(card.style.left);
|
|
439
|
+
const y = parseInt(card.style.top);
|
|
440
|
+
if (isNaN(x) || isNaN(y)) return; // Skip cards without positions
|
|
441
|
+
minX = Math.min(minX, x);
|
|
442
|
+
minY = Math.min(minY, y);
|
|
443
|
+
maxX = Math.max(maxX, x + (card.offsetWidth || 580));
|
|
444
|
+
maxY = Math.max(maxY, y + (card.offsetHeight || 700));
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// Include deferred cards in bounds
|
|
448
|
+
if (ctx.deferredCards) {
|
|
449
|
+
ctx.deferredCards.forEach((entry) => {
|
|
450
|
+
minX = Math.min(minX, entry.x);
|
|
451
|
+
minY = Math.min(minY, entry.y);
|
|
452
|
+
maxX = Math.max(maxX, entry.x + (entry.size?.width || 580));
|
|
453
|
+
maxY = Math.max(maxY, entry.y + (entry.size?.height || 700));
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const viewportRect = ctx.canvasViewport.getBoundingClientRect();
|
|
458
|
+
const contentWidth = maxX - minX + 100;
|
|
459
|
+
const contentHeight = maxY - minY + 100;
|
|
460
|
+
|
|
461
|
+
const newZoom = Math.min(
|
|
462
|
+
viewportRect.width / contentWidth,
|
|
463
|
+
viewportRect.height / contentHeight,
|
|
464
|
+
1
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
const newOffsetX = (viewportRect.width - contentWidth * newZoom) / 2 - minX * newZoom + 50;
|
|
468
|
+
const newOffsetY = (viewportRect.height - contentHeight * newZoom) / 2 - minY * newZoom + 50;
|
|
469
|
+
|
|
470
|
+
ctx.actor.send({ type: 'SET_ZOOM', zoom: newZoom });
|
|
471
|
+
ctx.actor.send({ type: 'SET_OFFSET', x: newOffsetX, y: newOffsetY });
|
|
472
|
+
updateCanvasTransform(ctx); // this also schedules re-culling
|
|
473
|
+
updateZoomUI(ctx);
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ─── Setup minimap click + scroll + resize handler ──────
|
|
478
|
+
export function setupMinimapClick(ctx: CanvasContext) {
|
|
479
|
+
measure('minimap:setupClick', () => {
|
|
480
|
+
const minimap = document.getElementById('minimap');
|
|
481
|
+
if (!minimap) return;
|
|
482
|
+
|
|
483
|
+
// ── Resize handle ──
|
|
484
|
+
const resizeHandle = document.createElement('div');
|
|
485
|
+
resizeHandle.className = 'minimap-resize-handle';
|
|
486
|
+
resizeHandle.textContent = '⤡';
|
|
487
|
+
minimap.parentElement?.insertBefore(resizeHandle, minimap);
|
|
488
|
+
// position handle at top-left of minimap
|
|
489
|
+
resizeHandle.style.position = 'absolute';
|
|
490
|
+
resizeHandle.style.bottom = `${minimap.offsetHeight - 2}px`;
|
|
491
|
+
resizeHandle.style.right = `${minimap.offsetWidth - 2}px`;
|
|
492
|
+
|
|
493
|
+
let isResizing = false;
|
|
494
|
+
let resizeStartX = 0, resizeStartY = 0;
|
|
495
|
+
let startW = 0, startH = 0;
|
|
496
|
+
|
|
497
|
+
resizeHandle.addEventListener('mousedown', (e) => {
|
|
498
|
+
e.preventDefault();
|
|
499
|
+
e.stopPropagation();
|
|
500
|
+
isResizing = true;
|
|
501
|
+
resizeStartX = e.clientX;
|
|
502
|
+
resizeStartY = e.clientY;
|
|
503
|
+
startW = minimap.offsetWidth;
|
|
504
|
+
startH = minimap.offsetHeight;
|
|
505
|
+
document.body.style.cursor = 'nwse-resize';
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
window.addEventListener('mousemove', (e) => {
|
|
509
|
+
if (!isResizing) return;
|
|
510
|
+
// Dragging top-left: moving left increases width, moving up increases height
|
|
511
|
+
const dx = resizeStartX - e.clientX;
|
|
512
|
+
const dy = resizeStartY - e.clientY;
|
|
513
|
+
const newW = Math.max(100, Math.min(600, startW + dx));
|
|
514
|
+
const newH = Math.max(70, Math.min(400, startH + dy));
|
|
515
|
+
minimap.style.width = `${newW}px`;
|
|
516
|
+
minimap.style.height = `${newH}px`;
|
|
517
|
+
// Reposition handle
|
|
518
|
+
resizeHandle.style.bottom = `${newH - 2}px`;
|
|
519
|
+
resizeHandle.style.right = `${newW - 2}px`;
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
window.addEventListener('mouseup', () => {
|
|
523
|
+
if (isResizing) {
|
|
524
|
+
isResizing = false;
|
|
525
|
+
document.body.style.cursor = '';
|
|
526
|
+
// Rebuild minimap to fit new size
|
|
527
|
+
_rebuildMinimap(ctx);
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// Scroll over minimap → pan camera (same as Space+scroll on canvas)
|
|
532
|
+
minimap.addEventListener('wheel', (e) => {
|
|
533
|
+
e.preventDefault();
|
|
534
|
+
e.stopPropagation();
|
|
535
|
+
|
|
536
|
+
const state = ctx.snap().context;
|
|
537
|
+
const panSpeed = 1.5;
|
|
538
|
+
|
|
539
|
+
if (e.shiftKey) {
|
|
540
|
+
// Shift+scroll = horizontal pan
|
|
541
|
+
const dx = e.deltaY * panSpeed;
|
|
542
|
+
ctx.actor.send({ type: 'SET_OFFSET', x: state.offsetX - dx, y: state.offsetY });
|
|
543
|
+
} else {
|
|
544
|
+
// Vertical scroll = vertical pan, deltaX for horizontal
|
|
545
|
+
const dy = e.deltaY * panSpeed;
|
|
546
|
+
const dx = e.deltaX * panSpeed;
|
|
547
|
+
ctx.actor.send({ type: 'SET_OFFSET', x: state.offsetX - dx, y: state.offsetY - dy });
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
updateCanvasTransform(ctx);
|
|
551
|
+
updateMinimap(ctx);
|
|
552
|
+
}, { passive: false });
|
|
553
|
+
|
|
554
|
+
minimap.addEventListener('click', (e) => {
|
|
555
|
+
const target = e.target as HTMLElement;
|
|
556
|
+
|
|
557
|
+
if (target.classList.contains('minimap-dot') && target.dataset.path) {
|
|
558
|
+
for (const [path] of ctx.fileCards) {
|
|
559
|
+
const name = path.split('/').pop() || path;
|
|
560
|
+
if (name === target.dataset.path) {
|
|
561
|
+
jumpToFile(ctx, path);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const rect = minimap.getBoundingClientRect();
|
|
569
|
+
const clickX = e.clientX - rect.left;
|
|
570
|
+
const clickY = e.clientY - rect.top;
|
|
571
|
+
|
|
572
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
573
|
+
ctx.fileCards.forEach((card) => {
|
|
574
|
+
const x = parseFloat(card.style.left) || 0;
|
|
575
|
+
const y = parseFloat(card.style.top) || 0;
|
|
576
|
+
const w = card.offsetWidth || 580;
|
|
577
|
+
const h = card.offsetHeight || 200;
|
|
578
|
+
minX = Math.min(minX, x); minY = Math.min(minY, y);
|
|
579
|
+
maxX = Math.max(maxX, x + w); maxY = Math.max(maxY, y + h);
|
|
580
|
+
});
|
|
581
|
+
if (minX === Infinity) return;
|
|
582
|
+
|
|
583
|
+
const pad = 200;
|
|
584
|
+
minX -= pad; minY -= pad;
|
|
585
|
+
maxX += pad; maxY += pad;
|
|
586
|
+
const contentW = maxX - minX;
|
|
587
|
+
const contentH = maxY - minY;
|
|
588
|
+
const mmW = minimap.offsetWidth;
|
|
589
|
+
const mmH = minimap.offsetHeight;
|
|
590
|
+
const scale = Math.min(mmW / contentW, mmH / contentH);
|
|
591
|
+
|
|
592
|
+
const worldX = clickX / scale + minX;
|
|
593
|
+
const worldY = clickY / scale + minY;
|
|
594
|
+
|
|
595
|
+
const state = ctx.snap().context;
|
|
596
|
+
const vpRect = ctx.canvasViewport.getBoundingClientRect();
|
|
597
|
+
const newOffsetX = vpRect.width / 2 - worldX * state.zoom;
|
|
598
|
+
const newOffsetY = vpRect.height / 2 - worldY * state.zoom;
|
|
599
|
+
|
|
600
|
+
ctx.actor.send({ type: 'SET_OFFSET', x: newOffsetX, y: newOffsetY });
|
|
601
|
+
updateCanvasTransform(ctx);
|
|
602
|
+
updateMinimap(ctx);
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// ─── Clear all cards from canvas ────────────────────────
|
|
608
|
+
export function clearCanvas(ctx: CanvasContext) {
|
|
609
|
+
ctx.fileCards.forEach(card => card.remove());
|
|
610
|
+
ctx.fileCards.clear();
|
|
611
|
+
ctx.canvas?.querySelectorAll('.dir-label').forEach(el => el.remove());
|
|
612
|
+
clearVirtualCards(ctx);
|
|
613
|
+
// Clear pill placeholders (zoomed-out view) — clears both DOM and internal Map
|
|
614
|
+
clearAllPills(ctx);
|
|
615
|
+
if (ctx.svgOverlay) ctx.svgOverlay.innerHTML = '';
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// ─── Auto column count based on viewport width ─────────
|
|
619
|
+
export function getAutoColumnCount(ctx: CanvasContext): number {
|
|
620
|
+
const vpWidth = ctx.canvasViewport?.getBoundingClientRect().width || window.innerWidth;
|
|
621
|
+
const cardWidth = 580;
|
|
622
|
+
const gap = 40;
|
|
623
|
+
const margin = 100;
|
|
624
|
+
return Math.max(1, Math.floor((vpWidth - margin) / (cardWidth + gap)));
|
|
625
|
+
}
|