gitmaps 1.1.0 → 1.1.2
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 +947 -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 +84 -75
- 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
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimap — Small overview of the entire canvas
|
|
3
|
-
*
|
|
4
|
-
* Renders a proportional view of all card positions as colored
|
|
5
|
-
* rectangles. Click/drag on the minimap to navigate.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { CanvasState } from './state';
|
|
9
|
-
import type { CardManager } from './cards';
|
|
10
|
-
|
|
11
|
-
export class Minimap {
|
|
12
|
-
private el: HTMLElement;
|
|
13
|
-
private mapCanvas: HTMLCanvasElement;
|
|
14
|
-
private ctx2d: CanvasRenderingContext2D | null;
|
|
15
|
-
private rafPending = false;
|
|
16
|
-
|
|
17
|
-
/** Minimap dimensions */
|
|
18
|
-
width = 180;
|
|
19
|
-
height = 120;
|
|
20
|
-
|
|
21
|
-
constructor(
|
|
22
|
-
private state: CanvasState,
|
|
23
|
-
private cards: CardManager,
|
|
24
|
-
container: HTMLElement,
|
|
25
|
-
) {
|
|
26
|
-
this.el = document.createElement('div');
|
|
27
|
-
this.el.className = 'gd-minimap';
|
|
28
|
-
this.el.style.cssText = `
|
|
29
|
-
position: absolute;
|
|
30
|
-
bottom: 12px;
|
|
31
|
-
right: 12px;
|
|
32
|
-
width: ${this.width}px;
|
|
33
|
-
height: ${this.height}px;
|
|
34
|
-
border-radius: 8px;
|
|
35
|
-
overflow: hidden;
|
|
36
|
-
backdrop-filter: blur(12px);
|
|
37
|
-
background: rgba(0, 0, 0, 0.5);
|
|
38
|
-
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
39
|
-
cursor: pointer;
|
|
40
|
-
z-index: 999;
|
|
41
|
-
`;
|
|
42
|
-
|
|
43
|
-
this.mapCanvas = document.createElement('canvas');
|
|
44
|
-
this.mapCanvas.width = this.width;
|
|
45
|
-
this.mapCanvas.height = this.height;
|
|
46
|
-
this.el.appendChild(this.mapCanvas);
|
|
47
|
-
container.appendChild(this.el);
|
|
48
|
-
|
|
49
|
-
this.ctx2d = this.mapCanvas.getContext('2d');
|
|
50
|
-
|
|
51
|
-
// Click to navigate
|
|
52
|
-
this.el.addEventListener('mousedown', (e) => this.handleClick(e));
|
|
53
|
-
|
|
54
|
-
// Auto-rebuild on state change
|
|
55
|
-
this.state.subscribe(() => this.scheduleRebuild());
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Schedule a redraw */
|
|
59
|
-
scheduleRebuild() {
|
|
60
|
-
if (this.rafPending) return;
|
|
61
|
-
this.rafPending = true;
|
|
62
|
-
requestAnimationFrame(() => {
|
|
63
|
-
this.rafPending = false;
|
|
64
|
-
this.rebuild();
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** Force immediate redraw */
|
|
69
|
-
rebuild() {
|
|
70
|
-
const ctx = this.ctx2d;
|
|
71
|
-
if (!ctx) return;
|
|
72
|
-
ctx.clearRect(0, 0, this.width, this.height);
|
|
73
|
-
|
|
74
|
-
// Compute world bounding box of all cards
|
|
75
|
-
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
76
|
-
for (const [, card] of this.cards.cards) {
|
|
77
|
-
const x = parseFloat(card.style.left) || 0;
|
|
78
|
-
const y = parseFloat(card.style.top) || 0;
|
|
79
|
-
const w = card.offsetWidth || 400;
|
|
80
|
-
const h = card.offsetHeight || 300;
|
|
81
|
-
minX = Math.min(minX, x);
|
|
82
|
-
minY = Math.min(minY, y);
|
|
83
|
-
maxX = Math.max(maxX, x + w);
|
|
84
|
-
maxY = Math.max(maxY, y + h);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Include deferred cards in bounding box
|
|
88
|
-
for (const [, data] of this.cards.deferred) {
|
|
89
|
-
minX = Math.min(minX, data.x);
|
|
90
|
-
minY = Math.min(minY, data.y);
|
|
91
|
-
maxX = Math.max(maxX, data.x + data.width);
|
|
92
|
-
maxY = Math.max(maxY, data.y + data.height);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (minX === Infinity) return; // no cards
|
|
96
|
-
|
|
97
|
-
const pad = 50;
|
|
98
|
-
const worldW = maxX - minX + pad * 2;
|
|
99
|
-
const worldH = maxY - minY + pad * 2;
|
|
100
|
-
const scale = Math.min(this.width / worldW, this.height / worldH);
|
|
101
|
-
|
|
102
|
-
const ox = (this.width - worldW * scale) / 2;
|
|
103
|
-
const oy = (this.height - worldH * scale) / 2;
|
|
104
|
-
|
|
105
|
-
// Draw card dots
|
|
106
|
-
ctx.fillStyle = 'rgba(147, 130, 255, 0.6)';
|
|
107
|
-
for (const [, card] of this.cards.cards) {
|
|
108
|
-
const x = (parseFloat(card.style.left) || 0) - minX + pad;
|
|
109
|
-
const y = (parseFloat(card.style.top) || 0) - minY + pad;
|
|
110
|
-
const w = card.offsetWidth || 400;
|
|
111
|
-
const h = card.offsetHeight || 300;
|
|
112
|
-
ctx.fillRect(ox + x * scale, oy + y * scale, Math.max(2, w * scale), Math.max(2, h * scale));
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Draw deferred card dots (dimmer)
|
|
116
|
-
ctx.fillStyle = 'rgba(147, 130, 255, 0.2)';
|
|
117
|
-
for (const [, data] of this.cards.deferred) {
|
|
118
|
-
const x = data.x - minX + pad;
|
|
119
|
-
const y = data.y - minY + pad;
|
|
120
|
-
ctx.fillRect(ox + x * scale, oy + y * scale, Math.max(2, data.width * scale), Math.max(2, data.height * scale));
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Draw viewport rect
|
|
124
|
-
const vp = this.state.getVisibleWorldRect();
|
|
125
|
-
if (vp) {
|
|
126
|
-
ctx.strokeStyle = 'rgba(255, 255, 255, 0.7)';
|
|
127
|
-
ctx.lineWidth = 1.5;
|
|
128
|
-
const rx = ox + (vp.left - minX + pad) * scale;
|
|
129
|
-
const ry = oy + (vp.top - minY + pad) * scale;
|
|
130
|
-
const rw = vp.width * scale;
|
|
131
|
-
const rh = vp.height * scale;
|
|
132
|
-
ctx.strokeRect(rx, ry, rw, rh);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
private handleClick(e: MouseEvent) {
|
|
137
|
-
e.stopPropagation();
|
|
138
|
-
e.preventDefault();
|
|
139
|
-
|
|
140
|
-
// Compute the same bounding box used in rebuild()
|
|
141
|
-
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
142
|
-
for (const [, card] of this.cards.cards) {
|
|
143
|
-
const x = parseFloat(card.style.left) || 0;
|
|
144
|
-
const y = parseFloat(card.style.top) || 0;
|
|
145
|
-
const w = card.offsetWidth || 400;
|
|
146
|
-
const h = card.offsetHeight || 300;
|
|
147
|
-
minX = Math.min(minX, x);
|
|
148
|
-
minY = Math.min(minY, y);
|
|
149
|
-
maxX = Math.max(maxX, x + w);
|
|
150
|
-
maxY = Math.max(maxY, y + h);
|
|
151
|
-
}
|
|
152
|
-
for (const [, data] of this.cards.deferred) {
|
|
153
|
-
minX = Math.min(minX, data.x);
|
|
154
|
-
minY = Math.min(minY, data.y);
|
|
155
|
-
maxX = Math.max(maxX, data.x + data.width);
|
|
156
|
-
maxY = Math.max(maxY, data.y + data.height);
|
|
157
|
-
}
|
|
158
|
-
if (minX === Infinity) return;
|
|
159
|
-
|
|
160
|
-
const pad = 50;
|
|
161
|
-
const worldW = maxX - minX + pad * 2;
|
|
162
|
-
const worldH = maxY - minY + pad * 2;
|
|
163
|
-
const scale = Math.min(this.width / worldW, this.height / worldH);
|
|
164
|
-
const ox = (this.width - worldW * scale) / 2;
|
|
165
|
-
const oy = (this.height - worldH * scale) / 2;
|
|
166
|
-
|
|
167
|
-
// Convert minimap click → world coordinates
|
|
168
|
-
const rect = this.el.getBoundingClientRect();
|
|
169
|
-
const clickX = e.clientX - rect.left;
|
|
170
|
-
const clickY = e.clientY - rect.top;
|
|
171
|
-
|
|
172
|
-
const worldX = (clickX - ox) / scale + minX - pad;
|
|
173
|
-
const worldY = (clickY - oy) / scale + minY - pad;
|
|
174
|
-
|
|
175
|
-
// Center the viewport on this world position
|
|
176
|
-
const vp = this.state.getVisibleWorldRect();
|
|
177
|
-
if (vp) {
|
|
178
|
-
const vpWorldW = vp.width;
|
|
179
|
-
const vpWorldH = vp.height;
|
|
180
|
-
const newOffsetX = -(worldX - vpWorldW / 2) * this.state.zoom;
|
|
181
|
-
const newOffsetY = -(worldY - vpWorldH / 2) * this.state.zoom;
|
|
182
|
-
this.state.set(this.state.zoom, newOffsetX, newOffsetY);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Support drag on the minimap
|
|
186
|
-
const onMove = (ev: MouseEvent) => {
|
|
187
|
-
const mx = ev.clientX - rect.left;
|
|
188
|
-
const my = ev.clientY - rect.top;
|
|
189
|
-
const wx = (mx - ox) / scale + minX - pad;
|
|
190
|
-
const wy = (my - oy) / scale + minY - pad;
|
|
191
|
-
const v = this.state.getVisibleWorldRect();
|
|
192
|
-
if (v) {
|
|
193
|
-
this.state.set(
|
|
194
|
-
this.state.zoom,
|
|
195
|
-
-(wx - v.width / 2) * this.state.zoom,
|
|
196
|
-
-(wy - v.height / 2) * this.state.zoom,
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
const onUp = () => {
|
|
201
|
-
window.removeEventListener('mousemove', onMove);
|
|
202
|
-
window.removeEventListener('mouseup', onUp);
|
|
203
|
-
};
|
|
204
|
-
window.addEventListener('mousemove', onMove);
|
|
205
|
-
window.addEventListener('mouseup', onUp);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/** Show/hide the minimap */
|
|
209
|
-
setVisible(visible: boolean) {
|
|
210
|
-
this.el.style.display = visible ? '' : 'none';
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
destroy() {
|
|
214
|
-
this.el.remove();
|
|
215
|
-
}
|
|
216
|
-
}
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CanvasState — Reactive state container for the infinite canvas
|
|
3
|
-
*
|
|
4
|
-
* Tracks zoom level, pan offset, and provides world↔screen
|
|
5
|
-
* coordinate conversion utilities.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export interface CanvasStateSnapshot {
|
|
9
|
-
zoom: number;
|
|
10
|
-
offsetX: number;
|
|
11
|
-
offsetY: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface ViewportRect {
|
|
15
|
-
left: number;
|
|
16
|
-
top: number;
|
|
17
|
-
right: number;
|
|
18
|
-
bottom: number;
|
|
19
|
-
width: number;
|
|
20
|
-
height: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export class CanvasState {
|
|
24
|
-
zoom = 1;
|
|
25
|
-
offsetX = 0;
|
|
26
|
-
offsetY = 0;
|
|
27
|
-
|
|
28
|
-
private viewportEl: HTMLElement | null = null;
|
|
29
|
-
private contentEl: HTMLElement | null = null;
|
|
30
|
-
private listeners = new Set<() => void>();
|
|
31
|
-
|
|
32
|
-
// ─── Zoom limits ─────────────────────────────────────
|
|
33
|
-
readonly MIN_ZOOM = 0.05;
|
|
34
|
-
readonly MAX_ZOOM = 5;
|
|
35
|
-
|
|
36
|
-
constructor(viewport?: HTMLElement, content?: HTMLElement) {
|
|
37
|
-
this.viewportEl = viewport ?? null;
|
|
38
|
-
this.contentEl = content ?? null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Bind to DOM elements after mount */
|
|
42
|
-
bind(viewport: HTMLElement, content: HTMLElement) {
|
|
43
|
-
this.viewportEl = viewport;
|
|
44
|
-
this.contentEl = content;
|
|
45
|
-
this.applyTransform();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Get a frozen snapshot of current state */
|
|
49
|
-
snapshot(): CanvasStateSnapshot {
|
|
50
|
-
return { zoom: this.zoom, offsetX: this.offsetX, offsetY: this.offsetY };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** Subscribe to state changes */
|
|
54
|
-
subscribe(fn: () => void): () => void {
|
|
55
|
-
this.listeners.add(fn);
|
|
56
|
-
return () => this.listeners.delete(fn);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
private notify() {
|
|
60
|
-
for (const fn of this.listeners) fn();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// ─── Transform ───────────────────────────────────────
|
|
64
|
-
|
|
65
|
-
/** Apply current state to the content element's CSS transform */
|
|
66
|
-
applyTransform() {
|
|
67
|
-
if (!this.contentEl) return;
|
|
68
|
-
// Round translate to whole pixels to prevent subpixel text blurring
|
|
69
|
-
const tx = Math.round(this.offsetX);
|
|
70
|
-
const ty = Math.round(this.offsetY);
|
|
71
|
-
this.contentEl.style.transform =
|
|
72
|
-
`translate(${tx}px, ${ty}px) scale(${this.zoom})`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** Set zoom + offset, clamping zoom to limits */
|
|
76
|
-
set(zoom: number, offsetX: number, offsetY: number) {
|
|
77
|
-
this.zoom = Math.max(this.MIN_ZOOM, Math.min(this.MAX_ZOOM, zoom));
|
|
78
|
-
this.offsetX = offsetX;
|
|
79
|
-
this.offsetY = offsetY;
|
|
80
|
-
this.applyTransform();
|
|
81
|
-
this.notify();
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Pan by delta pixels */
|
|
85
|
-
pan(dx: number, dy: number) {
|
|
86
|
-
this.offsetX += dx;
|
|
87
|
-
this.offsetY += dy;
|
|
88
|
-
this.applyTransform();
|
|
89
|
-
this.notify();
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Center the viewport on a world coordinate */
|
|
93
|
-
panTo(worldX: number, worldY: number) {
|
|
94
|
-
if (!this.viewportEl) return;
|
|
95
|
-
const vpW = this.viewportEl.clientWidth;
|
|
96
|
-
const vpH = this.viewportEl.clientHeight;
|
|
97
|
-
this.offsetX = vpW / 2 - worldX * this.zoom;
|
|
98
|
-
this.offsetY = vpH / 2 - worldY * this.zoom;
|
|
99
|
-
this.applyTransform();
|
|
100
|
-
this.notify();
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** Zoom toward a screen point (e.g. cursor position) */
|
|
104
|
-
zoomToward(screenX: number, screenY: number, factor: number) {
|
|
105
|
-
const newZoom = Math.max(this.MIN_ZOOM, Math.min(this.MAX_ZOOM, this.zoom * factor));
|
|
106
|
-
if (newZoom === this.zoom) return;
|
|
107
|
-
|
|
108
|
-
const rect = this.viewportEl?.getBoundingClientRect();
|
|
109
|
-
const mouseX = screenX - (rect?.left ?? 0);
|
|
110
|
-
const mouseY = screenY - (rect?.top ?? 0);
|
|
111
|
-
|
|
112
|
-
// Convert screen point to world coordinates at current zoom
|
|
113
|
-
const worldX = (mouseX - this.offsetX) / this.zoom;
|
|
114
|
-
const worldY = (mouseY - this.offsetY) / this.zoom;
|
|
115
|
-
|
|
116
|
-
// Update zoom and recalculate offset to keep world point under cursor
|
|
117
|
-
this.zoom = newZoom;
|
|
118
|
-
this.offsetX = mouseX - worldX * newZoom;
|
|
119
|
-
this.offsetY = mouseY - worldY * newZoom;
|
|
120
|
-
|
|
121
|
-
this.applyTransform();
|
|
122
|
-
this.notify();
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// ─── Coordinate conversion ───────────────────────────
|
|
126
|
-
|
|
127
|
-
/** Screen pixel → world coordinate */
|
|
128
|
-
screenToWorld(screenX: number, screenY: number): { x: number; y: number } {
|
|
129
|
-
const rect = this.viewportEl?.getBoundingClientRect();
|
|
130
|
-
const localX = screenX - (rect?.left ?? 0);
|
|
131
|
-
const localY = screenY - (rect?.top ?? 0);
|
|
132
|
-
return {
|
|
133
|
-
x: (localX - this.offsetX) / this.zoom,
|
|
134
|
-
y: (localY - this.offsetY) / this.zoom,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/** World coordinate → screen pixel */
|
|
139
|
-
worldToScreen(worldX: number, worldY: number): { x: number; y: number } {
|
|
140
|
-
const rect = this.viewportEl?.getBoundingClientRect();
|
|
141
|
-
return {
|
|
142
|
-
x: worldX * this.zoom + this.offsetX + (rect?.left ?? 0),
|
|
143
|
-
y: worldY * this.zoom + this.offsetY + (rect?.top ?? 0),
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** Get the visible world-space rectangle (with optional margin in screen px) */
|
|
148
|
-
getVisibleWorldRect(margin = 0): ViewportRect | null {
|
|
149
|
-
if (!this.viewportEl) return null;
|
|
150
|
-
const vpW = this.viewportEl.clientWidth;
|
|
151
|
-
const vpH = this.viewportEl.clientHeight;
|
|
152
|
-
|
|
153
|
-
const left = (-this.offsetX - margin) / this.zoom;
|
|
154
|
-
const top = (-this.offsetY - margin) / this.zoom;
|
|
155
|
-
const right = (vpW - this.offsetX + margin) / this.zoom;
|
|
156
|
-
const bottom = (vpH - this.offsetY + margin) / this.zoom;
|
|
157
|
-
|
|
158
|
-
return { left, top, right, bottom, width: right - left, height: bottom - top };
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/** Fit a world-space bounding box into the viewport */
|
|
162
|
-
fitRect(worldLeft: number, worldTop: number, worldRight: number, worldBottom: number, padding = 60) {
|
|
163
|
-
if (!this.viewportEl) return;
|
|
164
|
-
const vpW = this.viewportEl.clientWidth;
|
|
165
|
-
const vpH = this.viewportEl.clientHeight;
|
|
166
|
-
|
|
167
|
-
const w = worldRight - worldLeft + padding * 2;
|
|
168
|
-
const h = worldBottom - worldTop + padding * 2;
|
|
169
|
-
const zoom = Math.min(vpW / w, vpH / h, this.MAX_ZOOM);
|
|
170
|
-
|
|
171
|
-
this.set(
|
|
172
|
-
zoom,
|
|
173
|
-
(vpW - w * zoom) / 2 - (worldLeft - padding) * zoom,
|
|
174
|
-
(vpH - h * zoom) / 2 - (worldTop - padding) * zoom,
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ViewportCuller — Hide/show cards based on viewport visibility
|
|
3
|
-
*
|
|
4
|
-
* Uses content-visibility: hidden for off-screen cards (keeps dimensions
|
|
5
|
-
* for layout stability) and materializes deferred cards on demand.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { CanvasState } from './state';
|
|
9
|
-
import type { CardManager } from './cards';
|
|
10
|
-
import type { EventBus } from './events';
|
|
11
|
-
|
|
12
|
-
export interface CullResult {
|
|
13
|
-
shown: number;
|
|
14
|
-
culled: number;
|
|
15
|
-
materialized: number;
|
|
16
|
-
total: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export class ViewportCuller {
|
|
20
|
-
private rafPending = false;
|
|
21
|
-
private enabled = true;
|
|
22
|
-
/** Margin in screen pixels beyond the viewport for pre-rendering */
|
|
23
|
-
margin = 500;
|
|
24
|
-
|
|
25
|
-
constructor(
|
|
26
|
-
private state: CanvasState,
|
|
27
|
-
private cards: CardManager,
|
|
28
|
-
private bus: EventBus,
|
|
29
|
-
) { }
|
|
30
|
-
|
|
31
|
-
/** Enable/disable culling */
|
|
32
|
-
setEnabled(enabled: boolean) {
|
|
33
|
-
this.enabled = enabled;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Schedule a culling pass on the next animation frame */
|
|
37
|
-
schedule() {
|
|
38
|
-
if (this.rafPending || !this.enabled) return;
|
|
39
|
-
this.rafPending = true;
|
|
40
|
-
requestAnimationFrame(() => {
|
|
41
|
-
this.rafPending = false;
|
|
42
|
-
this.perform();
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Perform immediate culling pass */
|
|
47
|
-
perform(): CullResult {
|
|
48
|
-
const result: CullResult = { shown: 0, culled: 0, materialized: 0, total: 0 };
|
|
49
|
-
if (!this.enabled) return result;
|
|
50
|
-
|
|
51
|
-
const worldRect = this.state.getVisibleWorldRect(this.margin);
|
|
52
|
-
if (!worldRect) return result;
|
|
53
|
-
|
|
54
|
-
// 1. Cull/show existing DOM cards
|
|
55
|
-
for (const [id, card] of this.cards.cards) {
|
|
56
|
-
const visible = this.isCardInRect(card, worldRect);
|
|
57
|
-
const wasCulled = card.dataset.culled === 'true';
|
|
58
|
-
|
|
59
|
-
if (visible && wasCulled) {
|
|
60
|
-
card.style.contentVisibility = '';
|
|
61
|
-
card.style.visibility = '';
|
|
62
|
-
card.dataset.culled = 'false';
|
|
63
|
-
result.shown++;
|
|
64
|
-
} else if (!visible && !wasCulled) {
|
|
65
|
-
card.style.contentVisibility = 'hidden';
|
|
66
|
-
card.style.visibility = 'hidden';
|
|
67
|
-
card.dataset.culled = 'true';
|
|
68
|
-
result.culled++;
|
|
69
|
-
} else if (visible) {
|
|
70
|
-
result.shown++;
|
|
71
|
-
} else {
|
|
72
|
-
result.culled++;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// 2. Materialize deferred cards in viewport
|
|
77
|
-
if (this.cards.deferred.size > 0) {
|
|
78
|
-
result.materialized = this.cards.materializeInRect(worldRect);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
result.total = this.cards.cards.size + this.cards.deferred.size;
|
|
82
|
-
|
|
83
|
-
if (result.materialized > 0) {
|
|
84
|
-
this.bus.emit('viewport:cull', result);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return result;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Force all cards visible (for operations that need to measure everything) */
|
|
91
|
-
uncullAll() {
|
|
92
|
-
for (const [, card] of this.cards.cards) {
|
|
93
|
-
card.style.contentVisibility = '';
|
|
94
|
-
card.style.visibility = '';
|
|
95
|
-
card.dataset.culled = 'false';
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
private isCardInRect(card: HTMLElement, rect: { left: number; top: number; right: number; bottom: number }): boolean {
|
|
100
|
-
const x = parseFloat(card.style.left) || 0;
|
|
101
|
-
const y = parseFloat(card.style.top) || 0;
|
|
102
|
-
const w = card.offsetWidth || 400;
|
|
103
|
-
const h = card.offsetHeight || 300;
|
|
104
|
-
return x + w > rect.left && x < rect.right && y + h > rect.top && y < rect.bottom;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
/* galaxydraw.css — Base styles for the infinite canvas */
|
|
2
|
-
|
|
3
|
-
/* ─── Viewport & Canvas ─────────────────────────────────── */
|
|
4
|
-
.gd-viewport {
|
|
5
|
-
position: relative;
|
|
6
|
-
width: 100%;
|
|
7
|
-
height: 100%;
|
|
8
|
-
overflow: hidden;
|
|
9
|
-
background: radial-gradient(circle at 50% 50%, #0c0e1a 0%, #060812 100%);
|
|
10
|
-
user-select: none;
|
|
11
|
-
-webkit-user-select: none;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
.gd-viewport.gd-space-pan {
|
|
15
|
-
cursor: grab;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
.gd-canvas {
|
|
19
|
-
position: absolute;
|
|
20
|
-
top: 0;
|
|
21
|
-
left: 0;
|
|
22
|
-
transform-origin: 0 0;
|
|
23
|
-
will-change: transform;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/* ─── Dot grid background ───────────────────────────────── */
|
|
27
|
-
.gd-viewport::before {
|
|
28
|
-
content: '';
|
|
29
|
-
position: absolute;
|
|
30
|
-
inset: 0;
|
|
31
|
-
background-image: radial-gradient(circle, rgba(255, 255, 255, 0.04) 1px, transparent 1px);
|
|
32
|
-
background-size: 24px 24px;
|
|
33
|
-
pointer-events: none;
|
|
34
|
-
z-index: 0;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/* ─── Cards ─────────────────────────────────────────────── */
|
|
38
|
-
.gd-card {
|
|
39
|
-
position: absolute;
|
|
40
|
-
border-radius: 12px;
|
|
41
|
-
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
42
|
-
background: rgba(13, 15, 28, 0.92);
|
|
43
|
-
backdrop-filter: blur(8px);
|
|
44
|
-
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.03);
|
|
45
|
-
display: flex;
|
|
46
|
-
flex-direction: column;
|
|
47
|
-
overflow: hidden;
|
|
48
|
-
transition: box-shadow 0.2s ease, border-color 0.2s ease;
|
|
49
|
-
contain: layout style;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
.gd-card--selected {
|
|
53
|
-
border-color: rgba(147, 130, 255, 0.4);
|
|
54
|
-
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4), 0 0 0 2px rgba(147, 130, 255, 0.25);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
.gd-card--dragging {
|
|
58
|
-
opacity: 0.9;
|
|
59
|
-
transition: none;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
.gd-card--resizing {
|
|
63
|
-
transition: none;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
.gd-card--collapsed .gd-card-body {
|
|
67
|
-
display: none;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/* ─── Card header ───────────────────────────────────────── */
|
|
71
|
-
.gd-card-header {
|
|
72
|
-
display: flex;
|
|
73
|
-
align-items: center;
|
|
74
|
-
gap: 8px;
|
|
75
|
-
padding: 10px 14px;
|
|
76
|
-
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
|
77
|
-
cursor: grab;
|
|
78
|
-
min-height: 40px;
|
|
79
|
-
flex-shrink: 0;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
.gd-card-header:active {
|
|
83
|
-
cursor: grabbing;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.gd-card-header .title {
|
|
87
|
-
font-size: 13px;
|
|
88
|
-
font-weight: 600;
|
|
89
|
-
color: rgba(255, 255, 255, 0.9);
|
|
90
|
-
white-space: nowrap;
|
|
91
|
-
overflow: hidden;
|
|
92
|
-
text-overflow: ellipsis;
|
|
93
|
-
flex: 1;
|
|
94
|
-
font-family: 'Inter', sans-serif;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/* ─── Card body ─────────────────────────────────────────── */
|
|
98
|
-
.gd-card-body {
|
|
99
|
-
flex: 1;
|
|
100
|
-
overflow: auto;
|
|
101
|
-
min-height: 0;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/* ─── Resize handle ─────────────────────────────────────── */
|
|
105
|
-
.gd-resize-handle {
|
|
106
|
-
position: absolute;
|
|
107
|
-
bottom: 0;
|
|
108
|
-
right: 0;
|
|
109
|
-
width: 20px;
|
|
110
|
-
height: 20px;
|
|
111
|
-
cursor: nw-resize;
|
|
112
|
-
z-index: 1;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
.gd-resize-handle::after {
|
|
116
|
-
content: '';
|
|
117
|
-
position: absolute;
|
|
118
|
-
bottom: 4px;
|
|
119
|
-
right: 4px;
|
|
120
|
-
width: 8px;
|
|
121
|
-
height: 8px;
|
|
122
|
-
border-right: 2px solid rgba(255, 255, 255, 0.15);
|
|
123
|
-
border-bottom: 2px solid rgba(255, 255, 255, 0.15);
|
|
124
|
-
border-radius: 0 0 2px 0;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
.gd-card:hover .gd-resize-handle::after {
|
|
128
|
-
border-color: rgba(255, 255, 255, 0.3);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/* ─── Minimap ───────────────────────────────────────────── */
|
|
132
|
-
.gd-minimap {
|
|
133
|
-
position: absolute;
|
|
134
|
-
bottom: 12px;
|
|
135
|
-
right: 12px;
|
|
136
|
-
border-radius: 8px;
|
|
137
|
-
overflow: hidden;
|
|
138
|
-
backdrop-filter: blur(12px);
|
|
139
|
-
background: rgba(0, 0, 0, 0.5);
|
|
140
|
-
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
141
|
-
cursor: pointer;
|
|
142
|
-
z-index: 999;
|
|
143
|
-
transition: opacity 0.2s ease;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
.gd-minimap:hover {
|
|
147
|
-
opacity: 1;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/* ─── Scrollbar styling ─────────────────────────────────── */
|
|
151
|
-
.gd-card-body::-webkit-scrollbar {
|
|
152
|
-
width: 4px;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
.gd-card-body::-webkit-scrollbar-track {
|
|
156
|
-
background: transparent;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
.gd-card-body::-webkit-scrollbar-thumb {
|
|
160
|
-
background: rgba(255, 255, 255, 0.1);
|
|
161
|
-
border-radius: 2px;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
.gd-card-body::-webkit-scrollbar-thumb:hover {
|
|
165
|
-
background: rgba(255, 255, 255, 0.2);
|
|
166
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* galaxydraw — Infinite canvas framework
|
|
3
|
-
*
|
|
4
|
-
* Core engine for spatial applications. Provides:
|
|
5
|
-
* - Infinite pan/zoom canvas with GPU-accelerated transforms
|
|
6
|
-
* - Virtualized card rendering (only DOM for visible cards)
|
|
7
|
-
* - Drag, resize, z-order, collapse for cards
|
|
8
|
-
* - Minimap with click-to-navigate
|
|
9
|
-
* - Layout persistence (localStorage + optional server sync)
|
|
10
|
-
* - Dual control modes (Simple: drag=pan / Advanced: space+drag=pan)
|
|
11
|
-
* - Keyboard shortcuts system
|
|
12
|
-
* - Plugin architecture for custom card types
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
// ─── Core ────────────────────────────────────────────────
|
|
16
|
-
export { GalaxyDraw } from './core/engine';
|
|
17
|
-
export type { GalaxyDrawOptions, ControlMode } from './core/engine';
|
|
18
|
-
|
|
19
|
-
// ─── Canvas State ────────────────────────────────────────
|
|
20
|
-
export { CanvasState } from './core/state';
|
|
21
|
-
export type { CanvasStateSnapshot, ViewportRect } from './core/state';
|
|
22
|
-
|
|
23
|
-
// ─── Cards ───────────────────────────────────────────────
|
|
24
|
-
export { CardManager } from './core/cards';
|
|
25
|
-
export type { CardOptions, CardData, CardPlugin } from './core/cards';
|
|
26
|
-
|
|
27
|
-
// ─── Viewport & Virtualization ───────────────────────────
|
|
28
|
-
export { ViewportCuller } from './core/viewport';
|
|
29
|
-
export type { CullResult } from './core/viewport';
|
|
30
|
-
|
|
31
|
-
// ─── Layout ──────────────────────────────────────────────
|
|
32
|
-
export { LayoutManager } from './core/layout';
|
|
33
|
-
export type { LayoutData, LayoutProvider } from './core/layout';
|
|
34
|
-
|
|
35
|
-
// ─── Minimap ─────────────────────────────────────────────
|
|
36
|
-
export { Minimap } from './core/minimap';
|
|
37
|
-
|
|
38
|
-
// ─── Events ──────────────────────────────────────────────
|
|
39
|
-
export { EventBus } from './core/events';
|
|
40
|
-
export type { GalaxyDrawEvent, EventHandler } from './core/events';
|