iobroker.mywebui 1.42.17 → 1.42.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/io-package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "mywebui",
4
- "version": "1.42.17",
4
+ "version": "1.42.18",
5
5
  "titleLang": {
6
6
  "en": "mywebui",
7
7
  "de": "mywebui",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.mywebui",
3
- "version": "1.42.17",
3
+ "version": "1.42.18",
4
4
  "description": "ioBroker mywebui - Custom edited mywebui by gokturk413 with 3D Editor",
5
5
  "type": "module",
6
6
  "main": "dist/backend/main.js",
@@ -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
  }
@@ -447,6 +448,11 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
447
448
  this._threeObjects.push(model);
448
449
  if (this.driveEngine && Array.isArray(this.sceneData?.assets)) {
449
450
  this.driveEngine.createFromAssets(this.sceneData.assets, this._threeObjects);
451
+ // Wrap drives for AtMax/AtMin events
452
+ if (asset.drives?.length) {
453
+ const drives = asset.drives.map((_, i) => this.driveEngine.getDrive(asset.id, i));
454
+ this._wrapDriveEvents(asset, drives);
455
+ }
450
456
  }
451
457
  // AnimationMixer for built-in GLB animations
452
458
  if (gltf.animations?.length > 0) {
@@ -457,6 +463,12 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
457
463
  }
458
464
  this._mixers.set(asset.id, { mixer, clips: gltf.animations, actions });
459
465
  asset.availableClips = gltf.animations.map(c => c.name);
466
+ // animationEnd event
467
+ mixer.addEventListener('finished', (e) => {
468
+ const clipName = e.action.getClip().name;
469
+ this._executeNodeEvent(model, asset, `animationEnd:${clipName}`);
470
+ this._executeNodeEvent(model, asset, 'animationEnd');
471
+ });
460
472
  }
461
473
  // Extract named node list for scene tree
462
474
  asset._nodeList = this._extractNamedNodes(model);
@@ -512,6 +524,8 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
512
524
 
513
525
  if (hitNode && hitAssetRoot) {
514
526
  if (hitNode.userData.interaction) this._handleInteraction(hitNode);
527
+ // Fire node event: check by name AND by uuid
528
+ this._executeNodeEvent(hitNode, hitAssetRoot.userData.assetData, 'click');
515
529
  this.selectNode(hitNode, hitAssetRoot);
516
530
  } else {
517
531
  this.deselectAll();
@@ -637,6 +651,131 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
637
651
  }
638
652
  }
639
653
 
