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,814 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event handlers for mouse, keyboard, and search
|
|
3
|
+
*/
|
|
4
|
+
import { nodes, edges, camera, W, H, defaultZoom, dragging, setDragging, hoveredNode, setHoveredNode, searchTerm, setSearchTerm, currentView, expandedScene, expandedSceneHierarchy, sceneData, setExpandedScene, setExpandedSceneHierarchy, setSelectedSceneNode, setHoveredSceneNode, selectedSceneNode, scenePositions, setScenePosition, categories, activeCategories, toggleCategory, setAllCategories, categoryGroupMode, setCategoryGroupMode, changesVisible, setChangesVisible, gitChangeSummary, changesFilter, setChangesFilter } from './state.js';
|
|
5
|
+
import { getCanvas, screenToWorld, hitTest, groupBoxHitTest, draw, resize, updateZoomIndicator, centerOnNodes, savePositions, sceneHitTest, SCENE_CARD_W, SCENE_CARD_H, clearPositions, fitToView } from './canvas.js';
|
|
6
|
+
import { initLayout, initGroupedLayout } from './layout.js';
|
|
7
|
+
import { openPanel, closePanel, openSceneNodePanel, closeSceneNodePanel } from './panel.js';
|
|
8
|
+
import { sendCommand } from './websocket.js';
|
|
9
|
+
const DRAG_THRESHOLD = 5; // pixels - minimum movement to count as drag
|
|
10
|
+
export function initEvents() {
|
|
11
|
+
const canvas = getCanvas();
|
|
12
|
+
const searchInput = document.getElementById('search');
|
|
13
|
+
const statsEl = document.getElementById('stats');
|
|
14
|
+
// Mouse events
|
|
15
|
+
canvas.addEventListener('mousedown', (e) => {
|
|
16
|
+
const w = screenToWorld(e.clientX, e.clientY);
|
|
17
|
+
if (currentView === 'scenes') {
|
|
18
|
+
handleSceneMouseDown(e, w);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
handleScriptsMouseDown(e, w);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
canvas.addEventListener('mousemove', (e) => {
|
|
25
|
+
if (currentView === 'scenes') {
|
|
26
|
+
handleSceneMouseMove(e);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
handleScriptsMouseMove(e);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
canvas.addEventListener('mouseup', (e) => {
|
|
33
|
+
if (currentView === 'scenes') {
|
|
34
|
+
handleSceneMouseUp(e);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
handleScriptsMouseUp(e);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
// Prevent click from also opening panel (mouseup already handles it)
|
|
41
|
+
canvas.addEventListener('click', (e) => {
|
|
42
|
+
// Only handle clicks on empty space (not nodes) - nodes are handled by mouseup
|
|
43
|
+
});
|
|
44
|
+
canvas.addEventListener('wheel', (e) => {
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
// Smaller zoom increments for finer control
|
|
47
|
+
const zoomFactor = e.deltaY > 0 ? 0.95 : 1.05;
|
|
48
|
+
const newZoom = Math.max(0.1, Math.min(5, camera.zoom * zoomFactor));
|
|
49
|
+
const wx = (e.clientX - W / 2) / camera.zoom + camera.x;
|
|
50
|
+
const wy = (e.clientY - H / 2) / camera.zoom + camera.y;
|
|
51
|
+
camera.zoom = newZoom;
|
|
52
|
+
camera.x = wx - (e.clientX - W / 2) / camera.zoom;
|
|
53
|
+
camera.y = wy - (e.clientY - H / 2) / camera.zoom;
|
|
54
|
+
updateZoomIndicator();
|
|
55
|
+
draw();
|
|
56
|
+
}, { passive: false });
|
|
57
|
+
// Double-click to rename
|
|
58
|
+
canvas.addEventListener('dblclick', (e) => {
|
|
59
|
+
if (currentView === 'scenes' && expandedScene) {
|
|
60
|
+
const w = screenToWorld(e.clientX, e.clientY);
|
|
61
|
+
const hit = sceneHitTest(w.x, w.y);
|
|
62
|
+
if (hit && hit.type === 'sceneNode') {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
startInlineRename(e.clientX, e.clientY, hit.node, hit.scenePath);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
// Right-click context menu
|
|
69
|
+
canvas.addEventListener('contextmenu', (e) => {
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
if (currentView === 'scenes' && expandedScene) {
|
|
72
|
+
const w = screenToWorld(e.clientX, e.clientY);
|
|
73
|
+
const hit = sceneHitTest(w.x, w.y);
|
|
74
|
+
if (hit && hit.type === 'sceneNode') {
|
|
75
|
+
showSceneContextMenu(e.clientX, e.clientY, hit.node, hit.scenePath);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Hide scene context menu if clicking elsewhere
|
|
80
|
+
hideSceneContextMenu();
|
|
81
|
+
});
|
|
82
|
+
// Close context menus when clicking elsewhere
|
|
83
|
+
document.addEventListener('click', (e) => {
|
|
84
|
+
if (!e.target.closest('#scene-context-menu')) {
|
|
85
|
+
hideSceneContextMenu();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
// Search
|
|
89
|
+
searchInput.addEventListener('input', () => {
|
|
90
|
+
const term = searchInput.value.toLowerCase().trim();
|
|
91
|
+
setSearchTerm(term);
|
|
92
|
+
if (currentView === 'scripts') {
|
|
93
|
+
nodes.forEach(n => {
|
|
94
|
+
if (!term) {
|
|
95
|
+
n.highlighted = true;
|
|
96
|
+
n.visible = true;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const matches = n.filename.toLowerCase().includes(term) ||
|
|
100
|
+
(n.class_name && n.class_name.toLowerCase().includes(term)) ||
|
|
101
|
+
(n.description && n.description.toLowerCase().includes(term)) ||
|
|
102
|
+
(n.path && n.path.toLowerCase().includes(term));
|
|
103
|
+
n.highlighted = matches;
|
|
104
|
+
n.visible = matches;
|
|
105
|
+
});
|
|
106
|
+
const matchingNodes = nodes.filter(n => n.highlighted);
|
|
107
|
+
const count = matchingNodes.length;
|
|
108
|
+
statsEl.textContent = term
|
|
109
|
+
? `${count}/${nodes.length}`
|
|
110
|
+
: `${nodes.length} scripts · ${edges.length} connections`;
|
|
111
|
+
// If there are matching results, center the view on them
|
|
112
|
+
if (term && matchingNodes.length > 0) {
|
|
113
|
+
centerOnNodes(matchingNodes);
|
|
114
|
+
// Adjust zoom if needed to fit all matching nodes
|
|
115
|
+
if (matchingNodes.length === 1) {
|
|
116
|
+
camera.zoom = Math.max(defaultZoom, 1);
|
|
117
|
+
}
|
|
118
|
+
updateZoomIndicator();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Scene search
|
|
122
|
+
if (currentView === 'scenes') {
|
|
123
|
+
if (expandedScene && expandedSceneHierarchy) {
|
|
124
|
+
// Search within expanded scene - highlight matching nodes
|
|
125
|
+
const matchingPaths = [];
|
|
126
|
+
function searchNode(node) {
|
|
127
|
+
const matches = !term ||
|
|
128
|
+
node.name.toLowerCase().includes(term) ||
|
|
129
|
+
(node.type && node.type.toLowerCase().includes(term));
|
|
130
|
+
node.highlighted = matches;
|
|
131
|
+
if (matches && term)
|
|
132
|
+
matchingPaths.push(node.path);
|
|
133
|
+
if (node.children) {
|
|
134
|
+
for (const child of node.children) {
|
|
135
|
+
searchNode(child);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
searchNode(expandedSceneHierarchy);
|
|
140
|
+
const totalNodes = countNodes(expandedSceneHierarchy);
|
|
141
|
+
statsEl.textContent = term
|
|
142
|
+
? `${matchingPaths.length}/${totalNodes} nodes`
|
|
143
|
+
: `${totalNodes} nodes`;
|
|
144
|
+
}
|
|
145
|
+
else if (sceneData && sceneData.scenes) {
|
|
146
|
+
// Search in scene overview
|
|
147
|
+
let matchCount = 0;
|
|
148
|
+
for (const scene of sceneData.scenes) {
|
|
149
|
+
const sceneName = scene.name || scene.path.split('/').pop().replace('.tscn', '');
|
|
150
|
+
scene.highlighted = !term ||
|
|
151
|
+
sceneName.toLowerCase().includes(term) ||
|
|
152
|
+
(scene.root_type && scene.root_type.toLowerCase().includes(term));
|
|
153
|
+
if (scene.highlighted)
|
|
154
|
+
matchCount++;
|
|
155
|
+
}
|
|
156
|
+
statsEl.textContent = term
|
|
157
|
+
? `${matchCount}/${sceneData.scenes.length} scenes`
|
|
158
|
+
: `${sceneData.scenes.length} scenes`;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
draw();
|
|
162
|
+
});
|
|
163
|
+
function countNodes(node) {
|
|
164
|
+
let count = 1;
|
|
165
|
+
if (node.children) {
|
|
166
|
+
for (const child of node.children) {
|
|
167
|
+
count += countNodes(child);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return count;
|
|
171
|
+
}
|
|
172
|
+
// Keyboard shortcuts
|
|
173
|
+
document.addEventListener('keydown', (e) => {
|
|
174
|
+
if (e.key === 'Escape') {
|
|
175
|
+
// Also close context menus
|
|
176
|
+
hideSceneContextMenu();
|
|
177
|
+
if (currentView === 'scenes') {
|
|
178
|
+
if (selectedSceneNode) {
|
|
179
|
+
setSelectedSceneNode(null);
|
|
180
|
+
closeSceneNodePanel();
|
|
181
|
+
draw();
|
|
182
|
+
}
|
|
183
|
+
else if (expandedScene) {
|
|
184
|
+
goBackToSceneOverview();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
closePanel();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Focus search with /
|
|
192
|
+
if (e.key === '/' && document.activeElement !== searchInput) {
|
|
193
|
+
e.preventDefault();
|
|
194
|
+
searchInput.focus();
|
|
195
|
+
}
|
|
196
|
+
// Delete key to delete selected scene node
|
|
197
|
+
if ((e.key === 'Delete' || e.key === 'Backspace') && currentView === 'scenes' && selectedSceneNode && !e.target.matches('input, textarea')) {
|
|
198
|
+
e.preventDefault();
|
|
199
|
+
sceneNodeAction('delete');
|
|
200
|
+
}
|
|
201
|
+
// Enter to open properties panel for selected node
|
|
202
|
+
if (e.key === 'Enter' && currentView === 'scenes' && expandedScene && !e.target.matches('input, textarea')) {
|
|
203
|
+
// If no node selected, select root
|
|
204
|
+
// If node selected, this could toggle the panel (already handled by re-click)
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
// Window resize
|
|
208
|
+
window.addEventListener('resize', () => {
|
|
209
|
+
resize();
|
|
210
|
+
draw();
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
// ---- Scripts view event handlers ----
|
|
214
|
+
function handleScriptsMouseDown(e, w) {
|
|
215
|
+
const canvas = getCanvas();
|
|
216
|
+
const hit = hitTest(w.x, w.y);
|
|
217
|
+
if (hit && e.button === 0) {
|
|
218
|
+
setDragging({
|
|
219
|
+
type: 'node',
|
|
220
|
+
node: hit,
|
|
221
|
+
offX: hit.x - w.x,
|
|
222
|
+
offY: hit.y - w.y,
|
|
223
|
+
startScreenX: e.clientX,
|
|
224
|
+
startScreenY: e.clientY,
|
|
225
|
+
moved: false
|
|
226
|
+
});
|
|
227
|
+
canvas.classList.add('dragging');
|
|
228
|
+
}
|
|
229
|
+
else if (categoryGroupMode === 'grouped' && e.button === 0) {
|
|
230
|
+
const box = groupBoxHitTest(w.x, w.y);
|
|
231
|
+
if (box) {
|
|
232
|
+
// Collect all nodes belonging to this category
|
|
233
|
+
const groupNodes = nodes.filter(n => n.category === box.category);
|
|
234
|
+
setDragging({
|
|
235
|
+
type: 'group',
|
|
236
|
+
box: box,
|
|
237
|
+
groupNodes: groupNodes,
|
|
238
|
+
offX: box.x - w.x,
|
|
239
|
+
offY: box.y - w.y,
|
|
240
|
+
startScreenX: e.clientX,
|
|
241
|
+
startScreenY: e.clientY,
|
|
242
|
+
moved: false
|
|
243
|
+
});
|
|
244
|
+
canvas.classList.add('dragging');
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
setDragging({ type: 'pan', startX: e.clientX, startY: e.clientY, camX: camera.x, camY: camera.y });
|
|
248
|
+
canvas.classList.add('dragging');
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
setDragging({ type: 'pan', startX: e.clientX, startY: e.clientY, camX: camera.x, camY: camera.y });
|
|
253
|
+
canvas.classList.add('dragging');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function handleScriptsMouseMove(e) {
|
|
257
|
+
const canvas = getCanvas();
|
|
258
|
+
if (dragging) {
|
|
259
|
+
if (dragging.type === 'node') {
|
|
260
|
+
const w = screenToWorld(e.clientX, e.clientY);
|
|
261
|
+
dragging.node.x = w.x + dragging.offX;
|
|
262
|
+
dragging.node.y = w.y + dragging.offY;
|
|
263
|
+
// Check if moved past threshold
|
|
264
|
+
const dx = Math.abs(e.clientX - dragging.startScreenX);
|
|
265
|
+
const dy = Math.abs(e.clientY - dragging.startScreenY);
|
|
266
|
+
if (dx > DRAG_THRESHOLD || dy > DRAG_THRESHOLD) {
|
|
267
|
+
dragging.moved = true;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
else if (dragging.type === 'group') {
|
|
271
|
+
const w = screenToWorld(e.clientX, e.clientY);
|
|
272
|
+
const newBoxX = w.x + dragging.offX;
|
|
273
|
+
const newBoxY = w.y + dragging.offY;
|
|
274
|
+
const deltaX = newBoxX - dragging.box.x;
|
|
275
|
+
const deltaY = newBoxY - dragging.box.y;
|
|
276
|
+
// Move the group box
|
|
277
|
+
dragging.box.x = newBoxX;
|
|
278
|
+
dragging.box.y = newBoxY;
|
|
279
|
+
// Move all nodes in this category by the same delta
|
|
280
|
+
for (const n of dragging.groupNodes) {
|
|
281
|
+
n.x += deltaX;
|
|
282
|
+
n.y += deltaY;
|
|
283
|
+
}
|
|
284
|
+
const dx = Math.abs(e.clientX - dragging.startScreenX);
|
|
285
|
+
const dy = Math.abs(e.clientY - dragging.startScreenY);
|
|
286
|
+
if (dx > DRAG_THRESHOLD || dy > DRAG_THRESHOLD) {
|
|
287
|
+
dragging.moved = true;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
const dx = (e.clientX - dragging.startX) / camera.zoom;
|
|
292
|
+
const dy = (e.clientY - dragging.startY) / camera.zoom;
|
|
293
|
+
camera.x = dragging.camX - dx;
|
|
294
|
+
camera.y = dragging.camY - dy;
|
|
295
|
+
}
|
|
296
|
+
draw();
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
const w = screenToWorld(e.clientX, e.clientY);
|
|
300
|
+
const prev = hoveredNode;
|
|
301
|
+
setHoveredNode(hitTest(w.x, w.y));
|
|
302
|
+
if (hoveredNode !== prev) {
|
|
303
|
+
canvas.style.cursor = hoveredNode ? 'pointer' : 'grab';
|
|
304
|
+
draw();
|
|
305
|
+
}
|
|
306
|
+
else if (!hoveredNode && categoryGroupMode === 'grouped') {
|
|
307
|
+
const box = groupBoxHitTest(w.x, w.y);
|
|
308
|
+
canvas.style.cursor = box ? 'grab' : 'grab';
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function handleScriptsMouseUp(e) {
|
|
313
|
+
const canvas = getCanvas();
|
|
314
|
+
if (dragging && dragging.type === 'node') {
|
|
315
|
+
if (dragging.moved) {
|
|
316
|
+
savePositions();
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
openPanel(dragging.node);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
else if (dragging && dragging.type === 'group') {
|
|
323
|
+
if (dragging.moved) {
|
|
324
|
+
savePositions();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
canvas.classList.remove('dragging');
|
|
328
|
+
setDragging(null);
|
|
329
|
+
}
|
|
330
|
+
// ---- Scene view event handlers ----
|
|
331
|
+
function handleSceneMouseDown(e, w) {
|
|
332
|
+
const canvas = getCanvas();
|
|
333
|
+
const hit = sceneHitTest(w.x, w.y);
|
|
334
|
+
if (hit && e.button === 0) {
|
|
335
|
+
if (hit.type === 'sceneCard') {
|
|
336
|
+
// Scene card - prepare for possible drag or click
|
|
337
|
+
const pos = scenePositions[hit.scenePath];
|
|
338
|
+
setDragging({
|
|
339
|
+
type: 'sceneCard',
|
|
340
|
+
scene: hit.scene,
|
|
341
|
+
scenePath: hit.scenePath,
|
|
342
|
+
offX: pos.x - w.x,
|
|
343
|
+
offY: pos.y - w.y,
|
|
344
|
+
startScreenX: e.clientX,
|
|
345
|
+
startScreenY: e.clientY,
|
|
346
|
+
moved: false
|
|
347
|
+
});
|
|
348
|
+
canvas.classList.add('dragging');
|
|
349
|
+
}
|
|
350
|
+
else if (hit.type === 'sceneNode') {
|
|
351
|
+
// Scene node in expanded view - click to select
|
|
352
|
+
setDragging({
|
|
353
|
+
type: 'sceneNode',
|
|
354
|
+
node: hit.node,
|
|
355
|
+
scenePath: hit.scenePath,
|
|
356
|
+
startScreenX: e.clientX,
|
|
357
|
+
startScreenY: e.clientY,
|
|
358
|
+
moved: false
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
setDragging({ type: 'pan', startX: e.clientX, startY: e.clientY, camX: camera.x, camY: camera.y });
|
|
364
|
+
canvas.classList.add('dragging');
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function handleSceneMouseMove(e) {
|
|
368
|
+
const canvas = getCanvas();
|
|
369
|
+
if (dragging) {
|
|
370
|
+
if (dragging.type === 'sceneCard') {
|
|
371
|
+
const w = screenToWorld(e.clientX, e.clientY);
|
|
372
|
+
const newX = w.x + dragging.offX;
|
|
373
|
+
const newY = w.y + dragging.offY;
|
|
374
|
+
setScenePosition(dragging.scenePath, newX, newY);
|
|
375
|
+
// Check if moved past threshold
|
|
376
|
+
const dx = Math.abs(e.clientX - dragging.startScreenX);
|
|
377
|
+
const dy = Math.abs(e.clientY - dragging.startScreenY);
|
|
378
|
+
if (dx > DRAG_THRESHOLD || dy > DRAG_THRESHOLD) {
|
|
379
|
+
dragging.moved = true;
|
|
380
|
+
}
|
|
381
|
+
draw();
|
|
382
|
+
}
|
|
383
|
+
else if (dragging.type === 'pan') {
|
|
384
|
+
const dx = (e.clientX - dragging.startX) / camera.zoom;
|
|
385
|
+
const dy = (e.clientY - dragging.startY) / camera.zoom;
|
|
386
|
+
camera.x = dragging.camX - dx;
|
|
387
|
+
camera.y = dragging.camY - dy;
|
|
388
|
+
draw();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
const w = screenToWorld(e.clientX, e.clientY);
|
|
393
|
+
const hit = sceneHitTest(w.x, w.y);
|
|
394
|
+
if (hit) {
|
|
395
|
+
if (hit.type === 'sceneCard') {
|
|
396
|
+
setHoveredSceneNode({ scenePath: hit.scenePath, nodePath: null });
|
|
397
|
+
}
|
|
398
|
+
else if (hit.type === 'sceneNode') {
|
|
399
|
+
setHoveredSceneNode({ scenePath: hit.scenePath, nodePath: hit.node.path });
|
|
400
|
+
}
|
|
401
|
+
canvas.style.cursor = 'pointer';
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
setHoveredSceneNode(null);
|
|
405
|
+
canvas.style.cursor = 'grab';
|
|
406
|
+
}
|
|
407
|
+
draw();
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function handleSceneMouseUp(e) {
|
|
411
|
+
const canvas = getCanvas();
|
|
412
|
+
if (dragging) {
|
|
413
|
+
if (dragging.type === 'sceneCard' && !dragging.moved) {
|
|
414
|
+
// Scene card was clicked - expand the scene
|
|
415
|
+
expandScene(dragging.scenePath);
|
|
416
|
+
}
|
|
417
|
+
else if (dragging.type === 'sceneNode' && !dragging.moved) {
|
|
418
|
+
// Scene node was clicked - select it and open properties panel
|
|
419
|
+
selectSceneNode(dragging.node, dragging.scenePath);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
canvas.classList.remove('dragging');
|
|
423
|
+
setDragging(null);
|
|
424
|
+
}
|
|
425
|
+
// ---- Scene expansion and navigation ----
|
|
426
|
+
async function expandScene(scenePath) {
|
|
427
|
+
console.log('Expanding scene:', scenePath);
|
|
428
|
+
try {
|
|
429
|
+
// Fetch the scene hierarchy
|
|
430
|
+
const result = await sendCommand('get_scene_hierarchy', { scene_path: scenePath });
|
|
431
|
+
if (result.ok) {
|
|
432
|
+
setExpandedScene(scenePath);
|
|
433
|
+
setExpandedSceneHierarchy(result.hierarchy);
|
|
434
|
+
// Reset camera position but keep user's zoom level
|
|
435
|
+
camera.x = 0;
|
|
436
|
+
camera.y = 100;
|
|
437
|
+
// Don't change zoom - keep user's preference
|
|
438
|
+
// Update UI
|
|
439
|
+
updateSceneBackButton(true, scenePath);
|
|
440
|
+
draw();
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
console.error('Failed to get scene hierarchy:', result.error);
|
|
444
|
+
alert('Failed to load scene: ' + (result.error || 'Unknown error'));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
catch (err) {
|
|
448
|
+
console.error('Failed to expand scene:', err);
|
|
449
|
+
alert('Failed to load scene: ' + err.message);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
async function selectSceneNode(node, scenePath) {
|
|
453
|
+
console.log('Selected scene node:', node.name, 'in', scenePath);
|
|
454
|
+
// If clicking the same node that's already selected, close the panel
|
|
455
|
+
if (selectedSceneNode && selectedSceneNode.path === node.path) {
|
|
456
|
+
setSelectedSceneNode(null);
|
|
457
|
+
closeSceneNodePanel();
|
|
458
|
+
draw();
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
setSelectedSceneNode(node);
|
|
462
|
+
// Open the properties panel for this node
|
|
463
|
+
await openSceneNodePanel(scenePath, node);
|
|
464
|
+
draw();
|
|
465
|
+
}
|
|
466
|
+
export function goBackToSceneOverview() {
|
|
467
|
+
setExpandedScene(null);
|
|
468
|
+
setExpandedSceneHierarchy(null);
|
|
469
|
+
setSelectedSceneNode(null);
|
|
470
|
+
setHoveredSceneNode(null);
|
|
471
|
+
closeSceneNodePanel();
|
|
472
|
+
updateSceneBackButton(false);
|
|
473
|
+
draw();
|
|
474
|
+
}
|
|
475
|
+
function updateSceneBackButton(show, scenePath = '') {
|
|
476
|
+
const backBtn = document.getElementById('scene-back-btn');
|
|
477
|
+
const legend = document.getElementById('legend');
|
|
478
|
+
if (backBtn) {
|
|
479
|
+
backBtn.style.display = show ? 'flex' : 'none';
|
|
480
|
+
if (show) {
|
|
481
|
+
const sceneName = scenePath.split('/').pop().replace('.tscn', '');
|
|
482
|
+
backBtn.querySelector('.scene-name').textContent = sceneName;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// Hide legend when in expanded scene view (it's not relevant there)
|
|
486
|
+
if (legend) {
|
|
487
|
+
legend.classList.toggle('hidden', show);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
// Expose for global access
|
|
491
|
+
window.goBackToSceneOverview = goBackToSceneOverview;
|
|
492
|
+
window.expandSceneFromPanel = expandScene;
|
|
493
|
+
export function buildChangesPanel() {
|
|
494
|
+
const modifiedCountEl = document.getElementById('changes-modified-count');
|
|
495
|
+
const addedCountEl = document.getElementById('changes-added-count');
|
|
496
|
+
const untrackedCountEl = document.getElementById('changes-untracked-count');
|
|
497
|
+
const newCountEl = document.getElementById('changes-new-count');
|
|
498
|
+
const totalEl = document.getElementById('changes-total');
|
|
499
|
+
const toggleInput = document.getElementById('changes-toggle-input');
|
|
500
|
+
if (modifiedCountEl)
|
|
501
|
+
modifiedCountEl.textContent = String(gitChangeSummary.modified);
|
|
502
|
+
if (addedCountEl)
|
|
503
|
+
addedCountEl.textContent = String(gitChangeSummary.added);
|
|
504
|
+
if (untrackedCountEl)
|
|
505
|
+
untrackedCountEl.textContent = String(gitChangeSummary.untracked);
|
|
506
|
+
if (newCountEl)
|
|
507
|
+
newCountEl.textContent = String(gitChangeSummary.new || 0);
|
|
508
|
+
const totalChanges = gitChangeSummary.modified + gitChangeSummary.added + gitChangeSummary.untracked;
|
|
509
|
+
if (totalEl)
|
|
510
|
+
totalEl.textContent = `${totalChanges} changed files`;
|
|
511
|
+
if (toggleInput) {
|
|
512
|
+
toggleInput.checked = changesVisible;
|
|
513
|
+
}
|
|
514
|
+
const rows = document.querySelectorAll('#changes-stats .changes-stat-row');
|
|
515
|
+
rows.forEach((row) => {
|
|
516
|
+
row.classList.toggle('active', row.dataset.type === changesFilter);
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
// ---- Category filter functions ----
|
|
520
|
+
export function buildCategoryList() {
|
|
521
|
+
const catList = document.getElementById('cat-list');
|
|
522
|
+
if (!catList || !categories.length)
|
|
523
|
+
return;
|
|
524
|
+
catList.innerHTML = '';
|
|
525
|
+
categories.forEach(cat => {
|
|
526
|
+
const item = document.createElement('div');
|
|
527
|
+
item.className = 'cat-item' + (activeCategories.has(cat.id) ? '' : ' inactive');
|
|
528
|
+
item.dataset.catId = cat.id;
|
|
529
|
+
item.innerHTML = `
|
|
530
|
+
<div class="cat-dot" style="background:${cat.color}"></div>
|
|
531
|
+
<span class="cat-label">${cat.label}</span>
|
|
532
|
+
<span class="cat-count">${cat.count}</span>
|
|
533
|
+
`;
|
|
534
|
+
item.addEventListener('click', () => {
|
|
535
|
+
toggleCategory(cat.id);
|
|
536
|
+
item.classList.toggle('inactive', !activeCategories.has(cat.id));
|
|
537
|
+
draw();
|
|
538
|
+
});
|
|
539
|
+
catList.appendChild(item);
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
window.toggleGroupMode = function () {
|
|
543
|
+
const btn = document.getElementById('cat-mode-btn');
|
|
544
|
+
if (categoryGroupMode === 'free') {
|
|
545
|
+
setCategoryGroupMode('grouped');
|
|
546
|
+
btn.classList.add('active');
|
|
547
|
+
clearPositions();
|
|
548
|
+
initGroupedLayout();
|
|
549
|
+
fitToView(nodes);
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
setCategoryGroupMode('free');
|
|
553
|
+
btn.classList.remove('active');
|
|
554
|
+
window.__categoryGroupBoxes = null;
|
|
555
|
+
clearPositions();
|
|
556
|
+
initLayout();
|
|
557
|
+
fitToView(nodes);
|
|
558
|
+
}
|
|
559
|
+
draw();
|
|
560
|
+
};
|
|
561
|
+
window.toggleAllCategories = function () {
|
|
562
|
+
const allActive = activeCategories.size === categories.length;
|
|
563
|
+
setAllCategories(!allActive);
|
|
564
|
+
buildCategoryList();
|
|
565
|
+
draw();
|
|
566
|
+
};
|
|
567
|
+
window.toggleChangesVisible = function () {
|
|
568
|
+
const toggleInput = document.getElementById('changes-toggle-input');
|
|
569
|
+
const checked = !!(toggleInput && toggleInput.checked);
|
|
570
|
+
setChangesVisible(checked);
|
|
571
|
+
draw();
|
|
572
|
+
};
|
|
573
|
+
window.filterByChangeType = function (type) {
|
|
574
|
+
const nextFilter = changesFilter === type ? null : type;
|
|
575
|
+
setChangesFilter(nextFilter);
|
|
576
|
+
const matchingNodes = [];
|
|
577
|
+
nodes.forEach((node) => {
|
|
578
|
+
if (!nextFilter) {
|
|
579
|
+
node.visible = true;
|
|
580
|
+
node.highlighted = true;
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
const matches = type === 'new' ? node.isNew : node.gitStatus === nextFilter;
|
|
584
|
+
node.visible = matches;
|
|
585
|
+
node.highlighted = matches;
|
|
586
|
+
if (matches)
|
|
587
|
+
matchingNodes.push(node);
|
|
588
|
+
});
|
|
589
|
+
if (nextFilter && matchingNodes.length > 0) {
|
|
590
|
+
centerOnNodes(matchingNodes);
|
|
591
|
+
}
|
|
592
|
+
buildChangesPanel();
|
|
593
|
+
draw();
|
|
594
|
+
};
|
|
595
|
+
export function updateStats() {
|
|
596
|
+
const statsEl = document.getElementById('stats');
|
|
597
|
+
if (currentView === 'scripts') {
|
|
598
|
+
statsEl.textContent = `${nodes.length} scripts · ${edges.length} connections`;
|
|
599
|
+
}
|
|
600
|
+
else if (sceneData && sceneData.scenes) {
|
|
601
|
+
statsEl.textContent = `${sceneData.scenes.length} scenes`;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
// ---- Scene Node Context Menu ----
|
|
605
|
+
let contextMenuNode = null;
|
|
606
|
+
let contextMenuScenePath = null;
|
|
607
|
+
function showSceneContextMenu(x, y, node, scenePath) {
|
|
608
|
+
const menu = document.getElementById('scene-context-menu');
|
|
609
|
+
contextMenuNode = node;
|
|
610
|
+
contextMenuScenePath = scenePath;
|
|
611
|
+
menu.style.left = x + 'px';
|
|
612
|
+
menu.style.top = y + 'px';
|
|
613
|
+
menu.classList.add('visible');
|
|
614
|
+
}
|
|
615
|
+
function hideSceneContextMenu() {
|
|
616
|
+
const menu = document.getElementById('scene-context-menu');
|
|
617
|
+
menu.classList.remove('visible');
|
|
618
|
+
contextMenuNode = null;
|
|
619
|
+
contextMenuScenePath = null;
|
|
620
|
+
}
|
|
621
|
+
async function sceneNodeAction(action) {
|
|
622
|
+
// Save node info BEFORE hiding menu (which clears these variables)
|
|
623
|
+
const node = contextMenuNode;
|
|
624
|
+
const scenePath = contextMenuScenePath;
|
|
625
|
+
hideSceneContextMenu();
|
|
626
|
+
if (!node || !scenePath)
|
|
627
|
+
return;
|
|
628
|
+
try {
|
|
629
|
+
switch (action) {
|
|
630
|
+
case 'add_child': {
|
|
631
|
+
const nodeType = prompt('Enter node type (e.g., Node2D, Sprite2D, Label):', 'Node2D');
|
|
632
|
+
if (!nodeType)
|
|
633
|
+
return;
|
|
634
|
+
const nodeName = prompt('Enter node name:', 'NewNode');
|
|
635
|
+
if (!nodeName)
|
|
636
|
+
return;
|
|
637
|
+
const result = await sendCommand('add_node', {
|
|
638
|
+
scene_path: scenePath,
|
|
639
|
+
parent_path: node.path,
|
|
640
|
+
node_type: nodeType,
|
|
641
|
+
node_name: nodeName
|
|
642
|
+
});
|
|
643
|
+
if (result.ok) {
|
|
644
|
+
await refreshExpandedScene(scenePath);
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
alert('Failed to add node: ' + (result.error || 'Unknown error'));
|
|
648
|
+
}
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
case 'rename': {
|
|
652
|
+
const newName = prompt('Enter new name:', node.name);
|
|
653
|
+
if (!newName || newName === node.name)
|
|
654
|
+
return;
|
|
655
|
+
const result = await sendCommand('rename_node', {
|
|
656
|
+
scene_path: scenePath,
|
|
657
|
+
node_path: node.path,
|
|
658
|
+
new_name: newName
|
|
659
|
+
});
|
|
660
|
+
if (result.ok) {
|
|
661
|
+
await refreshExpandedScene(scenePath);
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
alert('Failed to rename: ' + (result.error || 'Unknown error'));
|
|
665
|
+
}
|
|
666
|
+
break;
|
|
667
|
+
}
|
|
668
|
+
case 'duplicate': {
|
|
669
|
+
const result = await sendCommand('duplicate_node', {
|
|
670
|
+
scene_path: scenePath,
|
|
671
|
+
node_path: node.path
|
|
672
|
+
});
|
|
673
|
+
if (result.ok) {
|
|
674
|
+
await refreshExpandedScene(scenePath);
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
alert('Failed to duplicate: ' + (result.error || 'Unknown error'));
|
|
678
|
+
}
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
case 'move_up': {
|
|
682
|
+
if (node.index === undefined || node.index <= 0) {
|
|
683
|
+
alert('Cannot move node up - already at top');
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const result = await sendCommand('reorder_node', {
|
|
687
|
+
scene_path: scenePath,
|
|
688
|
+
node_path: node.path,
|
|
689
|
+
new_index: node.index - 1
|
|
690
|
+
});
|
|
691
|
+
if (result.ok) {
|
|
692
|
+
await refreshExpandedScene(scenePath);
|
|
693
|
+
}
|
|
694
|
+
else {
|
|
695
|
+
alert('Failed to move: ' + (result.error || 'Unknown error'));
|
|
696
|
+
}
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
case 'move_down': {
|
|
700
|
+
const result = await sendCommand('reorder_node', {
|
|
701
|
+
scene_path: scenePath,
|
|
702
|
+
node_path: node.path,
|
|
703
|
+
new_index: (node.index || 0) + 1
|
|
704
|
+
});
|
|
705
|
+
if (result.ok) {
|
|
706
|
+
await refreshExpandedScene(scenePath);
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
alert('Failed to move: ' + (result.error || 'Unknown error'));
|
|
710
|
+
}
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
713
|
+
case 'delete': {
|
|
714
|
+
if (node.path === '.') {
|
|
715
|
+
alert('Cannot delete root node');
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
if (!confirm(`Delete node "${node.name}" and all its children?`))
|
|
719
|
+
return;
|
|
720
|
+
const result = await sendCommand('remove_node', {
|
|
721
|
+
scene_path: scenePath,
|
|
722
|
+
node_path: node.path
|
|
723
|
+
});
|
|
724
|
+
if (result.ok) {
|
|
725
|
+
closeSceneNodePanel();
|
|
726
|
+
setSelectedSceneNode(null);
|
|
727
|
+
await refreshExpandedScene(scenePath);
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
alert('Failed to delete: ' + (result.error || 'Unknown error'));
|
|
731
|
+
}
|
|
732
|
+
break;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
catch (err) {
|
|
737
|
+
console.error('Scene action failed:', err);
|
|
738
|
+
alert('Action failed: ' + err.message);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
async function refreshExpandedScene(scenePath) {
|
|
742
|
+
// Re-fetch the scene hierarchy
|
|
743
|
+
const result = await sendCommand('get_scene_hierarchy', { scene_path: scenePath });
|
|
744
|
+
if (result.ok) {
|
|
745
|
+
setExpandedSceneHierarchy(result.hierarchy);
|
|
746
|
+
draw();
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
// ---- Inline Rename ----
|
|
750
|
+
function startInlineRename(screenX, screenY, node, scenePath) {
|
|
751
|
+
// Create an input overlay at the node position
|
|
752
|
+
const existingInput = document.getElementById('inline-rename-input');
|
|
753
|
+
if (existingInput)
|
|
754
|
+
existingInput.remove();
|
|
755
|
+
const input = document.createElement('input');
|
|
756
|
+
input.id = 'inline-rename-input';
|
|
757
|
+
input.type = 'text';
|
|
758
|
+
input.value = node.name;
|
|
759
|
+
input.style.cssText = `
|
|
760
|
+
position: fixed;
|
|
761
|
+
left: ${screenX - 50}px;
|
|
762
|
+
top: ${screenY - 12}px;
|
|
763
|
+
width: 120px;
|
|
764
|
+
padding: 4px 8px;
|
|
765
|
+
font-size: 12px;
|
|
766
|
+
font-family: -apple-system, system-ui, sans-serif;
|
|
767
|
+
font-weight: 600;
|
|
768
|
+
background: #0f1014;
|
|
769
|
+
border: 2px solid #7aa2f7;
|
|
770
|
+
border-radius: 4px;
|
|
771
|
+
color: #f0f0f5;
|
|
772
|
+
z-index: 1000;
|
|
773
|
+
outline: none;
|
|
774
|
+
`;
|
|
775
|
+
document.body.appendChild(input);
|
|
776
|
+
input.focus();
|
|
777
|
+
input.select();
|
|
778
|
+
async function finishRename() {
|
|
779
|
+
const newName = input.value.trim();
|
|
780
|
+
input.remove();
|
|
781
|
+
if (newName && newName !== node.name) {
|
|
782
|
+
try {
|
|
783
|
+
const result = await sendCommand('rename_node', {
|
|
784
|
+
scene_path: scenePath,
|
|
785
|
+
node_path: node.path,
|
|
786
|
+
new_name: newName
|
|
787
|
+
});
|
|
788
|
+
if (result.ok) {
|
|
789
|
+
await refreshExpandedScene(scenePath);
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
alert('Failed to rename: ' + (result.error || 'Unknown error'));
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
catch (err) {
|
|
796
|
+
alert('Failed to rename: ' + err.message);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
input.addEventListener('blur', finishRename);
|
|
801
|
+
input.addEventListener('keydown', (e) => {
|
|
802
|
+
if (e.key === 'Enter') {
|
|
803
|
+
e.preventDefault();
|
|
804
|
+
input.blur();
|
|
805
|
+
}
|
|
806
|
+
else if (e.key === 'Escape') {
|
|
807
|
+
e.preventDefault();
|
|
808
|
+
input.value = node.name; // Reset to original
|
|
809
|
+
input.blur();
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
// Expose for global access
|
|
814
|
+
window.sceneNodeAction = sceneNodeAction;
|