iobroker.mywebui 1.42.18 → 1.42.20

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.20",
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.20",
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) {
@@ -1712,33 +1712,22 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
1712
1712
  // ── Interaction binding ──
1713
1713
  const nodeKey = (node?.userData?.assetData === asset || !node?.name) ? '__root__' : (node?.name || nodeName);
1714
1714
  const inter = asset?.interactions?.[nodeKey];
1715
- const interHtml = inter
1716
- ? `<div style="background:#1a2a1a;border:1px solid #2a4a2a;border-radius:3px;padding:6px;font-size:10px;">
1717
- <div>Signal: <span style="color:#9cdcfe;">${inter.signal}</span></div>
1718
- <div>Value: <span style="color:#ccc;">${JSON.stringify(inter.value)}</span></div>
1719
- <div>Visual press: <span style="color:#ccc;">${inter.visualPress !== false ? 'yes' : 'no'}</span></div>
1720
- <button class="inter-del-btn tb-btn" style="margin-top:4px;padding:1px 8px;font-size:9px;background:#5a1a1a;color:#f44747;border-color:#8a2a2a;">Remove</button>
1721
- </div>`
1722
- : `<div style="color:#555;font-size:10px;font-style:italic;padding:4px 0;">No interaction</div>`;
1723
1715
 
1724
1716
  // ── Events ──
1725
1717
  const nodeEvents = asset?.events?.[nodeKey] ?? {};
1726
1718
  const nodeEventEntries = Object.entries(nodeEvents);
1727
- // Build list of available preset events for "Add" dropdown
1728
1719
  const presetEvents = [
1729
1720
  { key: 'click', label: '🖱️ click' },
1730
1721
  { key: 'mouseenter', label: '↗️ mouseenter' },
1731
1722
  { key: 'mouseleave', label: '↙️ mouseleave' },
1732
1723
  { key: 'animationEnd', label: '🎬 animationEnd (any clip)' },
1733
1724
  ...( (asset?.availableClips ?? []).map(c => ({ key: `animationEnd:${c}`, label: `🎬 animationEnd:${c}` })) ),
1734
- ...( (asset?.drives ?? []).map((_, i) => ([
1725
+ ...( (asset?.drives ?? []).flatMap((_, i) => [
1735
1726
  { key: `driveAtMax:${i}`, label: `⬆️ driveAtMax:${i}` },
1736
1727
  { key: `driveAtMin:${i}`, label: `⬇️ driveAtMin:${i}` },
1737
- ])).flat() ),
1738
- ].filter(p => !nodeEvents[p.key]); // hide already-configured ones
1739
-
1728
+ ]) ),
1729
+ ].filter(p => !nodeEvents[p.key]);
1740
1730
  const presetOptsHtml = presetEvents.map(p => `<option value="${p.key}">${p.label}</option>`).join('');
1741
-
1742
1731
  const eventsHtml = nodeEventEntries.length > 0
1743
1732
  ? nodeEventEntries.map(([evtKey, script]) => `
1744
1733
  <div class="evt-row" style="background:#1a1a2e;border:1px solid #2a2a4a;border-radius:3px;padding:5px;margin-bottom:4px;">
@@ -1753,7 +1742,6 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
1753
1742
  >${this._escHtml(script)}</textarea>
1754
1743
  </div>`).join('')
1755
1744
  : `<div style="color:#555;font-size:10px;font-style:italic;padding:4px 0;">No events configured</div>`;
1756
-
1757
1745
  const eventsAddHtml = presetEvents.length > 0
1758
1746
  ? `<div style="display:flex;gap:4px;margin-top:4px;">
1759
1747
  <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;">
@@ -1764,6 +1752,14 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
1764
1752
  <button class="evt-add-btn tb-btn" style="padding:1px 10px;font-size:10px;">+ Add</button>
1765
1753
  </div>`
1766
1754
  : `<div style="color:#555;font-size:9px;margin-top:4px;">All event types configured</div>`;
