iobroker.mywebui 1.42.18 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "mywebui",
4
- "version": "1.42.18",
4
+ "version": "1.42.19",
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.18",
3
+ "version": "1.42.19",
4
4
  "description": "ioBroker mywebui - Custom edited mywebui by gokturk413 with 3D Editor",
5
5
  "type": "module",
6
6
  "main": "dist/backend/main.js",
@@ -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
  }
@@ -380,10 +380,13 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
380
380
 
381
381
  async loadScene(sceneName) {
382
382
  try {
383
- this.sceneData = await iobrokerHandler.get3DScreen(sceneName);
383
+ const sceneType = this.getAttribute('scene-type') || '3dscreen';
384
+ this.sceneData = sceneType === '3dcontrol'
385
+ ? await iobrokerHandler.getWebuiObject('3dcontrol', sceneName)
386
+ : await iobrokerHandler.get3DScreen(sceneName);
384
387
  if (!this.sceneData) { console.warn('Scene not found:', sceneName); return; }
385
- // Ensure script/style fields exist
386
- 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`;
387
390
  if (!this.sceneData.style) this.sceneData.style = '';
388
391
  console.log('Scene loaded:', sceneName);
389
392
  this._buildSceneView();
@@ -1113,51 +1116,99 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
1113
1116
 
1114
1117
  // ── Library tab in left panel ─────────────────────────
1115
1118
 
1116
- _renderLibraryTab(el) {
1119
+ async _renderLibraryTab(el) {
1117
1120
  el.innerHTML = '';
1118
1121
  const search = (this._getDomElement('treeSearch')?.value ?? '').toLowerCase();
1119
- const assets = Array.isArray(this.sceneData?.assets) ? this.sceneData.assets : [];
1120
- const filtered = assets.filter(a => !search || a.name.toLowerCase().includes(search));
1121
1122
 
1122
1123
  // Show/hide add button
1123
1124
  const addRow = this._getDomElement('libAddRow');
1124
1125
  if (addRow) addRow.style.display = '';
1125
1126
 
1126
- if (filtered.length === 0) {
1127
- el.innerHTML = `<div style="color:#555;padding:16px 8px;font-size:11px;text-align:center;line-height:1.8;">
1128
- No 3D components yet.<br>
1129
- <span style="color:#9cdcfe;">Import GLB</span> to create<br>a reusable component.
1130
- </div>`;
1131
- return;
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
+ }
1132
1155
  }
1133
1156
 
1134
- for (const asset of filtered) {
1135
- const shortName = asset.name.replace(/\.(glb|gltf)$/i, '');
1136
- const driveCnt = asset.drives?.length ?? 0;
1137
- const clipCnt = asset.availableClips?.length ?? 0;
1138
- const interCnt = Object.keys(asset.interactions ?? {}).length;
1139
- const meta = [
1140
- driveCnt ? `${driveCnt}D` : '',
1141
- clipCnt ? `${clipCnt}A` : '',
1142
- interCnt ? `${interCnt}I` : '',
1143
- ].filter(Boolean).join(' · ') || 'GLB';
1144
-
1145
- const item = document.createElement('div');
1146
- item.className = 'lib-item';
1147
- item.innerHTML = `
1148
- <span class="lib-icon">📦</span>
1149
- <div class="lib-info">
1150
- <div class="lib-name" title="${asset.name}">${shortName}</div>
1151
- <div class="lib-meta">${meta}</div>
1152
- </div>
1153
- <span class="lib-add" title="Add instance to scene">+</span>
1154
- `;
1155
- item.querySelector('.lib-add')?.addEventListener('click', (e) => {
1156
- e.stopPropagation();
1157
- this._addAssetInstance(asset);
1158
- });
1159
- item.addEventListener('click', () => this._selectAsset(asset));
1160
- el.appendChild(item);
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);
1161
1212
  }
1162
1213
  }
1163
1214
 
@@ -1234,7 +1285,8 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
1234
1285
  async saveScene() {
1235
1286
  try {
1236
1287
  this._setStatus('Saving...');
1237
- const ok = await iobrokerHandler.saveObject('3dscreen', this.sceneData.name, this.sceneData);
1288
+ const sceneType = this.getAttribute('scene-type') || '3dscreen';
1289
+ const ok = await iobrokerHandler.saveObject(sceneType, this.sceneData.name, this.sceneData);
1238
1290
  this._setStatus(ok ? 'Saved ✓' : 'Save failed');
1239
1291
  setTimeout(() => this._setStatus(''), 2000);
1240
1292
  } catch (err) {
@@ -1721,50 +1721,6 @@ 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
-
1768
1724
  panel.innerHTML = `
1769
1725
  <style>
1770
1726
  .pg-group{margin-bottom:8px;}
@@ -1815,19 +1771,6 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
1815
1771
  <div class="pg-header">🖱️ Interaction (onClick) <button class="inter-add-btn tb-btn" style="padding:1px 8px;font-size:9px;">+ Set</button></div>
1816
1772
  ${interHtml}
1817
1773
  </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>
1831
1774
  </div>
1832
1775
  `;
1833
1776
 
@@ -1913,9 +1856,92 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
1913
1856
  }
1914
1857
  });
1915
1858
 
1916
- // ── Events wiring ──
1917
- // Script textarea auto-save on blur
1918
- p.querySelectorAll('.evt-script').forEach(ta => {
1859
+ // Show events in native Events dock
1860
+ this._show3DEventsInDock(node, asset, nodeKey, onChange);
1861
+ }
1862
+
1863
+ _escHtml(str) {
1864
+ return (str || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
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&#10;// setState(sig,val) getState(sig)&#10;// 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 => {
1919
1945
  ta.addEventListener('blur', () => {
1920
1946
  const evtKey = ta.dataset.evtkey;
1921
1947
  if (!asset.events) asset.events = {};
@@ -1925,22 +1951,22 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
1925
1951
  });
1926
1952
  });
1927
1953
 
1928
- // Delete event
1929
- p.querySelectorAll('.evt-del-btn').forEach(btn => {
1954
+ // Wire delete buttons
1955
+ d3.querySelectorAll('.evt-del-btn').forEach(btn => {
1930
1956
  btn.addEventListener('click', () => {
1931
1957
  const evtKey = btn.dataset.evtkey;
1932
1958
  if (asset?.events?.[nodeKey]) {
1933
1959
  delete asset.events[nodeKey][evtKey];
1934
1960
  if (Object.keys(asset.events[nodeKey]).length === 0) delete asset.events[nodeKey];
1935
1961
  if (onChange) onChange(asset);
1936
- this.show3DNodeProperties(node, asset, mixerInfo, onChange);
1962
+ this._show3DEventsInDock(node, asset, nodeKey, onChange);
1937
1963
  }
1938
1964
  });
1939
1965
  });
1940
1966
 
1941
- // Add event from preset dropdown
1942
- p.querySelector('.evt-add-btn')?.addEventListener('click', () => {
1943
- const sel = p.querySelector('.evt-preset-sel');
1967
+ // Wire add button
1968
+ d3.querySelector('.evt-add-btn')?.addEventListener('click', () => {
1969
+ const sel = d3.querySelector('.evt-preset-sel');
1944
1970
  let evtKey = sel?.value;
1945
1971
  if (!evtKey) return;
1946
1972
  if (evtKey === '__custom__') {
@@ -1949,16 +1975,22 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
1949
1975
  }
1950
1976
  if (!asset.events) asset.events = {};
1951
1977
  if (!asset.events[nodeKey]) asset.events[nodeKey] = {};
1952
- if (!asset.events[nodeKey][evtKey]) {
1953
- asset.events[nodeKey][evtKey] = '// event: ' + evtKey + '\n';
1954
- }
1978
+ if (!asset.events[nodeKey][evtKey]) asset.events[nodeKey][evtKey] = '// event: ' + evtKey + '\n';
1955
1979
  if (onChange) onChange(asset);
1956
- this.show3DNodeProperties(node, asset, mixerInfo, onChange);
1980
+ this._show3DEventsInDock(node, asset, nodeKey, onChange);
1957
1981
  });
1982
+
1983
+ // Activate the events dock panel
1984
+ this.activateDockById('eventsDock');
1958
1985
  }
1959
1986
 
1960
- _escHtml(str) {
1961
- return (str || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
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 = '';
1962
1994
  }
1963
1995
 
1964
1996
  _showAddDriveDialog(obj, onChange) {
@@ -2082,6 +2114,18 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
2082
2114
  editor.id = id;
2083
2115
  editor.title = '3D: ' + name;
2084
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');
2085
2129
  this.openDock(editor);
2086
2130
  }
2087
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
- // Create new 3D screen with default scene
124
- const defaultScene = {
134
+ if (type === '3dscreen' || type === '3dcontrol') {
135
+ const makeDefaultScene = (nm) => ({
125
136
  id: 'scene_' + Date.now().toString(36),
126
- name: screenName,
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
- id: 'ambient_light',
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
- position: { x: 10, y: 10, z: 10 },
151
- target: { x: 0, y: 0, z: 0 },
152
- fov: 75
153
- },
154
- grid: {
155
- visible: true,
156
- size: 20,
157
- divisions: 20,
158
- colorCenterLine: '#888888',
159
- colorGrid: '#444444'
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
- let screen = (par).get('screenName');
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 = (par).get('subScreen');
98
+ let subScreen = par.get('subScreen');
79
99
  if (subScreen) {
80
- const targetSelector = (par).get('targetSelector') ?? 'iobroker-webui-screen-viewer';
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;