iobroker.mywebui 1.42.37 → 1.42.39

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.39",
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();
@@ -1,108 +1,109 @@
1
1
  import { BaseCustomWebComponentConstructorAppend, css, html } from "@gokturk413/base-custom-webcomponent";
2
- import { iobrokerHandler } from "../common/IobrokerHandler.js";
3
2
 
4
3
  const EDITOR_BASE = new URL('../../../3d-editor/', import.meta.url).href;
5
4
 
6
- const RAD2DEG = 180 / Math.PI;
7
- const DEG2RAD = Math.PI / 180;
8
-
9
5
  export class IobrokerWebui3DScreenPropertiesPanel extends BaseCustomWebComponentConstructorAppend {
10
6
 
11
7
  static template = html`
12
8
  <div id="root">
13
- <!-- Title row -->
14
- <div id="titleRow">
15
- <span class="lbl">Type:</span>
16
- <span id="typeVal">-</span>
9
+ <!-- Sub-tab bar -->
10
+ <div id="subBar">
11
+ <span class="stab active" data-sub="scene">Scene</span>
12
+ <span class="stab" data-sub="object">Object</span>
13
+ <span class="stab" data-sub="geom">Geom</span>
14
+ <span class="stab" data-sub="mater">Mater</span>
17
15
  </div>
18
16
 
19
- <!-- Main tab content area -->
20
- <div id="mainArea">
17
+ <!-- Scene tab -->
18
+ <div id="subScene" class="spanel active"></div>
21
19
 
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>
20
+ <!-- Object tab -->
21
+ <div id="subObject" class="spanel" style="display:none;">
22
+ <div id="objThreeWrap"></div>
23
+ <div class="sechdr">BINDINGS (IOBROKER STATES)</div>
24
+ <div id="bindRows"></div>
25
+ </div>
41
26
 
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>
27
+ <!-- Geom tab -->
28
+ <div id="subGeom" class="spanel" style="display:none;"></div>
50
29
 
51
- <!-- PROJ tab -->
52
- <div id="tabProj" class="mpanel" style="display:none;">
53
- <div id="projWrap"></div>
54
- </div>
30
+ <!-- Mater tab -->
31
+ <div id="subMater" class="spanel" style="display:none;"></div>
55
32
 
56
- <!-- VISI tab -->
57
- <div id="tabVisi" class="mpanel" style="display:none;">
58
- <div id="visiContent2" style="padding:10px;"></div>
59
- </div>
33
+ <!-- Context menu -->
34
+ <div id="ctxMenu">
35
+ <div class="cmItem" data-action="clear">clear</div>
36
+ <div class="cmItem" data-action="edittext">edit as text</div>
37
+ <div class="cmItem" data-action="editbind">edit binding</div>
38
+ </div>
60
39
 
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>
40
+ <!-- Binding dialog -->
41
+ <div id="bdOverlay">
42
+ <div id="bdBox">
43
+ <div id="bdTitle">Edit Binding of '<span id="bdPropLabel"></span>' - 3DScreen</div>
44
+
45
+ <div class="bdRow">
46
+ <div class="bdLbl">objects</div>
47
+ <div style="display:flex;gap:3px;align-items:center;">
48
+ <input id="bdObj" type="text" class="bdInput" style="flex:1;" placeholder="state id" autocomplete="off"/>
49
+ <button class="bdBtn sm" id="bdObjX">X</button>
50
+ <button class="bdBtn sm" id="bdObjBrowse">...</button>
51
+ <button class="bdBtn sm accent" id="bdObjIOB">IOB</button>
52
+ </div>
53
+ </div>
65
54
 
66
- <!-- TRANSL tab -->
67
- <div id="tabTransl" class="mpanel" style="display:none;">
68
- <div id="translWrap" style="padding:10px;"></div>
69
- </div>
55
+ <div class="bdRow" style="display:flex;align-items:center;gap:8px;">
56
+ <span class="bdLbl" style="margin-bottom:0;">type :</span>
57
+ <select id="bdType" class="bdSel">
58
+ <option value="signal">signal</option>
59
+ <option value="ignore" selected>ignore</option>
60
+ <option value="css">css</option>
61
+ <option value="attribute">attribute</option>
62
+ </select>
63
+ </div>
70
64
 
71
- </div>
65
+ <div class="bdRow" style="display:flex;align-items:center;gap:8px;">
66
+ <input id="bdTwoWay" type="checkbox"/>
67
+ <span class="bdChkLbl">two way binding</span>
68
+ </div>
69
+ <div class="bdRow" style="display:flex;align-items:center;gap:8px;">
70
+ <input id="bdInvert" type="checkbox"/>
71
+ <span class="bdChkLbl">invert logic</span>
72
+ </div>
72
73
 
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>
74
+ <div class="bdRow" style="display:flex;align-items:center;justify-content:space-between;">
75
+ <span class="bdLbl" style="margin-bottom:0;">formula</span>
76
+ <button class="bdBtn sm" id="bdHistoric">historic</button>
77
+ </div>
78
+ <div class="bdRow">
79
+ <textarea id="bdFormula" class="bdTa" rows="2" placeholder="val"></textarea>
80
+ </div>
82
81
 
83
- <!-- Binding dialog overlay -->
84
- <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>
89
- <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>
92
- </div>
82
+ <div class="bdRow">
83
+ <span class="bdLbl">write back signal :</span>
84
+ <input id="bdWriteBack" type="text" class="bdInput" placeholder=""/>
93
85
  </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" />
86
+
87
+ <div class="bdRow">
88
+ <span class="bdLbl">formula write back (two way)</span>
89
+ <textarea id="bdFormulaWB" class="bdTa" rows="2" placeholder="val"></textarea>
97
90
  </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>
91
+
92
+ <div class="bdRow">
93
+ <span class="bdLbl">converter:</span>
94
+ <table id="bdConvTable" class="bdTable">
95
+ <thead><tr><th>condition</th><th>value</th></tr></thead>
96
+ <tbody id="bdConvBody"></tbody>
97
+ </table>
98
+ <div style="display:flex;gap:4px;margin-top:4px;justify-content:flex-end;">
99
+ <button class="bdBtn" id="bdConvAdd">add</button>
100
+ <button class="bdBtn" id="bdConvRemove">remove</button>
101
+ </div>
101
102
  </div>
102
- <div id="bdFooter">
103
- <button id="bdClear">Clear</button>
104
- <button id="bdCancel">Cancel</button>
105
- <button id="bdOk" class="primary">OK</button>
103
+
104
+ <div id="bdFoot">
105
+ <button class="bdBtn accent-ok" id="bdOk">Ok</button>
106
+ <button class="bdBtn" id="bdCancel">Cancel</button>
106
107
  </div>
107
108
  </div>
108
109
  </div>
@@ -113,487 +114,410 @@ export class IobrokerWebui3DScreenPropertiesPanel extends BaseCustomWebComponent
113
114
  :host { display:block;width:100%;height:100%;overflow:hidden; }
114
115
  #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
116
 
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; }
117
+ /* Sub-tab bar */
118
+ #subBar { display:flex;background:#2d2d30;border-bottom:1px solid #3c3c3c;flex-shrink:0; }
127
119
  .stab { padding:5px 10px;cursor:pointer;font-size:11px;color:#888;border-right:1px solid #3c3c3c;white-space:nowrap;user-select:none; }
128
120
  .stab:hover { color:#ddd;background:#3e3e42; }
129
121
  .stab.active { color:#fff;background:#1e1e1e;border-top:2px solid #4ec9b0; }
130
122
 
131
- .scontent { flex:1;overflow:auto; }
123
+ /* Sub-tab panels */
124
+ .spanel { flex:1;overflow:auto;min-height:0; }
125
+ #subScene,#subGeom,#subMater { overflow:auto; }
126
+ #subObject { display:flex;flex-direction:column; }
127
+ #objThreeWrap { overflow:auto; }
132
128
 
133
129
  /* 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; }
152
-
153
- /* 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; }
176
-
177
- /* Binding dialog */
130
+ .sechdr { background:#252526;color:#9cdcfe;padding:4px 8px;font-size:10px;font-weight:bold;text-transform:uppercase;border-top:1px solid #3c3c3c;border-bottom:1px solid #3c3c3c;letter-spacing:.5px;flex-shrink:0; }
131
+
132
+ /* Binding rows — match 2D property grid style */
133
+ #bindRows { flex:1;overflow:auto; }
134
+ .brow { display:flex;align-items:center;padding:2px 4px;gap:0;border-bottom:1px solid #2a2a2a;min-height:20px; }
135
+ .brow:hover { background:#252526; }
136
+ /* square button */
137
+ .bsq { width:14px;height:14px;min-width:14px;border:1px solid #555;background:#2d2d30;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:8px;color:#888;flex-shrink:0;margin-right:4px;padding:0; }
138
+ .bsq:hover { border-color:#4ec9b0; }
139
+ .bsq.bound { background:#0e639c;border-color:#1177bb;color:#fff; }
140
+ /* property name */
141
+ .blbl { width:90px;color:#9cdcfe;font-size:11px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; }
142
+ /* signal display */
143
+ .bval { flex:1;color:#4ec9b0;font-size:10px;font-family:monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:0 2px; }
144
+ .bval.none { color:#555; }
145
+
146
+ /* Context menu */
147
+ #ctxMenu { display:none;position:fixed;z-index:9999;background:#2d2d30;border:1px solid #555;min-width:120px;box-shadow:2px 2px 8px rgba(0,0,0,0.6); }
148
+ #ctxMenu.open { display:block; }
149
+ .cmItem { padding:5px 12px;cursor:pointer;font-size:11px;color:#ccc; }
150
+ .cmItem:hover { background:#3e3e42;color:#fff; }
151
+
152
+ /* Binding dialog overlay */
178
153
  #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; }
154
+ #bdOverlay.open { display:flex; }
155
+ #bdBox { background:#2d2d30;border:1px solid #555;padding:0;width:340px;max-height:90%;overflow-y:auto;box-shadow:4px 4px 16px rgba(0,0,0,0.7); }
156
+ #bdTitle { background:#3e3e42;padding:7px 10px;font-size:12px;font-weight:bold;color:#9cdcfe;border-bottom:1px solid #555;margin-bottom:0; }
157
+ .bdRow { padding:4px 10px 2px; }
158
+ .bdLbl { color:#888;font-size:10px;margin-bottom:2px;display:block; }
159
+ .bdInput { width:100%;background:#3c3c3c;border:1px solid #555;color:#ccc;padding:3px 5px;font-size:11px;box-sizing:border-box; }
160
+ .bdInput:focus { border-color:#4ec9b0;outline:none; }
161
+ .bdSel { background:#3c3c3c;border:1px solid #555;color:#ccc;padding:2px 4px;font-size:11px; }
162
+ .bdTa { width:100%;background:#3c3c3c;border:1px solid #555;color:#ccc;padding:3px 5px;font-size:11px;box-sizing:border-box;resize:vertical;font-family:monospace; }
163
+ .bdTa:focus { border-color:#4ec9b0;outline:none; }
164
+ .bdChkLbl { color:#ccc;font-size:11px; }
165
+ .bdTable { width:100%;border-collapse:collapse;font-size:10px;color:#ccc; }
166
+ .bdTable th { background:#252526;padding:2px 6px;text-align:left;color:#888;border:1px solid #3c3c3c; }
167
+ .bdTable td { padding:2px 4px;border:1px solid #3c3c3c; }
168
+ .bdTable td input { width:100%;background:#3c3c3c;border:none;color:#ccc;padding:1px 3px;font-size:10px; }
169
+ .bdBtn { padding:3px 10px;background:#3c3c3c;border:1px solid #555;color:#ccc;cursor:pointer;font-size:11px; }
170
+ .bdBtn:hover { border-color:#999; }
171
+ .bdBtn.sm { padding:2px 6px;font-size:10px; }
172
+ .bdBtn.accent { background:#0e639c;border-color:#1177bb;color:#fff; }
173
+ .bdBtn.accent-ok { background:#0e639c;border-color:#1177bb;color:#fff; }
174
+ #bdFoot { display:flex;justify-content:flex-end;gap:6px;padding:8px 10px;border-top:1px solid #3c3c3c;margin-top:4px; }
190
175
  `;
191
176
 
192
177
  _editor = null;
193
178
  _editorHost = null;
194
- _bindings = {}; // { uuid: { 'position.x': { signal, formula, twoWay } } }
179
+ _bindings = {};
195
180
  _selectedObj = null;
196
- _bindingTarget = null; // { uuid, prop }
181
+ _ctxTarget = null; // { uuid, key }
197
182
 
198
- // ── public API ────────────────────────────────────────────────────────────
183
+ // ── Public API ─────────────────────────────────────────────────────────────
199
184
 
200
185
  async connect(editor, editorHost) {
201
- this._editor = editor;
186
+ this._editor = editor;
202
187
  this._editorHost = editorHost;
203
- this._bindings = editorHost?.sceneData?.bindings ?? {};
188
+ this._bindings = editorHost?.sceneData?.bindings ?? {};
204
189
 
205
190
  await this._injectThreeCSS();
206
- await this._buildPanels();
207
- this._setupBottomTabs();
208
- this._setupPropSubTabs();
191
+ await this._mountThreePanels();
192
+ this._setupSubTabs();
193
+ this._buildBindingRows();
194
+ this._setupContextMenu();
209
195
  this._setupBindingDialog();
210
- this._setupVisibilityUI();
211
196
 
212
- editor.signals.objectSelected.add((obj) => {
197
+ editor.signals.objectSelected.add(obj => {
213
198
  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 ?? '-') : '-';
199
+ this._refreshBindRows();
218
200
  });
219
-
220
- editor.signals.objectChanged.add((obj) => {
221
- if (obj && obj === this._selectedObj) this._updateObjectPanel(obj);
201
+ editor.signals.objectChanged.add(obj => {
202
+ if (obj && obj === this._selectedObj) this._refreshBindRows();
222
203
  });
223
204
 
224
- this._updateObjectPanel(null);
225
- this._updateBindRows(null);
205
+ this._refreshBindRows();
226
206
  }
227
207
 
228
- /** Call this before saving scene data */
229
208
  getBindings() { return this._bindings; }
230
209
 
231
- /** Call after loading scene data to restore bindings */
232
- setBindings(bindings) {
233
- this._bindings = bindings || {};
234
- if (this._selectedObj) this._updateBindRows(this._selectedObj);
210
+ setBindings(b) {
211
+ this._bindings = b || {};
212
+ this._refreshBindRows();
235
213
  }
236
214
 
237
- // ── CSS injection ─────────────────────────────────────────────────────────
215
+ // ── Three.js CSS ────────────────────────────────────────────────────────────
238
216
 
239
217
  async _injectThreeCSS() {
240
218
  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);
219
+ if (!sr || sr.querySelector('#tcss')) return;
220
+ const l = document.createElement('link');
221
+ l.id = 'tcss'; l.rel = 'stylesheet';
222
+ l.href = EDITOR_BASE + 'css/main.css';
223
+ sr.appendChild(l);
247
224
  }
248
225
 
249
- // ── Build Three.js sidebar panels ─────────────────────────────────────────
226
+ // ── Mount Three.js sidebar panels ──────────────────────────────────────────
250
227
 
251
- async _buildPanels() {
228
+ async _mountThreePanels() {
252
229
  const base = EDITOR_BASE + 'js/';
230
+ const ed = this._editor;
253
231
  const [
254
232
  { SidebarScene },
255
233
  { SidebarObject },
256
234
  { SidebarGeometry },
257
235
  { SidebarMaterial },
258
- { SidebarProject },
259
- { SidebarSettings },
260
236
  ] = await Promise.all([
261
237
  import(/* @vite-ignore */ base + 'Sidebar.Scene.js'),
262
238
  import(/* @vite-ignore */ base + 'Sidebar.Object.js'),
263
239
  import(/* @vite-ignore */ base + 'Sidebar.Geometry.js'),
264
240
  import(/* @vite-ignore */ base + 'Sidebar.Material.js'),
265
- import(/* @vite-ignore */ base + 'Sidebar.Project.js'),
266
- import(/* @vite-ignore */ base + 'Sidebar.Settings.js'),
267
241
  ]);
268
242
 
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);
243
+ this._getDomElement('subScene').appendChild(new SidebarScene(ed).dom);
244
+ this._getDomElement('objThreeWrap').appendChild(new SidebarObject(ed).dom);
245
+ this._getDomElement('subGeom').appendChild(new SidebarGeometry(ed).dom);
246
+ this._getDomElement('subMater').appendChild(new SidebarMaterial(ed).dom);
295
247
  }
296
248
 
297
- // ── Bottom tabs ───────────────────────────────────────────────────────────
249
+ // ── Sub-tabs ────────────────────────────────────────────────────────────────
298
250
 
299
- _setupBottomTabs() {
300
- const tabMap = {
301
- Prop: 'tabProp',
302
- Setti: 'tabSetti',
303
- Proj: 'tabProj',
304
- Visi: 'tabVisi',
305
- Refac: 'tabRefac',
306
- Transl: 'tabTransl',
251
+ _setupSubTabs() {
252
+ const bar = this._getDomElement('subBar');
253
+ const panels = {
254
+ scene: this._getDomElement('subScene'),
255
+ object: this._getDomElement('subObject'),
256
+ geom: this._getDomElement('subGeom'),
257
+ mater: this._getDomElement('subMater'),
307
258
  };
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
259
  bar?.querySelectorAll('.stab').forEach(btn => {
337
260
  btn.addEventListener('click', () => {
338
261
  bar.querySelectorAll('.stab').forEach(b => b.classList.remove('active'));
339
262
  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
- }
263
+ Object.values(panels).forEach(p => { if (p) p.style.display = 'none'; });
264
+ const p = panels[btn.dataset.sub];
265
+ if (p) p.style.display = btn.dataset.sub === 'object' ? 'flex' : 'block';
349
266
  });
350
267
  });
351
268
  }
352
269
 
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' },
270
+ // ── Bindable properties ─────────────────────────────────────────────────────
271
+
272
+ _BINDABLE = [
273
+ { key: 'position.x', label: 'Position X' },
274
+ { key: 'position.y', label: 'Position Y' },
275
+ { key: 'position.z', label: 'Position Z' },
276
+ { key: 'rotation.x', label: 'Rotation X (°)' },
277
+ { key: 'rotation.y', label: 'Rotation Y (°)' },
278
+ { key: 'rotation.z', label: 'Rotation Z (°)' },
279
+ { key: 'scale.x', label: 'Scale X' },
280
+ { key: 'scale.y', label: 'Scale Y' },
281
+ { key: 'scale.z', label: 'Scale Z' },
282
+ { key: 'visible', label: 'Visible' },
283
+ { key: 'material.opacity', label: 'Opacity' },
284
+ { key: 'material.color', label: 'Color (hex)' },
377
285
  ];
378
286
 
379
- _buildBindRows(editor) {
380
- const container = this._getDomElement('bindRows');
381
- if (!container) return;
382
- container.innerHTML = '';
383
-
384
- this._BINDABLE_PROPS.forEach(({ key, label }) => {
287
+ _buildBindingRows() {
288
+ const cont = this._getDomElement('bindRows');
289
+ if (!cont) return;
290
+ cont.innerHTML = '';
291
+ this._BINDABLE.forEach(({ key, label }) => {
385
292
  const row = document.createElement('div');
386
293
  row.className = 'brow';
387
294
  row.dataset.key = key;
388
295
 
296
+ // □ square button (left of label)
297
+ const sq = document.createElement('button');
298
+ sq.className = 'bsq';
299
+ sq.title = 'right-click to bind / clear';
300
+ sq.textContent = '■';
301
+ sq.addEventListener('contextmenu', e => {
302
+ e.preventDefault();
303
+ this._showCtxMenu(e, key);
304
+ });
305
+ sq.addEventListener('click', e => {
306
+ e.preventDefault();
307
+ this._showCtxMenu(e, key);
308
+ });
309
+
310
+ // property name
389
311
  const lbl = document.createElement('span');
390
- lbl.className = 'b-lbl';
312
+ lbl.className = 'blbl';
391
313
  lbl.textContent = label;
392
314
 
393
- const sig = document.createElement('span');
394
- sig.className = 'b-sig none';
395
- sig.textContent = '';
396
-
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));
315
+ // current signal display
316
+ const val = document.createElement('span');
317
+ val.className = 'bval none';
318
+ val.textContent = '';
402
319
 
320
+ row.appendChild(sq);
403
321
  row.appendChild(lbl);
404
- row.appendChild(sig);
405
- row.appendChild(editBtn);
406
- container.appendChild(row);
322
+ row.appendChild(val);
323
+ cont.appendChild(row);
407
324
  });
408
325
  }
409
326
 
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');
327
+ _refreshBindRows() {
328
+ const cont = this._getDomElement('bindRows');
329
+ if (!cont) return;
330
+ const uuid = this._selectedObj?.uuid;
331
+ cont.querySelectorAll('.brow').forEach(row => {
332
+ const key = row.dataset.key;
333
+ const sq = row.querySelector('.bsq');
334
+ const val = row.querySelector('.bval');
335
+ const bind = uuid ? this._bindings[uuid]?.[key] : null;
336
+
337
+ if (val) {
338
+ val.textContent = bind?.signal || '';
339
+ val.className = 'bval' + (bind?.signal ? '' : ' none');
423
340
  }
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' : '';
341
+ if (sq) {
342
+ sq.className = 'bsq' + (bind?.signal ? ' bound' : '');
428
343
  }
429
344
  });
430
345
  }
431
346
 
432
- // ── Binding dialog ────────────────────────────────────────────────────────
347
+ // ── Context menu ────────────────────────────────────────────────────────────
433
348
 
434
- _setupBindingDialog() {
435
- const overlay = this._getDomElement('bdOverlay');
349
+ _setupContextMenu() {
350
+ const menu = this._getDomElement('ctxMenu');
351
+ if (!menu) return;
436
352
 
437
- this._getDomElement('bdCancel')?.addEventListener('click', () => {
438
- overlay?.classList.remove('show');
439
- this._bindingTarget = null;
440
- });
353
+ menu.addEventListener('click', e => {
354
+ const item = e.target.closest('.cmItem');
355
+ if (!item) return;
356
+ const action = item.dataset.action;
357
+ menu.classList.remove('open');
441
358
 
442
- this._getDomElement('bdClear')?.addEventListener('click', () => {
443
- const { uuid, key } = this._bindingTarget || {};
444
- 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);
451
- }
452
- overlay?.classList.remove('show');
453
- this._bindingTarget = null;
454
- });
455
-
456
- 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
-
462
- if (uuid && key && signal) {
463
- if (!this._bindings[uuid]) this._bindings[uuid] = {};
464
- this._bindings[uuid][key] = { signal, formula, twoWay };
465
- this._syncBindingsToHost();
466
- this._updateBindRows(this._selectedObj);
467
- }
468
- overlay?.classList.remove('show');
469
- this._bindingTarget = null;
359
+ if (action === 'clear') this._clearBinding(this._ctxTarget);
360
+ if (action === 'editbind') this._openBindDialog(this._ctxTarget);
361
+ if (action === 'edittext') this._openTextEditor(this._ctxTarget);
470
362
  });
471
363
 
472
- this._getDomElement('bdBrowse')?.addEventListener('click', async () => {
473
- try {
474
- const id = await iobrokerHandler.showSelectIdDialog();
475
- if (id) {
476
- const inp = this._getDomElement('bdSignal');
477
- if (inp) inp.value = id;
478
- }
479
- } catch (_) {}
364
+ // close on outside click
365
+ document.addEventListener('click', e => {
366
+ if (!menu.contains(e.target)) menu.classList.remove('open');
480
367
  });
481
368
  }
482
369
 
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)) + ']';
370
+ _showCtxMenu(e, key) {
371
+ if (!this._selectedObj) {
372
+ alert('Select a 3D object first.');
373
+ return;
374
+ }
375
+ this._ctxTarget = { uuid: this._selectedObj.uuid, key };
376
+ const menu = this._getDomElement('ctxMenu');
377
+ if (!menu) return;
378
+ // position near click
379
+ menu.style.left = e.clientX + 'px';
380
+ menu.style.top = e.clientY + 'px';
381
+ menu.classList.add('open');
382
+ }
500
383
 
501
- const overlay = this._getDomElement('bdOverlay');
502
- overlay?.classList.add('show');
384
+ _clearBinding({ uuid, key } = {}) {
385
+ if (!uuid || !key) return;
386
+ delete this._bindings[uuid]?.[key];
387
+ if (this._bindings[uuid] && !Object.keys(this._bindings[uuid]).length)
388
+ delete this._bindings[uuid];
389
+ this._syncHost();
390
+ this._refreshBindRows();
503
391
  }
504
392
 
505
- _syncBindingsToHost() {
506
- if (this._editorHost?.sceneData) {
507
- this._editorHost.sceneData.bindings = this._bindings;
508
- }
393
+ _openTextEditor({ uuid, key } = {}) {
394
+ if (!uuid || !key) return;
395
+ const ex = this._bindings[uuid]?.[key];
396
+ const val = ex?.signal || '';
397
+ const nv = window.prompt(`State ID for "${key}":`, val);
398
+ if (nv === null) return;
399
+ if (!nv.trim()) { this._clearBinding({ uuid, key }); return; }
400
+ if (!this._bindings[uuid]) this._bindings[uuid] = {};
401
+ this._bindings[uuid][key] = { signal: nv.trim(), formula: ex?.formula || 'val', twoWay: ex?.twoWay || false };
402
+ this._syncHost();
403
+ this._refreshBindRows();
509
404
  }
510
405
 
511
- // ── Visibility UI (Settings tab) ──────────────────────────────────────────
406
+ // ── Binding dialog ──────────────────────────────────────────────────────────
512
407
 
513
- _setupVisibilityUI() {
514
- const host = this._editorHost;
515
- const settings = host?.sceneData?.settings ?? {};
408
+ _setupBindingDialog() {
409
+ const ov = this._getDomElement('bdOverlay');
516
410
 
517
- const content = this._getDomElement('visContent');
518
- if (!content) return;
411
+ this._getDomElement('bdCancel')?.addEventListener('click', () => {
412
+ ov?.classList.remove('open');
413
+ });
519
414
 
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', '');
415
+ this._getDomElement('bdObjX')?.addEventListener('click', () => {
416
+ const inp = this._getDomElement('bdObj');
417
+ if (inp) inp.value = '';
418
+ });
525
419
 
526
- const save = () => {
527
- if (host?.sceneData) host.sceneData.settings = settings;
528
- };
420
+ this._getDomElement('bdObjBrowse')?.addEventListener('click', async () => {
421
+ try {
422
+ const { openSelectIdDialog } = await import('@iobroker/webcomponent-selectid-dialog/dist/selectIdHelper.js');
423
+ const connection = (await import('../common/IobrokerHandler.js')).iobrokerHandler.connection;
424
+ const id = await openSelectIdDialog(connection, null, false);
425
+ if (id) { const inp = this._getDomElement('bdObj'); if (inp) inp.value = id; }
426
+ } catch (_) {}
427
+ });
529
428
 
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
- `;
429
+ this._getDomElement('bdObjIOB')?.addEventListener('click', async () => {
430
+ try {
431
+ const { openSelectIdDialog } = await import('@iobroker/webcomponent-selectid-dialog/dist/selectIdHelper.js');
432
+ const connection = (await import('../common/IobrokerHandler.js')).iobrokerHandler.connection;
433
+ const id = await openSelectIdDialog(connection, null, false);
434
+ if (id) { const inp = this._getDomElement('bdObj'); if (inp) inp.value = id; }
435
+ } catch (_) {}
436
+ });
554
437
 
555
- content.querySelector('#vis3d-enable')?.addEventListener('change', e => {
556
- settings.visibilityEnabled = e.target.checked; save();
438
+ this._getDomElement('bdHistoric')?.addEventListener('click', () => {
439
+ // historic signals - show simple list if available
557
440
  });
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();
441
+
442
+ this._getDomElement('bdConvAdd')?.addEventListener('click', () => {
443
+ const tbody = this._getDomElement('bdConvBody');
444
+ if (!tbody) return;
445
+ const tr = document.createElement('tr');
446
+ tr.innerHTML = '<td><input type="text" placeholder="condition"/></td><td><input type="text" placeholder="value"/></td>';
447
+ tbody.appendChild(tr);
563
448
  });
564
- content.querySelector('#vis3d-redir')?.addEventListener('input', e => {
565
- settings.visibilityRedirectScreen = e.target.value; save();
449
+
450
+ this._getDomElement('bdConvRemove')?.addEventListener('click', () => {
451
+ const tbody = this._getDomElement('bdConvBody');
452
+ if (!tbody) return;
453
+ const last = tbody.querySelector('tr:last-child');
454
+ if (last) last.remove();
566
455
  });
567
456
 
568
- // Load user groups
569
- this._loadUserGroups(settings, content.querySelector('#vis3d-groups'), save);
457
+ this._getDomElement('bdOk')?.addEventListener('click', () => {
458
+ const { uuid, key } = this._ctxTarget || {};
459
+ const signal = this._getDomElement('bdObj')?.value?.trim();
460
+ const formula = this._getDomElement('bdFormula')?.value?.trim() || 'val';
461
+ const formulaWB = this._getDomElement('bdFormulaWB')?.value?.trim() || '';
462
+ const writeBack = this._getDomElement('bdWriteBack')?.value?.trim() || '';
463
+ const twoWay = this._getDomElement('bdTwoWay')?.checked ?? false;
464
+ const invert = this._getDomElement('bdInvert')?.checked ?? false;
465
+ const type = this._getDomElement('bdType')?.value || 'signal';
466
+
467
+ // collect converter rows
468
+ const converter = [];
469
+ this._getDomElement('bdConvBody')?.querySelectorAll('tr').forEach(tr => {
470
+ const inputs = tr.querySelectorAll('input');
471
+ if (inputs[0]?.value || inputs[1]?.value)
472
+ converter.push({ condition: inputs[0]?.value || '', value: inputs[1]?.value || '' });
473
+ });
474
+
475
+ if (uuid && key && signal) {
476
+ if (!this._bindings[uuid]) this._bindings[uuid] = {};
477
+ this._bindings[uuid][key] = { signal, formula, twoWay, invert, type, writeBack, formulaWB, converter };
478
+ this._syncHost();
479
+ this._refreshBindRows();
480
+ } else if (uuid && key && !signal) {
481
+ this._clearBinding({ uuid, key });
482
+ }
483
+
484
+ ov?.classList.remove('open');
485
+ });
570
486
  }
571
487
 
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);
488
+ _openBindDialog({ uuid, key } = {}) {
489
+ if (!uuid || !key) return;
490
+ const ex = this._bindings[uuid]?.[key];
491
+
492
+ const getEl = id => this._getDomElement(id);
493
+ const label = this._BINDABLE.find(b => b.key === key)?.label || key;
494
+
495
+ getEl('bdPropLabel').textContent = label;
496
+ getEl('bdObj').value = ex?.signal || '';
497
+ getEl('bdFormula').value = ex?.formula || 'val';
498
+ getEl('bdFormulaWB').value = ex?.formulaWB || '';
499
+ getEl('bdWriteBack').value = ex?.writeBack || '';
500
+ getEl('bdTwoWay').checked = ex?.twoWay || false;
501
+ getEl('bdInvert').checked = ex?.invert || false;
502
+ if (getEl('bdType')) getEl('bdType').value = ex?.type || 'signal';
503
+
504
+ // fill converter
505
+ const tbody = getEl('bdConvBody');
506
+ if (tbody) {
507
+ tbody.innerHTML = '';
508
+ (ex?.converter || []).forEach(row => {
509
+ const tr = document.createElement('tr');
510
+ tr.innerHTML = `<td><input type="text" value="${row.condition || ''}"/></td><td><input type="text" value="${row.value || ''}"/></td>`;
511
+ tbody.appendChild(tr);
593
512
  });
594
- } catch (_) {
595
- container.innerHTML = '<span style="color:#666;font-size:11px;">Could not load groups</span>';
596
513
  }
514
+
515
+ this._ctxTarget = { uuid, key };
516
+ getEl('bdOverlay')?.classList.add('open');
517
+ }
518
+
519
+ _syncHost() {
520
+ if (this._editorHost?.sceneData) this._editorHost.sceneData.bindings = this._bindings;
597
521
  }
598
522
  }
599
523
 
@@ -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="Projects" 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) => {
@@ -309,7 +313,7 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
309
313
 
310
314
  setTimeout(makeSetup('visibilityDock', () => this._setupVisibilityPanel(), 'Visibility'), 1000);
311
315
  setTimeout(makeSetup('animationsDock', () => this._setupAnimationsPanel(), 'Animations'), 1200);
312
- setTimeout(makeSetup('effectsDock', () => this._setupEffectsPanel(), 'Effects'), 1400);
316
+ setTimeout(makeSetup('effectsDock', () => this._setupEffectsPanel(), 'Projects'), 1400);
313
317
  }
314
318
 
315
319
  _setupVisibilityPanel() {
@@ -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;