iobroker.mywebui 1.42.36 → 1.42.38

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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "mywebui",
4
- "version": "1.42.36",
4
+ "version": "1.42.38",
5
5
  "titleLang": {
6
6
  "en": "mywebui",
7
7
  "de": "mywebui",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.mywebui",
3
- "version": "1.42.36",
3
+ "version": "1.42.38",
4
4
  "description": "ioBroker mywebui - Custom edited mywebui by gokturk413 with 3D Editor",
5
5
  "type": "module",
6
6
  "main": "dist/backend/main.js",
@@ -1,3 +1,9 @@
1
- // three/webgpu stub — re-exports from standard Three.js bundle
2
- // Viewport.js only needs PMREMGenerator from this entry point
1
+ // three/webgpu stub — re-exports standard Three.js + stubs WebGPU-only classes
3
2
  export * from './three.module.js';
3
+ import { WebGLRenderer } from './three.module.js';
4
+
5
+ // WebGPURenderer stub: falls back to WebGLRenderer so the editor doesn't crash
6
+ export class WebGPURenderer extends WebGLRenderer {
7
+ constructor(params) { super(params); }
8
+ async init() {}
9
+ }
@@ -1,5 +1,6 @@
1
1
  import { BaseCustomWebComponentConstructorAppend, css, html } from "@gokturk413/base-custom-webcomponent";
2
2
  import { iobrokerHandler } from "../common/IobrokerHandler.js";
3
+ import './IobrokerWebui3DScreenPropertiesPanel.js';
3
4
  import './IobrokerWebuiMonacoEditor.js';
4
5
 
5
6
  // Base URL of the three.js editor files (resolved relative to this module)
@@ -97,6 +98,12 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
97
98
 
98
99
  if (this._editor) this.sceneData.threeScene = this._editor.toJSON();
99
100
 
101
+ // Save ioBroker bindings from properties panel
102
+ const iobPropPanel = this._getDomElement('iobPropPanel');
103
+ if (iobPropPanel?.getBindings) {
104
+ this.sceneData.bindings = iobPropPanel.getBindings();
105
+ }
106
+
100
107
  try {
101
108
  const sceneType = this.getAttribute('scene-type') || '3dscreen';
102
109
  const ok = await iobrokerHandler.saveObject(sceneType, this.sceneData.name, this.sceneData);
@@ -0,0 +1,329 @@
1
+ import { BaseCustomWebComponentConstructorAppend, css, html } from "@gokturk413/base-custom-webcomponent";
2
+ import { iobrokerHandler } from "../common/IobrokerHandler.js";
3
+
4
+ const EDITOR_BASE = new URL('../../../3d-editor/', import.meta.url).href;
5
+
6
+ /**
7
+ * IobrokerWebui3DScreenPropertiesPanel
8
+ * Injected into the webui's "Properties" (attributeDock) when a 3D screen editor is active.
9
+ * Has sub-tabs: Scene | Object | Geom | Mater — no own bottom tab bar.
10
+ * connect(editor, editorHost) must be called after Three.js editor is ready.
11
+ */
12
+ export class IobrokerWebui3DScreenPropertiesPanel extends BaseCustomWebComponentConstructorAppend {
13
+
14
+ static template = html`
15
+ <div id="root">
16
+ <!-- Sub-tab bar: Scene / Object / Geom / Mater -->
17
+ <div id="subBar">
18
+ <span class="stab active" data-sub="scene">Scene</span>
19
+ <span class="stab" data-sub="object">Object</span>
20
+ <span class="stab" data-sub="geom">Geom</span>
21
+ <span class="stab" data-sub="mater">Mater</span>
22
+ </div>
23
+
24
+ <!-- Sub-tab panels -->
25
+ <div id="subScene" class="spanel active"></div>
26
+
27
+ <div id="subObject" class="spanel" style="display:none;">
28
+ <!-- Three.js SidebarObject embedded here -->
29
+ <div id="objThreeWrap"></div>
30
+ <!-- ioBroker bindings section -->
31
+ <div id="bindSection">
32
+ <div class="sechdr">Bindings (ioBroker states)</div>
33
+ <div id="bindRows"></div>
34
+ </div>
35
+ </div>
36
+
37
+ <div id="subGeom" class="spanel" style="display:none;"></div>
38
+ <div id="subMater" class="spanel" style="display:none;"></div>
39
+
40
+ <!-- Binding dialog (inline overlay) -->
41
+ <div id="bdOverlay">
42
+ <div id="bdBox">
43
+ <div id="bdTitle">Binding — <span id="bdPropName"></span></div>
44
+ <div class="bdr">
45
+ <div class="bdl">ioBroker State ID</div>
46
+ <div style="display:flex;gap:4px;">
47
+ <input id="bdSig" type="text" placeholder="mywebui.0.data.value" autocomplete="off" />
48
+ <button id="bdBrowse">...</button>
49
+ </div>
50
+ </div>
51
+ <div class="bdr">
52
+ <div class="bdl">Formula <small>(use <b>val</b> for state value)</small></div>
53
+ <input id="bdForm" type="text" placeholder="val" />
54
+ </div>
55
+ <div class="bdr" style="display:flex;align-items:center;gap:8px;">
56
+ <input id="bdTw" type="checkbox" />
57
+ <span style="color:#ccc;font-size:11px;">Two-way (write back)</span>
58
+ </div>
59
+ <div id="bdFoot">
60
+ <button id="bdClear">Clear</button>
61
+ <button id="bdCancel">Cancel</button>
62
+ <button id="bdOk" class="primary">OK</button>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ </div>
67
+ `;
68
+
69
+ static style = css`
70
+ :host { display:block;width:100%;height:100%;overflow:hidden; }
71
+ #root { width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden;background:#1a1a1a;color:#ccc;font-family:Helvetica,Arial,sans-serif;font-size:12px;position:relative; }
72
+
73
+ /* Sub-tab bar */
74
+ #subBar { display:flex;background:#2d2d30;border-bottom:1px solid #3c3c3c;flex-shrink:0; }
75
+ .stab { padding:6px 12px;cursor:pointer;font-size:11px;color:#888;border-right:1px solid #3c3c3c;white-space:nowrap;user-select:none; }
76
+ .stab:hover { color:#ddd;background:#3e3e42; }
77
+ .stab.active { color:#fff;background:#1e1e1e;border-top:2px solid #4ec9b0; }
78
+
79
+ /* Sub-tab panels */
80
+ .spanel { flex:1;overflow:auto;min-height:0; }
81
+
82
+ /* Section header */
83
+ .sechdr { background:#2d2d30;color:#9cdcfe;padding:5px 8px;font-size:10px;font-weight:bold;text-transform:uppercase;border-top:1px solid #3c3c3c;border-bottom:1px solid #3c3c3c;letter-spacing:.5px; }
84
+
85
+ /* Binding rows */
86
+ #bindRows { padding:2px 0; }
87
+ .brow { display:flex;align-items:center;padding:3px 6px;gap:5px;border-bottom:1px solid #252526;min-height:22px; }
88
+ .brow:hover { background:#252526; }
89
+ .blbl { width:96px;color:#888;font-size:10px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; }
90
+ .bsig { flex:1;color:#4ec9b0;font-size:10px;font-family:monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; }
91
+ .bsig.none { color:#555; }
92
+ .bedit { padding:1px 6px;background:#2d2d30;border:1px solid #555;color:#ccc;cursor:pointer;font-size:10px;flex-shrink:0; }
93
+ .bedit:hover { border-color:#4ec9b0;color:#4ec9b0; }
94
+
95
+ /* Three.js panel host — reset some Three.js CSS for dark bg */
96
+ #objThreeWrap { overflow:auto; }
97
+ #subScene { overflow:auto; }
98
+ #subGeom { overflow:auto; }
99
+ #subMater { overflow:auto; }
100
+
101
+ /* Binding dialog */
102
+ #bdOverlay { display:none;position:absolute;inset:0;background:rgba(0,0,0,0.78);z-index:300;align-items:center;justify-content:center; }
103
+ #bdOverlay.open { display:flex; }
104
+ #bdBox { background:#252526;border:1px solid #555;border-radius:4px;padding:14px;width:90%;max-width:310px;color:#ccc;font-size:12px; }
105
+ #bdTitle { font-weight:bold;color:#9cdcfe;margin-bottom:10px;font-size:12px; }
106
+ .bdr { margin-bottom:8px; }
107
+ .bdl { color:#888;font-size:10px;margin-bottom:3px; }
108
+ .bdr input[type="text"] { width:100%;background:#3c3c3c;border:1px solid #555;color:#ccc;padding:4px;font-size:11px;box-sizing:border-box; }
109
+ .bdr input[type="text"]:focus { border-color:#4ec9b0;outline:none; }
110
+ #bdBrowse { padding:3px 8px;background:#3c3c3c;border:1px solid #555;color:#ccc;cursor:pointer;font-size:11px; }
111
+ #bdFoot { display:flex;justify-content:flex-end;gap:6px;margin-top:12px; }
112
+ #bdFoot button { padding:4px 12px;border:1px solid #555;background:#3c3c3c;color:#ccc;cursor:pointer;font-size:11px; }
113
+ #bdFoot button.primary { background:#0e639c;border-color:#1177bb;color:#fff; }
114
+ `;
115
+
116
+ _editor = null;
117
+ _editorHost = null;
118
+ _bindings = {};
119
+ _selectedObj = null;
120
+ _bindTarget = null;
121
+
122
+ // ── Public API ─────────────────────────────────────────────────────────────
123
+
124
+ async connect(editor, editorHost) {
125
+ this._editor = editor;
126
+ this._editorHost = editorHost;
127
+ this._bindings = editorHost?.sceneData?.bindings ?? {};
128
+
129
+ await this._injectThreeCSS();
130
+ await this._mountThreePanels();
131
+ this._setupSubTabs();
132
+ this._setupBindingRows();
133
+ this._setupBindingDialog();
134
+
135
+ editor.signals.objectSelected.add(obj => {
136
+ this._selectedObj = obj;
137
+ this._refreshBindRows();
138
+ });
139
+ editor.signals.objectChanged.add(obj => {
140
+ if (obj && obj === this._selectedObj) this._refreshBindRows();
141
+ });
142
+
143
+ this._refreshBindRows();
144
+ }
145
+
146
+ getBindings() { return this._bindings; }
147
+
148
+ setBindings(b) {
149
+ this._bindings = b || {};
150
+ this._refreshBindRows();
151
+ }
152
+
153
+ // ── Three.js CSS ────────────────────────────────────────────────────────────
154
+
155
+ async _injectThreeCSS() {
156
+ const sr = this.shadowRoot;
157
+ if (!sr || sr.querySelector('#tcss')) return;
158
+ const l = document.createElement('link');
159
+ l.id = 'tcss'; l.rel = 'stylesheet';
160
+ l.href = EDITOR_BASE + 'css/main.css';
161
+ sr.appendChild(l);
162
+ }
163
+
164
+ // ── Mount Three.js sidebar sub-panels ──────────────────────────────────────
165
+
166
+ async _mountThreePanels() {
167
+ const base = EDITOR_BASE + 'js/';
168
+ const ed = this._editor;
169
+ const [
170
+ { SidebarScene },
171
+ { SidebarObject },
172
+ { SidebarGeometry },
173
+ { SidebarMaterial },
174
+ ] = await Promise.all([
175
+ import(/* @vite-ignore */ base + 'Sidebar.Scene.js'),
176
+ import(/* @vite-ignore */ base + 'Sidebar.Object.js'),
177
+ import(/* @vite-ignore */ base + 'Sidebar.Geometry.js'),
178
+ import(/* @vite-ignore */ base + 'Sidebar.Material.js'),
179
+ ]);
180
+
181
+ this._getDomElement('subScene').appendChild(new SidebarScene(ed).dom);
182
+ this._getDomElement('objThreeWrap').appendChild(new SidebarObject(ed).dom);
183
+ this._getDomElement('subGeom').appendChild(new SidebarGeometry(ed).dom);
184
+ this._getDomElement('subMater').appendChild(new SidebarMaterial(ed).dom);
185
+ }
186
+
187
+ // ── Sub-tabs ────────────────────────────────────────────────────────────────
188
+
189
+ _setupSubTabs() {
190
+ const bar = this._getDomElement('subBar');
191
+ const panels = {
192
+ scene: this._getDomElement('subScene'),
193
+ object: this._getDomElement('subObject'),
194
+ geom: this._getDomElement('subGeom'),
195
+ mater: this._getDomElement('subMater'),
196
+ };
197
+ bar?.querySelectorAll('.stab').forEach(btn => {
198
+ btn.addEventListener('click', () => {
199
+ bar.querySelectorAll('.stab').forEach(b => b.classList.remove('active'));
200
+ btn.classList.add('active');
201
+ Object.values(panels).forEach(p => { if (p) p.style.display = 'none'; });
202
+ const p = panels[btn.dataset.sub];
203
+ if (p) p.style.display = 'block';
204
+ });
205
+ });
206
+ }
207
+
208
+ // ── Bindable properties list ────────────────────────────────────────────────
209
+
210
+ _BINDABLE = [
211
+ { key: 'position.x', label: 'Position X' },
212
+ { key: 'position.y', label: 'Position Y' },
213
+ { key: 'position.z', label: 'Position Z' },
214
+ { key: 'rotation.x', label: 'Rotation X (°)' },
215
+ { key: 'rotation.y', label: 'Rotation Y (°)' },
216
+ { key: 'rotation.z', label: 'Rotation Z (°)' },
217
+ { key: 'scale.x', label: 'Scale X' },
218
+ { key: 'scale.y', label: 'Scale Y' },
219
+ { key: 'scale.z', label: 'Scale Z' },
220
+ { key: 'visible', label: 'Visible' },
221
+ { key: 'material.opacity', label: 'Opacity' },
222
+ { key: 'material.color', label: 'Color (hex)' },
223
+ ];
224
+
225
+ _setupBindingRows() {
226
+ const cont = this._getDomElement('bindRows');
227
+ if (!cont) return;
228
+ cont.innerHTML = '';
229
+ this._BINDABLE.forEach(({ key, label }) => {
230
+ const row = document.createElement('div');
231
+ row.className = 'brow';
232
+ row.dataset.key = key;
233
+
234
+ const lbl = document.createElement('span');
235
+ lbl.className = 'blbl';
236
+ lbl.textContent = label;
237
+
238
+ const sig = document.createElement('span');
239
+ sig.className = 'bsig none';
240
+ sig.textContent = '—';
241
+
242
+ const btn = document.createElement('button');
243
+ btn.className = 'bedit';
244
+ btn.textContent = '□ bind';
245
+ btn.addEventListener('click', () => this._openBindDlg(key, label));
246
+
247
+ row.appendChild(lbl);
248
+ row.appendChild(sig);
249
+ row.appendChild(btn);
250
+ cont.appendChild(row);
251
+ });
252
+ }
253
+
254
+ _refreshBindRows() {
255
+ const cont = this._getDomElement('bindRows');
256
+ if (!cont) return;
257
+ const uuid = this._selectedObj?.uuid;
258
+ cont.querySelectorAll('.brow').forEach(row => {
259
+ const key = row.dataset.key;
260
+ const sig = row.querySelector('.bsig');
261
+ const btn = row.querySelector('.bedit');
262
+ const bind = uuid ? this._bindings[uuid]?.[key] : null;
263
+ if (sig) { sig.textContent = bind?.signal || '—'; sig.className = 'bsig' + (bind?.signal ? '' : ' none'); }
264
+ if (btn) { btn.textContent = bind?.signal ? '■ bound' : '□ bind'; btn.style.color = bind?.signal ? '#4ec9b0' : ''; }
265
+ });
266
+ }
267
+
268
+ // ── Binding dialog ──────────────────────────────────────────────────────────
269
+
270
+ _setupBindingDialog() {
271
+ const ov = this._getDomElement('bdOverlay');
272
+
273
+ this._getDomElement('bdCancel')?.addEventListener('click', () => {
274
+ ov?.classList.remove('open'); this._bindTarget = null;
275
+ });
276
+
277
+ this._getDomElement('bdClear')?.addEventListener('click', () => {
278
+ const { uuid, key } = this._bindTarget || {};
279
+ if (uuid && key) {
280
+ delete this._bindings[uuid]?.[key];
281
+ if (this._bindings[uuid] && !Object.keys(this._bindings[uuid]).length) delete this._bindings[uuid];
282
+ this._syncHost(); this._refreshBindRows();
283
+ }
284
+ ov?.classList.remove('open'); this._bindTarget = null;
285
+ });
286
+
287
+ this._getDomElement('bdOk')?.addEventListener('click', () => {
288
+ const { uuid, key } = this._bindTarget || {};
289
+ const signal = this._getDomElement('bdSig')?.value?.trim();
290
+ const formula = this._getDomElement('bdForm')?.value?.trim() || 'val';
291
+ const twoWay = this._getDomElement('bdTw')?.checked ?? false;
292
+ if (uuid && key && signal) {
293
+ if (!this._bindings[uuid]) this._bindings[uuid] = {};
294
+ this._bindings[uuid][key] = { signal, formula, twoWay };
295
+ this._syncHost(); this._refreshBindRows();
296
+ }
297
+ ov?.classList.remove('open'); this._bindTarget = null;
298
+ });
299
+
300
+ this._getDomElement('bdBrowse')?.addEventListener('click', async () => {
301
+ try {
302
+ const id = await iobrokerHandler.showSelectIdDialog?.();
303
+ if (id) { const inp = this._getDomElement('bdSig'); if (inp) inp.value = id; }
304
+ } catch (_) {}
305
+ });
306
+ }
307
+
308
+ _openBindDlg(key, label) {
309
+ if (!this._selectedObj) { alert('Select a 3D object first.'); return; }
310
+ const uuid = this._selectedObj.uuid;
311
+ this._bindTarget = { uuid, key };
312
+ const ex = this._bindings[uuid]?.[key];
313
+ const sigEl = this._getDomElement('bdSig');
314
+ const frmEl = this._getDomElement('bdForm');
315
+ const twEl = this._getDomElement('bdTw');
316
+ const nmEl = this._getDomElement('bdPropName');
317
+ if (sigEl) sigEl.value = ex?.signal || '';
318
+ if (frmEl) frmEl.value = ex?.formula || 'val';
319
+ if (twEl) twEl.checked = ex?.twoWay || false;
320
+ if (nmEl) nmEl.textContent = label + ' [' + (this._selectedObj.name || uuid.slice(0,8)) + ']';
321
+ this._getDomElement('bdOverlay')?.classList.add('open');
322
+ }
323
+
324
+ _syncHost() {
325
+ if (this._editorHost?.sceneData) this._editorHost.sceneData.bindings = this._bindings;
326
+ }
327
+ }
328
+
329
+ customElements.define('iobroker-webui-3dscreen-properties', IobrokerWebui3DScreenPropertiesPanel);
@@ -235,6 +235,68 @@ export class IobrokerWebui3DScreenViewer extends BaseCustomWebComponentConstruct
235
235
  const assets = new Map();
236
236
  const mixers = new Map();
237
237
 
238
+ // ── ioBroker state bindings ──────────────────────────────────────
239
+ if (sceneData.bindings && Object.keys(sceneData.bindings).length > 0) {
240
+ const objectByUUID = {};
241
+ scene.traverse(obj => { objectByUUID[obj.uuid] = obj; });
242
+
243
+ const applyBinding = (obj, prop, rawVal) => {
244
+ try {
245
+ const parts = prop.split('.');
246
+ if (parts.length === 2) {
247
+ const [p0, p1] = parts;
248
+ if (obj[p0] == null) return;
249
+ if (p0 === 'rotation') {
250
+ // store as degrees, apply as radians
251
+ obj[p0][p1] = parseFloat(rawVal) * DEG2RAD_RT;
252
+ } else if (p0 === 'material' && p1 === 'color') {
253
+ obj[p0]?.color?.set?.(rawVal);
254
+ if (obj[p0]) obj[p0].needsUpdate = true;
255
+ } else if (p0 === 'material') {
256
+ if (obj[p0] != null) { obj[p0][p1] = parseFloat(rawVal); obj[p0].needsUpdate = true; }
257
+ } else {
258
+ obj[p0][p1] = parseFloat(rawVal);
259
+ }
260
+ obj.updateMatrixWorld?.(true);
261
+ } else if (parts.length === 1) {
262
+ if (prop === 'visible') obj.visible = !!rawVal;
263
+ }
264
+ } catch (_) {}
265
+ };
266
+
267
+ const DEG2RAD_RT = Math.PI / 180;
268
+
269
+ for (const [uuid, propMap] of Object.entries(sceneData.bindings)) {
270
+ const obj = objectByUUID[uuid];
271
+ if (!obj) continue;
272
+ for (const [prop, cfg] of Object.entries(propMap)) {
273
+ if (!cfg?.signal) continue;
274
+ (async () => {
275
+ // Apply current value immediately
276
+ try {
277
+ const state = await iobrokerHandler.getState(cfg.signal);
278
+ if (state?.val != null) {
279
+ const val = cfg.formula && cfg.formula !== 'val'
280
+ ? new Function('val', `return (${cfg.formula})`)(state.val)
281
+ : state.val;
282
+ applyBinding(obj, prop, val);
283
+ }
284
+ } catch (_) {}
285
+
286
+ // Subscribe for future changes
287
+ const unsub = subscribe(cfg.signal, (id, state) => {
288
+ if (state?.val == null) return;
289
+ const val = cfg.formula && cfg.formula !== 'val'
290
+ ? new Function('val', `return (${cfg.formula})`)(state.val)
291
+ : state.val;
292
+ applyBinding(obj, prop, val);
293
+ });
294
+ _subscriptions.push(unsub);
295
+ })();
296
+ }
297
+ }
298
+ }
299
+
238
300
  if (sceneData.script) {
239
301
  try {
240
302
  const fn = new Function(
@@ -108,7 +108,7 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
108
108
  <div id="visibilityDock" title="Visibility" style="overflow: auto; width: 100%;" dock-spawn-dock-to="attributeDock">
109
109
  </div>
110
110
 
111
- <div id="effectsDock" title="Effects" style="overflow: auto; width: 100%;" dock-spawn-dock-to="attributeDock">
111
+ <div id="effectsDock" title="Effects / Project" style="overflow: auto; width: 100%;" dock-spawn-dock-to="attributeDock">
112
112
  </div>
113
113
 
114
114
  <div id="animationsDock" title="Animations" style="overflow: auto; width: 100%;" dock-spawn-dock-to="attributeDock">
@@ -182,11 +182,15 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
182
182
  let element = this._dock.getElementInSlot(previousPanel.elementContent);
183
183
  if (element?.deactivated)
184
184
  element?.deactivated();
185
+ if (element instanceof IobrokerWebui3DScreenEditor)
186
+ this._deactivate3DPropertiesMode();
185
187
  }
186
188
  if (panel) {
187
189
  let element = this._dock.getElementInSlot(panel.elementContent);
188
190
  if (element?.activated)
189
191
  element?.activated();
192
+ if (element instanceof IobrokerWebui3DScreenEditor)
193
+ this._activate3DPropertiesMode(element);
190
194
  }
191
195
  },
192
196
  onClosePanel: (manager, panel) => {
@@ -2129,6 +2133,188 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
2129
2133
  this.openDock(scriptEditor);
2130
2134
  }
2131
2135
  }
2136
+
2137
+ // ── 3D Screen Properties Mode ──────────────────────────────────────────────
2138
+
2139
+ _3dPropsActive = false;
2140
+ _3dPropsPanel = null;
2141
+ _3dSettPanel = null;
2142
+ _3dProjPanel = null;
2143
+
2144
+ _activate3DPropertiesMode(editor3DEl) {
2145
+ if (this._3dPropsActive) return;
2146
+ this._3dPropsActive = true;
2147
+
2148
+ // Helper: hide existing children and track state
2149
+ const hideChildren = (el) => {
2150
+ for (const c of el.children) {
2151
+ c.__3d_prev_display = c.style.display;
2152
+ c.style.display = 'none';
2153
+ }
2154
+ };
2155
+ // Helper: restore
2156
+ const showChildren = (el) => {
2157
+ for (const c of el.children) {
2158
+ if (c.__3d_prev_display !== undefined) {
2159
+ c.style.display = c.__3d_prev_display;
2160
+ delete c.__3d_prev_display;
2161
+ }
2162
+ }
2163
+ };
2164
+
2165
+ const attrDock = this._getDomElement('attributeDock');
2166
+ const settiDock = this._getDomElement('settingsDock');
2167
+ const projDock = this._getDomElement('effectsDock'); // reuse effectsDock for Project
2168
+
2169
+ if (attrDock) hideChildren(attrDock);
2170
+ if (settiDock) hideChildren(settiDock);
2171
+ if (projDock) hideChildren(projDock);
2172
+
2173
+ const tryConnect = () => {
2174
+ if (!editor3DEl._editor) { setTimeout(tryConnect, 200); return; }
2175
+ const ed = editor3DEl._editor;
2176
+ const base = new URL('../../../3d-editor/js/', import.meta.url).href;
2177
+
2178
+ // ── attributeDock: Scene / Object / Geom / Mater ───────────────
2179
+ if (attrDock) {
2180
+ const propPanel = document.createElement('iobroker-webui-3dscreen-properties');
2181
+ propPanel.id = '__3d_attr';
2182
+ propPanel.style.cssText = 'display:block;width:100%;height:100%;';
2183
+ attrDock.appendChild(propPanel);
2184
+ propPanel.connect(ed, editor3DEl).catch(e => console.warn('3D props connect:', e));
2185
+ this._3dPropsPanel = propPanel;
2186
+ }
2187
+
2188
+ // ── settingsDock: SidebarSettings + Visibility ─────────────────
2189
+ if (settiDock) {
2190
+ const wrap = document.createElement('div');
2191
+ wrap.id = '__3d_setti';
2192
+ wrap.style.cssText = 'width:100%;height:100%;overflow:auto;background:#1a1a1a;color:#ccc;font-family:Helvetica,Arial,sans-serif;font-size:12px;';
2193
+
2194
+ Promise.all([
2195
+ import(/* @vite-ignore */ base + 'Sidebar.Settings.js'),
2196
+ ]).then(([{ SidebarSettings }]) => {
2197
+ const settPanel = new SidebarSettings(ed);
2198
+ settPanel.dom.style.cssText = 'padding:8px;';
2199
+ wrap.appendChild(settPanel.dom);
2200
+
2201
+ // ── Visibility section ─────────────────────────────────
2202
+ const visDiv = document.createElement('div');
2203
+ visDiv.style.cssText = 'border-top:1px solid #3c3c3c;margin-top:8px;padding:10px;';
2204
+ visDiv.innerHTML = `
2205
+ <div style="color:#9cdcfe;font-size:10px;font-weight:bold;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px;">
2206
+ Screen Visibility Control
2207
+ </div>
2208
+ <label style="display:flex;align-items:center;gap:8px;margin-bottom:8px;cursor:pointer;font-size:11px;">
2209
+ <input type="checkbox" id="__3d_vis_en" />
2210
+ Enable Visibility Control
2211
+ </label>
2212
+ <div style="font-size:11px;color:#888;margin-bottom:4px;">Only for groups:</div>
2213
+ <div id="__3d_vis_groups" style="border:1px solid #444;padding:6px;max-height:100px;overflow-y:auto;background:#111;font-size:11px;margin-bottom:8px;">
2214
+ <span style="color:#555;">Loading...</span>
2215
+ </div>
2216
+ <div style="font-size:11px;color:#888;margin-bottom:4px;">If not in group:</div>
2217
+ <select id="__3d_vis_act" style="width:100%;background:#3c3c3c;border:1px solid #555;color:#ccc;padding:4px;font-size:11px;margin-bottom:8px;">
2218
+ <option value="hide">Show "Access Denied"</option>
2219
+ <option value="redirect">Redirect to screen</option>
2220
+ </select>
2221
+ <div id="__3d_vis_rdiv" style="display:none;">
2222
+ <div style="font-size:11px;color:#888;margin-bottom:4px;">Redirect screen:</div>
2223
+ <input id="__3d_vis_redir" type="text" style="width:100%;background:#3c3c3c;border:1px solid #555;color:#ccc;padding:4px;font-size:11px;box-sizing:border-box;" />
2224
+ </div>
2225
+ `;
2226
+
2227
+ const settings = editor3DEl.sceneData?.settings ?? {};
2228
+ editor3DEl.sceneData = editor3DEl.sceneData ?? {};
2229
+ editor3DEl.sceneData.settings = settings;
2230
+ if (!settings.visibilityEnabled) settings.visibilityEnabled = false;
2231
+ if (!settings.visibilityGroups) settings.visibilityGroups = [];
2232
+ if (!settings.visibilityAction) settings.visibilityAction = 'hide';
2233
+ if (!settings.visibilityRedirectScreen) settings.visibilityRedirectScreen = '';
2234
+
2235
+ const save = () => {};
2236
+ const enEl = visDiv.querySelector('#__3d_vis_en');
2237
+ const actEl = visDiv.querySelector('#__3d_vis_act');
2238
+ const rdDiv = visDiv.querySelector('#__3d_vis_rdiv');
2239
+ const rdEl = visDiv.querySelector('#__3d_vis_redir');
2240
+
2241
+ if (enEl) { enEl.checked = settings.visibilityEnabled; enEl.addEventListener('change', e => { settings.visibilityEnabled = e.target.checked; }); }
2242
+ if (actEl) { actEl.value = settings.visibilityAction; actEl.addEventListener('change', e => { settings.visibilityAction = e.target.value; if (rdDiv) rdDiv.style.display = e.target.value === 'redirect' ? 'block' : 'none'; }); }
2243
+ if (rdDiv) rdDiv.style.display = settings.visibilityAction === 'redirect' ? 'block' : 'none';
2244
+ if (rdEl) { rdEl.value = settings.visibilityRedirectScreen || ''; rdEl.addEventListener('input', e => { settings.visibilityRedirectScreen = e.target.value; }); }
2245
+
2246
+ // Load groups
2247
+ const grpEl = visDiv.querySelector('#__3d_vis_groups');
2248
+ iobrokerHandler.getUserGroups?.().then(groups => {
2249
+ if (!grpEl) return;
2250
+ if (!groups?.length) { grpEl.innerHTML = '<span style="color:#555;">No groups</span>'; return; }
2251
+ grpEl.innerHTML = '';
2252
+ groups.forEach(g => {
2253
+ const id = typeof g === 'string' ? g : (g.id ?? g._id ?? g.name ?? g);
2254
+ const lbl = typeof g === 'string' ? g : (g.name ?? id);
2255
+ const chk = (settings.visibilityGroups || []).includes(id);
2256
+ const row = document.createElement('label');
2257
+ row.style.cssText = 'display:flex;align-items:center;gap:6px;padding:2px 0;cursor:pointer;';
2258
+ row.innerHTML = `<input type="checkbox" ${chk ? 'checked' : ''} />${lbl}`;
2259
+ row.querySelector('input').addEventListener('change', ev => {
2260
+ const arr = settings.visibilityGroups || [];
2261
+ if (ev.target.checked) { if (!arr.includes(id)) arr.push(id); }
2262
+ else { const i = arr.indexOf(id); if (i >= 0) arr.splice(i, 1); }
2263
+ settings.visibilityGroups = arr;
2264
+ });
2265
+ grpEl.appendChild(row);
2266
+ });
2267
+ }).catch(() => { if (grpEl) grpEl.innerHTML = '<span style="color:#555;">—</span>'; });
2268
+
2269
+ wrap.appendChild(visDiv);
2270
+ });
2271
+
2272
+ settiDock.appendChild(wrap);
2273
+ this._3dSettPanel = wrap;
2274
+ }
2275
+
2276
+ // ── effectsDock: SidebarProject (Geometries / Materials / Textures) ──
2277
+ if (projDock) {
2278
+ const wrap = document.createElement('div');
2279
+ wrap.id = '__3d_proj';
2280
+ wrap.style.cssText = 'width:100%;height:100%;overflow:auto;background:#1a1a1a;';
2281
+ import(/* @vite-ignore */ base + 'Sidebar.Project.js').then(({ SidebarProject }) => {
2282
+ const p = new SidebarProject(ed);
2283
+ p.dom.style.cssText = 'padding:8px;';
2284
+ wrap.appendChild(p.dom);
2285
+ });
2286
+ projDock.appendChild(wrap);
2287
+ this._3dProjPanel = wrap;
2288
+ }
2289
+ };
2290
+
2291
+ tryConnect();
2292
+ }
2293
+
2294
+ _deactivate3DPropertiesMode() {
2295
+ if (!this._3dPropsActive) return;
2296
+ this._3dPropsActive = false;
2297
+
2298
+ const restore = (dockId, panelEl) => {
2299
+ const dock = this._getDomElement(dockId);
2300
+ if (!dock) return;
2301
+ panelEl?.remove();
2302
+ for (const c of dock.children) {
2303
+ if (c.__3d_prev_display !== undefined) {
2304
+ c.style.display = c.__3d_prev_display;
2305
+ delete c.__3d_prev_display;
2306
+ }
2307
+ }
2308
+ };
2309
+
2310
+ restore('attributeDock', this._3dPropsPanel);
2311
+ restore('settingsDock', this._3dSettPanel);
2312
+ restore('effectsDock', this._3dProjPanel);
2313
+
2314
+ this._3dPropsPanel = null;
2315
+ this._3dSettPanel = null;
2316
+ this._3dProjPanel = null;
2317
+ }
2132
2318
  }
2133
2319
  window.customElements.define('iobroker-webui-app-shell', IobrokerWebuiAppShell);
2134
2320
  const err = console.error;
@@ -49,6 +49,7 @@ const importMapConfig = {
49
49
  "toastify-js": "./node_modules/toastify-js/src/toastify-es.js",
50
50
  "@iobroker/webcomponent-selectid-dialog/": "./node_modules/@iobroker/webcomponent-selectid-dialog/",
51
51
  "three": "./3d-lib/three.module.js",
52
+ "three/webgpu": "./3d-lib/three.webgpu.js",
52
53
  "three/addons/": "./3d-lib/jsm/",
53
54
  "three-gpu-pathtracer": "data:text/javascript,export class WebGLPathTracer{constructor(){}setScene(){}renderSample(){}updateCamera(){}updateEnvironment(){}updateMaterials(){}get samples(){return 0;}filterGlossyFactor=0.5;}",
54
55
  "three-mesh-bvh": "data:text/javascript,export const MeshBVH={};export const acceleratedRaycast=()=>{};export const computeBoundsTree=()=>{};export const disposeBoundsTree=()=>{};"