iobroker.mywebui 1.42.16 → 1.42.17
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/io-package.json
CHANGED
package/package.json
CHANGED
|
@@ -6,8 +6,8 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
|
|
|
6
6
|
static template = html`
|
|
7
7
|
<div id="root" style="width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden;background:#1e1e1e;font-family:'Segoe UI',sans-serif;font-size:12px;">
|
|
8
8
|
|
|
9
|
-
<!-- Toolbar
|
|
10
|
-
<div style="height:40px;background:#252526;border-bottom:1px solid #3c3c3c;display:flex;align-items:center;padding:0 8px;gap:4px;">
|
|
9
|
+
<!-- Toolbar -->
|
|
10
|
+
<div style="height:40px;background:#252526;border-bottom:1px solid #3c3c3c;display:flex;align-items:center;padding:0 8px;gap:4px;flex-shrink:0;">
|
|
11
11
|
<span style="color:#ccc;font-size:11px;font-weight:bold;margin-right:8px;letter-spacing:0.5px;">3D EDITOR</span>
|
|
12
12
|
<button id="saveBtn" class="tb-btn primary">💾 Save</button>
|
|
13
13
|
<div class="tb-sep"></div>
|
|
@@ -22,44 +22,62 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
|
|
|
22
22
|
</div>
|
|
23
23
|
|
|
24
24
|
<!-- 3-column body -->
|
|
25
|
-
<div style="flex:1;display:flex;overflow:hidden;">
|
|
25
|
+
<div style="flex:1;display:flex;overflow:hidden;min-height:0;">
|
|
26
26
|
|
|
27
|
-
<!-- LEFT: Scene tree
|
|
28
|
-
<div id="leftPanel" style="width:
|
|
29
|
-
<!-- Tab row: All / Assets / Lights / Signals -->
|
|
30
|
-
<div style="display:flex;background:#2d2d2d;border-bottom:1px solid #3c3c3c;">
|
|
27
|
+
<!-- LEFT: Scene tree + Library -->
|
|
28
|
+
<div id="leftPanel" style="width:240px;min-width:160px;background:#252526;border-right:1px solid #3c3c3c;display:flex;flex-direction:column;overflow:hidden;">
|
|
29
|
+
<!-- Tab row: All / Assets / Lights / Signals / Library -->
|
|
30
|
+
<div style="display:flex;background:#2d2d2d;border-bottom:1px solid #3c3c3c;flex-shrink:0;">
|
|
31
31
|
<button id="tabAll" class="lt-tab active" data-tab="all">All</button>
|
|
32
32
|
<button id="tabAssets" class="lt-tab" data-tab="assets">Assets</button>
|
|
33
33
|
<button id="tabLights" class="lt-tab" data-tab="lights">Lights</button>
|
|
34
34
|
<button id="tabSignals" class="lt-tab" data-tab="signals">Signals</button>
|
|
35
|
+
<button id="tabLibrary" class="lt-tab" data-tab="library" style="color:#9cdcfe;" title="3D Components Library">Lib</button>
|
|
35
36
|
</div>
|
|
36
37
|
<!-- Search -->
|
|
37
|
-
<div style="padding:4px 6px;border-bottom:1px solid #3c3c3c;">
|
|
38
|
-
<input id="treeSearch" type="text" placeholder="Search
|
|
38
|
+
<div style="padding:4px 6px;border-bottom:1px solid #3c3c3c;flex-shrink:0;">
|
|
39
|
+
<input id="treeSearch" type="text" placeholder="Search..." style="width:100%;box-sizing:border-box;background:#3c3c3c;border:1px solid #555;color:#ccc;padding:3px 6px;border-radius:3px;font-size:11px;">
|
|
39
40
|
</div>
|
|
40
|
-
<!-- Tree content -->
|
|
41
|
+
<!-- Tree / Library content -->
|
|
41
42
|
<div id="treeContent" style="flex:1;overflow:auto;padding:4px 0;"></div>
|
|
43
|
+
<!-- Library quick-add button (shown in library tab) -->
|
|
44
|
+
<div id="libAddRow" style="display:none;padding:4px 6px;border-top:1px solid #3c3c3c;flex-shrink:0;">
|
|
45
|
+
<button id="addLibraryBtn" class="tb-btn" style="width:100%;font-size:10px;">+ Import GLB as Component</button>
|
|
46
|
+
</div>
|
|
42
47
|
</div>
|
|
43
48
|
|
|
44
49
|
<!-- CENTER: Viewport -->
|
|
45
|
-
<div id="viewport" style="flex:1;position:relative;overflow:hidden;background:#333;"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
<
|
|
53
|
-
</div>
|
|
54
|
-
<!-- Search library -->
|
|
55
|
-
<div style="padding:4px 6px;border-bottom:1px solid #3c3c3c;">
|
|
56
|
-
<input id="libSearch" type="text" placeholder="Search..." style="width:100%;box-sizing:border-box;background:#3c3c3c;border:1px solid #555;color:#ccc;padding:3px 6px;border-radius:3px;font-size:11px;">
|
|
50
|
+
<div id="viewport" style="flex:1;position:relative;overflow:hidden;background:#333;">
|
|
51
|
+
<!-- Loading overlay -->
|
|
52
|
+
<div id="loadOverlay" style="position:absolute;inset:0;background:#1e1e1e;display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:100;transition:opacity 0.3s;">
|
|
53
|
+
<div style="color:#9cdcfe;font-size:16px;font-weight:bold;margin-bottom:16px;letter-spacing:1px;">3D EDITOR</div>
|
|
54
|
+
<div style="width:220px;height:4px;background:#3c3c3c;border-radius:2px;overflow:hidden;margin-bottom:10px;">
|
|
55
|
+
<div id="loadBarFill" style="height:100%;background:linear-gradient(90deg,#0e639c,#4ec9b0);width:0%;transition:width 0.4s ease;"></div>
|
|
56
|
+
</div>
|
|
57
|
+
<div id="loadText" style="color:#666;font-size:11px;">Initializing...</div>
|
|
57
58
|
</div>
|
|
58
|
-
<!-- Thumbnail grid -->
|
|
59
|
-
<div id="libraryGrid" style="flex:1;overflow:auto;padding:6px;"></div>
|
|
60
59
|
</div>
|
|
61
60
|
|
|
62
61
|
</div>
|
|
62
|
+
|
|
63
|
+
<!-- Bottom: Script / Style editor panel -->
|
|
64
|
+
<div id="bottomPanel" style="height:0;flex-shrink:0;background:#1e1e1e;border-top:2px solid #3c3c3c;display:flex;flex-direction:column;overflow:hidden;transition:height 0.15s;">
|
|
65
|
+
<div style="display:flex;background:#252526;border-bottom:1px solid #3c3c3c;align-items:center;flex-shrink:0;">
|
|
66
|
+
<button id="btTabScript" class="bt-tab active" data-btab="script">JS Script</button>
|
|
67
|
+
<button id="btTabStyle" class="bt-tab" data-btab="style">CSS Style</button>
|
|
68
|
+
<div style="flex:1;"></div>
|
|
69
|
+
<span style="color:#555;font-size:10px;margin-right:6px;">Ctrl+Enter = Run</span>
|
|
70
|
+
<button id="runScriptBtn" class="tb-btn" style="padding:2px 10px;font-size:10px;margin-right:4px;">▶ Run</button>
|
|
71
|
+
<button id="scriptErrBtn" id="scriptErrBtn" style="display:none;padding:2px 8px;font-size:10px;background:#5a1a1a;color:#f44747;border:1px solid #8a2a2a;border-radius:3px;cursor:pointer;margin-right:4px;" title="Script error"></button>
|
|
72
|
+
</div>
|
|
73
|
+
<textarea id="scriptEditor" spellcheck="false" placeholder="// Three.js API available: // scene, camera, THREE, renderer, controls // assets → Map(name → Object3D) // mixers → Map(assetId → {mixer, clips, actions}) // driveEngine, subscribe(signal, cb), setState(signal, val) // onFrame(fn) → called every animation frame(dt, timestamp) // onSelect(fn) → called when node selected(node, assetData) // Example: // const robot = assets.get('Robot'); // onFrame(dt => { robot.rotation.y += dt; });"></textarea>
|
|
74
|
+
<textarea id="styleEditor" spellcheck="false" placeholder="/* CSS for overlay elements */ /* .label-3d { color: white; background: rgba(0,0,0,0.6); } */"></textarea>
|
|
75
|
+
</div>
|
|
76
|
+
<!-- Bottom panel toggle (draggable handle) -->
|
|
77
|
+
<div id="bottomHandle" style="height:6px;background:#3c3c3c;cursor:ns-resize;flex-shrink:0;display:flex;align-items:center;justify-content:center;">
|
|
78
|
+
<div style="width:40px;height:2px;background:#555;border-radius:1px;"></div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
63
81
|
</div>
|
|
64
82
|
`;
|
|
65
83
|
|
|
@@ -149,9 +167,57 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
|
|
|
149
167
|
text-align: center;
|
|
150
168
|
}
|
|
151
169
|
.lib-card:hover { border-color:#9cdcfe; background:#094771; }
|
|
152
|
-
.lib-card .lib-thumb { font-size:
|
|
170
|
+
.lib-card .lib-thumb { font-size:28px; margin-bottom:4px; line-height:1; }
|
|
153
171
|
.lib-card .lib-name { color:#ccc; font-size:10px; word-break:break-word; line-height:1.2; }
|
|
154
172
|
.lib-card .lib-type { color:#666; font-size:9px; margin-top:2px; }
|
|
173
|
+
|
|
174
|
+
.bt-tab {
|
|
175
|
+
padding: 5px 12px;
|
|
176
|
+
background: transparent;
|
|
177
|
+
color: #888;
|
|
178
|
+
border: none;
|
|
179
|
+
border-bottom: 2px solid transparent;
|
|
180
|
+
cursor: pointer;
|
|
181
|
+
font-size: 11px;
|
|
182
|
+
font-weight: bold;
|
|
183
|
+
}
|
|
184
|
+
.bt-tab:hover { color:#ccc; }
|
|
185
|
+
.bt-tab.active { color:#9cdcfe; border-bottom-color:#9cdcfe; }
|
|
186
|
+
|
|
187
|
+
#scriptEditor, #styleEditor {
|
|
188
|
+
flex: 1;
|
|
189
|
+
width: 100%;
|
|
190
|
+
background: #1e1e1e;
|
|
191
|
+
color: #d4d4d4;
|
|
192
|
+
border: none;
|
|
193
|
+
resize: none;
|
|
194
|
+
font-family: 'Consolas','Courier New',monospace;
|
|
195
|
+
font-size: 12px;
|
|
196
|
+
line-height: 1.5;
|
|
197
|
+
padding: 8px 12px;
|
|
198
|
+
box-sizing: border-box;
|
|
199
|
+
outline: none;
|
|
200
|
+
tab-size: 4;
|
|
201
|
+
display: none;
|
|
202
|
+
}
|
|
203
|
+
#scriptEditor.active, #styleEditor.active { display: block; }
|
|
204
|
+
|
|
205
|
+
.lib-item {
|
|
206
|
+
display: flex;
|
|
207
|
+
align-items: center;
|
|
208
|
+
padding: 5px 8px;
|
|
209
|
+
color: #ccc;
|
|
210
|
+
cursor: pointer;
|
|
211
|
+
border-left: 2px solid transparent;
|
|
212
|
+
gap: 6px;
|
|
213
|
+
}
|
|
214
|
+
.lib-item:hover { background:#2d2d2d; border-left-color:#9cdcfe; }
|
|
215
|
+
.lib-item .lib-icon { font-size:16px; flex-shrink:0; }
|
|
216
|
+
.lib-item .lib-info { flex:1; overflow:hidden; }
|
|
217
|
+
.lib-item .lib-name { font-size:11px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
218
|
+
.lib-item .lib-meta { font-size:9px; color:#555; }
|
|
219
|
+
.lib-item .lib-add { opacity:0; font-size:11px; color:#4ec9b0; padding:0 4px; }
|
|
220
|
+
.lib-item:hover .lib-add { opacity:1; }
|
|
155
221
|
`;
|
|
156
222
|
|
|
157
223
|
constructor() {
|
|
@@ -175,6 +241,9 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
|
|
|
175
241
|
this._threeObjects = [];
|
|
176
242
|
this._mixers = new Map(); // assetId → { mixer, clips, actions }
|
|
177
243
|
this._lastAnimTs = null;
|
|
244
|
+
this._scriptFrameCallbacks = [];
|
|
245
|
+
this._scriptSelectCallbacks = [];
|
|
246
|
+
this._scriptUnsubs = [];
|
|
178
247
|
}
|
|
179
248
|
|
|
180
249
|
async connectedCallback() {
|
|
@@ -183,25 +252,53 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
|
|
|
183
252
|
|
|
184
253
|
async init() {
|
|
185
254
|
try {
|
|
186
|
-
this.
|
|
187
|
-
|
|
188
|
-
const { OrbitControls } = await
|
|
189
|
-
|
|
255
|
+
this._setLoadProgress(5, 'Loading Three.js...');
|
|
256
|
+
// Parallel import - 2-3x faster than sequential
|
|
257
|
+
const [THREE, { OrbitControls }, { GLTFLoader }] = await Promise.all([
|
|
258
|
+
import('/mywebui.0.widgets/node_modules/three/build/three.module.js'),
|
|
259
|
+
import('/mywebui.0.widgets/node_modules/three/examples/jsm/controls/OrbitControls.js'),
|
|
260
|
+
import('/mywebui.0.widgets/node_modules/three/examples/jsm/loaders/GLTFLoader.js'),
|
|
261
|
+
]);
|
|
190
262
|
this.THREE = THREE;
|
|
191
263
|
this.gltfLoader = new GLTFLoader();
|
|
192
|
-
|
|
264
|
+
|
|
265
|
+
this._setLoadProgress(40, 'Setting up renderer...');
|
|
193
266
|
this.initThreeJS(THREE, OrbitControls);
|
|
267
|
+
|
|
268
|
+
this._setLoadProgress(65, 'Loading scene...');
|
|
194
269
|
const sceneName = this.getAttribute('scene-name') || 'new-scene';
|
|
195
270
|
await this.loadScene(sceneName);
|
|
196
|
-
|
|
197
|
-
|
|
271
|
+
|
|
272
|
+
this._setLoadProgress(90, 'Starting...');
|
|
198
273
|
this.setupEventListeners();
|
|
274
|
+
this._setupBottomPanel();
|
|
275
|
+
this._runUserScript();
|
|
276
|
+
|
|
277
|
+
this._setLoadProgress(100, 'Ready');
|
|
278
|
+
this._setStatus('Ready');
|
|
279
|
+
setTimeout(() => { this._setStatus(''); this._hideLoadOverlay(); }, 400);
|
|
199
280
|
} catch (err) {
|
|
200
281
|
console.error('3D Editor init failed:', err);
|
|
282
|
+
this._setLoadProgress(0, 'Error: ' + err.message);
|
|
201
283
|
this._setStatus('Error: ' + err.message);
|
|
202
284
|
}
|
|
203
285
|
}
|
|
204
286
|
|
|
287
|
+
_setLoadProgress(pct, msg) {
|
|
288
|
+
const fill = this._getDomElement('loadBarFill');
|
|
289
|
+
const text = this._getDomElement('loadText');
|
|
290
|
+
if (fill) fill.style.width = pct + '%';
|
|
291
|
+
if (text) text.textContent = msg;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
_hideLoadOverlay() {
|
|
295
|
+
const overlay = this._getDomElement('loadOverlay');
|
|
296
|
+
if (overlay) {
|
|
297
|
+
overlay.style.opacity = '0';
|
|
298
|
+
setTimeout(() => { if (overlay.parentNode) overlay.parentNode.removeChild(overlay); }, 350);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
205
302
|
_setStatus(msg) {
|
|
206
303
|
const el = this._getDomElement('statusLabel');
|
|
207
304
|
if (el) el.textContent = msg;
|
|
@@ -256,6 +353,10 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
|
|
|
256
353
|
this._lastAnimTs = ts;
|
|
257
354
|
if (this.driveEngine) this.driveEngine.tick(dt);
|
|
258
355
|
for (const { mixer } of this._mixers.values()) mixer.update(dt);
|
|
356
|
+
// User script frame callbacks
|
|
357
|
+
for (const fn of this._scriptFrameCallbacks) {
|
|
358
|
+
try { fn(dt, ts); } catch (_) {}
|
|
359
|
+
}
|
|
259
360
|
this.controls.update();
|
|
260
361
|
this.renderer.render(this.scene, this.camera);
|
|
261
362
|
};
|
|
@@ -280,6 +381,9 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
|
|
|
280
381
|
try {
|
|
281
382
|
this.sceneData = await iobrokerHandler.get3DScreen(sceneName);
|
|
282
383
|
if (!this.sceneData) { console.warn('Scene not found:', sceneName); return; }
|
|
384
|
+
// Ensure script/style fields exist
|
|
385
|
+
if (!this.sceneData.script) this.sceneData.script = '';
|
|
386
|
+
if (!this.sceneData.style) this.sceneData.style = '';
|
|
283
387
|
console.log('Scene loaded:', sceneName);
|
|
284
388
|
this._buildSceneView();
|
|
285
389
|
} catch (err) {
|
|
@@ -430,6 +534,10 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
|
|
|
430
534
|
this._expandedAssets.add(assetRoot.userData.assetData?.id);
|
|
431
535
|
this.refreshTree();
|
|
432
536
|
this._showNodeProperties(node, assetRoot);
|
|
537
|
+
// User script select callbacks
|
|
538
|
+
for (const fn of this._scriptSelectCallbacks) {
|
|
539
|
+
try { fn(node, assetRoot.userData.assetData); } catch (_) {}
|
|
540
|
+
}
|
|
433
541
|
}
|
|
434
542
|
|
|
435
543
|
selectObject(obj) {
|
|
@@ -546,11 +654,152 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
|
|
|
546
654
|
return result;
|
|
547
655
|
}
|
|
548
656
|
|
|
657
|
+
// ── Bottom Script/Style panel ─────────────────────────
|
|
658
|
+
|
|
659
|
+
_setupBottomPanel() {
|
|
660
|
+
const root = this._getDomElement('root');
|
|
661
|
+
const panel = this._getDomElement('bottomPanel');
|
|
662
|
+
const handle = this._getDomElement('bottomHandle');
|
|
663
|
+
const scriptTA = this._getDomElement('scriptEditor');
|
|
664
|
+
const styleTA = this._getDomElement('styleEditor');
|
|
665
|
+
|
|
666
|
+
// Populate from sceneData
|
|
667
|
+
if (scriptTA && this.sceneData?.script) scriptTA.value = this.sceneData.script;
|
|
668
|
+
if (styleTA && this.sceneData?.style) styleTA.value = this.sceneData.style;
|
|
669
|
+
|
|
670
|
+
// Set initial active editor
|
|
671
|
+
if (scriptTA) scriptTA.classList.add('active');
|
|
672
|
+
|
|
673
|
+
// Tab switching
|
|
674
|
+
['btTabScript','btTabStyle'].forEach(id => {
|
|
675
|
+
const btn = this._getDomElement(id);
|
|
676
|
+
if (!btn) return;
|
|
677
|
+
btn.addEventListener('click', () => {
|
|
678
|
+
this.shadowRoot.querySelectorAll('.bt-tab').forEach(b => b.classList.remove('active'));
|
|
679
|
+
btn.classList.add('active');
|
|
680
|
+
const tab = btn.dataset.btab;
|
|
681
|
+
if (scriptTA) scriptTA.classList.toggle('active', tab === 'script');
|
|
682
|
+
if (styleTA) styleTA.classList.toggle('active', tab === 'style');
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
// Run button
|
|
687
|
+
this._getDomElement('runScriptBtn')?.addEventListener('click', () => this._runUserScript());
|
|
688
|
+
|
|
689
|
+
// Ctrl+Enter to run while editing
|
|
690
|
+
scriptTA?.addEventListener('keydown', (e) => {
|
|
691
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); this._runUserScript(); }
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
// Auto-save script/style to sceneData on change
|
|
695
|
+
scriptTA?.addEventListener('input', () => { if (this.sceneData) this.sceneData.script = scriptTA.value; });
|
|
696
|
+
styleTA?.addEventListener('input', () => { if (this.sceneData) this.sceneData.style = styleTA.value; this._applyUserStyle(); });
|
|
697
|
+
|
|
698
|
+
// Draggable resize handle
|
|
699
|
+
let dragStart = null;
|
|
700
|
+
let startH = 0;
|
|
701
|
+
handle?.addEventListener('mousedown', (e) => {
|
|
702
|
+
dragStart = e.clientY;
|
|
703
|
+
startH = panel ? panel.offsetHeight : 0;
|
|
704
|
+
// If panel collapsed, open it
|
|
705
|
+
if (startH < 20) startH = 0;
|
|
706
|
+
document.addEventListener('mousemove', onDrag);
|
|
707
|
+
document.addEventListener('mouseup', onUp);
|
|
708
|
+
});
|
|
709
|
+
const onDrag = (e) => {
|
|
710
|
+
if (dragStart === null) return;
|
|
711
|
+
const delta = dragStart - e.clientY;
|
|
712
|
+
const h = Math.max(0, Math.min(startH + delta, 500));
|
|
713
|
+
if (panel) panel.style.height = h + 'px';
|
|
714
|
+
};
|
|
715
|
+
const onUp = () => { dragStart = null; document.removeEventListener('mousemove', onDrag); document.removeEventListener('mouseup', onUp); };
|
|
716
|
+
|
|
717
|
+
// Click handle to toggle
|
|
718
|
+
handle?.addEventListener('click', (e) => {
|
|
719
|
+
if (!panel) return;
|
|
720
|
+
const h = panel.offsetHeight;
|
|
721
|
+
panel.style.height = h < 20 ? '200px' : '0';
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
_applyUserStyle() {
|
|
726
|
+
const styleTA = this._getDomElement('styleEditor');
|
|
727
|
+
const css = styleTA?.value || this.sceneData?.style || '';
|
|
728
|
+
let styleEl = this.shadowRoot.getElementById('userStyle3d');
|
|
729
|
+
if (!styleEl) {
|
|
730
|
+
styleEl = document.createElement('style');
|
|
731
|
+
styleEl.id = 'userStyle3d';
|
|
732
|
+
this.shadowRoot.appendChild(styleEl);
|
|
733
|
+
}
|
|
734
|
+
styleEl.textContent = css;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
_runUserScript() {
|
|
738
|
+
// Clear previous script state
|
|
739
|
+
for (const unsub of this._scriptUnsubs) try { unsub(); } catch (_) {}
|
|
740
|
+
this._scriptUnsubs = [];
|
|
741
|
+
this._scriptFrameCallbacks = [];
|
|
742
|
+
this._scriptSelectCallbacks = [];
|
|
743
|
+
|
|
744
|
+
const scriptTA = this._getDomElement('scriptEditor');
|
|
745
|
+
const errBtn = this._getDomElement('scriptErrBtn');
|
|
746
|
+
const script = scriptTA?.value || this.sceneData?.script || '';
|
|
747
|
+
if (!script.trim()) return;
|
|
748
|
+
|
|
749
|
+
// Build assets map: name/id → Three.js Object3D
|
|
750
|
+
const assets = new Map();
|
|
751
|
+
for (const obj of this.scene.children) {
|
|
752
|
+
if (obj.userData.assetData) {
|
|
753
|
+
assets.set(obj.userData.assetData.name, obj);
|
|
754
|
+
assets.set(obj.userData.assetData.id, obj);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const self = this;
|
|
759
|
+
const ctx = {
|
|
760
|
+
THREE: this.THREE,
|
|
761
|
+
scene: this.scene,
|
|
762
|
+
camera: this.camera,
|
|
763
|
+
renderer: this.renderer,
|
|
764
|
+
controls: this.controls,
|
|
765
|
+
assets,
|
|
766
|
+
mixers: this._mixers,
|
|
767
|
+
driveEngine: this.driveEngine,
|
|
768
|
+
subscribe(signal, cb) {
|
|
769
|
+
const unsub = self.driveEngine.bus.subscribe(signal, cb);
|
|
770
|
+
self._scriptUnsubs.push(unsub);
|
|
771
|
+
return unsub;
|
|
772
|
+
},
|
|
773
|
+
setState(signal, val) { iobrokerHandler.setState(signal, val); },
|
|
774
|
+
getState(signal) { return self.driveEngine.bus.get(signal); },
|
|
775
|
+
onFrame(fn) { self._scriptFrameCallbacks.push(fn); },
|
|
776
|
+
onSelect(fn) { self._scriptSelectCallbacks.push(fn); },
|
|
777
|
+
log(...args) { console.log('[3D Script]', ...args); },
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
if (errBtn) errBtn.style.display = 'none';
|
|
781
|
+
try {
|
|
782
|
+
const fn = new Function(...Object.keys(ctx), script);
|
|
783
|
+
fn(...Object.values(ctx));
|
|
784
|
+
} catch (err) {
|
|
785
|
+
console.error('[3D Script] Error:', err);
|
|
786
|
+
if (errBtn) { errBtn.style.display = ''; errBtn.textContent = '⚠ ' + err.message; }
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
549
790
|
// ── Scene tree (left panel) ───────────────────────────
|
|
550
791
|
|
|
551
792
|
refreshTree() {
|
|
552
793
|
const el = this._getDomElement('treeContent');
|
|
553
|
-
if (!el
|
|
794
|
+
if (!el) return;
|
|
795
|
+
|
|
796
|
+
// Library tab - show reusable 3D components
|
|
797
|
+
if (this._activeTab === 'library') {
|
|
798
|
+
this._renderLibraryTab(el);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (!this.sceneData) return;
|
|
554
803
|
const search = (this._getDomElement('treeSearch')?.value ?? '').toLowerCase();
|
|
555
804
|
el.innerHTML = '';
|
|
556
805
|
|
|
@@ -714,42 +963,80 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
|
|
|
714
963
|
}
|
|
715
964
|
}
|
|
716
965
|
|
|
717
|
-
// ── Library (right panel)
|
|
966
|
+
// ── Library (right panel - kept for backward compat) ───
|
|
718
967
|
|
|
719
968
|
refreshLibrary() {
|
|
720
969
|
const el = this._getDomElement('libraryGrid');
|
|
721
970
|
if (!el || !this.sceneData) return;
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
971
|
+
// Library now lives primarily in left panel tab
|
|
972
|
+
// Right panel libraryGrid kept but hidden
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// ── Library tab in left panel ─────────────────────────
|
|
976
|
+
|
|
977
|
+
_renderLibraryTab(el) {
|
|
978
|
+
el.innerHTML = '';
|
|
979
|
+
const search = (this._getDomElement('treeSearch')?.value ?? '').toLowerCase();
|
|
980
|
+
const assets = Array.isArray(this.sceneData?.assets) ? this.sceneData.assets : [];
|
|
981
|
+
const filtered = assets.filter(a => !search || a.name.toLowerCase().includes(search));
|
|
725
982
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
983
|
+
// Show/hide add button
|
|
984
|
+
const addRow = this._getDomElement('libAddRow');
|
|
985
|
+
if (addRow) addRow.style.display = '';
|
|
986
|
+
|
|
987
|
+
if (filtered.length === 0) {
|
|
988
|
+
el.innerHTML = `<div style="color:#555;padding:16px 8px;font-size:11px;text-align:center;line-height:1.8;">
|
|
989
|
+
No 3D components yet.<br>
|
|
990
|
+
<span style="color:#9cdcfe;">Import GLB</span> to create<br>a reusable component.
|
|
729
991
|
</div>`;
|
|
730
992
|
return;
|
|
731
993
|
}
|
|
732
994
|
|
|
733
|
-
|
|
734
|
-
el.style.gridTemplateColumns = '1fr 1fr';
|
|
735
|
-
el.style.gap = '6px';
|
|
736
|
-
el.innerHTML = '';
|
|
737
|
-
|
|
738
|
-
for (const asset of assets) {
|
|
739
|
-
const card = document.createElement('div');
|
|
740
|
-
card.className = 'lib-card';
|
|
995
|
+
for (const asset of filtered) {
|
|
741
996
|
const shortName = asset.name.replace(/\.(glb|gltf)$/i, '');
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
997
|
+
const driveCnt = asset.drives?.length ?? 0;
|
|
998
|
+
const clipCnt = asset.availableClips?.length ?? 0;
|
|
999
|
+
const interCnt = Object.keys(asset.interactions ?? {}).length;
|
|
1000
|
+
const meta = [
|
|
1001
|
+
driveCnt ? `${driveCnt}D` : '',
|
|
1002
|
+
clipCnt ? `${clipCnt}A` : '',
|
|
1003
|
+
interCnt ? `${interCnt}I` : '',
|
|
1004
|
+
].filter(Boolean).join(' · ') || 'GLB';
|
|
1005
|
+
|
|
1006
|
+
const item = document.createElement('div');
|
|
1007
|
+
item.className = 'lib-item';
|
|
1008
|
+
item.innerHTML = `
|
|
1009
|
+
<span class="lib-icon">📦</span>
|
|
1010
|
+
<div class="lib-info">
|
|
1011
|
+
<div class="lib-name" title="${asset.name}">${shortName}</div>
|
|
1012
|
+
<div class="lib-meta">${meta}</div>
|
|
1013
|
+
</div>
|
|
1014
|
+
<span class="lib-add" title="Add instance to scene">+</span>
|
|
746
1015
|
`;
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1016
|
+
item.querySelector('.lib-add')?.addEventListener('click', (e) => {
|
|
1017
|
+
e.stopPropagation();
|
|
1018
|
+
this._addAssetInstance(asset);
|
|
1019
|
+
});
|
|
1020
|
+
item.addEventListener('click', () => this._selectAsset(asset));
|
|
1021
|
+
el.appendChild(item);
|
|
750
1022
|
}
|
|
751
1023
|
}
|
|
752
1024
|
|
|
1025
|
+
_addAssetInstance(templateAsset) {
|
|
1026
|
+
// Duplicate the asset as a new instance in the scene
|
|
1027
|
+
const newId = 'asset_' + Date.now().toString(36);
|
|
1028
|
+
const instance = JSON.parse(JSON.stringify(templateAsset));
|
|
1029
|
+
instance.id = newId;
|
|
1030
|
+
instance.name = templateAsset.name + '_' + newId.slice(-4);
|
|
1031
|
+
instance.position = { x: Math.random() * 2 - 1, y: 0, z: Math.random() * 2 - 1 };
|
|
1032
|
+
if (!this.sceneData.assets) this.sceneData.assets = [];
|
|
1033
|
+
this.sceneData.assets.push(instance);
|
|
1034
|
+
this.loadAsset(instance);
|
|
1035
|
+
this.refreshTree();
|
|
1036
|
+
this._setStatus('Instance added: ' + instance.name);
|
|
1037
|
+
setTimeout(() => this._setStatus(''), 2000);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
753
1040
|
// ── File input ────────────────────────────────────────
|
|
754
1041
|
|
|
755
1042
|
onFileSelected(event) {
|
|
@@ -839,19 +1126,25 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
|
|
|
839
1126
|
this._getDomElement('gridToggle').addEventListener('click', () => this.toggleGrid());
|
|
840
1127
|
this._getDomElement('axesToggle').addEventListener('click', () => this.toggleAxes());
|
|
841
1128
|
|
|
842
|
-
// Tree tabs
|
|
843
|
-
['tabAll','tabAssets','tabLights','tabSignals'].forEach(id => {
|
|
844
|
-
this._getDomElement(id)
|
|
1129
|
+
// Tree tabs (including Library)
|
|
1130
|
+
['tabAll','tabAssets','tabLights','tabSignals','tabLibrary'].forEach(id => {
|
|
1131
|
+
const el = this._getDomElement(id);
|
|
1132
|
+
if (!el) return;
|
|
1133
|
+
el.addEventListener('click', (e) => {
|
|
845
1134
|
this._activeTab = e.target.dataset.tab;
|
|
846
1135
|
this.shadowRoot.querySelectorAll('.lt-tab').forEach(t => t.classList.remove('active'));
|
|
847
1136
|
e.target.classList.add('active');
|
|
1137
|
+
const addRow = this._getDomElement('libAddRow');
|
|
1138
|
+
if (addRow) addRow.style.display = this._activeTab === 'library' ? '' : 'none';
|
|
848
1139
|
this.refreshTree();
|
|
849
1140
|
});
|
|
850
1141
|
});
|
|
851
1142
|
|
|
1143
|
+
// Library add button
|
|
1144
|
+
this._getDomElement('addLibraryBtn')?.addEventListener('click', () => this._getDomElement('fileInput').click());
|
|
1145
|
+
|
|
852
1146
|
// Search
|
|
853
|
-
this._getDomElement('treeSearch')
|
|
854
|
-
this._getDomElement('libSearch').addEventListener('input', () => this.refreshLibrary());
|
|
1147
|
+
this._getDomElement('treeSearch')?.addEventListener('input', () => this.refreshTree());
|
|
855
1148
|
}
|
|
856
1149
|
|
|
857
1150
|
disconnectedCallback() {
|
|
@@ -859,6 +1152,8 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
|
|
|
859
1152
|
if (this.driveEngine) this.driveEngine.destroy();
|
|
860
1153
|
for (const { mixer } of this._mixers.values()) mixer.stopAllAction();
|
|
861
1154
|
this._mixers.clear();
|
|
1155
|
+
for (const unsub of this._scriptUnsubs) try { unsub(); } catch (_) {}
|
|
1156
|
+
this._scriptUnsubs = [];
|
|
862
1157
|
if (this.renderer) {
|
|
863
1158
|
this.renderer.dispose();
|
|
864
1159
|
this.renderer.domElement?.remove();
|