iobroker.mywebui 1.42.17 → 1.42.19
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/dist/frontend/common/IobrokerHandler.js +20 -0
- package/www/dist/frontend/config/CommandHandling.js +5 -0
- package/www/dist/frontend/config/IobrokerWebui3DScreenEditor.js +231 -40
- package/www/dist/frontend/config/IobrokerWebuiAppShell.js +149 -1
- package/www/dist/frontend/config/IobrokerWebuiSolutionExplorer.js +37 -49
- package/www/runtime.html +23 -3
package/io-package.json
CHANGED
package/package.json
CHANGED
|
@@ -37,6 +37,7 @@ export class IobrokerHandler {
|
|
|
37
37
|
this.#cache.set('screen', new Map());
|
|
38
38
|
this.#cache.set('control', new Map());
|
|
39
39
|
this.#cache.set('3dscreen', new Map());
|
|
40
|
+
this.#cache.set('3dcontrol', new Map());
|
|
40
41
|
}
|
|
41
42
|
getNormalizedSignalName(id, relativeSignalPath, element) {
|
|
42
43
|
return (relativeSignalPath ?? '') + id;
|
|
@@ -208,6 +209,8 @@ export class IobrokerHandler {
|
|
|
208
209
|
return this.getCustomControl(name);
|
|
209
210
|
else if (type == '3dscreen')
|
|
210
211
|
return this.get3DScreen(name);
|
|
212
|
+
else if (type == '3dcontrol')
|
|
213
|
+
return this.get3DControl(name);
|
|
211
214
|
return null;
|
|
212
215
|
}
|
|
213
216
|
async getScreen(name) {
|
|
@@ -244,6 +247,23 @@ export class IobrokerHandler {
|
|
|
244
247
|
}
|
|
245
248
|
return scene;
|
|
246
249
|
}
|
|
250
|
+
async get3DControl(name) {
|
|
251
|
+
if (name[0] == '/')
|
|
252
|
+
name = name.substring(1);
|
|
253
|
+
let ctrl = this.#cache.get('3dcontrol').get(name);
|
|
254
|
+
if (!ctrl) {
|
|
255
|
+
if (this._readyPromises)
|
|
256
|
+
await this.waitForReady();
|
|
257
|
+
try {
|
|
258
|
+
ctrl = await this._getObjectFromFile(this.configPath + "3dcontrols/" + name + '.3dcontrol');
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
console.error("Error reading 3D Control", ctrl, err);
|
|
262
|
+
}
|
|
263
|
+
this.#cache.get('3dcontrol').set(name, ctrl);
|
|
264
|
+
}
|
|
265
|
+
return ctrl;
|
|
266
|
+
}
|
|
247
267
|
async saveObject(type, name, data) {
|
|
248
268
|
await this._saveObjectToFile(data, "/" + this.configPath + type + "s/" + name + '.' + type);
|
|
249
269
|
if (this.#cache.has(type))
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { iobrokerHandler } from '../common/IobrokerHandler.js';
|
|
2
2
|
import { IobrokerWebuiScreenEditor, defaultNewStyle, defaultNewControlScript } from './IobrokerWebuiScreenEditor.js';
|
|
3
|
+
import { IobrokerWebui3DScreenEditor } from './IobrokerWebui3DScreenEditor.js';
|
|
3
4
|
export class CommandHandling {
|
|
4
5
|
dockManager;
|
|
5
6
|
iobrokerWebuiAppShell;
|
|
@@ -17,6 +18,10 @@ export class CommandHandling {
|
|
|
17
18
|
if (target instanceof IobrokerWebuiScreenEditor) {
|
|
18
19
|
window.open("runtime.html#screenName=" + target.name);
|
|
19
20
|
}
|
|
21
|
+
else if (target instanceof IobrokerWebui3DScreenEditor) {
|
|
22
|
+
const scName = target.sceneData?.name || target.getAttribute?.('scene-name') || '';
|
|
23
|
+
window.open("runtime.html#3dscreen=" + encodeURIComponent(scName));
|
|
24
|
+
}
|
|
20
25
|
else {
|
|
21
26
|
window.open("runtime.html");
|
|
22
27
|
}
|
|
@@ -364,6 +364,7 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
|
|
|
364
364
|
this._stopAnimation = () => { animating = false; };
|
|
365
365
|
|
|
366
366
|
this.renderer.domElement.addEventListener('click', (e) => this.onMouseClick(e));
|
|
367
|
+
this._setupHoverDetection();
|
|
367
368
|
const resizeObs = new ResizeObserver(() => this.onViewportResize());
|
|
368
369
|
resizeObs.observe(viewport);
|
|
369
370
|
}
|
|
@@ -379,10 +380,13 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
|
|
|
379
380
|
|
|
380
381
|
async loadScene(sceneName) {
|
|
381
382
|
try {
|
|
382
|
-
|
|
383
|
+
const sceneType = this.getAttribute('scene-type') || '3dscreen';
|
|
384
|
+
this.sceneData = sceneType === '3dcontrol'
|
|
385
|
+
? await iobrokerHandler.getWebuiObject('3dcontrol', sceneName)
|
|
386
|
+
: await iobrokerHandler.get3DScreen(sceneName);
|
|
383
387
|
if (!this.sceneData) { console.warn('Scene not found:', sceneName); return; }
|
|
384
|
-
// Ensure script/style fields exist
|
|
385
|
-
if (!this.sceneData.script) this.sceneData.script =
|
|
388
|
+
// Ensure script/style fields exist with default boilerplate
|
|
389
|
+
if (!this.sceneData.script) this.sceneData.script = `// Available: THREE, scene, camera, renderer, controls\n// assets (Map<name, Object3D>) mixers driveEngine\n// setState(signal, val) getState(signal) subscribe(signal, cb)\n// onFrame(fn) onSelect(fn) log(...)\n\nonFrame((dt, ts) => {\n // Runs every animation frame. dt = delta seconds.\n});\n`;
|
|
386
390
|
if (!this.sceneData.style) this.sceneData.style = '';
|
|
387
391
|
console.log('Scene loaded:', sceneName);
|
|
388
392
|
this._buildSceneView();
|
|
@@ -447,6 +451,11 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
|
|
|
447
451
|
this._threeObjects.push(model);
|
|
448
452
|
if (this.driveEngine && Array.isArray(this.sceneData?.assets)) {
|
|
449
453
|
this.driveEngine.createFromAssets(this.sceneData.assets, this._threeObjects);
|
|
454
|
+
// Wrap drives for AtMax/AtMin events
|
|
455
|
+
if (asset.drives?.length) {
|
|
456
|
+
const drives = asset.drives.map((_, i) => this.driveEngine.getDrive(asset.id, i));
|
|
457
|
+
this._wrapDriveEvents(asset, drives);
|
|
458
|
+
}
|
|
450
459
|
}
|
|
451
460
|
// AnimationMixer for built-in GLB animations
|
|
452
461
|
if (gltf.animations?.length > 0) {
|
|
@@ -457,6 +466,12 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
|
|
|
457
466
|
}
|
|
458
467
|
this._mixers.set(asset.id, { mixer, clips: gltf.animations, actions });
|
|
459
468
|
asset.availableClips = gltf.animations.map(c => c.name);
|
|
469
|
+
// animationEnd event
|
|
470
|
+
mixer.addEventListener('finished', (e) => {
|
|
471
|
+
const clipName = e.action.getClip().name;
|
|
472
|
+
this._executeNodeEvent(model, asset, `animationEnd:${clipName}`);
|
|
473
|
+
this._executeNodeEvent(model, asset, 'animationEnd');
|
|
474
|
+
});
|
|
460
475
|
}
|
|
461
476
|
// Extract named node list for scene tree
|
|
462
477
|
asset._nodeList = this._extractNamedNodes(model);
|
|
@@ -512,6 +527,8 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
|
|
|
512
527
|
|
|
513
528
|
if (hitNode && hitAssetRoot) {
|
|
514
529
|
if (hitNode.userData.interaction) this._handleInteraction(hitNode);
|
|
530
|
+
// Fire node event: check by name AND by uuid
|
|
531
|
+
this._executeNodeEvent(hitNode, hitAssetRoot.userData.assetData, 'click');
|
|
515
532
|
this.selectNode(hitNode, hitAssetRoot);
|
|
516
533
|
} else {
|
|
517
534
|
this.deselectAll();
|
|
@@ -637,6 +654,131 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
|
|
|
637
654
|
}
|
|
638
655
|
}
|
|
639
656
|
|
|
657
|
+
// ── Event execution ───────────────────────────────────
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Execute a node event handler stored in asset.events[nodeName][eventKey].
|
|
661
|
+
* eventKey: 'click' | 'mouseenter' | 'mouseleave' | 'animationEnd' | 'animationEnd:ClipName' |
|
|
662
|
+
* 'driveAtMax:N' | 'driveAtMin:N' | 'signalChange:path'
|
|
663
|
+
* Node name '__root__' = the asset root object.
|
|
664
|
+
*/
|
|
665
|
+
_executeNodeEvent(node, asset, eventKey) {
|
|
666
|
+
if (!asset?.events) return;
|
|
667
|
+
const nodeKey = (node?.userData?.assetData === asset || !node?.name) ? '__root__' : node.name;
|
|
668
|
+
const script = asset.events[nodeKey]?.[eventKey];
|
|
669
|
+
if (!script?.trim()) return;
|
|
670
|
+
|
|
671
|
+
const ctx = {
|
|
672
|
+
node,
|
|
673
|
+
assetData: asset,
|
|
674
|
+
THREE: this.THREE,
|
|
675
|
+
scene: this.scene,
|
|
676
|
+
camera: this.camera,
|
|
677
|
+
renderer: this.renderer,
|
|
678
|
+
controls: this.controls,
|
|
679
|
+
assets: this._buildAssetsMap(),
|
|
680
|
+
mixers: this._mixers,
|
|
681
|
+
driveEngine: this.driveEngine,
|
|
682
|
+
setState(signal, val) { iobrokerHandler.setState(signal, val); },
|
|
683
|
+
getState: (s) => this.driveEngine.bus.get(s),
|
|
684
|
+
subscribe: (s, cb) => { const u = this.driveEngine.bus.subscribe(s, cb); this._scriptUnsubs.push(u); return u; },
|
|
685
|
+
log: (...a) => console.log('[3D Event]', nodeKey, eventKey, ...a),
|
|
686
|
+
};
|
|
687
|
+
try {
|
|
688
|
+
new Function(...Object.keys(ctx), script)(...Object.values(ctx));
|
|
689
|
+
} catch (err) {
|
|
690
|
+
console.error(`[3D Event ${nodeKey}:${eventKey}]`, err.message);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
_buildAssetsMap() {
|
|
695
|
+
const m = new Map();
|
|
696
|
+
for (const obj of this.scene.children) {
|
|
697
|
+
if (obj.userData.assetData) {
|
|
698
|
+
m.set(obj.userData.assetData.name, obj);
|
|
699
|
+
m.set(obj.userData.assetData.id, obj);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return m;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// ── Hover detection ───────────────────────────────────
|
|
706
|
+
|
|
707
|
+
_setupHoverDetection() {
|
|
708
|
+
let hoverNode = null;
|
|
709
|
+
let hoverAsset = null;
|
|
710
|
+
let throttle = null;
|
|
711
|
+
|
|
712
|
+
this.renderer.domElement.addEventListener('mousemove', (e) => {
|
|
713
|
+
if (throttle) return;
|
|
714
|
+
throttle = setTimeout(() => { throttle = null; }, 50); // 20 fps check
|
|
715
|
+
|
|
716
|
+
const rect = this.renderer.domElement.getBoundingClientRect();
|
|
717
|
+
this.mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
|
718
|
+
this.mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
|
|
719
|
+
this.raycaster.setFromCamera(this.mouse, this.camera);
|
|
720
|
+
const hits = this.raycaster.intersectObjects(this.scene.children, true);
|
|
721
|
+
|
|
722
|
+
let newNode = null, newRoot = null;
|
|
723
|
+
for (const hit of hits) {
|
|
724
|
+
const o = hit.object;
|
|
725
|
+
if (o.userData.isGrid || o.userData.isAxes || o.userData.isSelectionHelper) continue;
|
|
726
|
+
let p = o;
|
|
727
|
+
while (p && p !== this.scene) {
|
|
728
|
+
if (p.userData.assetData) { newRoot = p; break; }
|
|
729
|
+
p = p.parent;
|
|
730
|
+
}
|
|
731
|
+
if (newRoot) { newNode = o; break; }
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (newNode !== hoverNode) {
|
|
735
|
+
if (hoverNode && hoverAsset) {
|
|
736
|
+
this._executeNodeEvent(hoverNode, hoverAsset, 'mouseleave');
|
|
737
|
+
this.renderer.domElement.style.cursor = 'default';
|
|
738
|
+
}
|
|
739
|
+
if (newNode && newRoot) {
|
|
740
|
+
const asset = newRoot.userData.assetData;
|
|
741
|
+
this._executeNodeEvent(newNode, asset, 'mouseenter');
|
|
742
|
+
// Change cursor if node has any event or interaction
|
|
743
|
+
const hasEvent = asset?.events || newNode.userData.interaction;
|
|
744
|
+
if (hasEvent) this.renderer.domElement.style.cursor = 'pointer';
|
|
745
|
+
}
|
|
746
|
+
hoverNode = newNode;
|
|
747
|
+
hoverAsset = newRoot?.userData.assetData ?? null;
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
this.renderer.domElement.addEventListener('mouseleave', () => {
|
|
752
|
+
if (hoverNode && hoverAsset) {
|
|
753
|
+
this._executeNodeEvent(hoverNode, hoverAsset, 'mouseleave');
|
|
754
|
+
}
|
|
755
|
+
hoverNode = null; hoverAsset = null;
|
|
756
|
+
this.renderer.domElement.style.cursor = 'default';
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// ── Drive events (AtMax / AtMin) ──────────────────────
|
|
761
|
+
// Called from DriveEngine after createFromAssets - wraps onAfterUpdate
|
|
762
|
+
_wrapDriveEvents(asset, drives) {
|
|
763
|
+
for (let i = 0; i < drives.length; i++) {
|
|
764
|
+
const drive = drives[i];
|
|
765
|
+
if (!drive) continue;
|
|
766
|
+
const cfg = asset.drives?.[i];
|
|
767
|
+
const origCb = drive.onAfterUpdate;
|
|
768
|
+
drive.onAfterUpdate = (d) => {
|
|
769
|
+
if (origCb) origCb(d);
|
|
770
|
+
const root = this._threeObjects.find(o => o.userData.assetId === asset.id);
|
|
771
|
+
if (!root) return;
|
|
772
|
+
const atMax = cfg?.maxPos !== undefined ? Math.abs(d.currentPosition - cfg.maxPos) < 0.1 : false;
|
|
773
|
+
const atMin = cfg?.minPos !== undefined ? Math.abs(d.currentPosition - cfg.minPos) < 0.1 : false;
|
|
774
|
+
if (atMax && !d._evtWasAtMax) this._executeNodeEvent(root, asset, `driveAtMax:${i}`);
|
|
775
|
+
if (atMin && !d._evtWasAtMin) this._executeNodeEvent(root, asset, `driveAtMin:${i}`);
|
|
776
|
+
d._evtWasAtMax = atMax;
|
|
777
|
+
d._evtWasAtMin = atMin;
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
640
782
|
_extractNamedNodes(obj, depth = 0, result = []) {
|
|
641
783
|
if (depth > 7) return result;
|
|
642
784
|
if (depth > 0 && obj.name) {
|
|
@@ -974,51 +1116,99 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
|
|
|
974
1116
|
|
|
975
1117
|
// ── Library tab in left panel ─────────────────────────
|
|
976
1118
|
|
|
977
|
-
_renderLibraryTab(el) {
|
|
1119
|
+
async _renderLibraryTab(el) {
|
|
978
1120
|
el.innerHTML = '';
|
|
979
1121
|
const search = (this._getDomElement('treeSearch')?.value ?? '').toLowerCase();
|
|
980
|
-
const assets = Array.isArray(this.sceneData?.assets) ? this.sceneData.assets : [];
|
|
981
|
-
const filtered = assets.filter(a => !search || a.name.toLowerCase().includes(search));
|
|
982
1122
|
|
|
983
1123
|
// Show/hide add button
|
|
984
1124
|
const addRow = this._getDomElement('libAddRow');
|
|
985
1125
|
if (addRow) addRow.style.display = '';
|
|
986
1126
|
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1127
|
+
// ── Section 1: Scene-local assets (instances) ──────────────────
|
|
1128
|
+
const assets = Array.isArray(this.sceneData?.assets) ? this.sceneData.assets : [];
|
|
1129
|
+
const filteredAssets = assets.filter(a => !search || a.name.toLowerCase().includes(search));
|
|
1130
|
+
|
|
1131
|
+
const secHdr1 = document.createElement('div');
|
|
1132
|
+
secHdr1.style.cssText = 'padding:4px 8px;color:#888;font-size:10px;text-transform:uppercase;letter-spacing:0.5px;background:#2d2d2d;border-bottom:1px solid #3c3c3c;';
|
|
1133
|
+
secHdr1.textContent = 'Scene Assets';
|
|
1134
|
+
el.appendChild(secHdr1);
|
|
1135
|
+
|
|
1136
|
+
if (filteredAssets.length === 0) {
|
|
1137
|
+
const empty = document.createElement('div');
|
|
1138
|
+
empty.style.cssText = 'color:#555;padding:8px;font-size:11px;text-align:center;';
|
|
1139
|
+
empty.textContent = 'No local assets';
|
|
1140
|
+
el.appendChild(empty);
|
|
1141
|
+
} else {
|
|
1142
|
+
for (const asset of filteredAssets) {
|
|
1143
|
+
const shortName = asset.name.replace(/\.(glb|gltf)$/i, '');
|
|
1144
|
+
const driveCnt = asset.drives?.length ?? 0;
|
|
1145
|
+
const clipCnt = asset.availableClips?.length ?? 0;
|
|
1146
|
+
const interCnt = Object.keys(asset.interactions ?? {}).length;
|
|
1147
|
+
const meta = [driveCnt?`${driveCnt}D`:'', clipCnt?`${clipCnt}A`:'', interCnt?`${interCnt}I`:''].filter(Boolean).join(' · ') || 'GLB';
|
|
1148
|
+
const item = document.createElement('div');
|
|
1149
|
+
item.className = 'lib-item';
|
|
1150
|
+
item.innerHTML = `<span class="lib-icon">📦</span><div class="lib-info"><div class="lib-name" title="${asset.name}">${shortName}</div><div class="lib-meta">${meta}</div></div><span class="lib-add" title="Add instance">+</span>`;
|
|
1151
|
+
item.querySelector('.lib-add')?.addEventListener('click', (e) => { e.stopPropagation(); this._addAssetInstance(asset); });
|
|
1152
|
+
item.addEventListener('click', () => this._selectAsset(asset));
|
|
1153
|
+
el.appendChild(item);
|
|
1154
|
+
}
|
|
993
1155
|
}
|
|
994
1156
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1157
|
+
// ── Section 2: 3D Custom Controls from backend ─────────────────
|
|
1158
|
+
const secHdr2 = document.createElement('div');
|
|
1159
|
+
secHdr2.style.cssText = 'padding:4px 8px;color:#888;font-size:10px;text-transform:uppercase;letter-spacing:0.5px;background:#2d2d2d;border-bottom:1px solid #3c3c3c;border-top:1px solid #3c3c3c;margin-top:4px;';
|
|
1160
|
+
secHdr2.textContent = '3D Custom Controls';
|
|
1161
|
+
el.appendChild(secHdr2);
|
|
1162
|
+
|
|
1163
|
+
try {
|
|
1164
|
+
const ctrlNames = await iobrokerHandler.getAllNames('3dcontrol', '');
|
|
1165
|
+
const filteredCtrls = ctrlNames.filter(n => !search || n.toLowerCase().includes(search));
|
|
1166
|
+
if (filteredCtrls.length === 0) {
|
|
1167
|
+
const empty = document.createElement('div');
|
|
1168
|
+
empty.style.cssText = 'color:#555;padding:8px;font-size:11px;text-align:center;';
|
|
1169
|
+
empty.textContent = 'No 3D controls yet';
|
|
1170
|
+
el.appendChild(empty);
|
|
1171
|
+
} else {
|
|
1172
|
+
for (const ctrlName of filteredCtrls) {
|
|
1173
|
+
const shortName = ctrlName.replace(/^\/+/, '').split('/').pop();
|
|
1174
|
+
const item = document.createElement('div');
|
|
1175
|
+
item.className = 'lib-item';
|
|
1176
|
+
item.innerHTML = `<span class="lib-icon">🔧</span><div class="lib-info"><div class="lib-name" title="${ctrlName}">${shortName}</div><div class="lib-meta">3D Control</div></div><span class="lib-add" title="Add to scene">+</span>`;
|
|
1177
|
+
item.querySelector('.lib-add')?.addEventListener('click', async (e) => {
|
|
1178
|
+
e.stopPropagation();
|
|
1179
|
+
await this._addControlToScene(ctrlName);
|
|
1180
|
+
});
|
|
1181
|
+
el.appendChild(item);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
} catch(err) {
|
|
1185
|
+
const errDiv = document.createElement('div');
|
|
1186
|
+
errDiv.style.cssText = 'color:#666;padding:8px;font-size:10px;';
|
|
1187
|
+
errDiv.textContent = 'Could not load 3D controls';
|
|
1188
|
+
el.appendChild(errDiv);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
async _addControlToScene(ctrlName) {
|
|
1193
|
+
try {
|
|
1194
|
+
const ctrl = await iobrokerHandler.getWebuiObject('3dcontrol', ctrlName);
|
|
1195
|
+
if (!ctrl?.assets?.length) { alert('3D Control has no assets'); return; }
|
|
1196
|
+
const firstAsset = ctrl.assets[0];
|
|
1197
|
+
const newId = 'asset_' + Date.now().toString(36);
|
|
1198
|
+
const instance = JSON.parse(JSON.stringify(firstAsset));
|
|
1199
|
+
instance.id = newId;
|
|
1200
|
+
const shortName = ctrlName.replace(/^\/+/, '').split('/').pop();
|
|
1201
|
+
instance.name = shortName + '_' + newId.slice(-4);
|
|
1202
|
+
instance.position = { x: Math.random() * 4 - 2, y: 0, z: Math.random() * 4 - 2 };
|
|
1203
|
+
if (!this.sceneData.assets) this.sceneData.assets = [];
|
|
1204
|
+
this.sceneData.assets.push(instance);
|
|
1205
|
+
this.loadAsset(instance);
|
|
1206
|
+
this.refreshTree();
|
|
1207
|
+
this._setStatus('Added: ' + instance.name);
|
|
1208
|
+
setTimeout(() => this._setStatus(''), 2000);
|
|
1209
|
+
} catch(err) {
|
|
1210
|
+
console.error('Error adding 3D control to scene:', err);
|
|
1211
|
+
alert('Error adding 3D control: ' + err.message);
|
|
1022
1212
|
}
|
|
1023
1213
|
}
|
|
1024
1214
|
|
|
@@ -1095,7 +1285,8 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
|
|
|
1095
1285
|
async saveScene() {
|
|
1096
1286
|
try {
|
|
1097
1287
|
this._setStatus('Saving...');
|
|
1098
|
-
const
|
|
1288
|
+
const sceneType = this.getAttribute('scene-type') || '3dscreen';
|
|
1289
|
+
const ok = await iobrokerHandler.saveObject(sceneType, this.sceneData.name, this.sceneData);
|
|
1099
1290
|
this._setStatus(ok ? 'Saved ✓' : 'Save failed');
|
|
1100
1291
|
setTimeout(() => this._setStatus(''), 2000);
|
|
1101
1292
|
} catch (err) {
|
|
@@ -1710,7 +1710,7 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
|
|
|
1710
1710
|
: `<div style="color:#555;font-size:10px;font-style:italic;padding:4px 0;">No drives configured</div>`;
|
|
1711
1711
|
|
|
1712
1712
|
// ── Interaction binding ──
|
|
1713
|
-
const nodeKey = nodeName;
|
|
1713
|
+
const nodeKey = (node?.userData?.assetData === asset || !node?.name) ? '__root__' : (node?.name || nodeName);
|
|
1714
1714
|
const inter = asset?.interactions?.[nodeKey];
|
|
1715
1715
|
const interHtml = inter
|
|
1716
1716
|
? `<div style="background:#1a2a1a;border:1px solid #2a4a2a;border-radius:3px;padding:6px;font-size:10px;">
|
|
@@ -1855,6 +1855,142 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
|
|
|
1855
1855
|
this.show3DNodeProperties(node, asset, mixerInfo, onChange);
|
|
1856
1856
|
}
|
|
1857
1857
|
});
|
|
1858
|
+
|
|
1859
|
+
// Show events in native Events dock
|
|
1860
|
+
this._show3DEventsInDock(node, asset, nodeKey, onChange);
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
_escHtml(str) {
|
|
1864
|
+
return (str || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
_show3DEventsInDock(node, asset, nodeKey, onChange) {
|
|
1868
|
+
const dock = this._getDomElement('eventsDock');
|
|
1869
|
+
if (!dock) return;
|
|
1870
|
+
|
|
1871
|
+
// Get or create the 3D events overlay container
|
|
1872
|
+
let d3 = dock.querySelector('#_3dEventsOverlay');
|
|
1873
|
+
if (!d3) {
|
|
1874
|
+
d3 = document.createElement('div');
|
|
1875
|
+
d3.id = '_3dEventsOverlay';
|
|
1876
|
+
d3.style.cssText = 'width:100%;height:100%;overflow:auto;box-sizing:border-box;background:#1e1e1e;color:#ccc;font-family:"Segoe UI",sans-serif;font-size:12px;';
|
|
1877
|
+
dock.insertBefore(d3, dock.firstChild);
|
|
1878
|
+
}
|
|
1879
|
+
d3.style.display = 'block';
|
|
1880
|
+
|
|
1881
|
+
// Hide native event assignment
|
|
1882
|
+
const nativeEvt = this._getDomElement('eventsList');
|
|
1883
|
+
if (nativeEvt) nativeEvt.style.display = 'none';
|
|
1884
|
+
|
|
1885
|
+
// Build events HTML
|
|
1886
|
+
const nodeEvents = asset?.events?.[nodeKey] ?? {};
|
|
1887
|
+
const nodeEventEntries = Object.entries(nodeEvents);
|
|
1888
|
+
const presetEvents = [
|
|
1889
|
+
{ key: 'click', label: '🖱️ click' },
|
|
1890
|
+
{ key: 'mouseenter', label: '↗️ mouseenter' },
|
|
1891
|
+
{ key: 'mouseleave', label: '↙️ mouseleave' },
|
|
1892
|
+
{ key: 'animationEnd', label: '🎬 animationEnd' },
|
|
1893
|
+
...((asset?.availableClips ?? []).map(c => ({ key: `animationEnd:${c}`, label: `🎬 animationEnd:${c}` }))),
|
|
1894
|
+
...((asset?.drives ?? []).flatMap((_, i) => [
|
|
1895
|
+
{ key: `driveAtMax:${i}`, label: `⬆️ driveAtMax:${i}` },
|
|
1896
|
+
{ key: `driveAtMin:${i}`, label: `⬇️ driveAtMin:${i}` }
|
|
1897
|
+
])),
|
|
1898
|
+
].filter(p => !nodeEvents[p.key]);
|
|
1899
|
+
|
|
1900
|
+
const evtRowsHtml = nodeEventEntries.length > 0
|
|
1901
|
+
? nodeEventEntries.map(([evtKey, script]) => `
|
|
1902
|
+
<div style="background:#1a1a2e;border:1px solid #2a2a4a;border-radius:3px;padding:5px;margin-bottom:6px;">
|
|
1903
|
+
<div style="display:flex;align-items:center;gap:4px;margin-bottom:3px;">
|
|
1904
|
+
<span style="color:#dcdcaa;font-size:11px;flex:1;font-family:monospace;">${evtKey}</span>
|
|
1905
|
+
<button class="evt-del-btn" data-evtkey="${evtKey}" style="padding:1px 6px;font-size:10px;background:#5a1a1a;color:#f44747;border:1px solid #8a2a2a;border-radius:3px;cursor:pointer;">✕</button>
|
|
1906
|
+
</div>
|
|
1907
|
+
<textarea class="evt-script" data-evtkey="${evtKey}" rows="4"
|
|
1908
|
+
style="width:100%;box-sizing:border-box;background:#0d0d1a;border:1px solid #3c3c5c;color:#d4d4d4;font-family:'Consolas',monospace;font-size:11px;padding:4px;resize:vertical;line-height:1.4;border-radius:2px;"
|
|
1909
|
+
spellcheck="false"
|
|
1910
|
+
placeholder="// context: node, assetData, THREE, scene // setState(sig,val) getState(sig) // assets(Map) mixers driveEngine"
|
|
1911
|
+
>${this._escHtml(script)}</textarea>
|
|
1912
|
+
</div>`).join('')
|
|
1913
|
+
: `<div style="color:#555;font-size:11px;font-style:italic;padding:8px 0;">No events configured</div>`;
|
|
1914
|
+
|
|
1915
|
+
const presetOptsHtml = presetEvents.map(p => `<option value="${p.key}">${p.label}</option>`).join('');
|
|
1916
|
+
const addRowHtml = presetEvents.length > 0
|
|
1917
|
+
? `<div style="display:flex;gap:4px;margin-top:6px;">
|
|
1918
|
+
<select class="evt-preset-sel" style="flex:1;background:#3c3c3c;border:1px solid #555;color:#ccc;padding:3px 4px;border-radius:3px;font-size:11px;">
|
|
1919
|
+
<option value="">— select event type —</option>
|
|
1920
|
+
${presetOptsHtml}
|
|
1921
|
+
<option value="__custom__">Custom event name...</option>
|
|
1922
|
+
</select>
|
|
1923
|
+
<button class="evt-add-btn" style="padding:2px 12px;background:#3c3c3c;color:#ccc;border:1px solid #555;border-radius:3px;cursor:pointer;font-size:11px;">+ Add</button>
|
|
1924
|
+
</div>`
|
|
1925
|
+
: `<div style="color:#555;font-size:10px;margin-top:6px;">All standard events configured</div>`;
|
|
1926
|
+
|
|
1927
|
+
d3.innerHTML = `
|
|
1928
|
+
<div style="padding:6px 8px;border-bottom:1px solid #3c3c3c;background:#252526;display:flex;align-items:center;gap:6px;">
|
|
1929
|
+
<span style="color:#dcdcaa;font-size:11px;font-weight:bold;">⚡ 3D Events</span>
|
|
1930
|
+
<span style="color:#555;font-size:10px;">node: ${nodeKey}</span>
|
|
1931
|
+
</div>
|
|
1932
|
+
<div style="padding:8px;">
|
|
1933
|
+
${evtRowsHtml}
|
|
1934
|
+
${addRowHtml}
|
|
1935
|
+
<div style="color:#555;font-size:10px;margin-top:8px;line-height:1.6;">
|
|
1936
|
+
Context: <code style="color:#888;">node, assetData, THREE, scene</code><br>
|
|
1937
|
+
<code style="color:#888;">setState(sig,val) · getState(sig)</code><br>
|
|
1938
|
+
<code style="color:#888;">assets(Map) · mixers · driveEngine</code>
|
|
1939
|
+
</div>
|
|
1940
|
+
</div>
|
|
1941
|
+
`;
|
|
1942
|
+
|
|
1943
|
+
// Wire textarea auto-save
|
|
1944
|
+
d3.querySelectorAll('.evt-script').forEach(ta => {
|
|
1945
|
+
ta.addEventListener('blur', () => {
|
|
1946
|
+
const evtKey = ta.dataset.evtkey;
|
|
1947
|
+
if (!asset.events) asset.events = {};
|
|
1948
|
+
if (!asset.events[nodeKey]) asset.events[nodeKey] = {};
|
|
1949
|
+
asset.events[nodeKey][evtKey] = ta.value;
|
|
1950
|
+
if (onChange) onChange(asset);
|
|
1951
|
+
});
|
|
1952
|
+
});
|
|
1953
|
+
|
|
1954
|
+
// Wire delete buttons
|
|
1955
|
+
d3.querySelectorAll('.evt-del-btn').forEach(btn => {
|
|
1956
|
+
btn.addEventListener('click', () => {
|
|
1957
|
+
const evtKey = btn.dataset.evtkey;
|
|
1958
|
+
if (asset?.events?.[nodeKey]) {
|
|
1959
|
+
delete asset.events[nodeKey][evtKey];
|
|
1960
|
+
if (Object.keys(asset.events[nodeKey]).length === 0) delete asset.events[nodeKey];
|
|
1961
|
+
if (onChange) onChange(asset);
|
|
1962
|
+
this._show3DEventsInDock(node, asset, nodeKey, onChange);
|
|
1963
|
+
}
|
|
1964
|
+
});
|
|
1965
|
+
});
|
|
1966
|
+
|
|
1967
|
+
// Wire add button
|
|
1968
|
+
d3.querySelector('.evt-add-btn')?.addEventListener('click', () => {
|
|
1969
|
+
const sel = d3.querySelector('.evt-preset-sel');
|
|
1970
|
+
let evtKey = sel?.value;
|
|
1971
|
+
if (!evtKey) return;
|
|
1972
|
+
if (evtKey === '__custom__') {
|
|
1973
|
+
evtKey = prompt('Custom event name:', 'signalChange:mywebui.0.signal.path');
|
|
1974
|
+
if (!evtKey) return;
|
|
1975
|
+
}
|
|
1976
|
+
if (!asset.events) asset.events = {};
|
|
1977
|
+
if (!asset.events[nodeKey]) asset.events[nodeKey] = {};
|
|
1978
|
+
if (!asset.events[nodeKey][evtKey]) asset.events[nodeKey][evtKey] = '// event: ' + evtKey + '\n';
|
|
1979
|
+
if (onChange) onChange(asset);
|
|
1980
|
+
this._show3DEventsInDock(node, asset, nodeKey, onChange);
|
|
1981
|
+
});
|
|
1982
|
+
|
|
1983
|
+
// Activate the events dock panel
|
|
1984
|
+
this.activateDockById('eventsDock');
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
_restore3DEventsDock() {
|
|
1988
|
+
const dock = this._getDomElement('eventsDock');
|
|
1989
|
+
if (!dock) return;
|
|
1990
|
+
const d3 = dock.querySelector('#_3dEventsOverlay');
|
|
1991
|
+
if (d3) d3.style.display = 'none';
|
|
1992
|
+
const nativeEvt = this._getDomElement('eventsList');
|
|
1993
|
+
if (nativeEvt) nativeEvt.style.display = '';
|
|
1858
1994
|
}
|
|
1859
1995
|
|
|
1860
1996
|
_showAddDriveDialog(obj, onChange) {
|
|
@@ -1978,6 +2114,18 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
|
|
|
1978
2114
|
editor.id = id;
|
|
1979
2115
|
editor.title = '3D: ' + name;
|
|
1980
2116
|
editor.setAttribute('scene-name', name);
|
|
2117
|
+
editor.setAttribute('scene-type', '3dscreen');
|
|
2118
|
+
this.openDock(editor);
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
async open3DControlEditor(name) {
|
|
2122
|
+
let id = '3dcontrol_' + name;
|
|
2123
|
+
if (!this.isDockOpenAndActivate(id)) {
|
|
2124
|
+
let editor = new IobrokerWebui3DScreenEditor();
|
|
2125
|
+
editor.id = id;
|
|
2126
|
+
editor.title = '3D Ctrl: ' + name;
|
|
2127
|
+
editor.setAttribute('scene-name', name);
|
|
2128
|
+
editor.setAttribute('scene-type', '3dcontrol');
|
|
1981
2129
|
this.openDock(editor);
|
|
1982
2130
|
}
|
|
1983
2131
|
}
|
|
@@ -5,6 +5,17 @@ import { exportData, openFileDialog } from "../helper/Helper.js";
|
|
|
5
5
|
import { getCustomControlName, webuiCustomControlPrefix } from "../runtime/CustomControls.js";
|
|
6
6
|
import { defaultOptions, defaultStyle } from "@gokturk413/web-component-designer-widgets-wunderbaum";
|
|
7
7
|
import { Wunderbaum } from 'wunderbaum';
|
|
8
|
+
const default3DScript = `// 3D Scene Script
|
|
9
|
+
// Context: THREE, scene, camera, renderer, controls
|
|
10
|
+
// assets (Map<name, Object3D>) mixers (Map<assetId, {mixer, clips, actions}>)
|
|
11
|
+
// driveEngine setState(signal, val) getState(signal)
|
|
12
|
+
// subscribe(signal, cb) → returns unsubscribe fn
|
|
13
|
+
// onFrame(fn) onSelect(fn) log(...)
|
|
14
|
+
|
|
15
|
+
onFrame((dt, ts) => {
|
|
16
|
+
// Runs every animation frame. dt = delta seconds.
|
|
17
|
+
});
|
|
18
|
+
`;
|
|
8
19
|
//@ts-ignore
|
|
9
20
|
import wunderbaumStyle from 'wunderbaum/dist/wunderbaum.css' with { type: 'css' };
|
|
10
21
|
import { defaultNewStyle, defaultNewControlScript } from "./IobrokerWebuiScreenEditor.js";
|
|
@@ -102,6 +113,7 @@ export class IobrokerWebuiSolutionExplorer extends BaseCustomWebComponentConstru
|
|
|
102
113
|
const result = await Promise.allSettled([
|
|
103
114
|
this._createFolderNode('screen'),
|
|
104
115
|
this._createFolderNode('3dscreen'),
|
|
116
|
+
this._createFolderNode('3dcontrol'),
|
|
105
117
|
this._createControlsNode(),
|
|
106
118
|
this._createGlobalNode(),
|
|
107
119
|
this._createNpmsNode(),
|
|
@@ -119,62 +131,32 @@ export class IobrokerWebuiSolutionExplorer extends BaseCustomWebComponentConstru
|
|
|
119
131
|
try {
|
|
120
132
|
let screenName = prompt("New " + type + " Name:");
|
|
121
133
|
if (screenName) {
|
|
122
|
-
if (type === '3dscreen') {
|
|
123
|
-
|
|
124
|
-
const defaultScene = {
|
|
134
|
+
if (type === '3dscreen' || type === '3dcontrol') {
|
|
135
|
+
const makeDefaultScene = (nm) => ({
|
|
125
136
|
id: 'scene_' + Date.now().toString(36),
|
|
126
|
-
name:
|
|
137
|
+
name: nm,
|
|
127
138
|
version: '1.0',
|
|
128
139
|
createdAt: new Date().toISOString(),
|
|
129
140
|
modifiedAt: new Date().toISOString(),
|
|
130
141
|
assets: [],
|
|
142
|
+
script: default3DScript,
|
|
143
|
+
style: '',
|
|
131
144
|
lights: [
|
|
132
|
-
{
|
|
133
|
-
|
|
134
|
-
name: 'Ambient Light',
|
|
135
|
-
type: 'ambient',
|
|
136
|
-
color: '#ffffff',
|
|
137
|
-
intensity: 0.6
|
|
138
|
-
},
|
|
139
|
-
{
|
|
140
|
-
id: 'directional_light',
|
|
141
|
-
name: 'Directional Light',
|
|
142
|
-
type: 'directional',
|
|
143
|
-
color: '#ffffff',
|
|
144
|
-
intensity: 0.8,
|
|
145
|
-
position: { x: 10, y: 10, z: 10 },
|
|
146
|
-
castShadow: true
|
|
147
|
-
}
|
|
145
|
+
{ id: 'ambient_light', name: 'Ambient Light', type: 'ambient', color: '#ffffff', intensity: 0.6 },
|
|
146
|
+
{ id: 'directional_light', name: 'Directional Light', type: 'directional', color: '#ffffff', intensity: 0.8, position: { x: 10, y: 10, z: 10 }, castShadow: true }
|
|
148
147
|
],
|
|
149
|
-
camera: {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
axes: {
|
|
162
|
-
visible: true,
|
|
163
|
-
size: 5
|
|
164
|
-
},
|
|
165
|
-
settings: {
|
|
166
|
-
backgroundColor: '#333333',
|
|
167
|
-
enableControls: true,
|
|
168
|
-
enableRaycasting: true,
|
|
169
|
-
shadowsEnabled: true,
|
|
170
|
-
antialiasing: true
|
|
171
|
-
},
|
|
172
|
-
edits: {
|
|
173
|
-
ops: []
|
|
174
|
-
}
|
|
175
|
-
};
|
|
176
|
-
await iobrokerHandler.saveObject('3dscreen', (dir ?? '') + '/' + screenName, defaultScene);
|
|
177
|
-
window.appShell.open3DScreenEditor((dir ?? '') + '/' + screenName);
|
|
148
|
+
camera: { position: { x: 10, y: 10, z: 10 }, target: { x: 0, y: 0, z: 0 }, fov: 75 },
|
|
149
|
+
grid: { visible: true, size: 20, divisions: 20, colorCenterLine: '#888888', colorGrid: '#444444' },
|
|
150
|
+
axes: { visible: true, size: 5 },
|
|
151
|
+
settings: { backgroundColor: '#333333', enableControls: true, enableRaycasting: true, shadowsEnabled: true, antialiasing: true },
|
|
152
|
+
edits: { ops: [] }
|
|
153
|
+
});
|
|
154
|
+
const fullName = (dir ?? '') + '/' + screenName;
|
|
155
|
+
await iobrokerHandler.saveObject(type, fullName, makeDefaultScene(fullName));
|
|
156
|
+
if (type === '3dcontrol')
|
|
157
|
+
window.appShell.open3DControlEditor(fullName);
|
|
158
|
+
else
|
|
159
|
+
window.appShell.open3DScreenEditor(fullName);
|
|
178
160
|
} else {
|
|
179
161
|
const defaultScript = type === 'control' ? defaultNewControlScript : null;
|
|
180
162
|
window.appShell.openScreenEditor((dir ?? '') + '/' + screenName, type, '', defaultNewStyle, defaultScript, {});
|
|
@@ -263,6 +245,9 @@ export class IobrokerWebuiSolutionExplorer extends BaseCustomWebComponentConstru
|
|
|
263
245
|
case '3dscreen':
|
|
264
246
|
name = "3D Screens";
|
|
265
247
|
break;
|
|
248
|
+
case '3dcontrol':
|
|
249
|
+
name = "3D Custom Controls";
|
|
250
|
+
break;
|
|
266
251
|
//case '':
|
|
267
252
|
// name='Additional Files';
|
|
268
253
|
// break;
|
|
@@ -368,6 +353,9 @@ export class IobrokerWebuiSolutionExplorer extends BaseCustomWebComponentConstru
|
|
|
368
353
|
else if (type == '3dscreen') {
|
|
369
354
|
window.appShell.open3DScreenEditor(nm);
|
|
370
355
|
}
|
|
356
|
+
else if (type == '3dcontrol') {
|
|
357
|
+
window.appShell.open3DControlEditor(nm);
|
|
358
|
+
}
|
|
371
359
|
});
|
|
372
360
|
},
|
|
373
361
|
data: { type, name: (dir ?? '') + '/' + x }
|
package/www/runtime.html
CHANGED
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
|
|
50
50
|
<body>
|
|
51
51
|
<iobroker-webui-screen-viewer id="viewer"></iobroker-webui-screen-viewer>
|
|
52
|
+
<div id="viewer3dContainer" style="display:none;width:100%;height:100%;position:fixed;top:0;left:0;"></div>
|
|
52
53
|
<div id="overlayLayer"></div>
|
|
53
54
|
</body>
|
|
54
55
|
|
|
@@ -68,16 +69,35 @@
|
|
|
68
69
|
async function checkHash() {
|
|
69
70
|
const viewer = document.getElementById('viewer');
|
|
70
71
|
const par = new URLSearchParams(location.hash.substring(1));
|
|
71
|
-
|
|
72
|
+
const threeDScreen = par.get('3dscreen');
|
|
73
|
+
|
|
74
|
+
if (threeDScreen) {
|
|
75
|
+
// 3D screen runtime mode
|
|
76
|
+
viewer.style.display = 'none';
|
|
77
|
+
const container = document.getElementById('viewer3dContainer');
|
|
78
|
+
container.style.display = 'block';
|
|
79
|
+
container.innerHTML = '';
|
|
80
|
+
await importShim('./dist/frontend/config/IobrokerWebui3DScreenViewer.js');
|
|
81
|
+
await customElements.whenDefined('iobroker-webui-3dscreen-viewer');
|
|
82
|
+
const v3d = document.createElement('iobroker-webui-3dscreen-viewer');
|
|
83
|
+
v3d.setAttribute('scene-name', threeDScreen);
|
|
84
|
+
v3d.style.cssText = 'width:100%;height:100%;display:block;';
|
|
85
|
+
container.appendChild(v3d);
|
|
86
|
+
let imp = await importShim(iobrokerWebuiRootUrl + "dist/frontend/common/IobrokerHandler.js");
|
|
87
|
+
imp.iobrokerHandler.sendCommand("uiChangedView", threeDScreen);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let screen = par.get('screenName');
|
|
72
92
|
if (screen) {
|
|
73
93
|
await customElements.whenDefined(viewer.localName);
|
|
74
94
|
} else {
|
|
75
95
|
screen = 'start';
|
|
76
96
|
}
|
|
77
97
|
await viewer.setScreenNameAndLoad(screen);
|
|
78
|
-
let subScreen =
|
|
98
|
+
let subScreen = par.get('subScreen');
|
|
79
99
|
if (subScreen) {
|
|
80
|
-
const targetSelector =
|
|
100
|
+
const targetSelector = par.get('targetSelector') ?? 'iobroker-webui-screen-viewer';
|
|
81
101
|
const sv = viewer._getDomElements(targetSelector)[0];
|
|
82
102
|
sv.removeAttribute('screen-name');
|
|
83
103
|
sv.screenName = subScreen;
|