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 +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 +92 -40
- package/www/dist/frontend/config/IobrokerWebuiAppShell.js +116 -72
- 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
|
}
|
|
@@ -380,10 +380,13 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
|
|
|
380
380
|
|
|
381
381
|
async loadScene(sceneName) {
|
|
382
382
|
try {
|
|
383
|
-
|
|
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
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
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
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
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
|
|
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 // setState(signal, val), getState(signal), subscribe(signal, cb) // 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
|
-
//
|
|
1917
|
-
|
|
1918
|
-
|
|
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 => {
|
|
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
|
-
//
|
|
1929
|
-
|
|
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.
|
|
1962
|
+
this._show3DEventsInDock(node, asset, nodeKey, onChange);
|
|
1937
1963
|
}
|
|
1938
1964
|
});
|
|
1939
1965
|
});
|
|
1940
1966
|
|
|
1941
|
-
//
|
|
1942
|
-
|
|
1943
|
-
const 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.
|
|
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
|
-
|
|
1961
|
-
|
|
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
|
-
|
|
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;
|