iobroker.mywebui 1.42.36 → 1.42.37
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 +1 -1
- package/package.json +1 -1
- package/www/3d-lib/three.webgpu.js +8 -2
- package/www/dist/frontend/config/IobrokerWebui3DScreenEditor.js +22 -4
- package/www/dist/frontend/config/IobrokerWebui3DScreenPropertiesPanel.js +600 -0
- package/www/dist/frontend/config/IobrokerWebui3DScreenViewer.js +62 -0
- package/www/dist/importmaps/importmap-config.js +1 -0
package/io-package.json
CHANGED
package/package.json
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
-
// three/webgpu stub — re-exports
|
|
2
|
-
// Viewport.js only needs PMREMGenerator from this entry point
|
|
1
|
+
// three/webgpu stub — re-exports standard Three.js + stubs WebGPU-only classes
|
|
3
2
|
export * from './three.module.js';
|
|
3
|
+
import { WebGLRenderer } from './three.module.js';
|
|
4
|
+
|
|
5
|
+
// WebGPURenderer stub: falls back to WebGLRenderer so the editor doesn't crash
|
|
6
|
+
export class WebGPURenderer extends WebGLRenderer {
|
|
7
|
+
constructor(params) { super(params); }
|
|
8
|
+
async init() {}
|
|
9
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BaseCustomWebComponentConstructorAppend, css, html } from "@gokturk413/base-custom-webcomponent";
|
|
2
2
|
import { iobrokerHandler } from "../common/IobrokerHandler.js";
|
|
3
|
+
import './IobrokerWebui3DScreenPropertiesPanel.js';
|
|
3
4
|
import './IobrokerWebuiMonacoEditor.js';
|
|
4
5
|
|
|
5
6
|
// Base URL of the three.js editor files (resolved relative to this module)
|
|
@@ -18,10 +19,15 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
|
|
|
18
19
|
<button id="scriptToggleBtn" class="tb-btn" title="Toggle scene script panel">📄 Script</button>
|
|
19
20
|
</div>
|
|
20
21
|
|
|
21
|
-
<!-- three.js editor
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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>
|
|
25
31
|
|
|
26
32
|
<!-- Bottom Monaco panel for iobroker scene script -->
|
|
27
33
|
<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;">
|
|
@@ -97,6 +103,12 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
|
|
|
97
103
|
|
|
98
104
|
if (this._editor) this.sceneData.threeScene = this._editor.toJSON();
|
|
99
105
|
|
|
106
|
+
// Save ioBroker bindings from properties panel
|
|
107
|
+
const iobPropPanel = this._getDomElement('iobPropPanel');
|
|
108
|
+
if (iobPropPanel?.getBindings) {
|
|
109
|
+
this.sceneData.bindings = iobPropPanel.getBindings();
|
|
110
|
+
}
|
|
111
|
+
|
|
100
112
|
try {
|
|
101
113
|
const sceneType = this.getAttribute('scene-type') || '3dscreen';
|
|
102
114
|
const ok = await iobrokerHandler.saveObject(sceneType, this.sceneData.name, this.sceneData);
|
|
@@ -270,6 +282,12 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
|
|
|
270
282
|
this._resizeObserver = new ResizeObserver(() => editor.signals.windowResize.dispatch());
|
|
271
283
|
this._resizeObserver.observe(container);
|
|
272
284
|
|
|
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
|
+
|
|
273
291
|
// Dispatch resize after layout is complete (container may have 0 size on first tick)
|
|
274
292
|
requestAnimationFrame(() => {
|
|
275
293
|
editor.signals.windowResize.dispatch();
|
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
import { BaseCustomWebComponentConstructorAppend, css, html } from "@gokturk413/base-custom-webcomponent";
|
|
2
|
+
import { iobrokerHandler } from "../common/IobrokerHandler.js";
|
|
3
|
+
|
|
4
|
+
const EDITOR_BASE = new URL('../../../3d-editor/', import.meta.url).href;
|
|
5
|
+
|
|
6
|
+
const RAD2DEG = 180 / Math.PI;
|
|
7
|
+
const DEG2RAD = Math.PI / 180;
|
|
8
|
+
|
|
9
|
+
export class IobrokerWebui3DScreenPropertiesPanel extends BaseCustomWebComponentConstructorAppend {
|
|
10
|
+
|
|
11
|
+
static template = html`
|
|
12
|
+
<div id="root">
|
|
13
|
+
<!-- Title row -->
|
|
14
|
+
<div id="titleRow">
|
|
15
|
+
<span class="lbl">Type:</span>
|
|
16
|
+
<span id="typeVal">-</span>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
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>
|
|
50
|
+
|
|
51
|
+
<!-- PROJ tab -->
|
|
52
|
+
<div id="tabProj" class="mpanel" style="display:none;">
|
|
53
|
+
<div id="projWrap"></div>
|
|
54
|
+
</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
|
+
</div>
|
|
72
|
+
|
|
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>
|
|
82
|
+
|
|
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>
|
|
93
|
+
</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" />
|
|
97
|
+
</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>
|
|
101
|
+
</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>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
`;
|
|
111
|
+
|
|
112
|
+
static style = css`
|
|
113
|
+
:host { display:block;width:100%;height:100%;overflow:hidden; }
|
|
114
|
+
#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
|
+
/* 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; }
|
|
128
|
+
.stab:hover { color:#ddd;background:#3e3e42; }
|
|
129
|
+
.stab.active { color:#fff;background:#1e1e1e;border-top:2px solid #4ec9b0; }
|
|
130
|
+
|
|
131
|
+
.scontent { flex:1;overflow:auto; }
|
|
132
|
+
|
|
133
|
+
/* 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 */
|
|
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; }
|
|
190
|
+
`;
|
|
191
|
+
|
|
192
|
+
_editor = null;
|
|
193
|
+
_editorHost = null;
|
|
194
|
+
_bindings = {}; // { uuid: { 'position.x': { signal, formula, twoWay } } }
|
|
195
|
+
_selectedObj = null;
|
|
196
|
+
_bindingTarget = null; // { uuid, prop }
|
|
197
|
+
|
|
198
|
+
// ── public API ────────────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
async connect(editor, editorHost) {
|
|
201
|
+
this._editor = editor;
|
|
202
|
+
this._editorHost = editorHost;
|
|
203
|
+
this._bindings = editorHost?.sceneData?.bindings ?? {};
|
|
204
|
+
|
|
205
|
+
await this._injectThreeCSS();
|
|
206
|
+
await this._buildPanels();
|
|
207
|
+
this._setupBottomTabs();
|
|
208
|
+
this._setupPropSubTabs();
|
|
209
|
+
this._setupBindingDialog();
|
|
210
|
+
this._setupVisibilityUI();
|
|
211
|
+
|
|
212
|
+
editor.signals.objectSelected.add((obj) => {
|
|
213
|
+
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 ?? '-') : '-';
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
editor.signals.objectChanged.add((obj) => {
|
|
221
|
+
if (obj && obj === this._selectedObj) this._updateObjectPanel(obj);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
this._updateObjectPanel(null);
|
|
225
|
+
this._updateBindRows(null);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Call this before saving scene data */
|
|
229
|
+
getBindings() { return this._bindings; }
|
|
230
|
+
|
|
231
|
+
/** Call after loading scene data to restore bindings */
|
|
232
|
+
setBindings(bindings) {
|
|
233
|
+
this._bindings = bindings || {};
|
|
234
|
+
if (this._selectedObj) this._updateBindRows(this._selectedObj);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── CSS injection ─────────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
async _injectThreeCSS() {
|
|
240
|
+
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);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── Build Three.js sidebar panels ─────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
async _buildPanels() {
|
|
252
|
+
const base = EDITOR_BASE + 'js/';
|
|
253
|
+
const [
|
|
254
|
+
{ SidebarScene },
|
|
255
|
+
{ SidebarObject },
|
|
256
|
+
{ SidebarGeometry },
|
|
257
|
+
{ SidebarMaterial },
|
|
258
|
+
{ SidebarProject },
|
|
259
|
+
{ SidebarSettings },
|
|
260
|
+
] = await Promise.all([
|
|
261
|
+
import(/* @vite-ignore */ base + 'Sidebar.Scene.js'),
|
|
262
|
+
import(/* @vite-ignore */ base + 'Sidebar.Object.js'),
|
|
263
|
+
import(/* @vite-ignore */ base + 'Sidebar.Geometry.js'),
|
|
264
|
+
import(/* @vite-ignore */ base + 'Sidebar.Material.js'),
|
|
265
|
+
import(/* @vite-ignore */ base + 'Sidebar.Project.js'),
|
|
266
|
+
import(/* @vite-ignore */ base + 'Sidebar.Settings.js'),
|
|
267
|
+
]);
|
|
268
|
+
|
|
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);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ── Bottom tabs ───────────────────────────────────────────────────────────
|
|
298
|
+
|
|
299
|
+
_setupBottomTabs() {
|
|
300
|
+
const tabMap = {
|
|
301
|
+
Prop: 'tabProp',
|
|
302
|
+
Setti: 'tabSetti',
|
|
303
|
+
Proj: 'tabProj',
|
|
304
|
+
Visi: 'tabVisi',
|
|
305
|
+
Refac: 'tabRefac',
|
|
306
|
+
Transl: 'tabTransl',
|
|
307
|
+
};
|
|
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
|
+
bar?.querySelectorAll('.stab').forEach(btn => {
|
|
337
|
+
btn.addEventListener('click', () => {
|
|
338
|
+
bar.querySelectorAll('.stab').forEach(b => b.classList.remove('active'));
|
|
339
|
+
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
|
+
}
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
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' },
|
|
377
|
+
];
|
|
378
|
+
|
|
379
|
+
_buildBindRows(editor) {
|
|
380
|
+
const container = this._getDomElement('bindRows');
|
|
381
|
+
if (!container) return;
|
|
382
|
+
container.innerHTML = '';
|
|
383
|
+
|
|
384
|
+
this._BINDABLE_PROPS.forEach(({ key, label }) => {
|
|
385
|
+
const row = document.createElement('div');
|
|
386
|
+
row.className = 'brow';
|
|
387
|
+
row.dataset.key = key;
|
|
388
|
+
|
|
389
|
+
const lbl = document.createElement('span');
|
|
390
|
+
lbl.className = 'b-lbl';
|
|
391
|
+
lbl.textContent = label;
|
|
392
|
+
|
|
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));
|
|
402
|
+
|
|
403
|
+
row.appendChild(lbl);
|
|
404
|
+
row.appendChild(sig);
|
|
405
|
+
row.appendChild(editBtn);
|
|
406
|
+
container.appendChild(row);
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
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
|
+
}
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ── Binding dialog ────────────────────────────────────────────────────────
|
|
433
|
+
|
|
434
|
+
_setupBindingDialog() {
|
|
435
|
+
const overlay = this._getDomElement('bdOverlay');
|
|
436
|
+
|
|
437
|
+
this._getDomElement('bdCancel')?.addEventListener('click', () => {
|
|
438
|
+
overlay?.classList.remove('show');
|
|
439
|
+
this._bindingTarget = null;
|
|
440
|
+
});
|
|
441
|
+
|
|
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;
|
|
470
|
+
});
|
|
471
|
+
|
|
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 (_) {}
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
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');
|
|
503
|
+
}
|
|
504
|
+
|
|
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
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
customElements.define('iobroker-webui-3dscreen-properties', IobrokerWebui3DScreenPropertiesPanel);
|
|
@@ -235,6 +235,68 @@ export class IobrokerWebui3DScreenViewer extends BaseCustomWebComponentConstruct
|
|
|
235
235
|
const assets = new Map();
|
|
236
236
|
const mixers = new Map();
|
|
237
237
|
|
|
238
|
+
// ── ioBroker state bindings ──────────────────────────────────────
|
|
239
|
+
if (sceneData.bindings && Object.keys(sceneData.bindings).length > 0) {
|
|
240
|
+
const objectByUUID = {};
|
|
241
|
+
scene.traverse(obj => { objectByUUID[obj.uuid] = obj; });
|
|
242
|
+
|
|
243
|
+
const applyBinding = (obj, prop, rawVal) => {
|
|
244
|
+
try {
|
|
245
|
+
const parts = prop.split('.');
|
|
246
|
+
if (parts.length === 2) {
|
|
247
|
+
const [p0, p1] = parts;
|
|
248
|
+
if (obj[p0] == null) return;
|
|
249
|
+
if (p0 === 'rotation') {
|
|
250
|
+
// store as degrees, apply as radians
|
|
251
|
+
obj[p0][p1] = parseFloat(rawVal) * DEG2RAD_RT;
|
|
252
|
+
} else if (p0 === 'material' && p1 === 'color') {
|
|
253
|
+
obj[p0]?.color?.set?.(rawVal);
|
|
254
|
+
if (obj[p0]) obj[p0].needsUpdate = true;
|
|
255
|
+
} else if (p0 === 'material') {
|
|
256
|
+
if (obj[p0] != null) { obj[p0][p1] = parseFloat(rawVal); obj[p0].needsUpdate = true; }
|
|
257
|
+
} else {
|
|
258
|
+
obj[p0][p1] = parseFloat(rawVal);
|
|
259
|
+
}
|
|
260
|
+
obj.updateMatrixWorld?.(true);
|
|
261
|
+
} else if (parts.length === 1) {
|
|
262
|
+
if (prop === 'visible') obj.visible = !!rawVal;
|
|
263
|
+
}
|
|
264
|
+
} catch (_) {}
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const DEG2RAD_RT = Math.PI / 180;
|
|
268
|
+
|
|
269
|
+
for (const [uuid, propMap] of Object.entries(sceneData.bindings)) {
|
|
270
|
+
const obj = objectByUUID[uuid];
|
|
271
|
+
if (!obj) continue;
|
|
272
|
+
for (const [prop, cfg] of Object.entries(propMap)) {
|
|
273
|
+
if (!cfg?.signal) continue;
|
|
274
|
+
(async () => {
|
|
275
|
+
// Apply current value immediately
|
|
276
|
+
try {
|
|
277
|
+
const state = await iobrokerHandler.getState(cfg.signal);
|
|
278
|
+
if (state?.val != null) {
|
|
279
|
+
const val = cfg.formula && cfg.formula !== 'val'
|
|
280
|
+
? new Function('val', `return (${cfg.formula})`)(state.val)
|
|
281
|
+
: state.val;
|
|
282
|
+
applyBinding(obj, prop, val);
|
|
283
|
+
}
|
|
284
|
+
} catch (_) {}
|
|
285
|
+
|
|
286
|
+
// Subscribe for future changes
|
|
287
|
+
const unsub = subscribe(cfg.signal, (id, state) => {
|
|
288
|
+
if (state?.val == null) return;
|
|
289
|
+
const val = cfg.formula && cfg.formula !== 'val'
|
|
290
|
+
? new Function('val', `return (${cfg.formula})`)(state.val)
|
|
291
|
+
: state.val;
|
|
292
|
+
applyBinding(obj, prop, val);
|
|
293
|
+
});
|
|
294
|
+
_subscriptions.push(unsub);
|
|
295
|
+
})();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
238
300
|
if (sceneData.script) {
|
|
239
301
|
try {
|
|
240
302
|
const fn = new Function(
|
|
@@ -49,6 +49,7 @@ const importMapConfig = {
|
|
|
49
49
|
"toastify-js": "./node_modules/toastify-js/src/toastify-es.js",
|
|
50
50
|
"@iobroker/webcomponent-selectid-dialog/": "./node_modules/@iobroker/webcomponent-selectid-dialog/",
|
|
51
51
|
"three": "./3d-lib/three.module.js",
|
|
52
|
+
"three/webgpu": "./3d-lib/three.webgpu.js",
|
|
52
53
|
"three/addons/": "./3d-lib/jsm/",
|
|
53
54
|
"three-gpu-pathtracer": "data:text/javascript,export class WebGLPathTracer{constructor(){}setScene(){}renderSample(){}updateCamera(){}updateEnvironment(){}updateMaterials(){}get samples(){return 0;}filterGlossyFactor=0.5;}",
|
|
54
55
|
"three-mesh-bvh": "data:text/javascript,export const MeshBVH={};export const acceleratedRaycast=()=>{};export const computeBoundsTree=()=>{};export const disposeBoundsTree=()=>{};"
|