gopeak 2.1.0 → 2.2.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 +224 -75
- package/build/addon/godot_mcp_editor/mcp_client.gd +178 -0
- package/build/addon/godot_mcp_editor/plugin.cfg +6 -0
- package/build/addon/godot_mcp_editor/plugin.gd +84 -0
- package/build/addon/godot_mcp_editor/tool_executor.gd +114 -0
- package/build/addon/godot_mcp_editor/tools/animation_tools.gd +502 -0
- package/build/addon/godot_mcp_editor/tools/resource_tools.gd +425 -0
- package/build/addon/godot_mcp_editor/tools/scene_tools.gd +710 -0
- package/build/cli/check.js +77 -0
- package/build/cli/notify.js +88 -0
- package/build/cli/setup.js +115 -0
- package/build/cli/star.js +51 -0
- package/build/cli/uninstall.js +26 -0
- package/build/cli/utils.js +149 -0
- package/build/cli.js +91 -0
- package/build/gdscript_parser.js +828 -0
- package/build/godot-bridge.js +556 -0
- package/build/index.js +2761 -2064
- package/build/prompts.js +163 -0
- package/build/visualizer/canvas.js +832 -0
- package/build/visualizer/events.js +814 -0
- package/build/visualizer/layout.js +304 -0
- package/build/visualizer/main.js +245 -0
- package/build/visualizer/modals.js +239 -0
- package/build/visualizer/panel.js +1091 -0
- package/build/visualizer/state.js +210 -0
- package/build/visualizer/syntax.js +106 -0
- package/build/visualizer/usages.js +352 -0
- package/build/visualizer/websocket.js +85 -0
- package/build/visualizer-server.js +375 -0
- package/build/visualizer.html +6395 -0
- package/package.json +15 -6
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas rendering, camera controls, and drawing utilities
|
|
3
|
+
*/
|
|
4
|
+
import { nodes, edges, NODE_W, NODE_H, camera, defaultZoom, W, H, setDimensions, searchTerm, hoveredNode, selectedNode, currentView, sceneData, expandedScene, expandedSceneHierarchy, selectedSceneNode, hoveredSceneNode, scenePositions, setExpandedScene, setSelectedSceneNode, setHoveredSceneNode, setScenePosition, scriptToScenes, categoryGroupMode, categories, activeCategories, categoryColorMap, changesVisible } from './state.js';
|
|
5
|
+
let canvas, ctx;
|
|
6
|
+
let zoomIndicator, zoomText;
|
|
7
|
+
let dpr = 1; // Device pixel ratio
|
|
8
|
+
// Storage key for position persistence
|
|
9
|
+
const STORAGE_KEY = 'godot-visualizer-positions';
|
|
10
|
+
export function initCanvas() {
|
|
11
|
+
canvas = document.getElementById('canvas');
|
|
12
|
+
ctx = canvas.getContext('2d');
|
|
13
|
+
zoomIndicator = document.getElementById('zoom-indicator');
|
|
14
|
+
zoomText = document.getElementById('zoom-text');
|
|
15
|
+
// Get device pixel ratio for crisp rendering on high-DPI displays
|
|
16
|
+
dpr = window.devicePixelRatio || 1;
|
|
17
|
+
resize();
|
|
18
|
+
const positionsRestored = loadPositions(); // Restore saved positions
|
|
19
|
+
return { canvas, ctx, positionsRestored };
|
|
20
|
+
}
|
|
21
|
+
export function getDpr() {
|
|
22
|
+
return dpr;
|
|
23
|
+
}
|
|
24
|
+
export function getCanvas() {
|
|
25
|
+
return canvas;
|
|
26
|
+
}
|
|
27
|
+
export function getContext() {
|
|
28
|
+
return ctx;
|
|
29
|
+
}
|
|
30
|
+
export function resize() {
|
|
31
|
+
const w = window.innerWidth;
|
|
32
|
+
const h = window.innerHeight;
|
|
33
|
+
setDimensions(w, h);
|
|
34
|
+
// Update DPR in case it changed (e.g., moving window between displays)
|
|
35
|
+
dpr = window.devicePixelRatio || 1;
|
|
36
|
+
// Set canvas size accounting for device pixel ratio for crisp rendering
|
|
37
|
+
canvas.width = w * dpr;
|
|
38
|
+
canvas.height = h * dpr;
|
|
39
|
+
// Scale canvas back to CSS size
|
|
40
|
+
canvas.style.width = w + 'px';
|
|
41
|
+
canvas.style.height = h + 'px';
|
|
42
|
+
// Scale context to account for DPR
|
|
43
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
44
|
+
}
|
|
45
|
+
export function screenToWorld(sx, sy) {
|
|
46
|
+
return {
|
|
47
|
+
x: (sx - W / 2) / camera.zoom + camera.x,
|
|
48
|
+
y: (sy - H / 2) / camera.zoom + camera.y
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function updateZoomIndicator() {
|
|
52
|
+
const pct = Math.round(camera.zoom * 100);
|
|
53
|
+
zoomText.value = pct + '%';
|
|
54
|
+
zoomIndicator.classList.toggle('faded', Math.abs(camera.zoom - defaultZoom) < 0.01);
|
|
55
|
+
}
|
|
56
|
+
export function resetZoom() {
|
|
57
|
+
camera.zoom = defaultZoom;
|
|
58
|
+
updateZoomIndicator();
|
|
59
|
+
draw();
|
|
60
|
+
}
|
|
61
|
+
export function setCustomZoom(value) {
|
|
62
|
+
// Parse percentage string like "150%" or just "150" or "1.5"
|
|
63
|
+
let parsed = parseFloat(value.replace('%', '').trim());
|
|
64
|
+
if (isNaN(parsed))
|
|
65
|
+
return;
|
|
66
|
+
// If user entered a small number like 1.5, treat as multiplier
|
|
67
|
+
if (parsed > 0 && parsed < 10) {
|
|
68
|
+
parsed = parsed * 100;
|
|
69
|
+
}
|
|
70
|
+
// Clamp to valid range (10% - 500%)
|
|
71
|
+
const newZoom = Math.max(0.1, Math.min(5, parsed / 100));
|
|
72
|
+
camera.zoom = newZoom;
|
|
73
|
+
updateZoomIndicator();
|
|
74
|
+
draw();
|
|
75
|
+
}
|
|
76
|
+
// Make functions available globally for onclick
|
|
77
|
+
window.resetZoom = resetZoom;
|
|
78
|
+
window.setCustomZoom = setCustomZoom;
|
|
79
|
+
// ---- Position Persistence ----
|
|
80
|
+
export function savePositions() {
|
|
81
|
+
try {
|
|
82
|
+
const positions = {};
|
|
83
|
+
nodes.forEach(n => {
|
|
84
|
+
positions[n.path] = { x: n.x, y: n.y };
|
|
85
|
+
});
|
|
86
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
|
87
|
+
positions,
|
|
88
|
+
camera: { x: camera.x, y: camera.y, zoom: camera.zoom }
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
console.warn('Failed to save positions:', e);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export function loadPositions() {
|
|
96
|
+
try {
|
|
97
|
+
const saved = localStorage.getItem(STORAGE_KEY);
|
|
98
|
+
if (!saved)
|
|
99
|
+
return false;
|
|
100
|
+
const data = JSON.parse(saved);
|
|
101
|
+
let restored = 0;
|
|
102
|
+
if (data.positions) {
|
|
103
|
+
nodes.forEach(n => {
|
|
104
|
+
if (data.positions[n.path]) {
|
|
105
|
+
n.x = data.positions[n.path].x;
|
|
106
|
+
n.y = data.positions[n.path].y;
|
|
107
|
+
restored++;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (data.camera && restored > 0) {
|
|
112
|
+
camera.x = data.camera.x;
|
|
113
|
+
camera.y = data.camera.y;
|
|
114
|
+
camera.zoom = data.camera.zoom;
|
|
115
|
+
// Don't change defaultZoom - keep it at 1 (100%) so reset always goes to 100%
|
|
116
|
+
}
|
|
117
|
+
return restored > 0;
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
console.warn('Failed to load positions:', e);
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export function clearPositions() {
|
|
125
|
+
try {
|
|
126
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
console.warn('Failed to clear positions:', e);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Save positions when node is moved
|
|
133
|
+
export function onNodeMoved() {
|
|
134
|
+
savePositions();
|
|
135
|
+
}
|
|
136
|
+
// ---- Drawing ----
|
|
137
|
+
export function draw() {
|
|
138
|
+
if (currentView === 'scenes') {
|
|
139
|
+
drawSceneView();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// Ensure DPR transform is set for crisp rendering on high-DPI displays
|
|
143
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
144
|
+
// Use crisp line rendering
|
|
145
|
+
ctx.lineCap = 'round';
|
|
146
|
+
ctx.lineJoin = 'round';
|
|
147
|
+
ctx.clearRect(0, 0, W, H);
|
|
148
|
+
ctx.save();
|
|
149
|
+
ctx.translate(Math.round(W / 2), Math.round(H / 2));
|
|
150
|
+
ctx.scale(camera.zoom, camera.zoom);
|
|
151
|
+
ctx.translate(-camera.x, -camera.y);
|
|
152
|
+
// Build path index for quick lookup
|
|
153
|
+
const pathIdx = {};
|
|
154
|
+
nodes.forEach((n, i) => {
|
|
155
|
+
pathIdx[n.path] = i;
|
|
156
|
+
});
|
|
157
|
+
// Group edges by node pair, type, and direction for bundled drawing
|
|
158
|
+
const edgeGroups = {};
|
|
159
|
+
for (const e of edges) {
|
|
160
|
+
const si = pathIdx[e.from], ti = pathIdx[e.to];
|
|
161
|
+
if (si === undefined || ti === undefined)
|
|
162
|
+
continue;
|
|
163
|
+
// Keep direction (A->B is different from B->A)
|
|
164
|
+
const key = `${si}-${ti}-${e.type}`;
|
|
165
|
+
if (!edgeGroups[key]) {
|
|
166
|
+
edgeGroups[key] = { from: e.from, to: e.to, type: e.type, edges: [], si, ti };
|
|
167
|
+
}
|
|
168
|
+
edgeGroups[key].edges.push(e);
|
|
169
|
+
}
|
|
170
|
+
// Draw bundled edges
|
|
171
|
+
for (const key of Object.keys(edgeGroups)) {
|
|
172
|
+
const group = edgeGroups[key];
|
|
173
|
+
const s = nodes[group.si], t = nodes[group.ti];
|
|
174
|
+
const count = group.edges.length;
|
|
175
|
+
if (s.categoryVisible === false || t.categoryVisible === false)
|
|
176
|
+
continue;
|
|
177
|
+
if (searchTerm && s.visible === false && t.visible === false)
|
|
178
|
+
continue;
|
|
179
|
+
// Dim edges when one node is hidden, or when neither is highlighted
|
|
180
|
+
const bothVisible = s.visible !== false && t.visible !== false;
|
|
181
|
+
ctx.globalAlpha = (!bothVisible || (!s.highlighted && !t.highlighted)) ? 0.08 : 0.5;
|
|
182
|
+
// Calculate perpendicular offset for multiple edge types between same nodes
|
|
183
|
+
const angle = Math.atan2(t.y - s.y, t.x - s.x);
|
|
184
|
+
const perpAngle = angle + Math.PI / 2;
|
|
185
|
+
// Get offset based on edge type (so different types don't overlap)
|
|
186
|
+
const typeOffset = group.type === 'extends' ? 0 : group.type === 'preload' ? 8 : 16;
|
|
187
|
+
const offsetX = Math.cos(perpAngle) * typeOffset;
|
|
188
|
+
const offsetY = Math.sin(perpAngle) * typeOffset;
|
|
189
|
+
ctx.beginPath();
|
|
190
|
+
ctx.moveTo(s.x + offsetX, s.y + offsetY);
|
|
191
|
+
ctx.lineTo(t.x + offsetX, t.y + offsetY);
|
|
192
|
+
// Line widths scale with zoom (fixed world-space size)
|
|
193
|
+
if (group.type === 'extends') {
|
|
194
|
+
ctx.strokeStyle = '#7aa2f7';
|
|
195
|
+
ctx.setLineDash([]);
|
|
196
|
+
ctx.lineWidth = 2;
|
|
197
|
+
}
|
|
198
|
+
else if (group.type === 'preload') {
|
|
199
|
+
ctx.strokeStyle = '#d4a27f';
|
|
200
|
+
ctx.setLineDash([]);
|
|
201
|
+
ctx.lineWidth = 1.5;
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
ctx.strokeStyle = '#a6e3a1';
|
|
205
|
+
ctx.setLineDash([4, 4]);
|
|
206
|
+
ctx.lineWidth = 1.5;
|
|
207
|
+
}
|
|
208
|
+
ctx.stroke();
|
|
209
|
+
ctx.setLineDash([]);
|
|
210
|
+
// Arrow at midpoint - fixed world-space size
|
|
211
|
+
const al = 10;
|
|
212
|
+
const mx = (s.x + t.x) / 2 + offsetX, my = (s.y + t.y) / 2 + offsetY;
|
|
213
|
+
ctx.beginPath();
|
|
214
|
+
ctx.moveTo(mx + Math.cos(angle) * al, my + Math.sin(angle) * al);
|
|
215
|
+
ctx.lineTo(mx + Math.cos(angle + 2.5) * al * 0.6, my + Math.sin(angle + 2.5) * al * 0.6);
|
|
216
|
+
ctx.lineTo(mx + Math.cos(angle - 2.5) * al * 0.6, my + Math.sin(angle - 2.5) * al * 0.6);
|
|
217
|
+
ctx.closePath();
|
|
218
|
+
ctx.fillStyle = ctx.strokeStyle;
|
|
219
|
+
ctx.fill();
|
|
220
|
+
// Draw count badge if multiple connections of same type
|
|
221
|
+
if (count > 1) {
|
|
222
|
+
const badgeX = mx + Math.cos(perpAngle) * 12;
|
|
223
|
+
const badgeY = my + Math.sin(perpAngle) * 12;
|
|
224
|
+
const badgeSize = 16;
|
|
225
|
+
ctx.globalAlpha = bothVisible ? 0.9 : 0.3;
|
|
226
|
+
ctx.beginPath();
|
|
227
|
+
ctx.arc(badgeX, badgeY, badgeSize / 2, 0, Math.PI * 2);
|
|
228
|
+
ctx.fillStyle = ctx.strokeStyle;
|
|
229
|
+
ctx.fill();
|
|
230
|
+
// Count text - scales with zoom
|
|
231
|
+
ctx.fillStyle = '#08090a';
|
|
232
|
+
ctx.font = `bold 10px -apple-system, system-ui, sans-serif`;
|
|
233
|
+
ctx.textAlign = 'center';
|
|
234
|
+
ctx.textBaseline = 'middle';
|
|
235
|
+
ctx.fillText(count.toString(), badgeX, badgeY);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
ctx.globalAlpha = 1;
|
|
239
|
+
if (categoryGroupMode === 'grouped' && window.__categoryGroupBoxes) {
|
|
240
|
+
for (const box of window.__categoryGroupBoxes) {
|
|
241
|
+
const catInfo = categories.find(c => c.id === box.category);
|
|
242
|
+
if (!catInfo)
|
|
243
|
+
continue;
|
|
244
|
+
if (!activeCategories.has(box.category))
|
|
245
|
+
continue;
|
|
246
|
+
ctx.globalAlpha = 0.08;
|
|
247
|
+
ctx.beginPath();
|
|
248
|
+
roundRect(ctx, box.x, box.y, box.w, box.h, 16);
|
|
249
|
+
ctx.fillStyle = catInfo.color || categoryColorMap[box.category] || '#7aa2f7';
|
|
250
|
+
ctx.fill();
|
|
251
|
+
ctx.globalAlpha = 0.25;
|
|
252
|
+
ctx.strokeStyle = catInfo.color || categoryColorMap[box.category] || '#7aa2f7';
|
|
253
|
+
ctx.lineWidth = 1.5;
|
|
254
|
+
ctx.setLineDash([6, 4]);
|
|
255
|
+
ctx.stroke();
|
|
256
|
+
ctx.setLineDash([]);
|
|
257
|
+
ctx.globalAlpha = 0.9;
|
|
258
|
+
ctx.font = 'bold 14px -apple-system, system-ui, sans-serif';
|
|
259
|
+
ctx.fillStyle = catInfo.color || categoryColorMap[box.category] || '#7aa2f7';
|
|
260
|
+
ctx.textAlign = 'left';
|
|
261
|
+
ctx.textBaseline = 'top';
|
|
262
|
+
ctx.fillText(`${catInfo.label} (${catInfo.count})`, Math.round(box.x + 12), Math.round(box.y + 10));
|
|
263
|
+
ctx.globalAlpha = 1;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// Draw nodes
|
|
267
|
+
for (const n of nodes) {
|
|
268
|
+
if (n.categoryVisible === false)
|
|
269
|
+
continue;
|
|
270
|
+
// Skip hidden nodes during search
|
|
271
|
+
if (searchTerm && n.visible === false)
|
|
272
|
+
continue;
|
|
273
|
+
// Round coordinates for crisper rendering
|
|
274
|
+
const x = Math.round(n.x - NODE_W / 2);
|
|
275
|
+
const y = Math.round(n.y - NODE_H / 2);
|
|
276
|
+
const isHovered = n === hoveredNode, isSelected = n === selectedNode;
|
|
277
|
+
ctx.globalAlpha = n.highlighted ? 1 : 0.12;
|
|
278
|
+
// Shadow - fixed world-space size
|
|
279
|
+
const hasGitGlow = changesVisible && n.gitStatus;
|
|
280
|
+
if (hasGitGlow) {
|
|
281
|
+
const glowColors = {
|
|
282
|
+
modified: 'rgba(249, 226, 175, 0.4)',
|
|
283
|
+
added: 'rgba(166, 227, 161, 0.4)',
|
|
284
|
+
untracked: 'rgba(137, 180, 250, 0.3)'
|
|
285
|
+
};
|
|
286
|
+
ctx.shadowColor = glowColors[n.gitStatus] || 'rgba(0,0,0,0.4)';
|
|
287
|
+
ctx.shadowBlur = isHovered ? 20 : 12;
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
ctx.shadowColor = 'rgba(0,0,0,0.4)';
|
|
291
|
+
ctx.shadowBlur = isHovered ? 16 : 8;
|
|
292
|
+
}
|
|
293
|
+
ctx.shadowOffsetY = 2;
|
|
294
|
+
// Background
|
|
295
|
+
ctx.beginPath();
|
|
296
|
+
roundRect(ctx, x, y, NODE_W, NODE_H, 10);
|
|
297
|
+
ctx.fillStyle = isSelected ? '#1e2028' : isHovered ? '#181a20' : '#0f1014';
|
|
298
|
+
ctx.fill();
|
|
299
|
+
ctx.shadowBlur = 0;
|
|
300
|
+
ctx.shadowOffsetY = 0;
|
|
301
|
+
// Border - fixed world-space width
|
|
302
|
+
if (changesVisible && n.gitStatus) {
|
|
303
|
+
const gitBorderColors = { modified: '#f9e2af', added: '#a6e3a1', untracked: '#89b4fa' };
|
|
304
|
+
ctx.strokeStyle = isSelected ? n.color : gitBorderColors[n.gitStatus] || 'rgba(255,255,255,0.06)';
|
|
305
|
+
ctx.lineWidth = isSelected ? 2 : 1.5;
|
|
306
|
+
if (n.gitStatus === 'untracked') {
|
|
307
|
+
ctx.setLineDash([4, 3]);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
ctx.strokeStyle = isSelected ? n.color : isHovered ? n.color : 'rgba(255,255,255,0.06)';
|
|
312
|
+
ctx.lineWidth = isSelected ? 2 : 1;
|
|
313
|
+
}
|
|
314
|
+
ctx.stroke();
|
|
315
|
+
ctx.setLineDash([]);
|
|
316
|
+
// Left accent bar
|
|
317
|
+
ctx.beginPath();
|
|
318
|
+
ctx.roundRect(x + 4, y + 8, 3, NODE_H - 16, 2);
|
|
319
|
+
ctx.fillStyle = n.color;
|
|
320
|
+
ctx.fill();
|
|
321
|
+
// Title - scales with node (no zoom compensation)
|
|
322
|
+
const titleSize = 14;
|
|
323
|
+
ctx.font = `600 ${titleSize}px -apple-system, system-ui, sans-serif`;
|
|
324
|
+
ctx.fillStyle = '#f0f0f5';
|
|
325
|
+
ctx.textBaseline = 'middle';
|
|
326
|
+
ctx.textAlign = 'left';
|
|
327
|
+
const displayName = n.class_name || n.filename.replace('.gd', '');
|
|
328
|
+
ctx.fillText(displayName, x + 16, y + NODE_H / 2 - 6);
|
|
329
|
+
// Subtitle with colored stats - scales with node
|
|
330
|
+
const subSize = 11;
|
|
331
|
+
const varCount = n.variables ? n.variables.length : 0;
|
|
332
|
+
const funcCount = n.functions ? n.functions.length : 0;
|
|
333
|
+
const sigCount = n.signals ? n.signals.length : 0;
|
|
334
|
+
// Draw subtitle parts with colors
|
|
335
|
+
ctx.font = `${subSize}px -apple-system, system-ui, sans-serif`;
|
|
336
|
+
const subY = y + NODE_H / 2 + 9;
|
|
337
|
+
let subX = x + 16;
|
|
338
|
+
// Extends
|
|
339
|
+
ctx.fillStyle = '#484f58';
|
|
340
|
+
const extendsText = (n.extends || 'Node') + ' · ';
|
|
341
|
+
ctx.fillText(extendsText, subX, subY);
|
|
342
|
+
subX += ctx.measureText(extendsText).width;
|
|
343
|
+
// Functions (cyan/teal)
|
|
344
|
+
ctx.fillStyle = '#89dceb';
|
|
345
|
+
ctx.fillText(funcCount + 'f', subX, subY);
|
|
346
|
+
subX += ctx.measureText(funcCount + 'f').width;
|
|
347
|
+
// Space
|
|
348
|
+
ctx.fillStyle = '#484f58';
|
|
349
|
+
ctx.fillText(' ', subX, subY);
|
|
350
|
+
subX += ctx.measureText(' ').width;
|
|
351
|
+
// Variables (purple)
|
|
352
|
+
ctx.fillStyle = '#cba6f7';
|
|
353
|
+
ctx.fillText(varCount + 'v', subX, subY);
|
|
354
|
+
subX += ctx.measureText(varCount + 'v').width;
|
|
355
|
+
// Space
|
|
356
|
+
ctx.fillStyle = '#484f58';
|
|
357
|
+
ctx.fillText(' ', subX, subY);
|
|
358
|
+
subX += ctx.measureText(' ').width;
|
|
359
|
+
// Signals (green)
|
|
360
|
+
ctx.fillStyle = '#a6e3a1';
|
|
361
|
+
ctx.fillText(sigCount + 's', subX, subY);
|
|
362
|
+
subX += ctx.measureText(sigCount + 's').width;
|
|
363
|
+
// Separator
|
|
364
|
+
ctx.fillStyle = '#484f58';
|
|
365
|
+
ctx.fillText(' · ', subX, subY);
|
|
366
|
+
subX += ctx.measureText(' · ').width;
|
|
367
|
+
// Lines (yellow/amber)
|
|
368
|
+
ctx.fillStyle = '#f9e2af';
|
|
369
|
+
ctx.fillText(n.line_count + 'L', subX, subY);
|
|
370
|
+
// Scene usage badge (top-right corner)
|
|
371
|
+
const usedInScenes = scriptToScenes[n.path];
|
|
372
|
+
if (usedInScenes && usedInScenes.length > 0) {
|
|
373
|
+
const badgeX = x + NODE_W - 8;
|
|
374
|
+
const badgeY = y + 8;
|
|
375
|
+
ctx.fillStyle = 'rgba(166, 227, 161, 0.2)';
|
|
376
|
+
ctx.beginPath();
|
|
377
|
+
ctx.roundRect(badgeX - 20, badgeY - 4, 24, 14, 3);
|
|
378
|
+
ctx.fill();
|
|
379
|
+
ctx.fillStyle = '#a6e3a1';
|
|
380
|
+
ctx.font = `600 9px -apple-system, system-ui, sans-serif`;
|
|
381
|
+
ctx.textAlign = 'right';
|
|
382
|
+
ctx.fillText('📦' + usedInScenes.length, badgeX, badgeY + 4);
|
|
383
|
+
ctx.textAlign = 'left';
|
|
384
|
+
}
|
|
385
|
+
if (changesVisible && n.gitStatus) {
|
|
386
|
+
const badgeConfigs = {
|
|
387
|
+
modified: { bg: 'rgba(249, 226, 175, 0.25)', fg: '#f9e2af', label: 'M' },
|
|
388
|
+
added: { bg: 'rgba(166, 227, 161, 0.25)', fg: '#a6e3a1', label: '+' },
|
|
389
|
+
untracked: { bg: 'rgba(137, 180, 250, 0.25)', fg: '#89b4fa', label: '?' }
|
|
390
|
+
};
|
|
391
|
+
const badge = badgeConfigs[n.gitStatus];
|
|
392
|
+
if (badge) {
|
|
393
|
+
const bx = x + 8;
|
|
394
|
+
const by = y + 8;
|
|
395
|
+
ctx.fillStyle = badge.bg;
|
|
396
|
+
ctx.beginPath();
|
|
397
|
+
ctx.roundRect(bx - 2, by - 4, 18, 14, 3);
|
|
398
|
+
ctx.fill();
|
|
399
|
+
ctx.fillStyle = badge.fg;
|
|
400
|
+
ctx.font = '600 9px -apple-system, system-ui, sans-serif';
|
|
401
|
+
ctx.textAlign = 'left';
|
|
402
|
+
ctx.fillText(badge.label, bx + 2, by + 5);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
ctx.globalAlpha = 1;
|
|
407
|
+
ctx.restore();
|
|
408
|
+
}
|
|
409
|
+
// Scene view constants
|
|
410
|
+
const SCENE_CARD_W = 200; // Match NODE_W
|
|
411
|
+
const SCENE_CARD_H = 54; // Match NODE_H
|
|
412
|
+
const SCENE_NODE_MIN_W = 80; // Minimum node width
|
|
413
|
+
const SCENE_NODE_MAX_W = 200; // Maximum node width
|
|
414
|
+
const SCENE_NODE_H = 36;
|
|
415
|
+
const SCENE_NODE_GAP_X = 15; // Reduced for tighter layout
|
|
416
|
+
const SCENE_NODE_GAP_Y = 40;
|
|
417
|
+
// Calculate dynamic node width based on name
|
|
418
|
+
function calculateNodeWidth(name) {
|
|
419
|
+
// Approximate width: ~7px per character + padding
|
|
420
|
+
const textWidth = (name || 'Node').length * 7;
|
|
421
|
+
const padding = 35; // For script icon and margins
|
|
422
|
+
return Math.min(SCENE_NODE_MAX_W, Math.max(SCENE_NODE_MIN_W, textWidth + padding));
|
|
423
|
+
}
|
|
424
|
+
function drawSceneView() {
|
|
425
|
+
// Ensure DPR transform is set for crisp rendering on high-DPI displays
|
|
426
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
427
|
+
ctx.lineCap = 'round';
|
|
428
|
+
ctx.lineJoin = 'round';
|
|
429
|
+
ctx.clearRect(0, 0, W, H);
|
|
430
|
+
ctx.save();
|
|
431
|
+
ctx.translate(Math.round(W / 2), Math.round(H / 2));
|
|
432
|
+
ctx.scale(camera.zoom, camera.zoom);
|
|
433
|
+
ctx.translate(-camera.x, -camera.y);
|
|
434
|
+
if (!sceneData || !sceneData.scenes || sceneData.scenes.length === 0) {
|
|
435
|
+
drawSceneViewPlaceholder();
|
|
436
|
+
ctx.restore();
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
// Check if we're in expanded mode
|
|
440
|
+
if (expandedScene && expandedSceneHierarchy) {
|
|
441
|
+
drawExpandedSceneView();
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
drawSceneOverview();
|
|
445
|
+
}
|
|
446
|
+
ctx.restore();
|
|
447
|
+
}
|
|
448
|
+
function drawSceneOverview() {
|
|
449
|
+
const scenes = sceneData.scenes;
|
|
450
|
+
// Calculate positions if not set
|
|
451
|
+
scenes.forEach((scene, i) => {
|
|
452
|
+
if (!scenePositions[scene.path]) {
|
|
453
|
+
const cols = Math.max(1, Math.floor(Math.sqrt(scenes.length * 1.5)));
|
|
454
|
+
setScenePosition(scene.path, (i % cols) * (SCENE_CARD_W + 40) - ((cols - 1) * (SCENE_CARD_W + 40)) / 2, Math.floor(i / cols) * (SCENE_CARD_H + 30) - 100);
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
// Draw edges between scenes (instance relationships)
|
|
458
|
+
if (sceneData.edges) {
|
|
459
|
+
ctx.globalAlpha = 0.4;
|
|
460
|
+
for (const edge of sceneData.edges) {
|
|
461
|
+
const fromScene = scenes.find(s => s.path === edge.from);
|
|
462
|
+
const toScene = scenes.find(s => s.path === edge.to);
|
|
463
|
+
if (!fromScene || !toScene)
|
|
464
|
+
continue;
|
|
465
|
+
const fromPos = scenePositions[edge.from];
|
|
466
|
+
const toPos = scenePositions[edge.to];
|
|
467
|
+
if (!fromPos || !toPos)
|
|
468
|
+
continue;
|
|
469
|
+
const fromX = fromPos.x + SCENE_CARD_W / 2;
|
|
470
|
+
const fromY = fromPos.y + SCENE_CARD_H;
|
|
471
|
+
const toX = toPos.x + SCENE_CARD_W / 2;
|
|
472
|
+
const toY = toPos.y;
|
|
473
|
+
ctx.beginPath();
|
|
474
|
+
ctx.moveTo(fromX, fromY);
|
|
475
|
+
ctx.lineTo(toX, toY);
|
|
476
|
+
ctx.strokeStyle = '#89dceb';
|
|
477
|
+
ctx.lineWidth = 1.5;
|
|
478
|
+
ctx.setLineDash([4, 4]);
|
|
479
|
+
ctx.stroke();
|
|
480
|
+
ctx.setLineDash([]);
|
|
481
|
+
}
|
|
482
|
+
ctx.globalAlpha = 1;
|
|
483
|
+
}
|
|
484
|
+
// Draw scene cards
|
|
485
|
+
scenes.forEach((scene, i) => {
|
|
486
|
+
const pos = scenePositions[scene.path];
|
|
487
|
+
const x = pos.x;
|
|
488
|
+
const y = pos.y;
|
|
489
|
+
const isHovered = hoveredSceneNode && hoveredSceneNode.scenePath === scene.path && !hoveredSceneNode.nodePath;
|
|
490
|
+
const isExpanded = expandedScene === scene.path;
|
|
491
|
+
const sceneColor = getSceneColor(scene.path);
|
|
492
|
+
// Shadow - match script node styling
|
|
493
|
+
ctx.shadowColor = 'rgba(0,0,0,0.4)';
|
|
494
|
+
ctx.shadowBlur = isHovered ? 16 : 8;
|
|
495
|
+
ctx.shadowOffsetY = 2;
|
|
496
|
+
// Scene card background - match script node colors
|
|
497
|
+
ctx.beginPath();
|
|
498
|
+
roundRect(ctx, x, y, SCENE_CARD_W, SCENE_CARD_H, 10);
|
|
499
|
+
ctx.fillStyle = isExpanded ? '#1e2028' : isHovered ? '#181a20' : '#0f1014';
|
|
500
|
+
ctx.fill();
|
|
501
|
+
ctx.shadowBlur = 0;
|
|
502
|
+
ctx.shadowOffsetY = 0;
|
|
503
|
+
// Border - match script node styling
|
|
504
|
+
ctx.strokeStyle = isExpanded ? sceneColor : isHovered ? sceneColor : 'rgba(255,255,255,0.06)';
|
|
505
|
+
ctx.lineWidth = isExpanded ? 2 : 1;
|
|
506
|
+
ctx.stroke();
|
|
507
|
+
// Left accent bar (scene color)
|
|
508
|
+
ctx.beginPath();
|
|
509
|
+
ctx.roundRect(x + 4, y + 8, 3, SCENE_CARD_H - 16, 2);
|
|
510
|
+
ctx.fillStyle = sceneColor;
|
|
511
|
+
ctx.fill();
|
|
512
|
+
// Scene name (main label)
|
|
513
|
+
ctx.fillStyle = '#f0f0f5';
|
|
514
|
+
ctx.font = `600 13px -apple-system, system-ui, sans-serif`;
|
|
515
|
+
ctx.textAlign = 'left';
|
|
516
|
+
const sceneName = scene.name || scene.path.split('/').pop().replace('.tscn', '');
|
|
517
|
+
ctx.fillText(sceneName, x + 14, y + 22);
|
|
518
|
+
// Root type and stats on second line
|
|
519
|
+
const nodeCount = scene.node_count || (scene.nodes ? scene.nodes.length : 0);
|
|
520
|
+
ctx.fillStyle = '#484f58';
|
|
521
|
+
ctx.font = `11px -apple-system, system-ui, sans-serif`;
|
|
522
|
+
ctx.fillText(`${scene.root_type || 'Node'} · ${nodeCount} nodes`, x + 14, y + 40);
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
function drawExpandedSceneView() {
|
|
526
|
+
const hierarchy = expandedSceneHierarchy;
|
|
527
|
+
if (!hierarchy)
|
|
528
|
+
return;
|
|
529
|
+
// Draw back button area (handled by HTML overlay)
|
|
530
|
+
// Draw the node tree
|
|
531
|
+
const treeLayout = calculateTreeLayout(hierarchy);
|
|
532
|
+
// Draw connection lines first
|
|
533
|
+
drawTreeConnections(treeLayout.nodes);
|
|
534
|
+
// Draw nodes
|
|
535
|
+
for (const node of treeLayout.nodes) {
|
|
536
|
+
drawSceneNode(node);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
function calculateTreeLayout(hierarchy) {
|
|
540
|
+
const nodes = [];
|
|
541
|
+
const LEVEL_HEIGHT = SCENE_NODE_H + SCENE_NODE_GAP_Y;
|
|
542
|
+
// Simple layout: each node positions its children directly below,
|
|
543
|
+
// centered on itself, without considering grandchildren widths
|
|
544
|
+
function processNode(node, depth, centerX) {
|
|
545
|
+
const nodeWidth = calculateNodeWidth(node.name);
|
|
546
|
+
const x = centerX - nodeWidth / 2;
|
|
547
|
+
const y = depth * LEVEL_HEIGHT;
|
|
548
|
+
const nodeLayout = {
|
|
549
|
+
...node,
|
|
550
|
+
x,
|
|
551
|
+
y,
|
|
552
|
+
width: nodeWidth,
|
|
553
|
+
height: SCENE_NODE_H,
|
|
554
|
+
childPositions: []
|
|
555
|
+
};
|
|
556
|
+
nodes.push(nodeLayout);
|
|
557
|
+
// Layout children centered under this node
|
|
558
|
+
if (node.children && node.children.length > 0) {
|
|
559
|
+
// Calculate total width of direct children only
|
|
560
|
+
let totalChildrenWidth = 0;
|
|
561
|
+
for (const child of node.children) {
|
|
562
|
+
totalChildrenWidth += calculateNodeWidth(child.name) + SCENE_NODE_GAP_X;
|
|
563
|
+
}
|
|
564
|
+
totalChildrenWidth -= SCENE_NODE_GAP_X; // Remove last gap
|
|
565
|
+
// Start children centered under parent
|
|
566
|
+
let childX = centerX - totalChildrenWidth / 2;
|
|
567
|
+
for (const child of node.children) {
|
|
568
|
+
const childWidth = calculateNodeWidth(child.name);
|
|
569
|
+
const childCenterX = childX + childWidth / 2;
|
|
570
|
+
nodeLayout.childPositions.push({
|
|
571
|
+
x: childCenterX,
|
|
572
|
+
y: (depth + 1) * LEVEL_HEIGHT
|
|
573
|
+
});
|
|
574
|
+
processNode(child, depth + 1, childCenterX);
|
|
575
|
+
childX += childWidth + SCENE_NODE_GAP_X;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return nodeLayout;
|
|
579
|
+
}
|
|
580
|
+
processNode(hierarchy, 0, 0);
|
|
581
|
+
return { nodes };
|
|
582
|
+
}
|
|
583
|
+
function drawTreeConnections(nodes) {
|
|
584
|
+
ctx.strokeStyle = '#484f58';
|
|
585
|
+
ctx.lineWidth = 1.5;
|
|
586
|
+
ctx.setLineDash([]);
|
|
587
|
+
for (const node of nodes) {
|
|
588
|
+
if (node.childPositions && node.childPositions.length > 0) {
|
|
589
|
+
const parentX = node.x + node.width / 2;
|
|
590
|
+
const parentY = node.y + SCENE_NODE_H;
|
|
591
|
+
for (const childPos of node.childPositions) {
|
|
592
|
+
ctx.beginPath();
|
|
593
|
+
ctx.moveTo(parentX, parentY);
|
|
594
|
+
// Draw an elbow connector
|
|
595
|
+
const midY = parentY + (childPos.y - parentY) / 2;
|
|
596
|
+
ctx.lineTo(parentX, midY);
|
|
597
|
+
ctx.lineTo(childPos.x, midY);
|
|
598
|
+
ctx.lineTo(childPos.x, childPos.y);
|
|
599
|
+
ctx.stroke();
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
function drawSceneNode(node) {
|
|
605
|
+
const x = node.x;
|
|
606
|
+
const y = node.y;
|
|
607
|
+
const w = node.width;
|
|
608
|
+
const isSelected = selectedSceneNode && selectedSceneNode.path === node.path;
|
|
609
|
+
const isHovered = hoveredSceneNode && hoveredSceneNode.nodePath === node.path;
|
|
610
|
+
const isHighlighted = node.highlighted !== false; // Default to true if not set
|
|
611
|
+
// Node type color
|
|
612
|
+
const nodeColor = getNodeTypeColor(node.type);
|
|
613
|
+
// Dim non-highlighted nodes when searching
|
|
614
|
+
ctx.globalAlpha = isHighlighted ? 1 : 0.25;
|
|
615
|
+
// Shadow
|
|
616
|
+
ctx.shadowColor = 'rgba(0,0,0,0.25)';
|
|
617
|
+
ctx.shadowBlur = isHovered ? 12 : 6;
|
|
618
|
+
ctx.shadowOffsetY = 2;
|
|
619
|
+
// Background - highlight matching nodes with a glow
|
|
620
|
+
ctx.beginPath();
|
|
621
|
+
roundRect(ctx, x, y, w, SCENE_NODE_H, 6);
|
|
622
|
+
ctx.fillStyle = isSelected ? '#1e2028' : isHovered ? '#181a20' : '#0f1014';
|
|
623
|
+
ctx.fill();
|
|
624
|
+
ctx.shadowBlur = 0;
|
|
625
|
+
ctx.shadowOffsetY = 0;
|
|
626
|
+
// Border - use accent color for highlighted search results
|
|
627
|
+
const borderColor = isSelected ? nodeColor : isHovered ? nodeColor :
|
|
628
|
+
(isHighlighted && searchTerm ? '#f9e2af' : 'rgba(255,255,255,0.06)');
|
|
629
|
+
ctx.strokeStyle = borderColor;
|
|
630
|
+
ctx.lineWidth = (isSelected || (isHighlighted && searchTerm)) ? 2 : 1;
|
|
631
|
+
ctx.stroke();
|
|
632
|
+
// Left accent
|
|
633
|
+
ctx.beginPath();
|
|
634
|
+
ctx.roundRect(x + 3, y + 6, 2, SCENE_NODE_H - 12, 1);
|
|
635
|
+
ctx.fillStyle = nodeColor;
|
|
636
|
+
ctx.fill();
|
|
637
|
+
// Node name
|
|
638
|
+
ctx.fillStyle = '#f0f0f5';
|
|
639
|
+
ctx.font = `600 11px -apple-system, system-ui, sans-serif`;
|
|
640
|
+
ctx.textAlign = 'left';
|
|
641
|
+
ctx.textBaseline = 'middle';
|
|
642
|
+
const displayName = node.name || 'Node';
|
|
643
|
+
ctx.fillText(displayName, x + 10, y + SCENE_NODE_H / 2 - 4);
|
|
644
|
+
// Node type (smaller, below name)
|
|
645
|
+
ctx.fillStyle = '#484f58';
|
|
646
|
+
ctx.font = `9px -apple-system, system-ui, sans-serif`;
|
|
647
|
+
ctx.fillText(node.type, x + 10, y + SCENE_NODE_H / 2 + 7);
|
|
648
|
+
// Script indicator
|
|
649
|
+
if (node.script) {
|
|
650
|
+
ctx.fillStyle = '#a6e3a1';
|
|
651
|
+
ctx.font = `10px -apple-system, system-ui, sans-serif`;
|
|
652
|
+
ctx.textAlign = 'right';
|
|
653
|
+
ctx.fillText('📜', x + w - 6, y + SCENE_NODE_H / 2);
|
|
654
|
+
ctx.textAlign = 'left';
|
|
655
|
+
}
|
|
656
|
+
// Sibling index indicator (for node order)
|
|
657
|
+
if (node.index !== undefined && node.index > 0) {
|
|
658
|
+
ctx.fillStyle = '#484f58';
|
|
659
|
+
ctx.font = `9px -apple-system, system-ui, sans-serif`;
|
|
660
|
+
ctx.textAlign = 'right';
|
|
661
|
+
ctx.fillText(`#${node.index}`, x + w - 6, y + 10);
|
|
662
|
+
ctx.textAlign = 'left';
|
|
663
|
+
}
|
|
664
|
+
// Reset alpha
|
|
665
|
+
ctx.globalAlpha = 1;
|
|
666
|
+
}
|
|
667
|
+
function getSceneColor(scenePath) {
|
|
668
|
+
// Generate consistent color based on path
|
|
669
|
+
const colors = ['#89dceb', '#a6e3a1', '#f9e2af', '#cba6f7', '#f38ba8', '#fab387'];
|
|
670
|
+
let hash = 0;
|
|
671
|
+
for (let i = 0; i < scenePath.length; i++) {
|
|
672
|
+
hash = scenePath.charCodeAt(i) + ((hash << 5) - hash);
|
|
673
|
+
}
|
|
674
|
+
return colors[Math.abs(hash) % colors.length];
|
|
675
|
+
}
|
|
676
|
+
function getNodeTypeColor(nodeType) {
|
|
677
|
+
// Godot's actual node type colors
|
|
678
|
+
const GODOT_GREEN = '#8eef97'; // Control/UI nodes
|
|
679
|
+
const GODOT_BLUE = '#8da5f3'; // Node2D nodes
|
|
680
|
+
const GODOT_RED = '#fc7f7f'; // Node3D nodes
|
|
681
|
+
const GODOT_GRAY = '#b2b2b2'; // Base Node
|
|
682
|
+
// Control/UI nodes (green)
|
|
683
|
+
const controlTypes = [
|
|
684
|
+
'Control', 'Label', 'Button', 'LineEdit', 'TextEdit', 'RichTextLabel',
|
|
685
|
+
'Panel', 'PanelContainer', 'Container', 'BoxContainer', 'VBoxContainer',
|
|
686
|
+
'HBoxContainer', 'GridContainer', 'MarginContainer', 'ScrollContainer',
|
|
687
|
+
'TabContainer', 'ProgressBar', 'TextureRect', 'ColorRect', 'NinePatchRect',
|
|
688
|
+
'CheckBox', 'CheckButton', 'OptionButton', 'SpinBox', 'Slider', 'HSlider',
|
|
689
|
+
'VSlider', 'Tree', 'ItemList', 'MenuButton', 'LinkButton', 'CanvasLayer'
|
|
690
|
+
];
|
|
691
|
+
// Node2D nodes (blue)
|
|
692
|
+
const node2DTypes = [
|
|
693
|
+
'Node2D', 'Sprite2D', 'AnimatedSprite2D', 'CharacterBody2D', 'RigidBody2D',
|
|
694
|
+
'StaticBody2D', 'Area2D', 'CollisionShape2D', 'CollisionPolygon2D',
|
|
695
|
+
'Camera2D', 'Path2D', 'PathFollow2D', 'Line2D', 'Polygon2D', 'TileMap',
|
|
696
|
+
'TileMapLayer', 'Marker2D', 'RemoteTransform2D', 'VisibleOnScreenNotifier2D',
|
|
697
|
+
'GPUParticles2D', 'CPUParticles2D', 'LightOccluder2D', 'PointLight2D',
|
|
698
|
+
'DirectionalLight2D', 'AudioStreamPlayer2D', 'NavigationRegion2D'
|
|
699
|
+
];
|
|
700
|
+
// Node3D nodes (red)
|
|
701
|
+
const node3DTypes = [
|
|
702
|
+
'Node3D', 'Sprite3D', 'AnimatedSprite3D', 'CharacterBody3D', 'RigidBody3D',
|
|
703
|
+
'StaticBody3D', 'Area3D', 'CollisionShape3D', 'CollisionPolygon3D',
|
|
704
|
+
'Camera3D', 'MeshInstance3D', 'MultiMeshInstance3D', 'CSGBox3D',
|
|
705
|
+
'CSGCylinder3D', 'CSGSphere3D', 'CSGMesh3D', 'Path3D', 'PathFollow3D',
|
|
706
|
+
'GPUParticles3D', 'CPUParticles3D', 'OmniLight3D', 'SpotLight3D',
|
|
707
|
+
'DirectionalLight3D', 'AudioStreamPlayer3D', 'NavigationRegion3D'
|
|
708
|
+
];
|
|
709
|
+
// Check exact matches first, then partial
|
|
710
|
+
for (const type of controlTypes) {
|
|
711
|
+
if (nodeType === type || nodeType.includes(type))
|
|
712
|
+
return GODOT_GREEN;
|
|
713
|
+
}
|
|
714
|
+
for (const type of node2DTypes) {
|
|
715
|
+
if (nodeType === type || nodeType.includes(type))
|
|
716
|
+
return GODOT_BLUE;
|
|
717
|
+
}
|
|
718
|
+
for (const type of node3DTypes) {
|
|
719
|
+
if (nodeType === type || nodeType.includes(type))
|
|
720
|
+
return GODOT_RED;
|
|
721
|
+
}
|
|
722
|
+
// Fallback: check for 2D/3D suffix
|
|
723
|
+
if (nodeType.endsWith('2D'))
|
|
724
|
+
return GODOT_BLUE;
|
|
725
|
+
if (nodeType.endsWith('3D'))
|
|
726
|
+
return GODOT_RED;
|
|
727
|
+
return GODOT_GRAY; // Default gray for base Node
|
|
728
|
+
}
|
|
729
|
+
function drawSceneViewPlaceholder() {
|
|
730
|
+
ctx.fillStyle = '#484f58';
|
|
731
|
+
ctx.font = `16px -apple-system, system-ui, sans-serif`;
|
|
732
|
+
ctx.textAlign = 'center';
|
|
733
|
+
ctx.fillText('No scenes found', 0, 0);
|
|
734
|
+
ctx.fillText('Create a .tscn file in your project', 0, 24);
|
|
735
|
+
ctx.textAlign = 'left';
|
|
736
|
+
}
|
|
737
|
+
// Export scene hit testing
|
|
738
|
+
export function sceneHitTest(wx, wy) {
|
|
739
|
+
if (!sceneData || !sceneData.scenes)
|
|
740
|
+
return null;
|
|
741
|
+
if (expandedScene && expandedSceneHierarchy) {
|
|
742
|
+
// Hit test expanded scene nodes
|
|
743
|
+
const treeLayout = calculateTreeLayout(expandedSceneHierarchy);
|
|
744
|
+
for (let i = treeLayout.nodes.length - 1; i >= 0; i--) {
|
|
745
|
+
const node = treeLayout.nodes[i];
|
|
746
|
+
if (wx >= node.x && wx <= node.x + node.width &&
|
|
747
|
+
wy >= node.y && wy <= node.y + SCENE_NODE_H) {
|
|
748
|
+
return { type: 'sceneNode', node, scenePath: expandedScene };
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return null;
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
// Hit test scene cards
|
|
755
|
+
for (const scene of sceneData.scenes) {
|
|
756
|
+
const pos = scenePositions[scene.path];
|
|
757
|
+
if (!pos)
|
|
758
|
+
continue;
|
|
759
|
+
if (wx >= pos.x && wx <= pos.x + SCENE_CARD_W &&
|
|
760
|
+
wy >= pos.y && wy <= pos.y + SCENE_CARD_H) {
|
|
761
|
+
return { type: 'sceneCard', scene, scenePath: scene.path };
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
export { SCENE_CARD_W, SCENE_CARD_H, SCENE_NODE_H };
|
|
768
|
+
export function roundRect(ctx, x, y, w, h, r) {
|
|
769
|
+
ctx.moveTo(x + r, y);
|
|
770
|
+
ctx.arcTo(x + w, y, x + w, y + h, r);
|
|
771
|
+
ctx.arcTo(x + w, y + h, x, y + h, r);
|
|
772
|
+
ctx.arcTo(x, y + h, x, y, r);
|
|
773
|
+
ctx.arcTo(x, y, x + w, y, r);
|
|
774
|
+
}
|
|
775
|
+
export function hitTest(wx, wy) {
|
|
776
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
777
|
+
const n = nodes[i];
|
|
778
|
+
if (n.categoryVisible === false)
|
|
779
|
+
continue;
|
|
780
|
+
// Skip hidden nodes during search
|
|
781
|
+
if (searchTerm && n.visible === false)
|
|
782
|
+
continue;
|
|
783
|
+
if (wx >= n.x - NODE_W / 2 && wx <= n.x + NODE_W / 2 &&
|
|
784
|
+
wy >= n.y - NODE_H / 2 && wy <= n.y + NODE_H / 2)
|
|
785
|
+
return n;
|
|
786
|
+
}
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
export function groupBoxHitTest(wx, wy) {
|
|
790
|
+
if (!window.__categoryGroupBoxes)
|
|
791
|
+
return null;
|
|
792
|
+
for (const box of window.__categoryGroupBoxes) {
|
|
793
|
+
if (wx >= box.x && wx <= box.x + box.w &&
|
|
794
|
+
wy >= box.y && wy <= box.y + box.h) {
|
|
795
|
+
return box;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
export function centerOnNodes(nodeList) {
|
|
801
|
+
if (!nodeList || nodeList.length === 0)
|
|
802
|
+
return;
|
|
803
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
804
|
+
nodeList.forEach(n => {
|
|
805
|
+
minX = Math.min(minX, n.x);
|
|
806
|
+
maxX = Math.max(maxX, n.x);
|
|
807
|
+
minY = Math.min(minY, n.y);
|
|
808
|
+
maxY = Math.max(maxY, n.y);
|
|
809
|
+
});
|
|
810
|
+
camera.x = (minX + maxX) / 2;
|
|
811
|
+
camera.y = (minY + maxY) / 2;
|
|
812
|
+
updateZoomIndicator();
|
|
813
|
+
}
|
|
814
|
+
export function fitToView(nodeList) {
|
|
815
|
+
if (!nodeList || nodeList.length === 0)
|
|
816
|
+
return;
|
|
817
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
818
|
+
nodeList.forEach(n => {
|
|
819
|
+
minX = Math.min(minX, n.x);
|
|
820
|
+
maxX = Math.max(maxX, n.x);
|
|
821
|
+
minY = Math.min(minY, n.y);
|
|
822
|
+
maxY = Math.max(maxY, n.y);
|
|
823
|
+
});
|
|
824
|
+
camera.x = (minX + maxX) / 2;
|
|
825
|
+
camera.y = (minY + maxY) / 2;
|
|
826
|
+
const spanX = (maxX - minX) + NODE_W * 2;
|
|
827
|
+
const spanY = (maxY - minY) + NODE_H * 2;
|
|
828
|
+
// Calculate zoom to fit all nodes, but cap at 100% (1.0) to avoid zooming in too much
|
|
829
|
+
camera.zoom = Math.min(1.0, W / spanX, H / spanY) * 0.9;
|
|
830
|
+
// Don't change defaultZoom - keep it at 1 (100%) so reset always goes to 100%
|
|
831
|
+
updateZoomIndicator();
|
|
832
|
+
}
|