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