iobroker.mywebui 1.42.37 → 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.37",
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.37",
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",
@@ -19,15 +19,10 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
19
19
  <button id="scriptToggleBtn" class="tb-btn" title="Toggle scene script panel">&#128196; Script</button>
20
20
  </div>
21
21
 
22
- <!-- Center row: three.js editor + iob properties panel -->
23
- <div id="midRow" style="flex:1;display:flex;min-height:0;overflow:hidden;">
24
- <!-- three.js editor mount point -->
25
- <div id="editorContainer" style="flex:1;position:relative;overflow:hidden;min-height:0;"></div>
26
- <!-- ioBroker 3D properties panel -->
27
- <iobroker-webui-3dscreen-properties id="iobPropPanel"
28
- style="width:300px;flex-shrink:0;border-left:1px solid #3c3c3c;overflow:hidden;">
29
- </iobroker-webui-3dscreen-properties>
30
- </div>
22
+ <!-- three.js editor mount point
23
+ position:relative → contains position:absolute children (viewport, sidebar, etc.)
24
+ overflow:visible → allow position:fixed dropdowns to escape clip boundary -->
25
+ <div id="editorContainer" style="flex:1;position:relative;overflow:hidden;min-height:0;"></div>
31
26
 
32
27
  <!-- Bottom Monaco panel for iobroker scene script -->
33
28
  <div id="scriptPanel" style="height:0;flex-shrink:0;border-top:2px solid #3c3c3c;display:flex;flex-direction:column;overflow:hidden;background:#1e1e1e;transition:height 0.15s;">
@@ -282,12 +277,6 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
282
277
  this._resizeObserver = new ResizeObserver(() => editor.signals.windowResize.dispatch());
283
278
  this._resizeObserver.observe(container);
284
279
 
285
- // Connect ioBroker properties panel
286
- const iobPropPanel = this._getDomElement('iobPropPanel');
287
- if (iobPropPanel?.connect) {
288
- iobPropPanel.connect(editor, this).catch(e => console.warn('3D prop panel connect error:', e));
289
- }
290
-
291
280
  // Dispatch resize after layout is complete (container may have 0 size on first tick)
