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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "mywebui",
4
- "version": "1.42.33",
4
+ "version": "1.42.35",
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.33",
3
+ "version": "1.42.35",
4
4
  "description": "ioBroker mywebui - Custom edited mywebui by gokturk413 with 3D Editor",
5
5
  "type": "module",
6
6
  "main": "dist/backend/main.js",
@@ -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
- // Second dispatch in case WebGL canvas needs re-init
277
- requestAnimationFrame(() => editor.signals.windowResize.dispatch());
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 = null;
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) { this._cleanup(); this._cleanup = null; }
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 || !(camera.isCamera)) {
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) renderer.shadowMap.enabled = proj.shadows;
78
- if (proj.physicallyCorrectLights) renderer.useLegacyLights = false;
93
+ if (proj.shadows !== undefined) renderer.shadowMap.enabled = proj.shadows;
94
+ if (proj.physicallyCorrectLights) renderer.useLegacyLights = false;
79
95
  } else {
80
- // Old format backward compatibility
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 loader = new GLTFLoader();
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 => loader.load(asset.glbPath, gltf => {
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 content after scene/controls are ready
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
- // Script context helpers
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) => selectCallbacks.push(fn);
224
+ const onSelect = (fn) => {};
149
225
  const log = (...args) => console.log('[3D]', ...args);
150
-
151
- const setState = (id, val) => {
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, camera, renderer, controls,
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 = () => { _subscriptions.forEach(u => { try { u(); } catch (_) {} }); };
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
- camera.aspect = vw / vh;
188
- camera.updateProjectionMatrix();
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
- renderer.render(scene, camera);
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