654
+ // ── Event execution ───────────────────────────────────
655
+
656
+ /**
657
+ * Execute a node event handler stored in asset.events[nodeName][eventKey].
658
+ * eventKey: 'click' | 'mouseenter' | 'mouseleave' | 'animationEnd' | 'animationEnd:ClipName' |
659
+ * 'driveAtMax:N' | 'driveAtMin:N' | 'signalChange:path'
660
+ * Node name '__root__' = the asset root object.
661
+ */
662
+ _executeNodeEvent(node, asset, eventKey) {
663
+ if (!asset?.events) return;
664
+ const nodeKey = (node?.userData?.assetData === asset || !node?.name) ? '__root__' : node.name;
665
+ const script = asset.events[nodeKey]?.[eventKey];
666
+ if (!script?.trim()) return;
667
+
668
+ const ctx = {
669
+ node,
670
+ assetData: asset,
671
+ THREE: this.THREE,
672
+ scene: this.scene,
673
+ camera: this.camera,
674
+ renderer: this.renderer,
675
+ controls: this.controls,
676
+ assets: this._buildAssetsMap(),
677
+ mixers: this._mixers,
678
+ driveEngine: this.driveEngine,
679
+ setState(signal, val) { iobrokerHandler.setState(signal, val); },
680
+ getState: (s) => this.driveEngine.bus.get(s),
681
+ subscribe: (s, cb) => { const u = this.driveEngine.bus.subscribe(s, cb); this._scriptUnsubs.push(u); return u; },
682
+ log: (...a) => console.log('[3D Event]', nodeKey, eventKey, ...a),
683
+ };
684
+ try {
685
+ new Function(...Object.keys(ctx), script)(...Object.values(ctx));
686
+ } catch (err) {
687
+ console.error(`[3D Event ${nodeKey}:${eventKey}]`, err.message);
688
+ }
689
+ }
690
+
691
+ _buildAssetsMap() {
692
+ const m = new Map();
693
+ for (const obj of this.scene.children) {
694
+ if (obj.userData.assetData) {
695
+ m.set(obj.userData.assetData.name, obj);
696
+ m.set(obj.userData.assetData.id, obj);
697
+ }
698
+ }
699
+ return m;
700
+ }
701
+
702
+ // ── Hover detection ───────────────────────────────────
703
+
704
+ _setupHoverDetection() {
705
+ let hoverNode = null;
706
+ let hoverAsset = null;
707
+ let throttle = null;
708
+
709
+ this.renderer.domElement.addEventListener('mousemove', (e) => {
710
+ if (throttle) return;
711
+ throttle = setTimeout(() => { throttle = null; }, 50); // 20 fps check
712
+
713
+ const rect = this.renderer.domElement.getBoundingClientRect();
714
+ this.mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
715
+ this.mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
716
+ this.raycaster.setFromCamera(this.mouse, this.camera);
717
+ const hits = this.raycaster.intersectObjects(this.scene.children, true);
718
+
719
+ let newNode = null, newRoot = null;
720
+ for (const hit of hits) {
721
+ const o = hit.object;
722
+ if (o.userData.isGrid || o.userData.isAxes || o.userData.isSelectionHelper) continue;
723
+ let p = o;
724
+ while (p && p !== this.scene) {
725
+ if (p.userData.assetData) { newRoot = p; break; }
726
+ p = p.parent;
727
+ }
728
+ if (newRoot) { newNode = o; break; }
729
+ }
730
+
731
+ if (newNode !== hoverNode) {
732
+ if (hoverNode && hoverAsset) {
733
+ this._executeNodeEvent(hoverNode, hoverAsset, 'mouseleave');
734
+ this.renderer.domElement.style.cursor = 'default';
735
+ }
736
+ if (newNode && newRoot) {
737
+ const asset = newRoot.userData.assetData;
738
+ this._executeNodeEvent(newNode, asset, 'mouseenter');
739
+ // Change cursor if node has any event or interaction
740
+ const hasEvent = asset?.events || newNode.userData.interaction;
741
+ if (hasEvent) this.renderer.domElement.style.cursor = 'pointer';
742
+ }
743
+ hoverNode = newNode;
744
+ hoverAsset = newRoot?.userData.assetData ?? null;
745
+ }
746
+ });
747
+
748
+ this.renderer.domElement.addEventListener('mouseleave', () => {
749
+ if (hoverNode && hoverAsset) {
750
+ this._executeNodeEvent(hoverNode, hoverAsset, 'mouseleave');
751
+ }
752
+ hoverNode = null; hoverAsset = null;
753
+ this.renderer.domElement.style.cursor = 'default';
754
+ });
755
+ }
756
+
757
+ // ── Drive events (AtMax / AtMin) ──────────────────────
758
+ // Called from DriveEngine after createFromAssets - wraps onAfterUpdate
759
+ _wrapDriveEvents(asset, drives) {
760
+ for (let i = 0; i < drives.length; i++) {
761
+ const drive = drives[i];
762
+ if (!drive) continue;
763
+ const cfg = asset.drives?.[i];
764
+ const origCb = drive.onAfterUpdate;
765
+ drive.onAfterUpdate = (d) => {
766
+ if (origCb) origCb(d);
767
+ const root = this._threeObjects.find(o => o.userData.assetId === asset.id);
768
+ if (!root) return;
769
+ const atMax = cfg?.maxPos !== undefined ? Math.abs(d.currentPosition - cfg.maxPos) < 0.1 : false;
770
+ const atMin = cfg?.minPos !== undefined ? Math.abs(d.currentPosition - cfg.minPos) < 0.1 : false;
771
+ if (atMax && !d._evtWasAtMax) this._executeNodeEvent(root, asset, `driveAtMax:${i}`);
772
+ if (atMin && !d._evtWasAtMin) this._executeNodeEvent(root, asset, `driveAtMin:${i}`);
773
+ d._evtWasAtMax = atMax;
774
+ d._evtWasAtMin = atMin;
775
+ };
776
+ }
777
+ }
778
+
640
779
  _extractNamedNodes(obj, depth = 0, result = []) {
641
780
  if (depth > 7) return result;
642
781
  if (depth > 0 && obj.name) {
@@ -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;">
@@ -1721,6 +1721,50 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
1721
1721
  </div>`
1722
1722
  : `<div style="color:#555;font-size:10px;font-style:italic;padding:4px 0;">No interaction</div>`;
1723
1723
 
1724
+ // ── Events ──
1725
+ const nodeEvents = asset?.events?.[nodeKey] ?? {};
1726
+ const nodeEventEntries = Object.entries(nodeEvents);
1727
+ // Build list of available preset events for "Add" dropdown
1728
+ const presetEvents = [
1729
+ { key: 'click', label: '🖱️ click' },
1730
+ { key: 'mouseenter', label: '↗️ mouseenter' },
1731
+ { key: 'mouseleave', label: '↙️ mouseleave' },
1732
+ { key: 'animationEnd', label: '🎬 animationEnd (any clip)' },
1733
+ ...( (asset?.availableClips ?? []).map(c => ({ key: `animationEnd:${c}`, label: `🎬 animationEnd:${c}` })) ),
1734
+ ...( (asset?.drives ?? []).map((_, i) => ([
1735
+ { key: `driveAtMax:${i}`, label: `⬆️ driveAtMax:${i}` },
1736
+ { key: `driveAtMin:${i}`, label: `⬇️ driveAtMin:${i}` },
1737
+ ])).flat() ),
1738
+ ].filter(p => !nodeEvents[p.key]); // hide already-configured ones
1739
+
1740
+ const presetOptsHtml = presetEvents.map(p => `<option value="${p.key}">${p.label}</option>`).join('');
1741
+
1742
+ const eventsHtml = nodeEventEntries.length > 0
1743
+ ? nodeEventEntries.map(([evtKey, script]) => `
1744
+ <div class="evt-row" style="background:#1a1a2e;border:1px solid #2a2a4a;border-radius:3px;padding:5px;margin-bottom:4px;">
1745
+ <div style="display:flex;align-items:center;gap:4px;margin-bottom:3px;">
1746
+ <span style="color:#dcdcaa;font-size:10px;flex:1;font-family:monospace;">${evtKey}</span>
1747
+ <button class="evt-del-btn tb-btn" data-evtkey="${evtKey}" style="padding:0px 5px;font-size:9px;background:#5a1a1a;color:#f44747;border-color:#8a2a2a;">✕</button>
1748
+ </div>
1749
+ <textarea class="evt-script" data-evtkey="${evtKey}" rows="3"
1750
+ style="width:100%;box-sizing:border-box;background:#0d0d1a;border:1px solid #3c3c5c;color:#d4d4d4;font-family:'Consolas',monospace;font-size:10px;padding:4px;resize:vertical;line-height:1.4;"
1751
+ spellcheck="false"
1752
+ placeholder="// context: node, assetData, THREE, scene, camera&#10;// setState(signal, val), getState(signal), subscribe(signal, cb)&#10;// assets Map, mixers Map, driveEngine"
1753
+ >${this._escHtml(script)}</textarea>
1754
+ </div>`).join('')
1755
+ : `<div style="color:#555;font-size:10px;font-style:italic;padding:4px 0;">No events configured</div>`;
1756
+
1757
+ const eventsAddHtml = presetEvents.length > 0
1758
+ ? `<div style="display:flex;gap:4px;margin-top:4px;">
1759
+ <select class="evt-preset-sel" style="flex:1;background:#3c3c3c;border:1px solid #555;color:#ccc;padding:2px 4px;border-radius:3px;font-size:10px;">
1760
+ <option value="">— select event type —</option>
1761
+ ${presetOptsHtml}
1762
+ <option value="__custom__">Custom event name...</option>
1763
+ </select>
1764
+ <button class="evt-add-btn tb-btn" style="padding:1px 10px;font-size:10px;">+ Add</button>
1765
+ </div>`
1766
+ : `<div style="color:#555;font-size:9px;margin-top:4px;">All event types configured</div>`;
1767
+
1724
1768
  panel.innerHTML = `
1725
1769
  <style>
1726
1770
  .pg-group{margin-bottom:8px;}
@@ -1771,6 +1815,19 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
1771
1815
  <div class="pg-header">🖱️ Interaction (onClick) <button class="inter-add-btn tb-btn" style="padding:1px 8px;font-size:9px;">+ Set</button></div>
1772
1816
  ${interHtml}
1773
1817
  </div>
1818
+ <div class="pg-group">
1819
+ <div class="pg-header">⚡ Events
1820
+ <span style="color:#555;font-size:9px;font-weight:normal;">node: ${nodeKey}</span>
1821
+ </div>
1822
+ ${eventsHtml}
1823
+ ${eventsAddHtml}
1824
+ <div style="color:#555;font-size:9px;margin-top:6px;line-height:1.5;">
1825
+ Script context:<br>
1826
+ <code style="color:#888;">node, assetData, THREE, scene</code><br>
1827
+ <code style="color:#888;">setState(sig,val) · getState(sig)</code><br>
1828
+ <code style="color:#888;">assets(Map) · mixers · driveEngine</code>
1829
+ </div>
1830
+ </div>
1774
1831
  </div>
1775
1832
  `;
1776
1833
 
@@ -1855,6 +1912,53 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
1855
1912
  this.show3DNodeProperties(node, asset, mixerInfo, onChange);
1856
1913
  }
1857
1914
  });
1915
+
1916
+ // ── Events wiring ──
1917
+ // Script textarea auto-save on blur
1918
+ p.querySelectorAll('.evt-script').forEach(ta => {
1919
+ ta.addEventListener('blur', () => {
1920
+ const evtKey = ta.dataset.evtkey;
1921
+ if (!asset.events) asset.events = {};
1922
+ if (!asset.events[nodeKey]) asset.events[nodeKey] = {};
1923
+ asset.events[nodeKey][evtKey] = ta.value;
1924
+ if (onChange) onChange(asset);
1925
+ });
1926
+ });
1927
+
1928
+ // Delete event
1929
+ p.querySelectorAll('.evt-del-btn').forEach(btn => {
1930
+ btn.addEventListener('click', () => {
1931
+ const evtKey = btn.dataset.evtkey;
1932
+ if (asset?.events?.[nodeKey]) {
1933
+ delete asset.events[nodeKey][evtKey];
1934
+ if (Object.keys(asset.events[nodeKey]).length === 0) delete asset.events[nodeKey];
1935
+ if (onChange) onChange(asset);
1936
+ this.show3DNodeProperties(node, asset, mixerInfo, onChange);
1937
+ }
1938
+ });
1939
+ });
1940
+
1941
+ // Add event from preset dropdown
1942
+ p.querySelector('.evt-add-btn')?.addEventListener('click', () => {
1943
+ const sel = p.querySelector('.evt-preset-sel');
1944
+ let evtKey = sel?.value;
1945
+ if (!evtKey) return;
1946
+ if (evtKey === '__custom__') {
1947
+ evtKey = prompt('Custom event name:', 'signalChange:mywebui.0.signal.path');
1948
+ if (!evtKey) return;
1949
+ }
1950
+ if (!asset.events) asset.events = {};
1951
+ if (!asset.events[nodeKey]) asset.events[nodeKey] = {};
1952
+ if (!asset.events[nodeKey][evtKey]) {
1953
+ asset.events[nodeKey][evtKey] = '// event: ' + evtKey + '\n';
1954
+ }
1955
+ if (onChange) onChange(asset);
1956
+ this.show3DNodeProperties(node, asset, mixerInfo, onChange);
1957
+ });
1958
+ }
1959
+
1960
+ _escHtml(str) {
1961
+ return (str || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
1858
1962
  }
1859
1963
 
1860
1964
  _showAddDriveDialog(obj, onChange) {