292
281
  requestAnimationFrame(() => {
293
282
  editor.signals.windowResize.dispatch();
@@ -3,103 +3,60 @@ import { iobrokerHandler } from "../common/IobrokerHandler.js";
3
3
 
4
4
  const EDITOR_BASE = new URL('../../../3d-editor/', import.meta.url).href;
5
5
 
6
- const RAD2DEG = 180 / Math.PI;
7
- const DEG2RAD = Math.PI / 180;
8
-
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
+ */
9
12
  export class IobrokerWebui3DScreenPropertiesPanel extends BaseCustomWebComponentConstructorAppend {
10
13
 
11
14
  static template = html`
12
15
  <div id="root">
13
- <!-- Title row -->
14
- <div id="titleRow">
15
- <span class="lbl">Type:</span>
16
- <span id="typeVal">-</span>
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>
17
22
  </div>
18
23
 
19
- <!-- Main tab content area -->
20
- <div id="mainArea">
21
-
22
- <!-- PROP tab: Scene / Object / Geom / Mater sub-tabs -->
23
- <div id="tabProp" class="mpanel active">
24
- <div id="propSubBar">
25
- <span class="stab active" data-sub="scene">Scene</span>
26
- <span class="stab" data-sub="object">Object</span>
27
- <span class="stab" data-sub="geom">Geom</span>
28
- <span class="stab" data-sub="mater">Mater</span>
29
- </div>
30
- <div id="subScene" class="scontent active"></div>
31
- <div id="subObject" class="scontent" style="display:none;">
32
- <div id="objProps"></div>
33
- <div id="bindSection">
34
- <div class="sec-hdr">Bindings (ioBroker)</div>
35
- <div id="bindRows"></div>
36
- </div>
37
- </div>
38
- <div id="subGeom" class="scontent" style="display:none;"></div>
39
- <div id="subMater" class="scontent" style="display:none;"></div>
40
- </div>
41
-
42
- <!-- SETTI tab -->
43
- <div id="tabSetti" class="mpanel" style="display:none;">
44
- <div id="settingsWrap"></div>
45
- <div id="visSection">
46
- <div class="sec-hdr">Screen Visibility Control</div>
47
- <div id="visContent"></div>
48
- </div>
49
- </div>
24
+ <!-- Sub-tab panels -->
25
+ <div id="subScene" class="spanel active"></div>
50
26
 
51
- <!-- PROJ tab -->
52
- <div id="tabProj" class="mpanel" style="display:none;">
53
- <div id="projWrap"></div>
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>
54
34
  </div>
55
-
56
- <!-- VISI tab -->
57
- <div id="tabVisi" class="mpanel" style="display:none;">
58
- <div id="visiContent2" style="padding:10px;"></div>
59
- </div>
60
-
61
- <!-- REFAC tab -->
62
- <div id="tabRefac" class="mpanel" style="display:none;">
63
- <div style="padding:12px;color:#888;font-size:11px;">Refactor options for 3D screen.</div>
64
- </div>
65
-
66
- <!-- TRANSL tab -->
67
- <div id="tabTransl" class="mpanel" style="display:none;">
68
- <div id="translWrap" style="padding:10px;"></div>
69
- </div>
70
-
71
35
  </div>
72
36
 
73
- <!-- Bottom tab bar (webui style) -->
74
- <div id="botBar">
75
- <span class="btab active" data-t="Prop">Prop...</span>
76
- <span class="btab" data-t="Setti">Setti...</span>
77
- <span class="btab" data-t="Proj">Proj...</span>
78
- <span class="btab" data-t="Visi">Visi...</span>
79
- <span class="btab" data-t="Refac">Refac...</span>
80
- <span class="btab" data-t="Transl">Transl...</span>
81
- </div>
37
+ <div id="subGeom" class="spanel" style="display:none;"></div>
38
+ <div id="subMater" class="spanel" style="display:none;"></div>
82
39
 
83
- <!-- Binding dialog overlay -->
40
+ <!-- Binding dialog (inline overlay) -->
84
41
  <div id="bdOverlay">
85
- <div id="bdDialog">
86
- <div id="bdHead">Edit Binding — <span id="bdPropName"></span></div>
87
- <div class="bd-row">
88
- <div class="bd-lbl">State ID</div>
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>
89
46
  <div style="display:flex;gap:4px;">
90
- <input id="bdSignal" type="text" placeholder="mywebui.0.0_data.value" />
91
- <button id="bdBrowse" title="Browse">...</button>
47
+ <input id="bdSig" type="text" placeholder="mywebui.0.data.value" autocomplete="off" />
48
+ <button id="bdBrowse">...</button>
92
49
  </div>
93
50
  </div>
94
- <div class="bd-row">
95
- <div class="bd-lbl">Formula <small>(optional — use <b>val</b>)</small></div>
96
- <input id="bdFormula" type="text" placeholder="val" />
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" />
97
54
  </div>
98
- <div class="bd-row" style="display:flex;align-items:center;gap:8px;">
99
- <input id="bdTwoWay" type="checkbox" />
100
- <label style="color:#ccc;font-size:11px;cursor:pointer;" for="bdTwoWay2">Two-way (write back to ioBroker)</label>
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>
101
58
  </div>
102
- <div id="bdFooter">
59
+ <div id="bdFoot">
103
60
  <button id="bdClear">Clear</button>
104
61
  <button id="bdCancel">Cancel</button>
105
62
  <button id="bdOk" class="primary">OK</button>
@@ -113,487 +70,259 @@ export class IobrokerWebui3DScreenPropertiesPanel extends BaseCustomWebComponent
113
70
  :host { display:block;width:100%;height:100%;overflow:hidden; }
114
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; }
115
72
 
116
- /* Title */
117
- #titleRow { padding:4px 8px;background:#252526;border-bottom:1px solid #3c3c3c;font-size:11px;display:flex;gap:6px;align-items:center;flex-shrink:0; }
118
- .lbl { color:#888; }
119
- #typeVal { color:#9cdcfe; }
120
-
121
- /* Main area */
122
- #mainArea { flex:1;min-height:0;overflow:hidden;position:relative; }
123
- .mpanel { width:100%;height:100%;overflow:auto;display:flex;flex-direction:column; }
124
-
125
- /* Props sub-tab bar */
126
- #propSubBar { display:flex;background:#2d2d30;border-bottom:1px solid #3c3c3c;flex-shrink:0;overflow-x:auto; }
127
- .stab { padding:5px 10px;cursor:pointer;font-size:11px;color:#888;border-right:1px solid #3c3c3c;white-space:nowrap;user-select:none; }
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; }
128
76
  .stab:hover { color:#ddd;background:#3e3e42; }
129
77
  .stab.active { color:#fff;background:#1e1e1e;border-top:2px solid #4ec9b0; }
130
78
 
131
- .scontent { flex:1;overflow:auto; }
79
+ /* Sub-tab panels */
80
+ .spanel { flex:1;overflow:auto;min-height:0; }
132
81
 
133
82
  /* Section header */
134
- .sec-hdr { background:#2d2d30;color:#9cdcfe;padding:5px 8px;font-size:10px;font-weight:bold;text-transform:uppercase;border-bottom:1px solid #3c3c3c;letter-spacing:.5px; }
135
-
136
- /* Object property rows */
137
- #objProps { padding:4px 0; }
138
- .prop-row { display:flex;align-items:center;padding:2px 6px;min-height:22px;border-bottom:1px solid #252526; }
139
- .prop-row:hover { background:#2a2a2a; }
140
- .bind-btn { width:13px;height:13px;border:1px solid #555;background:#1a1a1a;cursor:pointer;margin-right:5px;flex-shrink:0;font-size:8px;display:flex;align-items:center;justify-content:center;border-radius:1px; }
141
- .bind-btn.bound { background:#0d7a5a;border-color:#4ec9b0; }
142
- .bind-btn:hover { border-color:#9cdcfe; }
143
- .prop-label { width:72px;flex-shrink:0;color:#aaa;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; }
144
- .prop-inputs { flex:1;display:flex;align-items:center;gap:2px; }
145
- .pnum { width:52px;background:#3c3c3c;border:1px solid #555;color:#ccc;padding:2px 4px;font-size:11px;text-align:right; }
146
- .pnum:focus { border-color:#4ec9b0;outline:none; }
147
- .ptxt { flex:1;background:#3c3c3c;border:1px solid #555;color:#ccc;padding:2px 4px;font-size:11px; }
148
- .ptxt:focus { border-color:#4ec9b0;outline:none; }
149
- .ax { color:#666;font-size:9px;margin-right:1px; }
150
- .pchk { cursor:pointer; }
151
- .pval-ro { color:#888;font-size:11px;font-family:monospace; }
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; }
152
84
 
153
85
  /* Binding rows */
154
- #bindSection { border-top:1px solid #3c3c3c; }
155
- #bindRows { padding:4px 0; }
156
- .brow { display:flex;align-items:center;padding:2px 6px;gap:4px;min-height:20px; }
157
- .brow:hover { background:#2a2a2a; }
158
- .b-lbl { width:90px;color:#888;font-size:10px;flex-shrink:0; }
159
- .b-sig { flex:1;color:#4ec9b0;font-size:10px;font-family:monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; }
160
- .b-sig.none { color:#555; }
161
- .b-edit { font-size:10px;padding:1px 5px;background:#3c3c3c;border:1px solid #555;color:#ccc;cursor:pointer; }
162
-
163
- /* Bottom tab bar */
164
- #botBar { display:flex;border-top:2px solid #3c3c3c;background:#2d2d30;flex-shrink:0;overflow-x:auto; }
165
- .btab { padding:5px 8px;cursor:pointer;font-size:10px;color:#888;border-right:1px solid #3c3c3c;white-space:nowrap;flex:1;text-align:center;user-select:none; }
166
- .btab:hover { background:#3e3e42;color:#ccc; }
167
- .btab.active { background:#1e1e1e;color:#fff;border-top:2px solid #4ec9b0; }
168
-
169
- /* Visibility UI */
170
- .vis-row { margin:8px 10px; }
171
- .vis-lbl { color:#888;font-size:11px;margin-bottom:3px;display:block; }
172
- .vis-select { width:100%;background:#3c3c3c;border:1px solid #555;color:#ccc;padding:4px;font-size:11px; }
173
- .vis-group-list { border:1px solid #444;padding:6px;max-height:100px;overflow-y:auto;background:#1e1e1e;font-size:11px; }
174
- .vis-group-item { display:flex;align-items:center;gap:6px;padding:2px 0; }
175
- .vis-check { cursor:pointer; }
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; }
176
100
 
177
101
  /* Binding dialog */
178
- #bdOverlay { display:none;position:absolute;inset:0;background:rgba(0,0,0,0.75);z-index:500;align-items:center;justify-content:center; }
179
- #bdOverlay.show { display:flex; }
180
- #bdDialog { background:#252526;border:1px solid #555;border-radius:4px;padding:14px;width:90%;max-width:320px;color:#ccc;font-size:12px; }
181
- #bdHead { font-weight:bold;color:#9cdcfe;margin-bottom:10px;font-size:12px; }
182
- .bd-row { margin-bottom:8px; }
183
- .bd-lbl { color:#888;font-size:10px;margin-bottom:3px; }
184
- .bd-row input[type="text"] { width:100%;background:#3c3c3c;border:1px solid #555;color:#ccc;padding:4px;font-size:11px;box-sizing:border-box; }
185
- .bd-row input[type="text"]:focus { border-color:#4ec9b0;outline:none; }
186
- #bdBrowse { padding:3px 7px;background:#3c3c3c;border:1px solid #555;color:#ccc;cursor:pointer;font-size:11px;white-space:nowrap; }
187
- #bdFooter { display:flex;justify-content:flex-end;gap:6px;margin-top:12px; }
188
- #bdFooter button { padding:4px 12px;border:1px solid #555;background:#3c3c3c;color:#ccc;cursor:pointer;font-size:11px; }
189
- #bdFooter button.primary { background:#0e639c;border-color:#1177bb;color:#fff; }
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; }
190
114
  `;
191
115
 
192
116
  _editor = null;
193
117
  _editorHost = null;
194
- _bindings = {}; // { uuid: { 'position.x': { signal, formula, twoWay } } }
118
+ _bindings = {};
195
119
  _selectedObj = null;
196
- _bindingTarget = null; // { uuid, prop }
120
+ _bindTarget = null;
197
121
 
198
- // ── public API ────────────────────────────────────────────────────────────
122
+ // ── Public API ─────────────────────────────────────────────────────────────
199
123
 
200
124
  async connect(editor, editorHost) {
201
- this._editor = editor;
125
+ this._editor = editor;
202
126
  this._editorHost = editorHost;
203
- this._bindings = editorHost?.sceneData?.bindings ?? {};
127
+ this._bindings = editorHost?.sceneData?.bindings ?? {};
204
128
 
205
129
  await this._injectThreeCSS();
206
- await this._buildPanels();
207
- this._setupBottomTabs();
208
- this._setupPropSubTabs();
130
+ await this._mountThreePanels();
131
+ this._setupSubTabs();
132
+ this._setupBindingRows();
209
133
  this._setupBindingDialog();
210
- this._setupVisibilityUI();
211
134
 
212
- editor.signals.objectSelected.add((obj) => {
135
+ editor.signals.objectSelected.add(obj => {
213
136
  this._selectedObj = obj;
214
- this._updateObjectPanel(obj);
215
- this._updateBindRows(obj);
216
- const typeEl = this._getDomElement('typeVal');
217
- if (typeEl) typeEl.textContent = obj ? (obj.type ?? '-') : '-';
137
+ this._refreshBindRows();
218
138
  });
219
-
220
- editor.signals.objectChanged.add((obj) => {
221
- if (obj && obj === this._selectedObj) this._updateObjectPanel(obj);
139
+ editor.signals.objectChanged.add(obj => {
140
+ if (obj && obj === this._selectedObj) this._refreshBindRows();
222
141
  });
223
142
 
224
- this._updateObjectPanel(null);
225
- this._updateBindRows(null);
143
+ this._refreshBindRows();
226
144
  }
227
145
 
228
- /** Call this before saving scene data */
229
146
  getBindings() { return this._bindings; }
230
147
 
231
- /** Call after loading scene data to restore bindings */
232
- setBindings(bindings) {
233
- this._bindings = bindings || {};
234
- if (this._selectedObj) this._updateBindRows(this._selectedObj);
148
+ setBindings(b) {
149
+ this._bindings = b || {};
150
+ this._refreshBindRows();
235
151
  }
236
152
 
237
- // ── CSS injection ─────────────────────────────────────────────────────────
153
+ // ── Three.js CSS ────────────────────────────────────────────────────────────
238
154
 
239
155
  async _injectThreeCSS() {
240
156
  const sr = this.shadowRoot;
241
- if (!sr || sr.querySelector('#three-css-props')) return;
242
- const lnk = document.createElement('link');
243
- lnk.id = 'three-css-props';
244
- lnk.rel = 'stylesheet';
245
- lnk.href = EDITOR_BASE + 'css/main.css';
246
- sr.appendChild(lnk);
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);
247
162
  }
248
163
 
249
- // ── Build Three.js sidebar panels ─────────────────────────────────────────
164
+ // ── Mount Three.js sidebar sub-panels ──────────────────────────────────────
250
165
 
251
- async _buildPanels() {
166
+ async _mountThreePanels() {
252
167
  const base = EDITOR_BASE + 'js/';
168
+ const ed = this._editor;
253
169
  const [
254
170
  { SidebarScene },
255
171
  { SidebarObject },
256
172
  { SidebarGeometry },
257
173
  { SidebarMaterial },
258
- { SidebarProject },
259
- { SidebarSettings },
260
174
  ] = await Promise.all([
261
175
  import(/* @vite-ignore */ base + 'Sidebar.Scene.js'),
262
176
  import(/* @vite-ignore */ base + 'Sidebar.Object.js'),
263
177
  import(/* @vite-ignore */ base + 'Sidebar.Geometry.js'),
264
178
  import(/* @vite-ignore */ base + 'Sidebar.Material.js'),
265
- import(/* @vite-ignore */ base + 'Sidebar.Project.js'),
266
- import(/* @vite-ignore */ base + 'Sidebar.Settings.js'),
267
179
  ]);
268
180
 
269
- const ed = this._editor;
270
-
271
- // Scene sub-tab
272
- const scenePanel = new SidebarScene(ed);
273
- this._getDomElement('subScene').appendChild(scenePanel.dom);
274
-
275
- // Object sub-tab: our own property rows + Three.js Object panel below
276
- const objPanel = new SidebarObject(ed);
277
- this._getDomElement('objProps').appendChild(objPanel.dom);
278
- this._buildBindRows(ed);
279
-
280
- // Geometry sub-tab
281
- const geomPanel = new SidebarGeometry(ed);
282
- this._getDomElement('subGeom').appendChild(geomPanel.dom);
283
-
284
- // Material sub-tab
285
- const matPanel = new SidebarMaterial(ed);
286
- this._getDomElement('subMater').appendChild(matPanel.dom);
287
-
288
- // Project tab
289
- const projPanel = new SidebarProject(ed);
290
- this._getDomElement('projWrap').appendChild(projPanel.dom);
291
-
292
- // Settings tab
293
- const settPanel = new SidebarSettings(ed);
294
- this._getDomElement('settingsWrap').appendChild(settPanel.dom);
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);
295
185
  }
296
186
 
297
- // ── Bottom tabs ───────────────────────────────────────────────────────────
187
+ // ── Sub-tabs ────────────────────────────────────────────────────────────────
298
188
 
299
- _setupBottomTabs() {
300
- const tabMap = {
301
- Prop: 'tabProp',
302
- Setti: 'tabSetti',
303
- Proj: 'tabProj',
304
- Visi: 'tabVisi',
305
- Refac: 'tabRefac',
306
- Transl: 'tabTransl',
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'),
307
196
  };
308
- const bar = this._getDomElement('botBar');
309
- bar?.querySelectorAll('.btab').forEach(btn => {
310
- btn.addEventListener('click', () => {
311
- bar.querySelectorAll('.btab').forEach(b => b.classList.remove('active'));
312
- btn.classList.add('active');
313
- Object.values(tabMap).forEach(id => {
314
- const el = this._getDomElement(id);
315
- if (el) el.style.display = 'none';
316
- });
317
- const target = tabMap[btn.dataset.t];
318
- if (target) {
319
- const el = this._getDomElement(target);
320
- if (el) el.style.display = 'flex';
321
- }
322
- });
323
- });
324
- }
325
-
326
- // ── Properties sub-tabs (Scene / Object / Geom / Mater) ──────────────────
327
-
328
- _setupPropSubTabs() {
329
- const subMap = {
330
- scene: 'subScene',
331
- object: 'subObject',
332
- geom: 'subGeom',
333
- mater: 'subMater',
334
- };
335
- const bar = this._getDomElement('propSubBar');
336
197
  bar?.querySelectorAll('.stab').forEach(btn => {
337
198
  btn.addEventListener('click', () => {
338
199
  bar.querySelectorAll('.stab').forEach(b => b.classList.remove('active'));
339
200
  btn.classList.add('active');
340
- Object.values(subMap).forEach(id => {
341
- const el = this._getDomElement(id);
342
- if (el) el.style.display = 'none';
343
- });
344
- const t = subMap[btn.dataset.sub];
345
- if (t) {
346
- const el = this._getDomElement(t);
347
- if (el) el.style.display = 'block';
348
- }
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';
349
204
  });
350
205
  });
351
206
  }
352
207
 
353
- // ── Object property display ───────────────────────────────────────────────
354
- // (Three.js SidebarObject already embedded; these rows are just for quick info)
355
-
356
- _updateObjectPanel(obj) {
357
- // The embedded SidebarObject panel handles display automatically via editor signals.
358
- // Nothing extra needed here.
359
- }
360
-
361
- // ── Binding rows ──────────────────────────────────────────────────────────
362
-
363
- _BINDABLE_PROPS = [
364
- { key: 'position.x', label: 'Position X' },
365
- { key: 'position.y', label: 'Position Y' },
366
- { key: 'position.z', label: 'Position Z' },
367
- { key: 'rotation.x', label: 'Rotation X (°)' },
368
- { key: 'rotation.y', label: 'Rotation Y (°)' },
369
- { key: 'rotation.z', label: 'Rotation Z (°)' },
370
- { key: 'scale.x', label: 'Scale X' },
371
- { key: 'scale.y', label: 'Scale Y' },
372
- { key: 'scale.z', label: 'Scale Z' },
373
- { key: 'visible', label: 'Visible' },
374
- { key: 'material.color', label: 'Mat. Color' },
375
- { key: 'material.opacity', label: 'Mat. Opacity' },
376
- { key: 'material.emissive', label: 'Emissive' },
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)' },
377
223
  ];
378
224
 
379
- _buildBindRows(editor) {
380
- const container = this._getDomElement('bindRows');
381
- if (!container) return;
382
- container.innerHTML = '';
383
-
384
- this._BINDABLE_PROPS.forEach(({ key, label }) => {
225
+ _setupBindingRows() {
226
+ const cont = this._getDomElement('bindRows');
227
+ if (!cont) return;
228
+ cont.innerHTML = '';
229
+ this._BINDABLE.forEach(({ key, label }) => {
385
230
  const row = document.createElement('div');
386
231
  row.className = 'brow';
387
232
  row.dataset.key = key;
388
233
 
389
234
  const lbl = document.createElement('span');
390
- lbl.className = 'b-lbl';
235
+ lbl.className = 'blbl';
391
236
  lbl.textContent = label;
392
237
 
393
238
  const sig = document.createElement('span');
394
- sig.className = 'b-sig none';
239
+ sig.className = 'bsig none';
395
240
  sig.textContent = '—';
396
241
 
397
- const editBtn = document.createElement('button');
398
- editBtn.className = 'b-edit';
399
- editBtn.textContent = '□';
400
- editBtn.title = 'Edit binding';
401
- editBtn.addEventListener('click', () => this._openBindingDialog(key, label, sig, editBtn));
242
+ const btn = document.createElement('button');
243
+ btn.className = 'bedit';
244
+ btn.textContent = '□ bind';
245
+ btn.addEventListener('click', () => this._openBindDlg(key, label));
402
246
 
403
247
  row.appendChild(lbl);
404
248
  row.appendChild(sig);
405
- row.appendChild(editBtn);
406
- container.appendChild(row);
249
+ row.appendChild(btn);
250
+ cont.appendChild(row);
407
251
  });
408
252
  }
409
253
 
410
- _updateBindRows(obj) {
411
- const container = this._getDomElement('bindRows');
412
- if (!container) return;
413
- const uuid = obj?.uuid;
414
-
415
- container.querySelectorAll('.brow').forEach(row => {
416
- const key = row.dataset.key;
417
- const sig = row.querySelector('.b-sig');
418
- const btn = row.querySelector('.b-edit');
419
- const binding = uuid ? (this._bindings[uuid]?.[key]) : null;
420
- if (sig) {
421
- sig.textContent = binding?.signal || '—';
422
- sig.className = 'b-sig' + (binding?.signal ? '' : ' none');
423
- }
424
- if (btn) {
425
- btn.textContent = binding?.signal ? '■' : '□';
426
- btn.title = binding?.signal ? `Bound: ${binding.signal}` : 'Edit binding';
427
- btn.style.color = binding?.signal ? '#4ec9b0' : '';
428
- }
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' : ''; }
429
265
  });
430
266
  }
431
267
 
432
- // ── Binding dialog ────────────────────────────────────────────────────────
268
+ // ── Binding dialog ──────────────────────────────────────────────────────────
433
269
 
434
270
  _setupBindingDialog() {
435
- const overlay = this._getDomElement('bdOverlay');
271
+ const ov = this._getDomElement('bdOverlay');
436
272
 
437
273
  this._getDomElement('bdCancel')?.addEventListener('click', () => {
438
- overlay?.classList.remove('show');
439
- this._bindingTarget = null;
274
+ ov?.classList.remove('open'); this._bindTarget = null;
440
275
  });
441
276
 
442
277
  this._getDomElement('bdClear')?.addEventListener('click', () => {
443
- const { uuid, key } = this._bindingTarget || {};
278
+ const { uuid, key } = this._bindTarget || {};
444
279
  if (uuid && key) {
445
- if (this._bindings[uuid]) {
446
- delete this._bindings[uuid][key];
447
- if (!Object.keys(this._bindings[uuid]).length) delete this._bindings[uuid];
448
- }
449
- this._syncBindingsToHost();
450
- this._updateBindRows(this._selectedObj);
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();
451
283
  }
452
- overlay?.classList.remove('show');
453
- this._bindingTarget = null;
284
+ ov?.classList.remove('open'); this._bindTarget = null;
454
285
  });
455
286
 
456
287
  this._getDomElement('bdOk')?.addEventListener('click', () => {
457
- const { uuid, key } = this._bindingTarget || {};
458
- const signal = this._getDomElement('bdSignal')?.value?.trim();
459
- const formula = this._getDomElement('bdFormula')?.value?.trim() || 'val';
460
- const twoWay = this._getDomElement('bdTwoWay')?.checked ?? false;
461
-
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;
462
292
  if (uuid && key && signal) {
463
293
  if (!this._bindings[uuid]) this._bindings[uuid] = {};
464
294
  this._bindings[uuid][key] = { signal, formula, twoWay };
465
- this._syncBindingsToHost();
466
- this._updateBindRows(this._selectedObj);
295
+ this._syncHost(); this._refreshBindRows();
467
296
  }
468
- overlay?.classList.remove('show');
469
- this._bindingTarget = null;
297
+ ov?.classList.remove('open'); this._bindTarget = null;
470
298
  });
471
299
 
472
300
  this._getDomElement('bdBrowse')?.addEventListener('click', async () => {
473
301
  try {
474
- const id = await iobrokerHandler.showSelectIdDialog();
475
- if (id) {
476
- const inp = this._getDomElement('bdSignal');
477
- if (inp) inp.value = id;
478
- }
302
+ const id = await iobrokerHandler.showSelectIdDialog?.();
303
+ if (id) { const inp = this._getDomElement('bdSig'); if (inp) inp.value = id; }
479
304
  } catch (_) {}
480
305
  });
481
306
  }
482
307
 
483
- _openBindingDialog(key, label, sigEl, btnEl) {
484
- const obj = this._selectedObj;
485
- if (!obj) { alert('Select a 3D object first.'); return; }
486
-
487
- const uuid = obj.uuid;
488
- this._bindingTarget = { uuid, key };
489
-
490
- const existing = this._bindings[uuid]?.[key];
491
- const sigInp = this._getDomElement('bdSignal');
492
- const frmInp = this._getDomElement('bdFormula');
493
- const twInp = this._getDomElement('bdTwoWay');
494
- const propNm = this._getDomElement('bdPropName');
495
-
496
- if (sigInp) sigInp.value = existing?.signal || '';
497
- if (frmInp) frmInp.value = existing?.formula || 'val';
498
- if (twInp) twInp.checked = existing?.twoWay || false;
499
- if (propNm) propNm.textContent = label + ' [' + (obj.name || obj.uuid.slice(0, 8)) + ']';
500
-
501
- const overlay = this._getDomElement('bdOverlay');
502
- overlay?.classList.add('show');
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');
503
322
  }
504
323
 
505
- _syncBindingsToHost() {
506
- if (this._editorHost?.sceneData) {
507
- this._editorHost.sceneData.bindings = this._bindings;
508
- }
509
- }
510
-
511
- // ── Visibility UI (Settings tab) ──────────────────────────────────────────
512
-
513
- _setupVisibilityUI() {
514
- const host = this._editorHost;
515
- const settings = host?.sceneData?.settings ?? {};
516
-
517
- const content = this._getDomElement('visContent');
518
- if (!content) return;
519
-
520
- const ensure = (key, def) => { if (settings[key] == null) settings[key] = def; };
521
- ensure('visibilityEnabled', false);
522
- ensure('visibilityGroups', []);
523
- ensure('visibilityAction', 'hide');
524
- ensure('visibilityRedirectScreen', '');
525
-
526
- const save = () => {
527
- if (host?.sceneData) host.sceneData.settings = settings;
528
- };
529
-
530
- content.innerHTML = `
531
- <div class="vis-row">
532
- <label class="vis-row" style="display:flex;align-items:center;gap:8px;cursor:pointer;">
533
- <input type="checkbox" id="vis3d-enable" ${settings.visibilityEnabled ? 'checked' : ''} />
534
- <span style="color:#ccc;font-size:11px;">Enable Visibility Control</span>
535
- </label>
536
- </div>
537
- <div class="vis-row">
538
- <span class="vis-lbl">Only for user groups:</span>
539
- <div class="vis-group-list" id="vis3d-groups"><span style="color:#666;font-size:11px;">Loading...</span></div>
540
- </div>
541
- <div class="vis-row">
542
- <span class="vis-lbl">If not in group:</span>
543
- <select class="vis-select" id="vis3d-action">
544
- <option value="hide" ${settings.visibilityAction==='hide'?'selected':''}>Show "Access Denied"</option>
545
- <option value="redirect" ${settings.visibilityAction==='redirect'?'selected':''}>Redirect to screen</option>
546
- </select>
547
- </div>
548
- <div class="vis-row" id="vis3d-redir-row" style="display:${settings.visibilityAction==='redirect'?'block':'none'}">
549
- <span class="vis-lbl">Redirect screen:</span>
550
- <input type="text" id="vis3d-redir" value="${settings.visibilityRedirectScreen||''}"
551
- style="width:100%;background:#3c3c3c;border:1px solid #555;color:#ccc;padding:4px;font-size:11px;box-sizing:border-box;" />
552
- </div>
553
- `;
554
-
555
- content.querySelector('#vis3d-enable')?.addEventListener('change', e => {
556
- settings.visibilityEnabled = e.target.checked; save();
557
- });
558
- content.querySelector('#vis3d-action')?.addEventListener('change', e => {
559
- settings.visibilityAction = e.target.value;
560
- const rd = content.querySelector('#vis3d-redir-row');
561
- if (rd) rd.style.display = e.target.value === 'redirect' ? 'block' : 'none';
562
- save();
563
- });
564
- content.querySelector('#vis3d-redir')?.addEventListener('input', e => {
565
- settings.visibilityRedirectScreen = e.target.value; save();
566
- });
567
-
568
- // Load user groups
569
- this._loadUserGroups(settings, content.querySelector('#vis3d-groups'), save);
570
- }
571
-
572
- async _loadUserGroups(settings, container, save) {
573
- if (!container) return;
574
- try {
575
- const groups = await iobrokerHandler.getUserGroups?.() ?? [];
576
- if (!groups.length) { container.innerHTML = '<span style="color:#666;font-size:11px;">No groups found</span>'; return; }
577
- container.innerHTML = '';
578
- groups.forEach(g => {
579
- const id = typeof g === 'string' ? g : (g.id ?? g._id ?? g.name);
580
- const lbl = typeof g === 'string' ? g : (g.name ?? id);
581
- const checked = (settings.visibilityGroups || []).includes(id);
582
- const item = document.createElement('label');
583
- item.className = 'vis-group-item';
584
- item.innerHTML = `<input type="checkbox" class="vis-check" ${checked?'checked':''} />${lbl}`;
585
- item.querySelector('input').addEventListener('change', ev => {
586
- const arr = settings.visibilityGroups || [];
587
- if (ev.target.checked) { if (!arr.includes(id)) arr.push(id); }
588
- else { const i = arr.indexOf(id); if (i >= 0) arr.splice(i, 1); }
589
- settings.visibilityGroups = arr;
590
- save();
591
- });
592
- container.appendChild(item);
593
- });
594
- } catch (_) {
595
- container.innerHTML = '<span style="color:#666;font-size:11px;">Could not load groups</span>';
596
- }
324
+ _syncHost() {
325
+ if (this._editorHost?.sceneData) this._editorHost.sceneData.bindings = this._bindings;
597
326
  }
598
327
  }
599
328
 
@@ -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;