iobroker.mywebui 1.42.33 → 1.42.35
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
package/package.json
CHANGED
|
@@ -273,8 +273,12 @@ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstruct
|
|
|
273
273
|
// Dispatch resize after layout is complete (container may have 0 size on first tick)
|
|
274
274
|
requestAnimationFrame(() => {
|
|
275
275
|
editor.signals.windowResize.dispatch();
|
|
276
|
-
|
|
277
|
-
|
|
276
|
+
requestAnimationFrame(() => {
|
|
277
|
+
editor.signals.windowResize.dispatch();
|
|
278
|
+
// Force continuous render so ViewHelper gizmo is always visible
|
|
279
|
+
// The three.js editor renders on-demand; we keep it alive via sceneGraphChanged
|
|
280
|
+
editor.signals.sceneGraphChanged.dispatch();
|
|
281
|
+
});
|
|
278
282
|
});
|
|
279
283
|
}
|
|
280
284
|
|
|
@@ -5,17 +5,37 @@ export class IobrokerWebui3DScreenViewer extends BaseCustomWebComponentConstruct
|
|
|
5
5
|
static template = html`
|
|
6
6
|
<div id="container" style="width:100%;height:100%;position:relative;overflow:hidden;background:#1a1a1a;">
|
|
7
7
|
<div id="viewport" style="width:100%;height:100%;"></div>
|
|
8
|
+
|
|
9
|
+
<!-- Camera label (top-left) -->
|
|
10
|
+
<div id="camLabel" style="
|
|
11
|
+
position:absolute;top:10px;left:12px;
|
|
12
|
+
color:#ddd;font-size:11px;font-family:monospace;
|
|
13
|
+
background:rgba(0,0,0,0.45);padding:2px 7px;border-radius:3px;
|
|
14
|
+
pointer-events:none;user-select:none;">Persp</div>
|
|
15
|
+
|
|
16
|
+
<!-- Camera buttons (bottom-right) -->
|
|
17
|
+
<div id="camButtons" style="
|
|
18
|
+
position:absolute;bottom:12px;right:12px;
|
|
19
|
+
display:flex;gap:5px;flex-wrap:wrap;justify-content:flex-end;"></div>
|
|
8
20
|
</div>
|
|
9
21
|
`;
|
|
10
22
|
|
|
11
23
|
static style = css`
|
|
12
24
|
:host { display:block;width:100%;height:100%; }
|
|
25
|
+
#camButtons button {
|
|
26
|
+
background:rgba(30,30,30,0.82);color:#ddd;
|
|
27
|
+
border:1px solid #555;border-radius:4px;
|
|
28
|
+
padding:4px 11px;font-size:11px;cursor:pointer;
|
|
29
|
+
font-family:monospace;white-space:nowrap;
|
|
30
|
+
}
|
|
31
|
+
#camButtons button:hover { background:rgba(80,80,80,0.9);color:#fff; }
|
|
32
|
+
#camButtons button.active { background:rgba(0,120,200,0.85);border-color:#1177bb;color:#fff; }
|
|
13
33
|
`;
|
|
14
34
|
|
|
15
35
|
constructor() {
|
|
16
36
|
super();
|
|
17
37
|
this._animating = false;
|
|
18
|
-
this._renderer
|
|
38
|
+
this._renderer = null;
|
|
19
39
|
}
|
|
20
40
|
|
|
21
41
|
async connectedCallback() {
|
|
@@ -27,7 +47,7 @@ export class IobrokerWebui3DScreenViewer extends BaseCustomWebComponentConstruct
|
|
|
27
47
|
|
|
28
48
|
disconnectedCallback() {
|
|
29
49
|
this._animating = false;
|
|
30
|
-
if (this._cleanup)
|
|
50
|
+
if (this._cleanup) { this._cleanup(); this._cleanup = null; }
|
|
31
51
|
if (this._renderer) { this._renderer.dispose(); this._renderer.domElement.remove(); this._renderer = null; }
|
|
32
52
|
window.removeEventListener('resize', this._onResize);
|
|
33
53
|
}
|
|
@@ -37,7 +57,6 @@ export class IobrokerWebui3DScreenViewer extends BaseCustomWebComponentConstruct
|
|
|
37
57
|
const THREE = await import('three');
|
|
38
58
|
const { OrbitControls } = await import('three/addons/controls/OrbitControls.js');
|
|
39
59
|
|
|
40
|
-
// Load scene data
|
|
41
60
|
const sceneData = sceneType === '3dcontrol'
|
|
42
61
|
? await iobrokerHandler.getWebuiObject('3dcontrol', sceneName)
|
|
43
62
|
: await iobrokerHandler.get3DScreen(sceneName);
|
|
@@ -48,46 +67,42 @@ export class IobrokerWebui3DScreenViewer extends BaseCustomWebComponentConstruct
|
|
|
48
67
|
const w = viewport.clientWidth || 800;
|
|
49
68
|
const h = viewport.clientHeight || 600;
|
|
50
69
|
|
|
51
|
-
// Renderer
|
|
52
70
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
53
71
|
renderer.setPixelRatio(window.devicePixelRatio);
|
|
54
72
|
renderer.setSize(w, h);
|
|
55
73
|
renderer.shadowMap.enabled = true;
|
|
74
|
+
renderer.autoClear = false;
|
|
56
75
|
viewport.appendChild(renderer.domElement);
|
|
57
76
|
this._renderer = renderer;
|
|
58
77
|
|
|
59
78
|
let scene, camera;
|
|
60
79
|
|
|
61
80
|
if (sceneData.threeScene && sceneData.threeScene.scene) {
|
|
62
|
-
// New format: three.js editor JSON → ObjectLoader
|
|
63
81
|
const loader = new THREE.ObjectLoader();
|
|
64
82
|
scene = loader.parse(sceneData.threeScene.scene);
|
|
65
83
|
|
|
66
|
-
// Restore camera
|
|
67
84
|
if (sceneData.threeScene.camera) {
|
|
68
85
|
try { camera = loader.parse(sceneData.threeScene.camera); } catch (_) {}
|
|
69
86
|
}
|
|
70
|
-
if (!camera || !
|
|
87
|
+
if (!camera || !camera.isCamera) {
|
|
71
88
|
camera = new THREE.PerspectiveCamera(50, w / h, 0.1, 10000);
|
|
72
89
|
camera.position.set(0, 0, 50);
|
|
73
90
|
}
|
|
74
91
|
|
|
75
|
-
// Renderer settings from project
|
|
76
92
|
const proj = sceneData.threeScene.project || {};
|
|
77
|
-
if (proj.shadows !== undefined)
|
|
78
|
-
if (proj.physicallyCorrectLights)
|
|
93
|
+
if (proj.shadows !== undefined) renderer.shadowMap.enabled = proj.shadows;
|
|
94
|
+
if (proj.physicallyCorrectLights) renderer.useLegacyLights = false;
|
|
79
95
|
} else {
|
|
80
|
-
|
|
81
|
-
scene = new THREE.Scene();
|
|
96
|
+
scene = new THREE.Scene();
|
|
82
97
|
camera = new THREE.PerspectiveCamera(75, w / h, 0.1, 1000);
|
|
83
98
|
camera.position.set(10, 10, 10);
|
|
84
99
|
|
|
85
100
|
if (sceneData.assets) {
|
|
86
101
|
const { GLTFLoader } = await import('three/addons/loaders/GLTFLoader.js');
|
|
87
|
-
const
|
|
102
|
+
const ldr = new GLTFLoader();
|
|
88
103
|
for (const asset of sceneData.assets) {
|
|
89
104
|
if (asset.type === 'model' && asset.glbPath) {
|
|
90
|
-
await new Promise(res =>
|
|
105
|
+
await new Promise(res => ldr.load(asset.glbPath, gltf => {
|
|
91
106
|
const m = gltf.scene;
|
|
92
107
|
if (asset.position) m.position.set(asset.position.x, asset.position.y, asset.position.z);
|
|
93
108
|
if (asset.scale) m.scale.set(asset.scale.x, asset.scale.y, asset.scale.z);
|
|
@@ -98,10 +113,8 @@ export class IobrokerWebui3DScreenViewer extends BaseCustomWebComponentConstruct
|
|
|
98
113
|
}
|
|
99
114
|
}
|
|
100
115
|
|
|
101
|
-
// Ensure scene has a visible background
|
|
102
116
|
if (!scene.background) scene.background = new THREE.Color(0x222222);
|
|
103
117
|
|
|
104
|
-
// Ensure scene has at least one light source
|
|
105
118
|
const existingLights = [];
|
|
106
119
|
scene.traverse(o => { if (o.isLight) existingLights.push(o); });
|
|
107
120
|
if (existingLights.length === 0) {
|
|
@@ -117,12 +130,10 @@ export class IobrokerWebui3DScreenViewer extends BaseCustomWebComponentConstruct
|
|
|
117
130
|
const controls = new OrbitControls(camera, renderer.domElement);
|
|
118
131
|
controls.enableDamping = true;
|
|
119
132
|
|
|
120
|
-
// Auto-fit camera to scene
|
|
133
|
+
// Auto-fit camera to scene mesh bounds
|
|
121
134
|
{
|
|
122
135
|
const box = new THREE.Box3();
|
|
123
|
-
scene.traverse(o => {
|
|
124
|
-
if (o.isMesh || o.isPoints || o.isLine) box.expandByObject(o);
|
|
125
|
-
});
|
|
136
|
+
scene.traverse(o => { if (o.isMesh || o.isPoints || o.isLine) box.expandByObject(o); });
|
|
126
137
|
if (!box.isEmpty()) {
|
|
127
138
|
const center = new THREE.Vector3();
|
|
128
139
|
const size = new THREE.Vector3();
|
|
@@ -139,21 +150,81 @@ export class IobrokerWebui3DScreenViewer extends BaseCustomWebComponentConstruct
|
|
|
139
150
|
}
|
|
140
151
|
}
|
|
141
152
|
|
|
142
|
-
//
|
|
153
|
+
// ── Camera label ─────────────────────────────────────
|
|
154
|
+
const camLabel = this._getDomElement('camLabel');
|
|
155
|
+
const updateCamLabel = (cam) => {
|
|
156
|
+
if (!camLabel) return;
|
|
157
|
+
camLabel.textContent = cam.isOrthographicCamera ? 'Ortho' : 'Persp';
|
|
158
|
+
};
|
|
159
|
+
updateCamLabel(camera);
|
|
160
|
+
|
|
161
|
+
// ── Collect named cameras from scene ─────────────────
|
|
162
|
+
let activeCamera = camera;
|
|
163
|
+
const sceneCameras = [];
|
|
164
|
+
scene.traverse(obj => {
|
|
165
|
+
if (obj.isCamera && obj !== camera) sceneCameras.push(obj);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const switchCamera = (cam, btnEl) => {
|
|
169
|
+
activeCamera = cam;
|
|
170
|
+
activeCamera.aspect = viewport.clientWidth / viewport.clientHeight;
|
|
171
|
+
activeCamera.updateProjectionMatrix?.();
|
|
172
|
+
controls.object = activeCamera;
|
|
173
|
+
controls.update();
|
|
174
|
+
updateCamLabel(activeCamera);
|
|
175
|
+
// Update active button style
|
|
176
|
+
this._getDomElement('camButtons')
|
|
177
|
+
?.querySelectorAll('button')
|
|
178
|
+
.forEach(b => b.classList.remove('active'));
|
|
179
|
+
if (btnEl) btnEl.classList.add('active');
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Build camera buttons
|
|
183
|
+
const camBtns = this._getDomElement('camButtons');
|
|
184
|
+
if (camBtns) {
|
|
185
|
+
// Default camera button
|
|
186
|
+
const btnDefault = document.createElement('button');
|
|
187
|
+
btnDefault.textContent = 'HOME';
|
|
188
|
+
btnDefault.classList.add('active');
|
|
189
|
+
btnDefault.addEventListener('click', () => switchCamera(camera, btnDefault));
|
|
190
|
+
camBtns.appendChild(btnDefault);
|
|
191
|
+
|
|
192
|
+
// Named scene cameras
|
|
193
|
+
sceneCameras.forEach((cam, i) => {
|
|
194
|
+
const btn = document.createElement('button');
|
|
195
|
+
btn.textContent = cam.name || ('CAM ' + (i + 1));
|
|
196
|
+
btn.addEventListener('click', () => switchCamera(cam, btn));
|
|
197
|
+
camBtns.appendChild(btn);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── Orientation gizmo ────────────────────────────────
|
|
202
|
+
let viewHelper = null;
|
|
203
|
+
try {
|
|
204
|
+
const { ViewHelper } = await import('three/addons/helpers/ViewHelper.js');
|
|
205
|
+
// ViewHelper renders into a sub-viewport of the main WebGL canvas
|
|
206
|
+
viewHelper = new ViewHelper(activeCamera, renderer.domElement);
|
|
207
|
+
viewHelper.center = controls.target;
|
|
208
|
+
// Top-right corner (8px from top, 8px from right)
|
|
209
|
+
viewHelper.location.top = 8;
|
|
210
|
+
viewHelper.location.right = 8;
|
|
211
|
+
viewHelper.location.left = null;
|
|
212
|
+
viewHelper.location.bottom = null;
|
|
213
|
+
|
|
214
|
+
// Forward clicks on the canvas to ViewHelper for camera snapping
|
|
215
|
+
renderer.domElement.addEventListener('pointerup', (e) => {
|
|
216
|
+
if (viewHelper.animating) viewHelper.handleClick(e);
|
|
217
|
+
});
|
|
218
|
+
} catch (_) { /* ViewHelper not available */ }
|
|
219
|
+
|
|
220
|
+
// ── Script context ───────────────────────────────────
|
|
143
221
|
const frameCallbacks = [];
|
|
144
|
-
const selectCallbacks = [];
|
|
145
222
|
const _subscriptions = [];
|
|
146
|
-
|
|
147
223
|
const onFrame = (fn) => frameCallbacks.push(fn);
|
|
148
|
-
const onSelect = (fn) =>
|
|
224
|
+
const onSelect = (fn) => {};
|
|
149
225
|
const log = (...args) => console.log('[3D]', ...args);
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
try { iobrokerHandler.setState(id, val); } catch (_) {}
|
|
153
|
-
};
|
|
154
|
-
const getState = async (id) => {
|
|
155
|
-
try { return await iobrokerHandler.getState(id); } catch (_) { return null; }
|
|
156
|
-
};
|
|
226
|
+
const setState = (id, val) => { try { iobrokerHandler.setState(id, val); } catch (_) {} };
|
|
227
|
+
const getState = async (id) => { try { return await iobrokerHandler.getState(id); } catch (_) { return null; } };
|
|
157
228
|
const subscribe = (id, cb) => {
|
|
158
229
|
try {
|
|
159
230
|
const unsub = iobrokerHandler.subscribeState(id, cb);
|
|
@@ -161,11 +232,9 @@ export class IobrokerWebui3DScreenViewer extends BaseCustomWebComponentConstruct
|
|
|
161
232
|
return unsub;
|
|
162
233
|
} catch (_) { return () => {}; }
|
|
163
234
|
};
|
|
235
|
+
const assets = new Map();
|
|
236
|
+
const mixers = new Map();
|
|
164
237
|
|
|
165
|
-
const assets = new Map();
|
|
166
|
-
const mixers = new Map();
|
|
167
|
-
|
|
168
|
-
// Run scene script if present
|
|
169
238
|
if (sceneData.script) {
|
|
170
239
|
try {
|
|
171
240
|
const fn = new Function(
|
|
@@ -174,31 +243,50 @@ export class IobrokerWebui3DScreenViewer extends BaseCustomWebComponentConstruct
|
|
|
174
243
|
'assets', 'mixers', 'log',
|
|
175
244
|
sceneData.script
|
|
176
245
|
);
|
|
177
|
-
fn(THREE, scene,
|
|
246
|
+
fn(THREE, scene, activeCamera, renderer, controls,
|
|
178
247
|
onFrame, onSelect, setState, getState, subscribe,
|
|
179
248
|
assets, mixers, log);
|
|
180
249
|
} catch (e) { console.warn('3D scene script error:', e); }
|
|
181
250
|
}
|
|
182
251
|
|
|
183
252
|
this._animating = true;
|
|
184
|
-
this._cleanup
|
|
185
|
-
this._onResize
|
|
253
|
+
this._cleanup = () => { _subscriptions.forEach(u => { try { u(); } catch (_) {} }); };
|
|
254
|
+
this._onResize = () => {
|
|
186
255
|
const vw = viewport.clientWidth, vh = viewport.clientHeight;
|
|
187
|
-
|
|
188
|
-
|
|
256
|
+
if (activeCamera.isPerspectiveCamera) {
|
|
257
|
+
activeCamera.aspect = vw / vh;
|
|
258
|
+
activeCamera.updateProjectionMatrix();
|
|
259
|
+
}
|
|
189
260
|
renderer.setSize(vw, vh);
|
|
261
|
+
if (viewHelper) viewHelper.camera = activeCamera;
|
|
190
262
|
};
|
|
191
263
|
window.addEventListener('resize', this._onResize);
|
|
192
264
|
|
|
265
|
+
// ── Render loop ──────────────────────────────────────
|
|
193
266
|
let _lastTime = 0;
|
|
194
267
|
const animate = (ts) => {
|
|
195
268
|
if (!this._animating) return;
|
|
196
269
|
requestAnimationFrame(animate);
|
|
197
270
|
const dt = _lastTime ? (ts - _lastTime) / 1000 : 0;
|
|
198
271
|
_lastTime = ts;
|
|
272
|
+
|
|
273
|
+
controls.object = activeCamera;
|
|
199
274
|
controls.update(dt);
|
|
275
|
+
|
|
200
276
|
for (const cb of frameCallbacks) { try { cb(dt, ts / 1000); } catch (_) {} }
|
|
201
|
-
|
|
277
|
+
|
|
278
|
+
renderer.clear();
|
|
279
|
+
renderer.render(scene, activeCamera);
|
|
280
|
+
|
|
281
|
+
// ViewHelper manages its own sub-viewport — just call render
|
|
282
|
+
if (viewHelper) {
|
|
283
|
+
viewHelper.camera = activeCamera;
|
|
284
|
+
viewHelper.center = controls.target;
|
|
285
|
+
if (viewHelper.animating) viewHelper.update(dt);
|
|
286
|
+
renderer.autoClear = false;
|
|
287
|
+
viewHelper.render(renderer);
|
|
288
|
+
renderer.autoClear = false; // keep false for next frame
|
|
289
|
+
}
|
|
202
290
|
};
|
|
203
291
|
animate(0);
|
|
204
292
|
|