1755
+ const interHtml = inter
1756
+ ? `<div style="background:#1a2a1a;border:1px solid #2a4a2a;border-radius:3px;padding:6px;font-size:10px;">
1757
+ <div>Signal: <span style="color:#9cdcfe;">${inter.signal}</span></div>
1758
+ <div>Value: <span style="color:#ccc;">${JSON.stringify(inter.value)}</span></div>
1759
+ <div>Visual press: <span style="color:#ccc;">${inter.visualPress !== false ? 'yes' : 'no'}</span></div>
1760
+ <button class="inter-del-btn tb-btn" style="margin-top:4px;padding:1px 8px;font-size:9px;background:#5a1a1a;color:#f44747;border-color:#8a2a2a;">Remove</button>
1761
+ </div>`
1762
+ : `<div style="color:#555;font-size:10px;font-style:italic;padding:4px 0;">No interaction</div>`;
1767
1763
 
1768
1764
  panel.innerHTML = `
1769
1765
  <style>
@@ -1914,7 +1910,6 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
1914
1910
  });
1915
1911
 
1916
1912
  // ── Events wiring ──
1917
- // Script textarea auto-save on blur
1918
1913
  p.querySelectorAll('.evt-script').forEach(ta => {
1919
1914
  ta.addEventListener('blur', () => {
1920
1915
  const evtKey = ta.dataset.evtkey;
@@ -1924,8 +1919,6 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
1924
1919
  if (onChange) onChange(asset);
1925
1920
  });
1926
1921
  });
1927
-
1928
- // Delete event
1929
1922
  p.querySelectorAll('.evt-del-btn').forEach(btn => {
1930
1923
  btn.addEventListener('click', () => {
1931
1924
  const evtKey = btn.dataset.evtkey;
@@ -1937,8 +1930,6 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
1937
1930
  }
1938
1931
  });
1939
1932
  });
1940
-
1941
- // Add event from preset dropdown
1942
1933
  p.querySelector('.evt-add-btn')?.addEventListener('click', () => {
1943
1934
  const sel = p.querySelector('.evt-preset-sel');
1944
1935
  let evtKey = sel?.value;
@@ -1949,9 +1940,7 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
1949
1940
  }
1950
1941
  if (!asset.events) asset.events = {};
1951
1942
  if (!asset.events[nodeKey]) asset.events[nodeKey] = {};
1952
- if (!asset.events[nodeKey][evtKey]) {
1953
- asset.events[nodeKey][evtKey] = '// event: ' + evtKey + '\n';
1954
- }
1943
+ if (!asset.events[nodeKey][evtKey]) asset.events[nodeKey][evtKey] = '// event: ' + evtKey + '\n';
1955
1944
  if (onChange) onChange(asset);
1956
1945
  this.show3DNodeProperties(node, asset, mixerInfo, onChange);
1957
1946
  });
@@ -1961,6 +1950,7 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
1961
1950
  return (str || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
1962
1951
  }
1963
1952
 
1953
+
1964
1954
  _showAddDriveDialog(obj, onChange) {
1965
1955
  // Build a modal dialog for drive configuration
1966
1956
  const overlay = document.createElement('div');
@@ -2082,6 +2072,18 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
2082
2072
  editor.id = id;
2083
2073
  editor.title = '3D: ' + name;
2084
2074
  editor.setAttribute('scene-name', name);
2075
+ editor.setAttribute('scene-type', '3dscreen');
2076
+ this.openDock(editor);
2077
+ }
2078
+ }
2079
+ async open3DControlEditor(name) {
2080
+ let id = '3dcontrol_' + name;
2081
+ if (!this.isDockOpenAndActivate(id)) {
2082
+ let editor = new IobrokerWebui3DScreenEditor();
2083
+ editor.id = id;
2084
+ editor.title = '3D Ctrl: ' + name;
2085
+ editor.setAttribute('scene-name', name);
2086
+ editor.setAttribute('scene-type', '3dcontrol');
2085
2087
  this.openDock(editor);
2086
2088
  }
2087
2089
  }
@@ -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;