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,1091 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detail panel, inline editing, function viewer, and section management
|
|
3
|
+
*/
|
|
4
|
+
import { nodes, edges, selectedNode, setSelectedNode, esc, selectedSceneNode, setSelectedSceneNode, sceneNodeProperties, setSceneNodeProperties, expandedScene, scriptToScenes } from './state.js';
|
|
5
|
+
import { sendCommand } from './websocket.js';
|
|
6
|
+
import { highlightGDScript } from './syntax.js';
|
|
7
|
+
import { draw } from './canvas.js';
|
|
8
|
+
let detailPanel;
|
|
9
|
+
let currentPanelMode = 'script'; // 'script' or 'sceneNode'
|
|
10
|
+
export function initPanel() {
|
|
11
|
+
detailPanel = document.getElementById('detail-panel');
|
|
12
|
+
initPanelResizing();
|
|
13
|
+
}
|
|
14
|
+
export function openPanel(node) {
|
|
15
|
+
setSelectedNode(node);
|
|
16
|
+
document.getElementById('panel-title').textContent = node.class_name || node.filename.replace('.gd', '');
|
|
17
|
+
document.getElementById('panel-path').textContent = node.path;
|
|
18
|
+
let html = '';
|
|
19
|
+
// Description
|
|
20
|
+
if (node.description) {
|
|
21
|
+
html += `<div class="desc-block">${esc(node.description)}</div>`;
|
|
22
|
+
}
|
|
23
|
+
// Meta badges
|
|
24
|
+
html += `<div class="meta-row">`;
|
|
25
|
+
html += `<div class="meta-badge"><span>${node.line_count}</span> lines</div>`;
|
|
26
|
+
html += `<div class="meta-badge">extends <span>${node.extends || 'Node'}</span></div>`;
|
|
27
|
+
if (node.class_name)
|
|
28
|
+
html += `<div class="meta-badge">class <span>${esc(node.class_name)}</span></div>`;
|
|
29
|
+
html += `</div>`;
|
|
30
|
+
// Scene usage (if this script is used in scenes)
|
|
31
|
+
const usedInScenes = scriptToScenes[node.path];
|
|
32
|
+
if (usedInScenes && usedInScenes.length > 0) {
|
|
33
|
+
html += `<div class="section scene-usage-section">`;
|
|
34
|
+
html += `<div class="section-header">Used in Scenes <span class="section-count">${usedInScenes.length}</span></div>`;
|
|
35
|
+
html += `<ul class="item-list scene-list">`;
|
|
36
|
+
for (const scene of usedInScenes) {
|
|
37
|
+
html += `<li class="scene-link" onclick="jumpToScene('${esc(scene.path)}')">`;
|
|
38
|
+
html += `<span class="scene-icon">📦</span>`;
|
|
39
|
+
html += `<span class="scene-name">${esc(scene.name)}</span>`;
|
|
40
|
+
html += `</li>`;
|
|
41
|
+
}
|
|
42
|
+
html += `</ul>`;
|
|
43
|
+
html += `</div>`;
|
|
44
|
+
}
|
|
45
|
+
// Variables - split into @export and regular
|
|
46
|
+
const exportVars = (node.variables || []).filter(v => v.exported);
|
|
47
|
+
const regularVars = (node.variables || []).filter(v => !v.exported);
|
|
48
|
+
// Exports section (always show for adding)
|
|
49
|
+
html += `<div class="section">`;
|
|
50
|
+
html += `<div class="section-header">Exports <span class="section-count">${exportVars.length}</span></div>`;
|
|
51
|
+
html += `<ul class="item-list" id="exports-list">`;
|
|
52
|
+
for (let vi = 0; vi < exportVars.length; vi++) {
|
|
53
|
+
const v = exportVars[vi];
|
|
54
|
+
html += `<li data-var-index="${vi}" data-exported="true">`;
|
|
55
|
+
html += `<span class="exp">@export</span> `;
|
|
56
|
+
html += `<span class="kw">var</span> `;
|
|
57
|
+
html += `<span class="editable var-name" contenteditable="true" data-field="name" data-original="${esc(v.name)}">${esc(v.name)}</span>`;
|
|
58
|
+
html += `<span class="ret">:</span> `;
|
|
59
|
+
html += `<span class="tp editable var-type" contenteditable="true" data-field="type" data-placeholder="Type" data-original="${esc(v.type || '')}">${esc(v.type || '')}</span>`;
|
|
60
|
+
html += ` <span class="ret">=</span> `;
|
|
61
|
+
html += `<span class="num editable var-default" contenteditable="true" data-field="default" data-placeholder="value" data-original="${esc(v.default || '')}">${esc(v.default || '')}</span>`;
|
|
62
|
+
html += `<span class="item-actions">`;
|
|
63
|
+
html += `<button class="delete" onclick="showDeleteUsages(${vi}, true, 'variable')" title="Delete">×</button>`;
|
|
64
|
+
html += `</span>`;
|
|
65
|
+
html += `</li>`;
|
|
66
|
+
}
|
|
67
|
+
html += `</ul>`;
|
|
68
|
+
html += `<div class="add-item-btn" onclick="addNewVariable(true)">`;
|
|
69
|
+
html += `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>`;
|
|
70
|
+
html += `Add export</div>`;
|
|
71
|
+
html += `</div>`;
|
|
72
|
+
// Variables section (always show for adding)
|
|
73
|
+
html += `<div class="section">`;
|
|
74
|
+
html += `<div class="section-header">Variables <span class="section-count">${regularVars.length}</span></div>`;
|
|
75
|
+
html += `<ul class="item-list" id="vars-list">`;
|
|
76
|
+
for (let vi = 0; vi < regularVars.length; vi++) {
|
|
77
|
+
const v = regularVars[vi];
|
|
78
|
+
html += `<li data-var-index="${vi}" data-exported="false" data-onready="${v.onready || false}">`;
|
|
79
|
+
if (v.onready) {
|
|
80
|
+
html += `<span class="onready-badge" onclick="toggleOnready(${vi}, false)" title="Click to toggle @onready">@onready</span>`;
|
|
81
|
+
}
|
|
82
|
+
html += `<span class="kw">var</span> `;
|
|
83
|
+
html += `<span class="editable var-name" contenteditable="true" data-field="name" data-original="${esc(v.name)}">${esc(v.name)}</span>`;
|
|
84
|
+
html += `<span class="ret">:</span> `;
|
|
85
|
+
html += `<span class="tp editable var-type" contenteditable="true" data-field="type" data-placeholder="Type" data-original="${esc(v.type || '')}">${esc(v.type || '')}</span>`;
|
|
86
|
+
html += ` <span class="ret">=</span> `;
|
|
87
|
+
html += `<span class="num editable var-default" contenteditable="true" data-field="default" data-placeholder="value" data-original="${esc(v.default || '')}">${esc(v.default || '')}</span>`;
|
|
88
|
+
html += `<span class="item-actions">`;
|
|
89
|
+
if (!v.onready) {
|
|
90
|
+
html += `<button onclick="toggleOnready(${vi}, false)" title="Add @onready" style="font-size:9px;width:auto;padding:0 4px;">⏱</button>`;
|
|
91
|
+
}
|
|
92
|
+
html += `<button class="delete" onclick="showDeleteUsages(${vi}, false, 'variable')" title="Delete">×</button>`;
|
|
93
|
+
html += `</span>`;
|
|
94
|
+
html += `</li>`;
|
|
95
|
+
}
|
|
96
|
+
html += `</ul>`;
|
|
97
|
+
html += `<div class="add-item-btn" onclick="addNewVariable(false)">`;
|
|
98
|
+
html += `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>`;
|
|
99
|
+
html += `Add variable</div>`;
|
|
100
|
+
html += `</div>`;
|
|
101
|
+
// Functions
|
|
102
|
+
if ((node.functions || []).length > 0) {
|
|
103
|
+
html += `<div class="section">`;
|
|
104
|
+
html += `<div class="section-header">Functions <span class="section-count">${node.functions.length}</span></div>`;
|
|
105
|
+
html += `<ul class="item-list">`;
|
|
106
|
+
for (let fi = 0; fi < node.functions.length; fi++) {
|
|
107
|
+
const f = node.functions[fi];
|
|
108
|
+
html += `<li class="clickable" onclick="toggleFunc(${fi})">`;
|
|
109
|
+
html += `<span class="kw">func</span> <span class="fn">${esc(f.name)}</span>`;
|
|
110
|
+
html += `<span class="param">(${esc(f.params)})</span>`;
|
|
111
|
+
if (f.return_type)
|
|
112
|
+
html += ` <span class="ret">→</span> <span class="tp">${esc(f.return_type)}</span>`;
|
|
113
|
+
html += `<span style="margin-left:auto;display:flex;gap:4px;align-items:center">`;
|
|
114
|
+
if (f.body_lines)
|
|
115
|
+
html += `<span class="tag tag-lines">${f.body_lines}L</span>`;
|
|
116
|
+
html += `<button class="delete" onclick="event.stopPropagation();showDeleteUsages(${fi}, false, 'function')" title="Delete function" style="opacity:0">×</button>`;
|
|
117
|
+
html += `</span>`;
|
|
118
|
+
html += `</li>`;
|
|
119
|
+
html += `<div id="func-viewer-${fi}" class="func-viewer" style="display:none"></div>`;
|
|
120
|
+
}
|
|
121
|
+
html += `</ul></div>`;
|
|
122
|
+
}
|
|
123
|
+
// Signals section (always show for adding)
|
|
124
|
+
const signalsList = node.signals || [];
|
|
125
|
+
html += `<div class="section">`;
|
|
126
|
+
html += `<div class="section-header">Signals <span class="section-count">${signalsList.length}</span></div>`;
|
|
127
|
+
html += `<ul class="item-list" id="signals-list">`;
|
|
128
|
+
for (let si = 0; si < signalsList.length; si++) {
|
|
129
|
+
const s = signalsList[si];
|
|
130
|
+
const sigName = typeof s === 'string' ? s : s.name;
|
|
131
|
+
const sigParams = typeof s === 'object' ? s.params : '';
|
|
132
|
+
html += `<li data-signal-index="${si}">`;
|
|
133
|
+
html += `<span class="kw">signal</span> `;
|
|
134
|
+
html += `<span class="sig editable signal-name" contenteditable="true" data-field="name" data-original="${esc(sigName)}">${esc(sigName)}</span>`;
|
|
135
|
+
html += `<span class="param">(</span>`;
|
|
136
|
+
html += `<span class="editable signal-params" contenteditable="true" data-field="params" data-placeholder="params" data-original="${esc(sigParams)}">${esc(sigParams)}</span>`;
|
|
137
|
+
html += `<span class="param">)</span>`;
|
|
138
|
+
html += `<span class="item-actions">`;
|
|
139
|
+
html += `<button class="delete" onclick="showDeleteUsages(${si}, false, 'signal')" title="Delete">×</button>`;
|
|
140
|
+
html += `</span>`;
|
|
141
|
+
html += `</li>`;
|
|
142
|
+
}
|
|
143
|
+
html += `</ul>`;
|
|
144
|
+
html += `<div class="add-item-btn" onclick="addNewSignal()">`;
|
|
145
|
+
html += `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>`;
|
|
146
|
+
html += `Add signal</div>`;
|
|
147
|
+
html += `</div>`;
|
|
148
|
+
// Connections - group by target and show signal names
|
|
149
|
+
const related = edges.filter(e => e.from === node.path || e.to === node.path);
|
|
150
|
+
if (related.length > 0) {
|
|
151
|
+
// Group connections by target and type
|
|
152
|
+
const connGroups = {};
|
|
153
|
+
for (const e of related) {
|
|
154
|
+
const other = e.from === node.path ? e.to : e.from;
|
|
155
|
+
const dir = e.from === node.path ? 'out' : 'in';
|
|
156
|
+
const key = `${other}-${e.type}-${dir}`;
|
|
157
|
+
if (!connGroups[key]) {
|
|
158
|
+
connGroups[key] = { other, type: e.type, dir, signals: [] };
|
|
159
|
+
}
|
|
160
|
+
if (e.signal_name)
|
|
161
|
+
connGroups[key].signals.push(e.signal_name);
|
|
162
|
+
}
|
|
163
|
+
html += `<div class="section">`;
|
|
164
|
+
html += `<div class="section-header">Connections <span class="section-count">${related.length}</span></div>`;
|
|
165
|
+
html += `<ul class="item-list">`;
|
|
166
|
+
for (const key of Object.keys(connGroups)) {
|
|
167
|
+
const g = connGroups[key];
|
|
168
|
+
const dirIcon = g.dir === 'out' ? '→' : '←';
|
|
169
|
+
const color = g.type === 'extends' ? 'var(--edge-extends)' : g.type === 'preload' ? 'var(--edge-preload)' : 'var(--edge-signal)';
|
|
170
|
+
const filename = g.other.split('/').pop();
|
|
171
|
+
html += `<li style="flex-wrap:wrap">`;
|
|
172
|
+
html += `${dirIcon} <span style="color:${color}">${esc(filename)}</span> <span class="ret">(${g.type})</span>`;
|
|
173
|
+
// Show signal names if this is a signal connection
|
|
174
|
+
if (g.type === 'signal' && g.signals.length > 0) {
|
|
175
|
+
const uniqueSignals = [...new Set(g.signals)];
|
|
176
|
+
html += `<div style="width:100%;margin-top:4px;padding-left:20px;font-size:11px;color:var(--text-muted)">`;
|
|
177
|
+
html += uniqueSignals.map(s => `<span class="sig">${esc(s)}</span>`).join(', ');
|
|
178
|
+
html += `</div>`;
|
|
179
|
+
}
|
|
180
|
+
html += `</li>`;
|
|
181
|
+
}
|
|
182
|
+
html += `</ul></div>`;
|
|
183
|
+
}
|
|
184
|
+
// Preloads
|
|
185
|
+
if ((node.preloads || []).length > 0) {
|
|
186
|
+
html += `<div class="section">`;
|
|
187
|
+
html += `<div class="section-header">Preloads <span class="section-count">${node.preloads.length}</span></div>`;
|
|
188
|
+
html += `<ul class="item-list">`;
|
|
189
|
+
for (const p of node.preloads) {
|
|
190
|
+
html += `<li><span class="str">"${esc(p)}"</span></li>`;
|
|
191
|
+
}
|
|
192
|
+
html += `</ul></div>`;
|
|
193
|
+
}
|
|
194
|
+
if (node.gitStatus) {
|
|
195
|
+
const statusLabel = node.gitStatus === 'modified' ? 'Modified' :
|
|
196
|
+
node.gitStatus === 'added' ? 'Added' :
|
|
197
|
+
node.gitStatus === 'untracked' ? 'Untracked' : node.gitStatus;
|
|
198
|
+
const statusColor = node.gitStatus === 'modified' ? '#f9e2af' :
|
|
199
|
+
node.gitStatus === 'added' ? '#a6e3a1' : '#89b4fa';
|
|
200
|
+
html += `<div class="section">`;
|
|
201
|
+
html += `<div class="section-header">Changes <span class="section-count" style="background:${statusColor};color:#1a1a2e">${statusLabel}</span></div>`;
|
|
202
|
+
html += `<div id="diff-container" class="diff-container"><div class="diff-loading">Loading diff...</div></div>`;
|
|
203
|
+
html += `</div>`;
|
|
204
|
+
}
|
|
205
|
+
document.getElementById('panel-body').innerHTML = html;
|
|
206
|
+
detailPanel.classList.add('open');
|
|
207
|
+
initSectionResizing();
|
|
208
|
+
initInlineEditing();
|
|
209
|
+
if (node.gitStatus) {
|
|
210
|
+
fetchAndRenderDiff(node);
|
|
211
|
+
}
|
|
212
|
+
draw();
|
|
213
|
+
}
|
|
214
|
+
async function fetchAndRenderDiff(node) {
|
|
215
|
+
const container = document.getElementById('diff-container');
|
|
216
|
+
if (!container)
|
|
217
|
+
return;
|
|
218
|
+
try {
|
|
219
|
+
const result = await sendCommand('get_file_diff', { path: node.path });
|
|
220
|
+
if (!result || !result.hunks || result.hunks.length === 0) {
|
|
221
|
+
if (node.gitStatus === 'untracked' || node.gitStatus === 'added') {
|
|
222
|
+
container.innerHTML = '<div class="diff-info">New file — no previous version to diff against</div>';
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
container.innerHTML = '<div class="diff-info">No changes detected</div>';
|
|
226
|
+
}
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const grouped = groupHunksByFunction(result.hunks, node);
|
|
230
|
+
container.innerHTML = renderGroupedDiff(grouped);
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
console.error('Failed to fetch diff:', err);
|
|
234
|
+
container.innerHTML = '<div class="diff-error">Could not load diff</div>';
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function groupHunksByFunction(hunks, node) {
|
|
238
|
+
const functions = node.functions || [];
|
|
239
|
+
const groups = {};
|
|
240
|
+
for (const hunk of hunks) {
|
|
241
|
+
let matchedFunc = null;
|
|
242
|
+
for (const f of functions) {
|
|
243
|
+
const funcStart = f.line || 0;
|
|
244
|
+
const funcEnd = funcStart + (f.body_lines || 0);
|
|
245
|
+
if (hunk.newStart <= funcEnd && (hunk.newStart + hunk.newCount) >= funcStart) {
|
|
246
|
+
matchedFunc = f.name;
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const groupKey = matchedFunc ? `${matchedFunc}()` : 'Top-level changes';
|
|
251
|
+
if (!groups[groupKey])
|
|
252
|
+
groups[groupKey] = [];
|
|
253
|
+
groups[groupKey].push(hunk);
|
|
254
|
+
}
|
|
255
|
+
const result = [];
|
|
256
|
+
for (const [label, groupHunks] of Object.entries(groups)) {
|
|
257
|
+
if (label !== 'Top-level changes') {
|
|
258
|
+
result.push({ label, hunks: groupHunks });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (groups['Top-level changes']) {
|
|
262
|
+
result.push({ label: 'Top-level changes', hunks: groups['Top-level changes'] });
|
|
263
|
+
}
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
function renderGroupedDiff(groups) {
|
|
267
|
+
let html = '';
|
|
268
|
+
for (const group of groups) {
|
|
269
|
+
let additions = 0, deletions = 0;
|
|
270
|
+
for (const hunk of group.hunks) {
|
|
271
|
+
for (const line of hunk.lines) {
|
|
272
|
+
if (line.startsWith('+'))
|
|
273
|
+
additions++;
|
|
274
|
+
else if (line.startsWith('-'))
|
|
275
|
+
deletions++;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
html += `<div class="diff-group">`;
|
|
279
|
+
html += `<div class="diff-group-header">`;
|
|
280
|
+
html += `<span class="diff-func-name">${esc(group.label)}</span>`;
|
|
281
|
+
html += `<span class="diff-stats">`;
|
|
282
|
+
if (additions > 0)
|
|
283
|
+
html += `<span class="diff-stat-add">+${additions}</span>`;
|
|
284
|
+
if (deletions > 0)
|
|
285
|
+
html += `<span class="diff-stat-del">−${deletions}</span>`;
|
|
286
|
+
html += `</span>`;
|
|
287
|
+
html += `</div>`;
|
|
288
|
+
for (const hunk of group.hunks) {
|
|
289
|
+
html += `<div class="diff-hunk">`;
|
|
290
|
+
for (const line of hunk.lines) {
|
|
291
|
+
const type = line.startsWith('+') ? 'add' : line.startsWith('-') ? 'del' : 'ctx';
|
|
292
|
+
html += `<div class="diff-line diff-${type}">${esc(line)}</div>`;
|
|
293
|
+
}
|
|
294
|
+
html += `</div>`;
|
|
295
|
+
}
|
|
296
|
+
html += `</div>`;
|
|
297
|
+
}
|
|
298
|
+
return html;
|
|
299
|
+
}
|
|
300
|
+
export function closePanel() {
|
|
301
|
+
setSelectedNode(null);
|
|
302
|
+
detailPanel.classList.remove('open');
|
|
303
|
+
draw();
|
|
304
|
+
}
|
|
305
|
+
// Make closePanel available globally for onclick
|
|
306
|
+
window.closePanel = closePanel;
|
|
307
|
+
// Toggle function body viewer with inline editing
|
|
308
|
+
window.toggleFunc = function (fi) {
|
|
309
|
+
const viewer = document.getElementById(`func-viewer-${fi}`);
|
|
310
|
+
if (!viewer)
|
|
311
|
+
return;
|
|
312
|
+
if (viewer.style.display !== 'none') {
|
|
313
|
+
viewer.style.display = 'none';
|
|
314
|
+
viewer.innerHTML = '';
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const f = selectedNode.functions[fi];
|
|
318
|
+
if (!f.body)
|
|
319
|
+
return;
|
|
320
|
+
const editorId = `code-editor-${fi}`;
|
|
321
|
+
viewer.innerHTML = `
|
|
322
|
+
<div class="func-viewer-header">
|
|
323
|
+
<span><span class="func-title">${esc(f.name)}</span> · <span id="line-count-${fi}">${f.body.split('\n').length}</span> lines</span>
|
|
324
|
+
<button class="func-viewer-close" onclick="toggleFunc(${fi})">×</button>
|
|
325
|
+
</div>
|
|
326
|
+
<div class="func-viewer-code">
|
|
327
|
+
<div class="code-editor-container" id="${editorId}">
|
|
328
|
+
<div class="code-editor-highlight" id="highlight-${fi}"></div>
|
|
329
|
+
<textarea class="code-editor-textarea" id="textarea-${fi}" spellcheck="false"></textarea>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
<div class="func-viewer-footer">
|
|
333
|
+
<span class="status" id="status-${fi}">Modified</span>
|
|
334
|
+
<span style="display:flex;gap:8px;align-items:center">
|
|
335
|
+
<span style="opacity:0.6">Ctrl+S to save</span>
|
|
336
|
+
<button class="save-btn" id="save-btn-${fi}" onclick="saveFunction(${fi})">Save</button>
|
|
337
|
+
</span>
|
|
338
|
+
</div>
|
|
339
|
+
`;
|
|
340
|
+
const textarea = document.getElementById(`textarea-${fi}`);
|
|
341
|
+
const highlight = document.getElementById(`highlight-${fi}`);
|
|
342
|
+
const statusEl = document.getElementById(`status-${fi}`);
|
|
343
|
+
const saveBtn = document.getElementById(`save-btn-${fi}`);
|
|
344
|
+
const lineCountEl = document.getElementById(`line-count-${fi}`);
|
|
345
|
+
// Store original code for comparison
|
|
346
|
+
textarea.dataset.original = f.body;
|
|
347
|
+
textarea.dataset.funcIndex = fi;
|
|
348
|
+
textarea.dataset.scriptPath = selectedNode.path;
|
|
349
|
+
textarea.dataset.funcName = f.name;
|
|
350
|
+
textarea.value = f.body;
|
|
351
|
+
// Initial highlight
|
|
352
|
+
updateHighlight(fi);
|
|
353
|
+
// Sync highlight on input
|
|
354
|
+
textarea.addEventListener('input', () => {
|
|
355
|
+
updateHighlight(fi);
|
|
356
|
+
const modified = textarea.value !== textarea.dataset.original;
|
|
357
|
+
statusEl.classList.toggle('visible', modified);
|
|
358
|
+
saveBtn.classList.toggle('active', modified);
|
|
359
|
+
lineCountEl.textContent = textarea.value.split('\n').length;
|
|
360
|
+
});
|
|
361
|
+
// Sync scroll
|
|
362
|
+
textarea.addEventListener('scroll', () => {
|
|
363
|
+
highlight.style.transform = `translate(-${textarea.scrollLeft}px, -${textarea.scrollTop}px)`;
|
|
364
|
+
});
|
|
365
|
+
// Handle tab key
|
|
366
|
+
textarea.addEventListener('keydown', (e) => {
|
|
367
|
+
if (e.key === 'Tab') {
|
|
368
|
+
e.preventDefault();
|
|
369
|
+
const start = textarea.selectionStart;
|
|
370
|
+
const end = textarea.selectionEnd;
|
|
371
|
+
textarea.value = textarea.value.substring(0, start) + '\t' + textarea.value.substring(end);
|
|
372
|
+
textarea.selectionStart = textarea.selectionEnd = start + 1;
|
|
373
|
+
textarea.dispatchEvent(new Event('input'));
|
|
374
|
+
}
|
|
375
|
+
// Ctrl+S to save
|
|
376
|
+
if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
|
|
377
|
+
e.preventDefault();
|
|
378
|
+
saveFunction(fi);
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
// Auto-resize textarea height
|
|
382
|
+
function autoResize() {
|
|
383
|
+
textarea.style.height = 'auto';
|
|
384
|
+
textarea.style.height = textarea.scrollHeight + 'px';
|
|
385
|
+
highlight.style.height = textarea.scrollHeight + 'px';
|
|
386
|
+
}
|
|
387
|
+
textarea.addEventListener('input', autoResize);
|
|
388
|
+
setTimeout(autoResize, 0);
|
|
389
|
+
viewer.style.display = 'block';
|
|
390
|
+
};
|
|
391
|
+
// Update syntax highlighting
|
|
392
|
+
function updateHighlight(fi) {
|
|
393
|
+
const textarea = document.getElementById(`textarea-${fi}`);
|
|
394
|
+
const highlight = document.getElementById(`highlight-${fi}`);
|
|
395
|
+
if (!textarea || !highlight)
|
|
396
|
+
return;
|
|
397
|
+
// Highlight each line, wrap in spans for line-level highlighting
|
|
398
|
+
const lines = textarea.value.split('\n');
|
|
399
|
+
highlight.innerHTML = lines.map((line, i) => `<div class="code-line" data-line="${i}">${highlightGDScript(line) || ' '}</div>`).join('');
|
|
400
|
+
}
|
|
401
|
+
// Save function changes back to Godot
|
|
402
|
+
window.saveFunction = async function (fi) {
|
|
403
|
+
const textarea = document.getElementById(`textarea-${fi}`);
|
|
404
|
+
const statusEl = document.getElementById(`status-${fi}`);
|
|
405
|
+
const saveBtn = document.getElementById(`save-btn-${fi}`);
|
|
406
|
+
if (!textarea || textarea.value === textarea.dataset.original)
|
|
407
|
+
return;
|
|
408
|
+
const scriptPath = textarea.dataset.scriptPath;
|
|
409
|
+
const funcName = textarea.dataset.funcName;
|
|
410
|
+
const newCode = textarea.value;
|
|
411
|
+
statusEl.textContent = 'Saving...';
|
|
412
|
+
statusEl.classList.add('visible');
|
|
413
|
+
try {
|
|
414
|
+
// Send to Godot
|
|
415
|
+
await sendCommand('modify_function', {
|
|
416
|
+
path: scriptPath,
|
|
417
|
+
name: funcName,
|
|
418
|
+
body: newCode
|
|
419
|
+
});
|
|
420
|
+
// Update local state
|
|
421
|
+
const funcIndex = parseInt(textarea.dataset.funcIndex);
|
|
422
|
+
selectedNode.functions[funcIndex].body = newCode;
|
|
423
|
+
selectedNode.functions[funcIndex].body_lines = newCode.split('\n').length;
|
|
424
|
+
textarea.dataset.original = newCode;
|
|
425
|
+
statusEl.textContent = 'Saved!';
|
|
426
|
+
saveBtn.classList.remove('active');
|
|
427
|
+
setTimeout(() => {
|
|
428
|
+
statusEl.classList.remove('visible');
|
|
429
|
+
}, 2000);
|
|
430
|
+
console.log(`Saved function "${funcName}" in ${scriptPath}`);
|
|
431
|
+
}
|
|
432
|
+
catch (err) {
|
|
433
|
+
statusEl.textContent = 'Error: ' + err.message;
|
|
434
|
+
console.error('Failed to save:', err);
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
// ---- Inline Editing for Variables/Signals ----
|
|
438
|
+
function initInlineEditing() {
|
|
439
|
+
// Handle blur on editable fields - save changes
|
|
440
|
+
document.querySelectorAll('.editable').forEach(el => {
|
|
441
|
+
el.addEventListener('blur', handleInlineEdit);
|
|
442
|
+
el.addEventListener('keydown', (e) => {
|
|
443
|
+
if (e.key === 'Enter') {
|
|
444
|
+
e.preventDefault();
|
|
445
|
+
el.blur();
|
|
446
|
+
}
|
|
447
|
+
if (e.key === 'Escape') {
|
|
448
|
+
el.textContent = el.dataset.original || '';
|
|
449
|
+
el.blur();
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
async function handleInlineEdit(e) {
|
|
455
|
+
const el = e.target;
|
|
456
|
+
const li = el.closest('li');
|
|
457
|
+
if (!li)
|
|
458
|
+
return;
|
|
459
|
+
const newValue = el.textContent.trim();
|
|
460
|
+
const original = el.dataset.original || '';
|
|
461
|
+
const field = el.dataset.field;
|
|
462
|
+
if (newValue === original)
|
|
463
|
+
return; // No change
|
|
464
|
+
// Determine what type of item this is
|
|
465
|
+
const isSignal = li.dataset.signalIndex !== undefined;
|
|
466
|
+
const isExport = li.dataset.exported === 'true';
|
|
467
|
+
const index = parseInt(isSignal ? li.dataset.signalIndex : li.dataset.varIndex);
|
|
468
|
+
try {
|
|
469
|
+
if (isSignal) {
|
|
470
|
+
// Update signal
|
|
471
|
+
const sig = selectedNode.signals[index];
|
|
472
|
+
const oldName = typeof sig === 'string' ? sig : sig.name;
|
|
473
|
+
const oldParams = typeof sig === 'object' ? sig.params : '';
|
|
474
|
+
const newSig = {
|
|
475
|
+
name: field === 'name' ? newValue : oldName,
|
|
476
|
+
params: field === 'params' ? newValue : oldParams
|
|
477
|
+
};
|
|
478
|
+
// Send to Godot
|
|
479
|
+
await sendCommand('modify_signal', {
|
|
480
|
+
path: selectedNode.path,
|
|
481
|
+
action: 'update',
|
|
482
|
+
old_name: oldName,
|
|
483
|
+
name: newSig.name,
|
|
484
|
+
params: newSig.params
|
|
485
|
+
});
|
|
486
|
+
// Update local state
|
|
487
|
+
selectedNode.signals[index] = newSig;
|
|
488
|
+
console.log(`Updated signal in ${selectedNode.path}:`, newSig);
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
// Update variable
|
|
492
|
+
const vars = selectedNode.variables.filter(v => v.exported === isExport);
|
|
493
|
+
const v = vars[index];
|
|
494
|
+
const actualIndex = selectedNode.variables.findIndex(vr => vr.name === v.name);
|
|
495
|
+
if (actualIndex !== -1) {
|
|
496
|
+
const newVar = { ...selectedNode.variables[actualIndex] };
|
|
497
|
+
if (field === 'name')
|
|
498
|
+
newVar.name = newValue;
|
|
499
|
+
if (field === 'type')
|
|
500
|
+
newVar.type = newValue;
|
|
501
|
+
if (field === 'default')
|
|
502
|
+
newVar.default = newValue;
|
|
503
|
+
// Send to Godot
|
|
504
|
+
await sendCommand('modify_variable', {
|
|
505
|
+
path: selectedNode.path,
|
|
506
|
+
action: 'update',
|
|
507
|
+
old_name: v.name,
|
|
508
|
+
name: newVar.name,
|
|
509
|
+
type: newVar.type,
|
|
510
|
+
default: newVar.default,
|
|
511
|
+
exported: isExport
|
|
512
|
+
});
|
|
513
|
+
// Update local state
|
|
514
|
+
selectedNode.variables[actualIndex] = newVar;
|
|
515
|
+
console.log(`Updated variable in ${selectedNode.path}:`, newVar);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
// Update the original value
|
|
519
|
+
el.dataset.original = newValue;
|
|
520
|
+
}
|
|
521
|
+
catch (err) {
|
|
522
|
+
console.error('Failed to update:', err);
|
|
523
|
+
// Revert on error
|
|
524
|
+
el.textContent = original;
|
|
525
|
+
alert('Failed to save: ' + err.message);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
// ---- Toggle @onready ----
|
|
529
|
+
window.toggleOnready = async function (index, isExport) {
|
|
530
|
+
const vars = selectedNode.variables.filter(v => v.exported === isExport);
|
|
531
|
+
const v = vars[index];
|
|
532
|
+
const actualIndex = selectedNode.variables.findIndex(vr => vr.name === v.name);
|
|
533
|
+
if (actualIndex === -1)
|
|
534
|
+
return;
|
|
535
|
+
const newOnready = !v.onready;
|
|
536
|
+
try {
|
|
537
|
+
await sendCommand('modify_variable', {
|
|
538
|
+
path: selectedNode.path,
|
|
539
|
+
action: 'update',
|
|
540
|
+
old_name: v.name,
|
|
541
|
+
name: v.name,
|
|
542
|
+
type: v.type || '',
|
|
543
|
+
default: v.default || '',
|
|
544
|
+
exported: isExport,
|
|
545
|
+
onready: newOnready
|
|
546
|
+
});
|
|
547
|
+
selectedNode.variables[actualIndex].onready = newOnready;
|
|
548
|
+
openPanel(selectedNode);
|
|
549
|
+
}
|
|
550
|
+
catch (err) {
|
|
551
|
+
console.error('Failed to toggle @onready:', err);
|
|
552
|
+
alert('Failed to update: ' + err.message);
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
// ---- Add New Items ----
|
|
556
|
+
window.addNewVariable = async function (isExport) {
|
|
557
|
+
const newVar = { name: 'new_var', type: '', default: '', exported: isExport };
|
|
558
|
+
try {
|
|
559
|
+
// Send to Godot first
|
|
560
|
+
await sendCommand('modify_variable', {
|
|
561
|
+
path: selectedNode.path,
|
|
562
|
+
action: 'add',
|
|
563
|
+
name: newVar.name,
|
|
564
|
+
type: newVar.type,
|
|
565
|
+
default: newVar.default,
|
|
566
|
+
exported: isExport
|
|
567
|
+
});
|
|
568
|
+
// Update local state
|
|
569
|
+
selectedNode.variables.push(newVar);
|
|
570
|
+
openPanel(selectedNode);
|
|
571
|
+
// Focus the new variable name after panel refresh
|
|
572
|
+
setTimeout(() => {
|
|
573
|
+
const list = document.getElementById(isExport ? 'exports-list' : 'vars-list');
|
|
574
|
+
const lastItem = list?.querySelector('li:last-of-type .var-name');
|
|
575
|
+
if (lastItem) {
|
|
576
|
+
lastItem.focus();
|
|
577
|
+
const range = document.createRange();
|
|
578
|
+
range.selectNodeContents(lastItem);
|
|
579
|
+
const sel = window.getSelection();
|
|
580
|
+
sel.removeAllRanges();
|
|
581
|
+
sel.addRange(range);
|
|
582
|
+
}
|
|
583
|
+
}, 50);
|
|
584
|
+
}
|
|
585
|
+
catch (err) {
|
|
586
|
+
console.error('Failed to add variable:', err);
|
|
587
|
+
alert('Failed to add variable: ' + err.message);
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
window.addNewSignal = async function () {
|
|
591
|
+
const newSig = { name: 'new_signal', params: '' };
|
|
592
|
+
try {
|
|
593
|
+
// Send to Godot first
|
|
594
|
+
await sendCommand('modify_signal', {
|
|
595
|
+
path: selectedNode.path,
|
|
596
|
+
action: 'add',
|
|
597
|
+
name: newSig.name,
|
|
598
|
+
params: newSig.params
|
|
599
|
+
});
|
|
600
|
+
// Update local state
|
|
601
|
+
if (!selectedNode.signals)
|
|
602
|
+
selectedNode.signals = [];
|
|
603
|
+
selectedNode.signals.push(newSig);
|
|
604
|
+
openPanel(selectedNode);
|
|
605
|
+
setTimeout(() => {
|
|
606
|
+
const list = document.getElementById('signals-list');
|
|
607
|
+
const lastItem = list?.querySelector('li:last-of-type .signal-name');
|
|
608
|
+
if (lastItem) {
|
|
609
|
+
lastItem.focus();
|
|
610
|
+
const range = document.createRange();
|
|
611
|
+
range.selectNodeContents(lastItem);
|
|
612
|
+
const sel = window.getSelection();
|
|
613
|
+
sel.removeAllRanges();
|
|
614
|
+
sel.addRange(range);
|
|
615
|
+
}
|
|
616
|
+
}, 50);
|
|
617
|
+
}
|
|
618
|
+
catch (err) {
|
|
619
|
+
console.error('Failed to add signal:', err);
|
|
620
|
+
alert('Failed to add signal: ' + err.message);
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
// Section resizing
|
|
624
|
+
let resizingList = null;
|
|
625
|
+
let resizeStartY = 0;
|
|
626
|
+
let resizeStartHeight = 0;
|
|
627
|
+
function initSectionResizing() {
|
|
628
|
+
document.querySelectorAll('.section-resize-handle').forEach(handle => {
|
|
629
|
+
// Remove old listeners
|
|
630
|
+
handle.replaceWith(handle.cloneNode(true));
|
|
631
|
+
});
|
|
632
|
+
document.querySelectorAll('.section-resize-handle').forEach(handle => {
|
|
633
|
+
handle.addEventListener('mousedown', (e) => {
|
|
634
|
+
e.preventDefault();
|
|
635
|
+
e.stopPropagation();
|
|
636
|
+
// Find the item-list in this section
|
|
637
|
+
const section = handle.closest('.section');
|
|
638
|
+
resizingList = section?.querySelector('.item-list');
|
|
639
|
+
if (!resizingList)
|
|
640
|
+
return;
|
|
641
|
+
section.classList.add('resizing');
|
|
642
|
+
resizeStartY = e.clientY;
|
|
643
|
+
resizeStartHeight = resizingList.offsetHeight;
|
|
644
|
+
document.addEventListener('mousemove', onSectionResize);
|
|
645
|
+
document.addEventListener('mouseup', onSectionResizeEnd);
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
function onSectionResize(e) {
|
|
650
|
+
if (!resizingList)
|
|
651
|
+
return;
|
|
652
|
+
const dy = e.clientY - resizeStartY;
|
|
653
|
+
const newHeight = Math.max(50, Math.min(500, resizeStartHeight + dy));
|
|
654
|
+
resizingList.style.maxHeight = newHeight + 'px';
|
|
655
|
+
}
|
|
656
|
+
function onSectionResizeEnd() {
|
|
657
|
+
if (resizingList) {
|
|
658
|
+
const section = resizingList.closest('.section');
|
|
659
|
+
section?.classList.remove('resizing');
|
|
660
|
+
resizingList = null;
|
|
661
|
+
}
|
|
662
|
+
document.removeEventListener('mousemove', onSectionResize);
|
|
663
|
+
document.removeEventListener('mouseup', onSectionResizeEnd);
|
|
664
|
+
}
|
|
665
|
+
// Panel horizontal resizing
|
|
666
|
+
let panelResizing = false;
|
|
667
|
+
let panelResizeStartX = 0;
|
|
668
|
+
let panelStartWidth = 460;
|
|
669
|
+
function initPanelResizing() {
|
|
670
|
+
const handle = document.getElementById('panel-resize-handle');
|
|
671
|
+
const panel = document.getElementById('detail-panel');
|
|
672
|
+
handle.addEventListener('mousedown', (e) => {
|
|
673
|
+
e.preventDefault();
|
|
674
|
+
e.stopPropagation();
|
|
675
|
+
panelResizing = true;
|
|
676
|
+
panel.classList.add('resizing');
|
|
677
|
+
panelResizeStartX = e.clientX;
|
|
678
|
+
panelStartWidth = panel.offsetWidth;
|
|
679
|
+
document.addEventListener('mousemove', onPanelResize);
|
|
680
|
+
document.addEventListener('mouseup', onPanelResizeEnd);
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
function onPanelResize(e) {
|
|
684
|
+
if (!panelResizing)
|
|
685
|
+
return;
|
|
686
|
+
const panel = document.getElementById('detail-panel');
|
|
687
|
+
const dx = panelResizeStartX - e.clientX; // Dragging left = wider
|
|
688
|
+
const newWidth = Math.max(300, Math.min(window.innerWidth * 0.8, panelStartWidth + dx));
|
|
689
|
+
panel.style.width = newWidth + 'px';
|
|
690
|
+
panel.style.right = '0';
|
|
691
|
+
}
|
|
692
|
+
function onPanelResizeEnd() {
|
|
693
|
+
panelResizing = false;
|
|
694
|
+
const panel = document.getElementById('detail-panel');
|
|
695
|
+
panel.classList.remove('resizing');
|
|
696
|
+
document.removeEventListener('mousemove', onPanelResize);
|
|
697
|
+
document.removeEventListener('mouseup', onPanelResizeEnd);
|
|
698
|
+
}
|
|
699
|
+
// Function to expand and highlight a specific line in a function viewer
|
|
700
|
+
export function expandAndHighlightFunction(funcName, targetLine, nodeData) {
|
|
701
|
+
const node = nodeData || selectedNode;
|
|
702
|
+
// Find the function index
|
|
703
|
+
const funcIndex = node.functions.findIndex(f => f.name === funcName);
|
|
704
|
+
if (funcIndex === -1) {
|
|
705
|
+
console.log('Function not found:', funcName);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
console.log(`Expanding function ${funcName} (index ${funcIndex}) to line ${targetLine}`);
|
|
709
|
+
// Get the function viewer element
|
|
710
|
+
const viewer = document.getElementById(`func-viewer-${funcIndex}`);
|
|
711
|
+
if (!viewer) {
|
|
712
|
+
console.log('Viewer element not found for index:', funcIndex);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
// Check if already expanded
|
|
716
|
+
const isExpanded = viewer.style.display !== 'none';
|
|
717
|
+
if (!isExpanded) {
|
|
718
|
+
// Need to expand - call toggleFunc
|
|
719
|
+
window.toggleFunc(funcIndex);
|
|
720
|
+
}
|
|
721
|
+
// Wait for expansion, then highlight
|
|
722
|
+
setTimeout(() => {
|
|
723
|
+
highlightLineInViewer(viewer, funcName, targetLine, node);
|
|
724
|
+
// Scroll the viewer into view
|
|
725
|
+
viewer.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
726
|
+
}, isExpanded ? 100 : 300);
|
|
727
|
+
}
|
|
728
|
+
function highlightLineInViewer(viewer, funcName, targetLine, nodeData) {
|
|
729
|
+
// Find the function to get its start line
|
|
730
|
+
const node = nodeData || selectedNode;
|
|
731
|
+
const func = node.functions.find(f => f.name === funcName);
|
|
732
|
+
if (!func) {
|
|
733
|
+
console.log('Function not found for highlighting:', funcName);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const funcStartLine = func.line || 1;
|
|
737
|
+
const relativeLineIndex = targetLine - funcStartLine;
|
|
738
|
+
console.log(`Highlighting line ${targetLine} in ${funcName} (start: ${funcStartLine}, relative: ${relativeLineIndex})`);
|
|
739
|
+
// Find the highlight overlay within the viewer
|
|
740
|
+
const highlightDiv = viewer.querySelector('.code-editor-highlight');
|
|
741
|
+
if (!highlightDiv) {
|
|
742
|
+
console.log('Highlight div not found in viewer');
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
// Clear all previous highlights
|
|
746
|
+
document.querySelectorAll('.code-line-highlight').forEach(el => {
|
|
747
|
+
el.classList.remove('code-line-highlight');
|
|
748
|
+
});
|
|
749
|
+
// Get all lines and highlight the target
|
|
750
|
+
const lines = highlightDiv.querySelectorAll('.code-line');
|
|
751
|
+
console.log(`Found ${lines.length} lines in viewer`);
|
|
752
|
+
if (relativeLineIndex >= 0 && relativeLineIndex < lines.length) {
|
|
753
|
+
const targetLineEl = lines[relativeLineIndex];
|
|
754
|
+
targetLineEl.classList.add('code-line-highlight');
|
|
755
|
+
// Scroll the line into view within the code editor
|
|
756
|
+
setTimeout(() => {
|
|
757
|
+
targetLineEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
758
|
+
}, 50);
|
|
759
|
+
}
|
|
760
|
+
else {
|
|
761
|
+
console.log(`Line index ${relativeLineIndex} out of range (0-${lines.length - 1})`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
// ============================================================================
|
|
765
|
+
// SCENE NODE PROPERTIES PANEL
|
|
766
|
+
// ============================================================================
|
|
767
|
+
export async function openSceneNodePanel(scenePath, node) {
|
|
768
|
+
currentPanelMode = 'sceneNode';
|
|
769
|
+
setSelectedSceneNode(node);
|
|
770
|
+
// Show loading state
|
|
771
|
+
document.getElementById('panel-title').textContent = node.name;
|
|
772
|
+
document.getElementById('panel-path').textContent = `${node.type} • ${node.path}`;
|
|
773
|
+
document.getElementById('panel-body').innerHTML = '<div class="loading-state">Loading properties...</div>';
|
|
774
|
+
detailPanel.classList.add('open');
|
|
775
|
+
try {
|
|
776
|
+
// Fetch properties from Godot
|
|
777
|
+
const result = await sendCommand('get_scene_node_properties', {
|
|
778
|
+
scene_path: scenePath,
|
|
779
|
+
node_path: node.path
|
|
780
|
+
});
|
|
781
|
+
if (result.ok) {
|
|
782
|
+
setSceneNodeProperties(result);
|
|
783
|
+
renderSceneNodePanel(result, scenePath, node);
|
|
784
|
+
}
|
|
785
|
+
else {
|
|
786
|
+
document.getElementById('panel-body').innerHTML = `<div class="error-state">Failed to load properties: ${result.error}</div>`;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
catch (err) {
|
|
790
|
+
console.error('Failed to fetch node properties:', err);
|
|
791
|
+
document.getElementById('panel-body').innerHTML = `<div class="error-state">Error: ${err.message}</div>`;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
export function closeSceneNodePanel() {
|
|
795
|
+
if (currentPanelMode === 'sceneNode') {
|
|
796
|
+
setSelectedSceneNode(null);
|
|
797
|
+
setSceneNodeProperties(null);
|
|
798
|
+
detailPanel.classList.remove('open');
|
|
799
|
+
draw();
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
function renderSceneNodePanel(data, scenePath, node) {
|
|
803
|
+
document.getElementById('panel-title').textContent = data.node_name;
|
|
804
|
+
document.getElementById('panel-path').textContent = `${data.node_type} • ${data.node_path}`;
|
|
805
|
+
let html = '';
|
|
806
|
+
// Meta badges
|
|
807
|
+
html += `<div class="meta-row">`;
|
|
808
|
+
html += `<div class="meta-badge"><span>${data.node_type}</span></div>`;
|
|
809
|
+
html += `<div class="meta-badge">${data.property_count} <span>properties</span></div>`;
|
|
810
|
+
if (node.script) {
|
|
811
|
+
html += `<div class="meta-badge script-badge" onclick="jumpToScript('${esc(node.script)}')">📜 <span>${node.script.split('/').pop()}</span></div>`;
|
|
812
|
+
}
|
|
813
|
+
html += `</div>`;
|
|
814
|
+
// Render categories
|
|
815
|
+
const categories = data.categories || {};
|
|
816
|
+
const categoryOrder = data.inheritance_chain || Object.keys(categories);
|
|
817
|
+
for (const category of categoryOrder) {
|
|
818
|
+
const props = categories[category];
|
|
819
|
+
if (!props || props.length === 0)
|
|
820
|
+
continue;
|
|
821
|
+
html += `<div class="section property-section" data-category="${esc(category)}">`;
|
|
822
|
+
html += `<div class="section-header clickable" onclick="togglePropertySection(this)">`;
|
|
823
|
+
html += `<span>${category}</span>`;
|
|
824
|
+
html += `<span class="section-count">${props.length}</span>`;
|
|
825
|
+
html += `</div>`;
|
|
826
|
+
html += `<div class="property-list">`;
|
|
827
|
+
for (const prop of props) {
|
|
828
|
+
html += renderPropertyRow(prop, scenePath, data.node_path);
|
|
829
|
+
}
|
|
830
|
+
html += `</div></div>`;
|
|
831
|
+
}
|
|
832
|
+
document.getElementById('panel-body').innerHTML = html;
|
|
833
|
+
initPropertyEditing(scenePath, data.node_path);
|
|
834
|
+
}
|
|
835
|
+
// Convert snake_case to Title Case for display
|
|
836
|
+
function formatPropertyName(name) {
|
|
837
|
+
return name
|
|
838
|
+
.replace(/_/g, ' ')
|
|
839
|
+
.replace(/\b\w/g, c => c.toUpperCase());
|
|
840
|
+
}
|
|
841
|
+
function renderPropertyRow(prop, scenePath, nodePath) {
|
|
842
|
+
const { name, type, type_name, value, hint, hint_string } = prop;
|
|
843
|
+
const displayName = formatPropertyName(name);
|
|
844
|
+
let html = `<div class="property-row" data-prop="${esc(name)}" data-type="${type}">`;
|
|
845
|
+
html += `<label class="property-name" title="${esc(name)}">${esc(displayName)}</label>`;
|
|
846
|
+
html += `<div class="property-value">`;
|
|
847
|
+
// Render appropriate control based on type
|
|
848
|
+
switch (type) {
|
|
849
|
+
case 1: { // TYPE_BOOL
|
|
850
|
+
const boolChecked = value === true ? 'checked' : '';
|
|
851
|
+
html += `<label class="toggle-switch">
|
|
852
|
+
<input type="checkbox" ${boolChecked} data-prop="${esc(name)}" data-type="${type}">
|
|
853
|
+
<span class="toggle-slider"></span>
|
|
854
|
+
</label>`;
|
|
855
|
+
break;
|
|
856
|
+
}
|
|
857
|
+
case 2: // TYPE_INT
|
|
858
|
+
if (hint === 2 && hint_string) { // PROPERTY_HINT_ENUM
|
|
859
|
+
html += renderEnumSelect(name, type, value, hint_string);
|
|
860
|
+
}
|
|
861
|
+
else if (hint === 1 && hint_string) { // PROPERTY_HINT_RANGE
|
|
862
|
+
html += renderRangeSlider(name, type, value, hint_string, true);
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
html += `<input type="number" class="property-input" value="${value ?? 0}" step="1" data-prop="${esc(name)}" data-type="${type}">`;
|
|
866
|
+
}
|
|
867
|
+
break;
|
|
868
|
+
case 3: // TYPE_FLOAT
|
|
869
|
+
if (hint === 1 && hint_string) { // PROPERTY_HINT_RANGE
|
|
870
|
+
html += renderRangeSlider(name, type, value, hint_string, false);
|
|
871
|
+
}
|
|
872
|
+
else {
|
|
873
|
+
html += `<input type="number" class="property-input" value="${value ?? 0}" step="0.01" data-prop="${esc(name)}" data-type="${type}">`;
|
|
874
|
+
}
|
|
875
|
+
break;
|
|
876
|
+
case 4: // TYPE_STRING
|
|
877
|
+
html += `<input type="text" class="property-input" value="${esc(value || '')}" data-prop="${esc(name)}" data-type="${type}">`;
|
|
878
|
+
break;
|
|
879
|
+
case 5: // TYPE_VECTOR2
|
|
880
|
+
html += renderVector2Input(name, type, value);
|
|
881
|
+
break;
|
|
882
|
+
case 6: // TYPE_VECTOR2I
|
|
883
|
+
html += renderVector2Input(name, type, value, true);
|
|
884
|
+
break;
|
|
885
|
+
case 9: // TYPE_VECTOR3
|
|
886
|
+
html += renderVector3Input(name, type, value);
|
|
887
|
+
break;
|
|
888
|
+
case 10: // TYPE_VECTOR3I
|
|
889
|
+
html += renderVector3Input(name, type, value, true);
|
|
890
|
+
break;
|
|
891
|
+
case 20: // TYPE_COLOR
|
|
892
|
+
html += renderColorInput(name, type, value);
|
|
893
|
+
break;
|
|
894
|
+
case 24: // TYPE_OBJECT (Resource)
|
|
895
|
+
if (value && value.type === 'Resource') {
|
|
896
|
+
html += `<span class="resource-path">${esc(value.path || 'null')}</span>`;
|
|
897
|
+
}
|
|
898
|
+
else {
|
|
899
|
+
html += `<span class="resource-path">null</span>`;
|
|
900
|
+
}
|
|
901
|
+
break;
|
|
902
|
+
default: {
|
|
903
|
+
const displayValue = typeof value === 'object' ? JSON.stringify(value) : String(value ?? 'null');
|
|
904
|
+
html += `<span class="property-readonly">${esc(displayValue.substring(0, 50))}${displayValue.length > 50 ? '...' : ''}</span>`;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
html += `</div></div>`;
|
|
908
|
+
return html;
|
|
909
|
+
}
|
|
910
|
+
function renderEnumSelect(name, type, value, hintString) {
|
|
911
|
+
const options = hintString.split(',').map(opt => {
|
|
912
|
+
const parts = opt.split(':');
|
|
913
|
+
return { value: parts.length > 1 ? parseInt(parts[0]) : opt.trim(), label: parts.length > 1 ? parts[1].trim() : opt.trim() };
|
|
914
|
+
});
|
|
915
|
+
let html = `<select class="property-select" data-prop="${esc(name)}" data-type="${type}">`;
|
|
916
|
+
for (const opt of options) {
|
|
917
|
+
const selected = opt.value === value ? 'selected' : '';
|
|
918
|
+
html += `<option value="${opt.value}" ${selected}>${esc(opt.label)}</option>`;
|
|
919
|
+
}
|
|
920
|
+
html += `</select>`;
|
|
921
|
+
return html;
|
|
922
|
+
}
|
|
923
|
+
function renderRangeSlider(name, type, value, hintString, isInt) {
|
|
924
|
+
const parts = hintString.split(',');
|
|
925
|
+
const min = parseFloat(parts[0]) || 0;
|
|
926
|
+
const max = parseFloat(parts[1]) || 100;
|
|
927
|
+
const step = parts[2] ? parseFloat(parts[2]) : (isInt ? 1 : 0.01);
|
|
928
|
+
return `<div class="range-input-group">
|
|
929
|
+
<input type="range" class="property-range" value="${value ?? min}" min="${min}" max="${max}" step="${step}" data-prop="${esc(name)}" data-type="${type}">
|
|
930
|
+
<input type="number" class="property-input range-number" value="${value ?? min}" min="${min}" max="${max}" step="${step}" data-prop="${esc(name)}" data-type="${type}">
|
|
931
|
+
</div>`;
|
|
932
|
+
}
|
|
933
|
+
function renderVector2Input(name, type, value, isInt = false) {
|
|
934
|
+
const x = value?.x ?? 0;
|
|
935
|
+
const y = value?.y ?? 0;
|
|
936
|
+
const step = isInt ? '1' : '0.01';
|
|
937
|
+
return `<div class="vector-input-group" data-prop="${esc(name)}" data-type="${type}">
|
|
938
|
+
<label>x</label><input type="number" class="property-input vec-x" value="${x}" step="${step}" data-component="x">
|
|
939
|
+
<label>y</label><input type="number" class="property-input vec-y" value="${y}" step="${step}" data-component="y">
|
|
940
|
+
</div>`;
|
|
941
|
+
}
|
|
942
|
+
function renderVector3Input(name, type, value, isInt = false) {
|
|
943
|
+
const x = value?.x ?? 0;
|
|
944
|
+
const y = value?.y ?? 0;
|
|
945
|
+
const z = value?.z ?? 0;
|
|
946
|
+
const step = isInt ? '1' : '0.01';
|
|
947
|
+
return `<div class="vector-input-group vec3" data-prop="${esc(name)}" data-type="${type}">
|
|
948
|
+
<label>x</label><input type="number" class="property-input vec-x" value="${x}" step="${step}" data-component="x">
|
|
949
|
+
<label>y</label><input type="number" class="property-input vec-y" value="${y}" step="${step}" data-component="y">
|
|
950
|
+
<label>z</label><input type="number" class="property-input vec-z" value="${z}" step="${step}" data-component="z">
|
|
951
|
+
</div>`;
|
|
952
|
+
}
|
|
953
|
+
function renderColorInput(name, type, value) {
|
|
954
|
+
const r = Math.round((value?.r ?? 1) * 255);
|
|
955
|
+
const g = Math.round((value?.g ?? 1) * 255);
|
|
956
|
+
const b = Math.round((value?.b ?? 1) * 255);
|
|
957
|
+
const a = value?.a ?? 1;
|
|
958
|
+
const hex = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
|
959
|
+
return `<div class="color-input-group" data-prop="${esc(name)}" data-type="${type}">
|
|
960
|
+
<input type="color" class="property-color" value="${hex}" data-prop="${esc(name)}">
|
|
961
|
+
<input type="number" class="property-input color-alpha" value="${a}" min="0" max="1" step="0.01" placeholder="α" data-component="a">
|
|
962
|
+
</div>`;
|
|
963
|
+
}
|
|
964
|
+
function initPropertyEditing(scenePath, nodePath) {
|
|
965
|
+
// Boolean toggles
|
|
966
|
+
document.querySelectorAll('.property-row input[type="checkbox"]').forEach(el => {
|
|
967
|
+
el.addEventListener('change', () => {
|
|
968
|
+
const propName = el.dataset.prop;
|
|
969
|
+
const value = el.checked;
|
|
970
|
+
saveSceneNodeProperty(scenePath, nodePath, propName, value, parseInt(el.dataset.type));
|
|
971
|
+
});
|
|
972
|
+
});
|
|
973
|
+
// Number and text inputs
|
|
974
|
+
document.querySelectorAll('.property-row input.property-input:not(.vec-x):not(.vec-y):not(.vec-z):not(.color-alpha):not(.range-number)').forEach(el => {
|
|
975
|
+
el.addEventListener('change', () => {
|
|
976
|
+
const propName = el.dataset.prop;
|
|
977
|
+
const type = parseInt(el.dataset.type);
|
|
978
|
+
let value = el.value;
|
|
979
|
+
if (type === 2 || type === 3)
|
|
980
|
+
value = parseFloat(value);
|
|
981
|
+
saveSceneNodeProperty(scenePath, nodePath, propName, value, type);
|
|
982
|
+
});
|
|
983
|
+
});
|
|
984
|
+
// Select dropdowns
|
|
985
|
+
document.querySelectorAll('.property-row select.property-select').forEach(el => {
|
|
986
|
+
el.addEventListener('change', () => {
|
|
987
|
+
const propName = el.dataset.prop;
|
|
988
|
+
const type = parseInt(el.dataset.type);
|
|
989
|
+
saveSceneNodeProperty(scenePath, nodePath, propName, parseInt(el.value), type);
|
|
990
|
+
});
|
|
991
|
+
});
|
|
992
|
+
// Range sliders (sync with number input)
|
|
993
|
+
document.querySelectorAll('.range-input-group').forEach(group => {
|
|
994
|
+
const range = group.querySelector('input[type="range"]');
|
|
995
|
+
const number = group.querySelector('input[type="number"]');
|
|
996
|
+
range.addEventListener('input', () => {
|
|
997
|
+
number.value = range.value;
|
|
998
|
+
});
|
|
999
|
+
range.addEventListener('change', () => {
|
|
1000
|
+
const propName = range.dataset.prop;
|
|
1001
|
+
const type = parseInt(range.dataset.type);
|
|
1002
|
+
saveSceneNodeProperty(scenePath, nodePath, propName, parseFloat(range.value), type);
|
|
1003
|
+
});
|
|
1004
|
+
number.addEventListener('change', () => {
|
|
1005
|
+
range.value = number.value;
|
|
1006
|
+
const propName = number.dataset.prop;
|
|
1007
|
+
const type = parseInt(number.dataset.type);
|
|
1008
|
+
saveSceneNodeProperty(scenePath, nodePath, propName, parseFloat(number.value), type);
|
|
1009
|
+
});
|
|
1010
|
+
});
|
|
1011
|
+
// Vector inputs
|
|
1012
|
+
document.querySelectorAll('.vector-input-group').forEach(group => {
|
|
1013
|
+
const propName = group.dataset.prop;
|
|
1014
|
+
const type = parseInt(group.dataset.type);
|
|
1015
|
+
const inputs = group.querySelectorAll('input');
|
|
1016
|
+
inputs.forEach(input => {
|
|
1017
|
+
input.addEventListener('change', () => {
|
|
1018
|
+
const x = parseFloat(group.querySelector('.vec-x').value);
|
|
1019
|
+
const y = parseFloat(group.querySelector('.vec-y').value);
|
|
1020
|
+
const zInput = group.querySelector('.vec-z');
|
|
1021
|
+
const value = zInput ? { x, y, z: parseFloat(zInput.value) } : { x, y };
|
|
1022
|
+
saveSceneNodeProperty(scenePath, nodePath, propName, value, type);
|
|
1023
|
+
});
|
|
1024
|
+
});
|
|
1025
|
+
});
|
|
1026
|
+
// Color inputs
|
|
1027
|
+
document.querySelectorAll('.color-input-group').forEach(group => {
|
|
1028
|
+
const propName = group.dataset.prop;
|
|
1029
|
+
const type = parseInt(group.dataset.type);
|
|
1030
|
+
const colorInput = group.querySelector('input[type="color"]');
|
|
1031
|
+
const alphaInput = group.querySelector('.color-alpha');
|
|
1032
|
+
const saveColor = () => {
|
|
1033
|
+
const hex = colorInput.value;
|
|
1034
|
+
const r = parseInt(hex.substr(1, 2), 16) / 255;
|
|
1035
|
+
const g = parseInt(hex.substr(3, 2), 16) / 255;
|
|
1036
|
+
const b = parseInt(hex.substr(5, 2), 16) / 255;
|
|
1037
|
+
const a = parseFloat(alphaInput.value);
|
|
1038
|
+
saveSceneNodeProperty(scenePath, nodePath, propName, { r, g, b, a }, type);
|
|
1039
|
+
};
|
|
1040
|
+
colorInput.addEventListener('change', saveColor);
|
|
1041
|
+
alphaInput.addEventListener('change', saveColor);
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
async function saveSceneNodeProperty(scenePath, nodePath, propName, value, valueType) {
|
|
1045
|
+
console.log(`Saving property: ${propName} =`, value);
|
|
1046
|
+
try {
|
|
1047
|
+
const result = await sendCommand('set_scene_node_property', {
|
|
1048
|
+
scene_path: scenePath,
|
|
1049
|
+
node_path: nodePath,
|
|
1050
|
+
property_name: propName,
|
|
1051
|
+
value: value,
|
|
1052
|
+
value_type: valueType
|
|
1053
|
+
});
|
|
1054
|
+
if (result.ok) {
|
|
1055
|
+
console.log(`Property ${propName} saved successfully`);
|
|
1056
|
+
}
|
|
1057
|
+
else {
|
|
1058
|
+
console.error('Failed to save property:', result.error);
|
|
1059
|
+
alert('Failed to save: ' + (result.error || 'Unknown error'));
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
catch (err) {
|
|
1063
|
+
console.error('Failed to save property:', err);
|
|
1064
|
+
alert('Failed to save: ' + err.message);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
// Toggle property section visibility
|
|
1068
|
+
window.togglePropertySection = function (header) {
|
|
1069
|
+
const section = header.closest('.property-section');
|
|
1070
|
+
section.classList.toggle('collapsed');
|
|
1071
|
+
};
|
|
1072
|
+
// Jump to script in scripts view
|
|
1073
|
+
window.jumpToScript = function (scriptPath) {
|
|
1074
|
+
// Switch to scripts view and select the script
|
|
1075
|
+
window.switchView('scripts');
|
|
1076
|
+
// Find and select the node
|
|
1077
|
+
const scriptNode = nodes.find(n => n.path === scriptPath);
|
|
1078
|
+
if (scriptNode) {
|
|
1079
|
+
setTimeout(() => openPanel(scriptNode), 100);
|
|
1080
|
+
}
|
|
1081
|
+
};
|
|
1082
|
+
// Jump to scene in scenes view
|
|
1083
|
+
window.jumpToScene = function (scenePath) {
|
|
1084
|
+
// Switch to scenes view and expand the scene
|
|
1085
|
+
closePanel();
|
|
1086
|
+
window.switchView('scenes');
|
|
1087
|
+
// Trigger scene expansion after view switch
|
|
1088
|
+
setTimeout(() => {
|
|
1089
|
+
window.expandSceneFromPanel && window.expandSceneFromPanel(scenePath);
|
|
1090
|
+
}, 100);
|
|
1091
|
+
};
|