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.
@@ -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">&rarr;</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})">&times;</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
+ };