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