iobroker.mywebui 1.42.0 → 1.42.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.mywebui",
3
- "version": "1.42.0",
3
+ "version": "1.42.1",
4
4
  "description": "ioBroker mywebui - Custom edited mywebui by gokturk413 with 3D Editor",
5
5
  "type": "module",
6
6
  "main": "dist/backend/main.js",
@@ -36,6 +36,7 @@ export class IobrokerHandler {
36
36
  this.clientId = Date.now().toString(16);
37
37
  this.#cache.set('screen', new Map());
38
38
  this.#cache.set('control', new Map());
39
+ this.#cache.set('3dscreen', new Map());
39
40
  }
40
41
  getNormalizedSignalName(id, relativeSignalPath, element) {
41
42
  return (relativeSignalPath ?? '') + id;
@@ -205,6 +206,8 @@ export class IobrokerHandler {
205
206
  return this.getScreen(name);
206
207
  else if (type == 'control')
207
208
  return this.getCustomControl(name);
209
+ else if (type == '3dscreen')
210
+ return this.get3DScreen(name);
208
211
  return null;
209
212
  }
210
213
  async getScreen(name) {
@@ -224,6 +227,23 @@ export class IobrokerHandler {
224
227
  }
225
228
  return screen;
226
229
  }
230
+ async get3DScreen(name) {
231
+ if (name[0] == '/')
232
+ name = name.substring(1);
233
+ let scene = this.#cache.get('3dscreen').get(name);
234
+ if (!scene) {
235
+ if (this._readyPromises)
236
+ await this.waitForReady();
237
+ try {
238
+ scene = await this._getObjectFromFile(this.configPath + "3dscreens/" + name + '.3dscreen');
239
+ }
240
+ catch (err) {
241
+ console.error("Error reading 3D Screen", scene, err);
242
+ }
243
+ this.#cache.get('3dscreen').set(name, scene);
244
+ }
245
+ return scene;
246
+ }
227
247
  async saveObject(type, name, data) {
228
248
  await this._saveObjectToFile(data, "/" + this.configPath + type + "s/" + name + '.' + type);
229
249
  if (this.#cache.has(type))
@@ -0,0 +1,304 @@
1
+ /**
2
+ * 3D Scene Store
3
+ *
4
+ * Manages scene state, undo/redo, and persistence
5
+ * Adapts realvirtual's scene store pattern for mywebui
6
+ */
7
+
8
+ import { TypedEvent } from "@gokturk413/base-custom-webcomponent";
9
+ import {
10
+ Scene3D,
11
+ Scene3DSession,
12
+ EditOp,
13
+ Asset,
14
+ Light,
15
+ CameraPreset,
16
+ createDefaultScene,
17
+ SetAssetPropertyOp,
18
+ TransformAssetOp,
19
+ AddAssetOp,
20
+ RemoveAssetOp,
21
+ Vector3
22
+ } from './3d-scene-types';
23
+ import { iobrokerHandler } from "../common/IobrokerHandler.js";
24
+
25
+ export class Scene3DStore {
26
+ private session: Scene3DSession;
27
+ private undoStack: EditOp[] = [];
28
+ private redoStack: EditOp[] = [];
29
+
30
+ sceneChanged = new TypedEvent<Scene3D>();
31
+ selectionChanged = new TypedEvent<string | null>();
32
+ dirty = new TypedEvent<boolean>();
33
+
34
+ private selectedId: string | null = null;
35
+
36
+ constructor(scene: Scene3D) {
37
+ this.session = {
38
+ saved: JSON.parse(JSON.stringify(scene)),
39
+ draft: JSON.parse(JSON.stringify(scene)),
40
+ isDraft: false,
41
+ dirty: false
42
+ };
43
+ }
44
+
45
+ // ─── Getters ────────────────────────────────────────────────────────
46
+
47
+ getDraft(): Scene3D {
48
+ return this.session.draft;
49
+ }
50
+
51
+ getSaved(): Scene3D | null {
52
+ return this.session.saved;
53
+ }
54
+
55
+ isDirty(): boolean {
56
+ return this.session.dirty;
57
+ }
58
+
59
+ getSelectedId(): string | null {
60
+ return this.selectedId;
61
+ }
62
+
63
+ // ─── Selection ───────────────────────────────────────────────────────
64
+
65
+ select(id: string | null): void {
66
+ this.selectedId = id;
67
+ this.selectionChanged.emit(id);
68
+ }
69
+
70
+ getSelected(): Asset | Light | null {
71
+ if (!this.selectedId) return null;
72
+
73
+ const asset = this.session.draft.assets.find(a => a.id === this.selectedId);
74
+ if (asset) return asset;
75
+
76
+ const light = this.session.draft.lights.find(l => l.id === this.selectedId);
77
+ return light || null;
78
+ }
79
+
80
+ // ─── Operations ──────────────────────────────────────────────────────
81
+
82
+ setAssetProperty(assetId: string, property: string, value: any): void {
83
+ const asset = this.session.draft.assets.find(a => a.id === assetId);
84
+ if (!asset) return;
85
+
86
+ const prev = this.getNestedProperty(asset, property);
87
+ this.setNestedProperty(asset, property, value);
88
+
89
+ const op: SetAssetPropertyOp = {
90
+ id: this.generateOpId(),
91
+ ts: Date.now(),
92
+ schemaV: 1,
93
+ kind: 'setAssetProperty',
94
+ assetId,
95
+ property,
96
+ value,
97
+ prev
98
+ };
99
+
100
+ this.applyOp(op);
101
+ }
102
+
103
+ setLightProperty(lightId: string, property: string, value: any): void {
104
+ const light = this.session.draft.lights.find(l => l.id === lightId);
105
+ if (!light) return;
106
+
107
+ const prev = this.getNestedProperty(light, property);
108
+ this.setNestedProperty(light, property, value);
109
+
110
+ const op = {
111
+ id: this.generateOpId(),
112
+ ts: Date.now(),
113
+ schemaV: 1,
114
+ kind: 'setLightProperty' as const,
115
+ lightId,
116
+ property,
117
+ value,
118
+ prev
119
+ };
120
+
121
+ this.applyOp(op);
122
+ }
123
+
124
+ transformAsset(assetId: string, position: Vector3, rotation: Vector3, scale: Vector3): void {
125
+ const asset = this.session.draft.assets.find(a => a.id === assetId);
126
+ if (!asset) return;
127
+
128
+ const prev = {
129
+ position: { ...asset.position },
130
+ rotation: { ...asset.rotation },
131
+ scale: { ...asset.scale }
132
+ };
133
+
134
+ asset.position = position;
135
+ asset.rotation = rotation;
136
+ asset.scale = scale;
137
+
138
+ const op: TransformAssetOp = {
139
+ id: this.generateOpId(),
140
+ ts: Date.now(),
141
+ schemaV: 1,
142
+ kind: 'transformAsset',
143
+ assetId,
144
+ position,
145
+ rotation,
146
+ scale,
147
+ prev
148
+ };
149
+
150
+ this.applyOp(op);
151
+ }
152
+
153
+ addAsset(asset: Asset): void {
154
+ this.session.draft.assets.push(asset);
155
+
156
+ const op: AddAssetOp = {
157
+ id: this.generateOpId(),
158
+ ts: Date.now(),
159
+ schemaV: 1,
160
+ kind: 'addAsset',
161
+ asset: JSON.parse(JSON.stringify(asset))
162
+ };
163
+
164
+ this.applyOp(op);
165
+ }
166
+
167
+ removeAsset(assetId: string): void {
168
+ const index = this.session.draft.assets.findIndex(a => a.id === assetId);
169
+ if (index === -1) return;
170
+
171
+ const asset = this.session.draft.assets[index];
172
+ this.session.draft.assets.splice(index, 1);
173
+
174
+ const op: RemoveAssetOp = {
175
+ id: this.generateOpId(),
176
+ ts: Date.now(),
177
+ schemaV: 1,
178
+ kind: 'removeAsset',
179
+ assetId,
180
+ asset: JSON.parse(JSON.stringify(asset))
181
+ };
182
+
183
+ this.applyOp(op);
184
+ }
185
+
186
+ setCamera(camera: CameraPreset): void {
187
+ const prev = { ...this.session.draft.camera };
188
+ this.session.draft.camera = camera;
189
+
190
+ const op = {
191
+ id: this.generateOpId(),
192
+ ts: Date.now(),
193
+ schemaV: 1,
194
+ kind: 'setCamera' as const,
195
+ camera,
196
+ prev
197
+ };
198
+
199
+ this.applyOp(op);
200
+ }
201
+
202
+ // ─── Undo/Redo ───────────────────────────────────────────────────────
203
+
204
+ canUndo(): boolean {
205
+ return this.undoStack.length > 0;
206
+ }
207
+
208
+ canRedo(): boolean {
209
+ return this.redoStack.length > 0;
210
+ }
211
+
212
+ undo(): void {
213
+ if (!this.canUndo()) return;
214
+
215
+ const op = this.undoStack.pop()!;
216
+ this.redoStack.push(op);
217
+ this.inverseOp(op);
218
+ }
219
+
220
+ redo(): void {
221
+ if (!this.canRedo()) return;
222
+
223
+ const op = this.redoStack.pop()!;
224
+ this.undoStack.push(op);
225
+ this.applyOp(op);
226
+ }
227
+
228
+ // ─── Persistence ────────────────────────────────────────────────────
229
+
230
+ async save(): Promise<boolean> {
231
+ try {
232
+ const success = await iobrokerHandler.saveObject('3dscreen', this.session.draft.name, this.session.draft);
233
+ if (success) {
234
+ this.session.saved = JSON.parse(JSON.stringify(this.session.draft));
235
+ this.session.dirty = false;
236
+ this.dirty.emit(false);
237
+ return true;
238
+ }
239
+ return false;
240
+ } catch (err) {
241
+ console.error('Error saving 3D scene:', err);
242
+ return false;
243
+ }
244
+ }
245
+
246
+ // ─── Private helpers ────────────────────────────────────────────────
247
+
248
+ private applyOp(op: EditOp): void {
249
+ this.undoStack.push(op);
250
+ this.redoStack = [];
251
+ this.session.draft.edits.ops.push(op);
252
+ this.markDirty();
253
+ }
254
+
255
+ private inverseOp(op: EditOp): void {
256
+ if (op.kind === 'setAssetProperty') {
257
+ const asset = this.session.draft.assets.find(a => a.id === op.assetId);
258
+ if (asset) {
259
+ this.setNestedProperty(asset, op.property, op.prev);
260
+ }
261
+ } else if (op.kind === 'transformAsset') {
262
+ const asset = this.session.draft.assets.find(a => a.id === op.assetId);
263
+ if (asset) {
264
+ asset.position = op.prev.position;
265
+ asset.rotation = op.prev.rotation;
266
+ asset.scale = op.prev.scale;
267
+ }
268
+ } else if (op.kind === 'addAsset') {
269
+ this.session.draft.assets = this.session.draft.assets.filter(a => a.id !== op.asset.id);
270
+ } else if (op.kind === 'removeAsset') {
271
+ this.session.draft.assets.push(op.asset);
272
+ } else if (op.kind === 'setCamera') {
273
+ this.session.draft.camera = op.prev;
274
+ }
275
+ this.markDirty();
276
+ }
277
+
278
+ private markDirty(): void {
279
+ this.session.dirty = true;
280
+ this.dirty.emit(true);
281
+ this.sceneChanged.emit(this.session.draft);
282
+ }
283
+
284
+ private getNestedProperty(obj: any, path: string): any {
285
+ return path.split('.').reduce((curr, prop) => curr?.[prop], obj);
286
+ }
287
+
288
+ private setNestedProperty(obj: any, path: string, value: any): void {
289
+ const parts = path.split('.');
290
+ const last = parts.pop();
291
+ if (!last) return;
292
+
293
+ let current = obj;
294
+ for (const part of parts) {
295
+ if (!current[part]) current[part] = {};
296
+ current = current[part];
297
+ }
298
+ current[last] = value;
299
+ }
300
+
301
+ private generateOpId(): string {
302
+ return 'op_' + Date.now().toString(36) + '_' + Math.random().toString(36).substr(2, 6);
303
+ }
304
+ }
@@ -0,0 +1,277 @@
1
+ /**
2
+ * 3D Scene Type Definitions
3
+ *
4
+ * A 3D Scene consists of:
5
+ * - Assets (GLB models with position, rotation, scale)
6
+ * - Lights (ambient, directional, point, spot)
7
+ * - Camera presets
8
+ * - Bindings (signals to properties)
9
+ * - Animations (from GLB or custom)
10
+ */
11
+
12
+ export interface Vector3 {
13
+ x: number;
14
+ y: number;
15
+ z: number;
16
+ }
17
+
18
+ export interface Binding {
19
+ signal?: string;
20
+ scale?: number;
21
+ offset?: number;
22
+ min?: number;
23
+ max?: number;
24
+ type?: 'boolean' | 'color' | 'number' | 'string';
25
+ expression?: string;
26
+ [key: string]: any;
27
+ }
28
+
29
+ export interface Material {
30
+ color?: string;
31
+ emissive?: string;
32
+ metalness?: number;
33
+ roughness?: number;
34
+ opacity?: number;
35
+ [key: string]: any;
36
+ }
37
+
38
+ export interface Animation {
39
+ id: string;
40
+ name: string;
41
+ duration: number;
42
+ loop?: boolean;
43
+ speed?: number;
44
+ binding?: {
45
+ monitoredState?: string;
46
+ behavior?: 'autoplay' | 'monitorstate';
47
+ stateMaxValue?: number;
48
+ startValue?: number;
49
+ repeat?: boolean;
50
+ };
51
+ }
52
+
53
+ export interface Asset {
54
+ id: string;
55
+ name: string;
56
+ type: 'model' | 'light' | 'control';
57
+ glbPath?: string;
58
+ position: Vector3;
59
+ rotation: Vector3;
60
+ scale: Vector3;
61
+ visible: boolean;
62
+ material?: Material;
63
+ bindings?: Record<string, Binding>;
64
+ animations?: Animation[];
65
+ userData?: Record<string, any>;
66
+ }
67
+
68
+ export interface Light {
69
+ id: string;
70
+ name: string;
71
+ type: 'ambient' | 'directional' | 'point' | 'spot';
72
+ color: string;
73
+ intensity: number;
74
+ position?: Vector3;
75
+ castShadow?: boolean;
76
+ binding?: Record<string, Binding>;
77
+ }
78
+
79
+ export interface CameraPreset {
80
+ position: Vector3;
81
+ target: Vector3;
82
+ fov?: number;
83
+ binding?: Record<string, Binding>;
84
+ }
85
+
86
+ export interface GridConfig {
87
+ visible: boolean;
88
+ size: number;
89
+ divisions: number;
90
+ colorCenterLine: string;
91
+ colorGrid: string;
92
+ }
93
+
94
+ export interface AxesConfig {
95
+ visible: boolean;
96
+ size: number;
97
+ }
98
+
99
+ export interface SceneSettings {
100
+ backgroundColor: string;
101
+ enableControls: boolean;
102
+ enableRaycasting: boolean;
103
+ enablePropertyPanel: boolean;
104
+ shadowsEnabled: boolean;
105
+ antialiasing: boolean;
106
+ [key: string]: any;
107
+ }
108
+
109
+ export interface SceneEdits {
110
+ ops: EditOp[];
111
+ workspaceSettings?: Record<string, any>;
112
+ }
113
+
114
+ export type EditOp =
115
+ | SetAssetPropertyOp
116
+ | SetLightPropertyOp
117
+ | SetCameraOp
118
+ | AddAssetOp
119
+ | RemoveAssetOp
120
+ | TransformAssetOp
121
+ | AddLightOp
122
+ | RemoveLightOp
123
+ | TransformLightOp;
124
+
125
+ export interface EditOpBase {
126
+ id: string;
127
+ ts: number;
128
+ schemaV: 1;
129
+ selectionAfter?: string | null;
130
+ selectionBefore?: string | null;
131
+ }
132
+
133
+ export interface SetAssetPropertyOp extends EditOpBase {
134
+ kind: 'setAssetProperty';
135
+ assetId: string;
136
+ property: string;
137
+ value: any;
138
+ prev: any;
139
+ }
140
+
141
+ export interface SetLightPropertyOp extends EditOpBase {
142
+ kind: 'setLightProperty';
143
+ lightId: string;
144
+ property: string;
145
+ value: any;
146
+ prev: any;
147
+ }
148
+
149
+ export interface SetCameraOp extends EditOpBase {
150
+ kind: 'setCamera';
151
+ camera: CameraPreset;
152
+ prev: CameraPreset;
153
+ }
154
+
155
+ export interface AddAssetOp extends EditOpBase {
156
+ kind: 'addAsset';
157
+ asset: Asset;
158
+ }
159
+
160
+ export interface RemoveAssetOp extends EditOpBase {
161
+ kind: 'removeAsset';
162
+ assetId: string;
163
+ asset: Asset;
164
+ }
165
+
166
+ export interface TransformAssetOp extends EditOpBase {
167
+ kind: 'transformAsset';
168
+ assetId: string;
169
+ position: Vector3;
170
+ rotation: Vector3;
171
+ scale: Vector3;
172
+ prev: { position: Vector3; rotation: Vector3; scale: Vector3 };
173
+ }
174
+
175
+ export interface AddLightOp extends EditOpBase {
176
+ kind: 'addLight';
177
+ light: Light;
178
+ }
179
+
180
+ export interface RemoveLightOp extends EditOpBase {
181
+ kind: 'removeLight';
182
+ lightId: string;
183
+ light: Light;
184
+ }
185
+
186
+ export interface TransformLightOp extends EditOpBase {
187
+ kind: 'transformLight';
188
+ lightId: string;
189
+ position: Vector3;
190
+ prev: Vector3;
191
+ }
192
+
193
+ export interface Scene3D {
194
+ id: string;
195
+ name: string;
196
+ description?: string;
197
+ version: string;
198
+ createdAt: string;
199
+ modifiedAt: string;
200
+
201
+ assets: Asset[];
202
+ lights: Light[];
203
+ camera: CameraPreset;
204
+
205
+ grid: GridConfig;
206
+ axes: AxesConfig;
207
+ settings: SceneSettings;
208
+
209
+ edits: SceneEdits;
210
+ properties?: Record<string, any>;
211
+ bindings?: Record<string, any>;
212
+
213
+ thumbnailDataUrl?: string;
214
+ }
215
+
216
+ export interface Scene3DSession {
217
+ saved: Scene3D | null;
218
+ draft: Scene3D;
219
+ isDraft: boolean;
220
+ dirty: boolean;
221
+ }
222
+
223
+ export function createDefaultScene(id: string, name: string): Scene3D {
224
+ return {
225
+ id,
226
+ name,
227
+ version: '1.0',
228
+ createdAt: new Date().toISOString(),
229
+ modifiedAt: new Date().toISOString(),
230
+ assets: [],
231
+ lights: [
232
+ {
233
+ id: 'ambient_light',
234
+ name: 'Ambient Light',
235
+ type: 'ambient',
236
+ color: '#ffffff',
237
+ intensity: 0.6
238
+ },
239
+ {
240
+ id: 'directional_light',
241
+ name: 'Directional Light',
242
+ type: 'directional',
243
+ color: '#ffffff',
244
+ intensity: 0.8,
245
+ position: { x: 10, y: 10, z: 10 },
246
+ castShadow: true
247
+ }
248
+ ],
249
+ camera: {
250
+ position: { x: 10, y: 10, z: 10 },
251
+ target: { x: 0, y: 0, z: 0 },
252
+ fov: 75
253
+ },
254
+ grid: {
255
+ visible: true,
256
+ size: 20,
257
+ divisions: 20,
258
+ colorCenterLine: '#888888',
259
+ colorGrid: '#444444'
260
+ },
261
+ axes: {
262
+ visible: true,
263
+ size: 5
264
+ },
265
+ settings: {
266
+ backgroundColor: '#333333',
267
+ enableControls: true,
268
+ enableRaycasting: true,
269
+ enablePropertyPanel: true,
270
+ shadowsEnabled: true,
271
+ antialiasing: true
272
+ },
273
+ edits: {
274
+ ops: []
275
+ }
276
+ };
277
+ }
@@ -0,0 +1,480 @@
1
+ import { BaseCustomWebComponentConstructorAppend, LazyLoader, css, html } from "@gokturk413/base-custom-webcomponent";
2
+ import { iobrokerHandler } from "../common/IobrokerHandler.js";
3
+
4
+ export class IobrokerWebui3DScreenEditor extends BaseCustomWebComponentConstructorAppend {
5
+ static template = html`
6
+ <div id="editor-container" style="width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden;">
7
+ <div id="toolbar" style="height:40px;background:#2a2a2a;border-bottom:1px solid #444;display:flex;align-items:center;padding:0 10px;gap:10px;">
8
+ <button id="saveBtn" style="padding:5px 15px;background:#0078d4;color:white;border:none;cursor:pointer;border-radius:3px;">Save</button>
9
+ <button id="undoBtn" style="padding:5px 15px;background:#444;color:#aaa;border:none;cursor:pointer;border-radius:3px;">↶ Undo</button>
10
+ <button id="redoBtn" style="padding:5px 15px;background:#444;color:#aaa;border:none;cursor:pointer;border-radius:3px;">↷ Redo</button>
11
+ <div style="flex:1;"></div>
12
+ <button id="gridToggle" style="padding:5px 10px;background:#444;color:#aaa;border:none;cursor:pointer;border-radius:3px;">Grid</button>
13
+ <button id="axesToggle" style="padding:5px 10px;background:#444;color:#aaa;border:none;cursor:pointer;border-radius:3px;">Axes</button>
14
+ </div>
15
+ <div id="mainContent" style="flex:1;display:flex;overflow:hidden;">
16
+ <div id="leftPanel" style="width:250px;background:#1e1e1e;border-right:1px solid #444;overflow:auto;">
17
+ <div style="padding:10px;font-weight:bold;color:#aaa;border-bottom:1px solid #444;">Scene Tree</div>
18
+ <div id="sceneTree" style="padding:10px;"></div>
19
+ </div>
20
+ <div id="viewport" style="flex:1;background:#333;position:relative;overflow:hidden;"></div>
21
+ <div id="rightPanel" style="width:300px;background:#1e1e1e;border-left:1px solid #444;overflow:auto;">
22
+ <div style="padding:10px;font-weight:bold;color:#aaa;border-bottom:1px solid #444;">Properties</div>
23
+ <div id="propertyPanel" style="padding:10px;"></div>
24
+ </div>
25
+ </div>
26
+ </div>
27
+ `;
28
+
29
+ static style = css`
30
+ :host {
31
+ display: block;
32
+ width: 100%;
33
+ height: 100%;
34
+ background: #1a1a1a;
35
+ color: #aaa;
36
+ }
37
+
38
+ button:hover {
39
+ background: #555 !important;
40
+ }
41
+
42
+ button:active {
43
+ background: #666 !important;
44
+ }
45
+
46
+ input, select {
47
+ background: #333;
48
+ color: #aaa;
49
+ border: 1px solid #444;
50
+ padding: 5px;
51
+ border-radius: 3px;
52
+ }
53
+
54
+ .property-group {
55
+ margin-bottom: 15px;
56
+ padding: 10px;
57
+ background: #242424;
58
+ border-radius: 3px;
59
+ border: 1px solid #333;
60
+ }
61
+
62
+ .property-label {
63
+ font-size: 12px;
64
+ color: #888;
65
+ margin-bottom: 5px;
66
+ }
67
+
68
+ .property-input {
69
+ width: 100%;
70
+ box-sizing: border-box;
71
+ }
72
+ `;
73
+
74
+ constructor() {
75
+ super();
76
+ this.scene = null;
77
+ this.camera = null;
78
+ this.renderer = null;
79
+ this.controls = null;
80
+ this.raycaster = null;
81
+ this.mouse = null;
82
+ this.THREE = null;
83
+ this.gltfLoader = null;
84
+ this.loadedModel = null;
85
+ this.selectedObject = null;
86
+ this.sceneData = null;
87
+ this.sceneStore = null;
88
+ }
89
+
90
+ async connectedCallback() {
91
+ await this.init();
92
+ }
93
+
94
+ async init() {
95
+ try {
96
+ const statusEl = document.createElement('div');
97
+ statusEl.textContent = '⏳ Loading Three.js...';
98
+ statusEl.style.cssText = 'position:absolute;top:10px;left:10px;background:rgba(0,0,0,0.7);color:#0f0;padding:10px;font-family:monospace;z-index:100;';
99
+ this._getDomElement('viewport').appendChild(statusEl);
100
+
101
+ // Load Three.js
102
+ const THREE = await import('/mywebui.0.widgets/node_modules/three/build/three.module.js');
103
+ const { OrbitControls } = await import('/mywebui.0.widgets/node_modules/three/examples/jsm/controls/OrbitControls.js');
104
+ const { GLTFLoader } = await import('/mywebui.0.widgets/node_modules/three/examples/jsm/loaders/GLTFLoader.js');
105
+
106
+ this.THREE = THREE;
107
+ this.gltfLoader = new GLTFLoader();
108
+
109
+ statusEl.textContent = '⏳ Initializing scene...';
110
+
111
+ // Initialize Three.js
112
+ this.initThreeJS(THREE, OrbitControls);
113
+
114
+ // Load scene data
115
+ const sceneName = this.getAttribute('scene-name') || 'new-scene';
116
+ await this.loadScene(sceneName);
117
+
118
+ statusEl.textContent = '✅ 3D Editor Ready';
119
+ setTimeout(() => statusEl.remove(), 2000);
120
+
121
+ this.setupEventListeners();
122
+ } catch (error) {
123
+ console.error('❌ 3D Editor init failed:', error);
124
+ }
125
+ }
126
+
127
+ initThreeJS(THREE, OrbitControls) {
128
+ const viewport = this._getDomElement('viewport');
129
+ const width = viewport.clientWidth;
130
+ const height = viewport.clientHeight;
131
+
132
+ // Scene
133
+ this.scene = new THREE.Scene();
134
+ this.scene.background = new THREE.Color(0x333333);
135
+
136
+ // Camera
137
+ this.camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
138
+ this.camera.position.set(10, 10, 10);
139
+
140
+ // Renderer
141
+ this.renderer = new THREE.WebGLRenderer({ antialias: true });
142
+ this.renderer.setSize(width, height);
143
+ this.renderer.shadowMap.enabled = true;
144
+ viewport.appendChild(this.renderer.domElement);
145
+
146
+ // Lighting
147
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
148
+ this.scene.add(ambientLight);
149
+
150
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
151
+ directionalLight.position.set(10, 10, 10);
152
+ directionalLight.castShadow = true;
153
+ directionalLight.shadow.mapSize.width = 2048;
154
+ directionalLight.shadow.mapSize.height = 2048;
155
+ this.scene.add(directionalLight);
156
+
157
+ // Grid
158
+ const gridHelper = new THREE.GridHelper(20, 20, 0x888888, 0x444444);
159
+ gridHelper.position.y = -0.01;
160
+ gridHelper.userData.isGrid = true;
161
+ this.scene.add(gridHelper);
162
+
163
+ // Axes
164
+ const axesHelper = new THREE.AxesHelper(5);
165
+ axesHelper.userData.isAxes = true;
166
+ this.scene.add(axesHelper);
167
+
168
+ // Controls
169
+ this.controls = new OrbitControls(this.camera, this.renderer.domElement);
170
+ this.controls.autoRotate = false;
171
+ this.controls.enableZoom = true;
172
+ this.controls.enablePan = true;
173
+
174
+ // Raycasting
175
+ this.raycaster = new THREE.Raycaster();
176
+ this.mouse = new THREE.Vector2();
177
+
178
+ // Animation loop
179
+ const animate = () => {
180
+ requestAnimationFrame(animate);
181
+ this.controls.update();
182
+ this.renderer.render(this.scene, this.camera);
183
+ };
184
+ animate();
185
+
186
+ // Mouse click
187
+ this.renderer.domElement.addEventListener('click', (e) => this.onMouseClick(e));
188
+
189
+ // Resize
190
+ window.addEventListener('resize', () => this.onWindowResize());
191
+ }
192
+
193
+ async loadScene(sceneName) {
194
+ try {
195
+ this.sceneData = await iobrokerHandler.get3DScreen(sceneName);
196
+ if (!this.sceneData) {
197
+ console.warn(`Scene not found: ${sceneName}`);
198
+ return;
199
+ }
200
+
201
+ console.log('✅ Scene loaded:', this.sceneData);
202
+
203
+ // Update viewport
204
+ this.updateSceneView();
205
+ } catch (error) {
206
+ console.error('Error loading scene:', error);
207
+ }
208
+ }
209
+
210
+ updateSceneView() {
211
+ if (!this.sceneData) return;
212
+
213
+ // Add assets to scene
214
+ for (const asset of this.sceneData.assets) {
215
+ if (asset.type === 'model' && asset.glbPath) {
216
+ this.loadAsset(asset);
217
+ }
218
+ }
219
+
220
+ // Update lights
221
+ for (const light of this.sceneData.lights) {
222
+ this.addLightToScene(light);
223
+ }
224
+
225
+ // Update camera
226
+ if (this.sceneData.camera) {
227
+ this.camera.position.set(
228
+ this.sceneData.camera.position.x,
229
+ this.sceneData.camera.position.y,
230
+ this.sceneData.camera.position.z
231
+ );
232
+ }
233
+
234
+ // Update scene tree
235
+ this.updateSceneTree();
236
+
237
+ // Update property panel
238
+ this.updatePropertyPanel();
239
+ }
240
+
241
+ loadAsset(asset) {
242
+ if (!asset.glbPath) return;
243
+
244
+ this.gltfLoader.load(
245
+ asset.glbPath,
246
+ (gltf) => {
247
+ const model = gltf.scene;
248
+ model.userData.assetId = asset.id;
249
+ model.userData.assetData = asset;
250
+ model.position.set(asset.position.x, asset.position.y, asset.position.z);
251
+ model.rotation.set(asset.rotation.x, asset.rotation.y, asset.rotation.z);
252
+ model.scale.set(asset.scale.x, asset.scale.y, asset.scale.z);
253
+ this.scene.add(model);
254
+ console.log('✅ Asset loaded:', asset.name);
255
+ },
256
+ undefined,
257
+ (error) => console.error('Error loading asset:', error)
258
+ );
259
+ }
260
+
261
+ addLightToScene(light) {
262
+ let threeLight;
263
+
264
+ switch (light.type) {
265
+ case 'ambient':
266
+ threeLight = new this.THREE.AmbientLight(light.color, light.intensity);
267
+ break;
268
+ case 'directional':
269
+ threeLight = new this.THREE.DirectionalLight(light.color, light.intensity);
270
+ if (light.position) {
271
+ threeLight.position.set(light.position.x, light.position.y, light.position.z);
272
+ }
273
+ if (light.castShadow) {
274
+ threeLight.castShadow = true;
275
+ }
276
+ break;
277
+ case 'point':
278
+ threeLight = new this.THREE.PointLight(light.color, light.intensity);
279
+ if (light.position) {
280
+ threeLight.position.set(light.position.x, light.position.y, light.position.z);
281
+ }
282
+ break;
283
+ case 'spot':
284
+ threeLight = new this.THREE.SpotLight(light.color, light.intensity);
285
+ if (light.position) {
286
+ threeLight.position.set(light.position.x, light.position.y, light.position.z);
287
+ }
288
+ break;
289
+ }
290
+
291
+ if (threeLight) {
292
+ threeLight.userData.lightId = light.id;
293
+ threeLight.userData.lightData = light;
294
+ this.scene.add(threeLight);
295
+ }
296
+ }
297
+
298
+ onMouseClick(event) {
299
+ if (!this.scene || !this.camera) return;
300
+
301
+ const rect = this.renderer.domElement.getBoundingClientRect();
302
+ this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
303
+ this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
304
+
305
+ this.raycaster.setFromCamera(this.mouse, this.camera);
306
+ const intersects = this.raycaster.intersectObjects(this.scene.children, true);
307
+
308
+ for (const intersection of intersects) {
309
+ const obj = intersection.object;
310
+ if (obj.userData.assetId) {
311
+ this.selectObject(obj);
312
+ break;
313
+ }
314
+ }
315
+ }
316
+
317
+ selectObject(obj) {
318
+ if (this.selectedObject === obj) return;
319
+
320
+ // Deselect previous
321
+ if (this.selectedObject) {
322
+ this.selectedObject.userData.selected = false;
323
+ }
324
+
325
+ // Select new
326
+ this.selectedObject = obj;
327
+ obj.userData.selected = true;
328
+
329
+ this.updatePropertyPanel();
330
+ }
331
+
332
+ updateSceneTree() {
333
+ const treeEl = this._getDomElement('sceneTree');
334
+ treeEl.innerHTML = '';
335
+
336
+ // Assets
337
+ if (this.sceneData.assets && this.sceneData.assets.length > 0) {
338
+ const assetsDiv = document.createElement('div');
339
+ assetsDiv.style.cssText = 'margin-bottom:10px;';
340
+
341
+ const assetsTitle = document.createElement('div');
342
+ assetsTitle.textContent = '📦 Assets';
343
+ assetsTitle.style.cssText = 'color:#0f0;margin-bottom:5px;font-weight:bold;';
344
+ assetsDiv.appendChild(assetsTitle);
345
+
346
+ for (const asset of this.sceneData.assets) {
347
+ const itemDiv = document.createElement('div');
348
+ itemDiv.textContent = asset.name;
349
+ itemDiv.style.cssText = 'padding:5px;cursor:pointer;color:#aaa;margin-left:10px;';
350
+ itemDiv.addEventListener('click', () => console.log('Select asset:', asset.id));
351
+ assetsDiv.appendChild(itemDiv);
352
+ }
353
+
354
+ treeEl.appendChild(assetsDiv);
355
+ }
356
+
357
+ // Lights
358
+ if (this.sceneData.lights && this.sceneData.lights.length > 0) {
359
+ const lightsDiv = document.createElement('div');
360
+ lightsDiv.style.cssText = 'margin-bottom:10px;';
361
+
362
+ const lightsTitle = document.createElement('div');
363
+ lightsTitle.textContent = '💡 Lights';
364
+ lightsTitle.style.cssText = 'color:#ff0;margin-bottom:5px;font-weight:bold;';
365
+ lightsDiv.appendChild(lightsTitle);
366
+
367
+ for (const light of this.sceneData.lights) {
368
+ const itemDiv = document.createElement('div');
369
+ itemDiv.textContent = light.name;
370
+ itemDiv.style.cssText = 'padding:5px;cursor:pointer;color:#aaa;margin-left:10px;';
371
+ lightsDiv.appendChild(itemDiv);
372
+ }
373
+
374
+ treeEl.appendChild(lightsDiv);
375
+ }
376
+ }
377
+
378
+ updatePropertyPanel() {
379
+ const panelEl = this._getDomElement('propertyPanel');
380
+ panelEl.innerHTML = '';
381
+
382
+ if (!this.selectedObject || !this.selectedObject.userData.assetData) {
383
+ panelEl.innerHTML = '<div style="color:#888;">No selection</div>';
384
+ return;
385
+ }
386
+
387
+ const asset = this.selectedObject.userData.assetData;
388
+
389
+ // Position
390
+ const posGroup = document.createElement('div');
391
+ posGroup.className = 'property-group';
392
+ posGroup.innerHTML = `
393
+ <div class="property-label">Position</div>
394
+ <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:5px;">
395
+ <input type="number" id="posX" value="${asset.position.x}" step="0.1" class="property-input" placeholder="X">
396
+ <input type="number" id="posY" value="${asset.position.y}" step="0.1" class="property-input" placeholder="Y">
397
+ <input type="number" id="posZ" value="${asset.position.z}" step="0.1" class="property-input" placeholder="Z">
398
+ </div>
399
+ `;
400
+ panelEl.appendChild(posGroup);
401
+
402
+ // Rotation
403
+ const rotGroup = document.createElement('div');
404
+ rotGroup.className = 'property-group';
405
+ rotGroup.innerHTML = `
406
+ <div class="property-label">Rotation</div>
407
+ <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:5px;">
408
+ <input type="number" id="rotX" value="${asset.rotation.x}" step="0.1" class="property-input" placeholder="X">
409
+ <input type="number" id="rotY" value="${asset.rotation.y}" step="0.1" class="property-input" placeholder="Y">
410
+ <input type="number" id="rotZ" value="${asset.rotation.z}" step="0.1" class="property-input" placeholder="Z">
411
+ </div>
412
+ `;
413
+ panelEl.appendChild(rotGroup);
414
+
415
+ // Scale
416
+ const scaleGroup = document.createElement('div');
417
+ scaleGroup.className = 'property-group';
418
+ scaleGroup.innerHTML = `
419
+ <div class="property-label">Scale</div>
420
+ <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:5px;">
421
+ <input type="number" id="scaleX" value="${asset.scale.x}" step="0.1" class="property-input" placeholder="X">
422
+ <input type="number" id="scaleY" value="${asset.scale.y}" step="0.1" class="property-input" placeholder="Y">
423
+ <input type="number" id="scaleZ" value="${asset.scale.z}" step="0.1" class="property-input" placeholder="Z">
424
+ </div>
425
+ `;
426
+ panelEl.appendChild(scaleGroup);
427
+ }
428
+
429
+ setupEventListeners() {
430
+ this._getDomElement('saveBtn').addEventListener('click', () => this.saveScene());
431
+ this._getDomElement('undoBtn').addEventListener('click', () => console.log('Undo'));
432
+ this._getDomElement('redoBtn').addEventListener('click', () => console.log('Redo'));
433
+ this._getDomElement('gridToggle').addEventListener('click', () => this.toggleGrid());
434
+ this._getDomElement('axesToggle').addEventListener('click', () => this.toggleAxes());
435
+ }
436
+
437
+ async saveScene() {
438
+ try {
439
+ const success = await iobrokerHandler.saveObject('3dscreen', this.sceneData.name, this.sceneData);
440
+ if (success) {
441
+ console.log('✅ Scene saved:', this.sceneData.name);
442
+ }
443
+ } catch (error) {
444
+ console.error('Error saving scene:', error);
445
+ }
446
+ }
447
+
448
+ toggleGrid() {
449
+ const grid = this.scene.children.find(c => c.userData.isGrid);
450
+ if (grid) {
451
+ grid.visible = !grid.visible;
452
+ }
453
+ }
454
+
455
+ toggleAxes() {
456
+ const axes = this.scene.children.find(c => c.userData.isAxes);
457
+ if (axes) {
458
+ axes.visible = !axes.visible;
459
+ }
460
+ }
461
+
462
+ onWindowResize() {
463
+ const viewport = this._getDomElement('viewport');
464
+ const width = viewport.clientWidth;
465
+ const height = viewport.clientHeight;
466
+
467
+ this.camera.aspect = width / height;
468
+ this.camera.updateProjectionMatrix();
469
+ this.renderer.setSize(width, height);
470
+ }
471
+
472
+ disconnectedCallback() {
473
+ if (this.renderer) {
474
+ this.renderer.dispose();
475
+ this.renderer.domElement.remove();
476
+ }
477
+ }
478
+ }
479
+
480
+ customElements.define('iobroker-webui-3dscreen-editor', IobrokerWebui3DScreenEditor);
@@ -0,0 +1,251 @@
1
+ import { BaseCustomWebComponentConstructorAppend, css, html } from "@gokturk413/base-custom-webcomponent";
2
+ import { iobrokerHandler } from "../common/IobrokerHandler.js";
3
+
4
+ export class IobrokerWebui3DScreenViewer extends BaseCustomWebComponentConstructorAppend {
5
+ static template = html`
6
+ <div id="container" style="width:100%;height:100%;position:relative;overflow:hidden;background:#333;">
7
+ <div id="viewport" style="width:100%;height:100%;"></div>
8
+ <div id="status" style="position:absolute;bottom:10px;left:10px;background:rgba(0,0,0,0.7);color:#0f0;padding:10px;font-family:monospace;font-size:12px;z-index:10;border-radius:3px;">Ready</div>
9
+ </div>
10
+ `;
11
+
12
+ static style = css`
13
+ :host {
14
+ display: block;
15
+ width: 100%;
16
+ height: 100%;
17
+ background: #333;
18
+ }
19
+ `;
20
+
21
+ constructor() {
22
+ super();
23
+ this.scene = null;
24
+ this.camera = null;
25
+ this.renderer = null;
26
+ this.controls = null;
27
+ this.THREE = null;
28
+ this.gltfLoader = null;
29
+ this.sceneData = null;
30
+ this.sceneNameAttr = null;
31
+ }
32
+
33
+ async connectedCallback() {
34
+ this.sceneNameAttr = this.getAttribute('scene-name');
35
+ if (!this.sceneNameAttr) {
36
+ this._getDomElement('status').textContent = '❌ No scene name';
37
+ return;
38
+ }
39
+
40
+ await this.init();
41
+ }
42
+
43
+ async init() {
44
+ try {
45
+ const statusEl = this._getDomElement('status');
46
+ statusEl.textContent = '⏳ Loading...';
47
+
48
+ // Load Three.js
49
+ const THREE = await import('/mywebui.0.widgets/node_modules/three/build/three.module.js');
50
+ const { OrbitControls } = await import('/mywebui.0.widgets/node_modules/three/examples/jsm/controls/OrbitControls.js');
51
+ const { GLTFLoader } = await import('/mywebui.0.widgets/node_modules/three/examples/jsm/loaders/GLTFLoader.js');
52
+
53
+ this.THREE = THREE;
54
+ this.gltfLoader = new GLTFLoader();
55
+
56
+ // Initialize Three.js
57
+ this.initThreeJS(THREE, OrbitControls);
58
+
59
+ // Load scene
60
+ await this.loadScene(this.sceneNameAttr);
61
+
62
+ statusEl.textContent = '✅ Ready';
63
+ } catch (error) {
64
+ console.error('❌ 3D Viewer init failed:', error);
65
+ this._getDomElement('status').textContent = '❌ ' + error.message;
66
+ }
67
+ }
68
+
69
+ initThreeJS(THREE, OrbitControls) {
70
+ const viewport = this._getDomElement('viewport');
71
+ const width = viewport.clientWidth;
72
+ const height = viewport.clientHeight;
73
+
74
+ // Scene
75
+ this.scene = new THREE.Scene();
76
+ this.scene.background = new THREE.Color(0x333333);
77
+
78
+ // Camera
79
+ this.camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
80
+ this.camera.position.set(10, 10, 10);
81
+
82
+ // Renderer
83
+ this.renderer = new THREE.WebGLRenderer({ antialias: true });
84
+ this.renderer.setSize(width, height);
85
+ this.renderer.shadowMap.enabled = true;
86
+ viewport.appendChild(this.renderer.domElement);
87
+
88
+ // Lighting
89
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
90
+ this.scene.add(ambientLight);
91
+
92
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
93
+ directionalLight.position.set(10, 10, 10);
94
+ directionalLight.castShadow = true;
95
+ directionalLight.shadow.mapSize.width = 2048;
96
+ directionalLight.shadow.mapSize.height = 2048;
97
+ this.scene.add(directionalLight);
98
+
99
+ // Grid
100
+ const gridHelper = new THREE.GridHelper(20, 20, 0x888888, 0x444444);
101
+ gridHelper.position.y = -0.01;
102
+ this.scene.add(gridHelper);
103
+
104
+ // Axes
105
+ const axesHelper = new THREE.AxesHelper(5);
106
+ this.scene.add(axesHelper);
107
+
108
+ // Controls
109
+ this.controls = new OrbitControls(this.camera, this.renderer.domElement);
110
+ this.controls.autoRotate = false;
111
+ this.controls.enableZoom = true;
112
+ this.controls.enablePan = true;
113
+
114
+ // Animation loop
115
+ let animating = true;
116
+ const animate = () => {
117
+ if (animating) {
118
+ requestAnimationFrame(animate);
119
+ this.controls.update();
120
+ this.renderer.render(this.scene, this.camera);
121
+ }
122
+ };
123
+ animate();
124
+
125
+ // Resize
126
+ window.addEventListener('resize', () => this.onWindowResize());
127
+
128
+ this._stopAnimation = () => { animating = false; };
129
+ }
130
+
131
+ async loadScene(sceneName) {
132
+ try {
133
+ this.sceneData = await iobrokerHandler.get3DScreen(sceneName);
134
+ if (!this.sceneData) {
135
+ throw new Error(`Scene not found: ${sceneName}`);
136
+ }
137
+
138
+ // Load assets
139
+ if (this.sceneData.assets) {
140
+ for (const asset of this.sceneData.assets) {
141
+ if (asset.type === 'model' && asset.glbPath) {
142
+ await this.loadAsset(asset);
143
+ }
144
+ }
145
+ }
146
+
147
+ // Add lights
148
+ if (this.sceneData.lights) {
149
+ for (const light of this.sceneData.lights) {
150
+ this.addLightToScene(light);
151
+ }
152
+ }
153
+
154
+ // Set camera
155
+ if (this.sceneData.camera) {
156
+ this.camera.position.set(
157
+ this.sceneData.camera.position.x,
158
+ this.sceneData.camera.position.y,
159
+ this.sceneData.camera.position.z
160
+ );
161
+ this.camera.lookAt(
162
+ this.sceneData.camera.target.x,
163
+ this.sceneData.camera.target.y,
164
+ this.sceneData.camera.target.z
165
+ );
166
+ }
167
+
168
+ console.log('✅ Scene loaded:', sceneName);
169
+ } catch (error) {
170
+ console.error('Error loading scene:', error);
171
+ throw error;
172
+ }
173
+ }
174
+
175
+ loadAsset(asset) {
176
+ return new Promise((resolve, reject) => {
177
+ this.gltfLoader.load(
178
+ asset.glbPath,
179
+ (gltf) => {
180
+ const model = gltf.scene;
181
+ model.position.set(asset.position.x, asset.position.y, asset.position.z);
182
+ model.rotation.set(asset.rotation.x, asset.rotation.y, asset.rotation.z);
183
+ model.scale.set(asset.scale.x, asset.scale.y, asset.scale.z);
184
+ this.scene.add(model);
185
+ console.log('✅ Asset loaded:', asset.name);
186
+ resolve();
187
+ },
188
+ undefined,
189
+ (error) => {
190
+ console.error('Error loading asset:', error);
191
+ reject(error);
192
+ }
193
+ );
194
+ });
195
+ }
196
+
197
+ addLightToScene(light) {
198
+ let threeLight;
199
+
200
+ switch (light.type) {
201
+ case 'ambient':
202
+ threeLight = new this.THREE.AmbientLight(light.color, light.intensity);
203
+ break;
204
+ case 'directional':
205
+ threeLight = new this.THREE.DirectionalLight(light.color, light.intensity);
206
+ if (light.position) {
207
+ threeLight.position.set(light.position.x, light.position.y, light.position.z);
208
+ }
209
+ if (light.castShadow) {
210
+ threeLight.castShadow = true;
211
+ }
212
+ break;
213
+ case 'point':
214
+ threeLight = new this.THREE.PointLight(light.color, light.intensity);
215
+ if (light.position) {
216
+ threeLight.position.set(light.position.x, light.position.y, light.position.z);
217
+ }
218
+ break;
219
+ case 'spot':
220
+ threeLight = new this.THREE.SpotLight(light.color, light.intensity);
221
+ if (light.position) {
222
+ threeLight.position.set(light.position.x, light.position.y, light.position.z);
223
+ }
224
+ break;
225
+ }
226
+
227
+ if (threeLight) {
228
+ this.scene.add(threeLight);
229
+ }
230
+ }
231
+
232
+ onWindowResize() {
233
+ const viewport = this._getDomElement('viewport');
234
+ const width = viewport.clientWidth;
235
+ const height = viewport.clientHeight;
236
+
237
+ this.camera.aspect = width / height;
238
+ this.camera.updateProjectionMatrix();
239
+ this.renderer.setSize(width, height);
240
+ }
241
+
242
+ disconnectedCallback() {
243
+ if (this._stopAnimation) this._stopAnimation();
244
+ if (this.renderer) {
245
+ this.renderer.dispose();
246
+ this.renderer.domElement.remove();
247
+ }
248
+ }
249
+ }
250
+
251
+ customElements.define('iobroker-webui-3dscreen-viewer', IobrokerWebui3DScreenViewer);
@@ -99,6 +99,7 @@ export class IobrokerWebuiSolutionExplorer extends BaseCustomWebComponentConstru
99
99
  async createTreeNodes() {
100
100
  const result = await Promise.allSettled([
101
101
  this._createFolderNode('screen'),
102
+ this._createFolderNode('3dscreen'),
102
103
  this._createControlsNode(),
103
104
  this._createGlobalNode(),
104
105
  this._createNpmsNode(),
@@ -199,6 +200,9 @@ export class IobrokerWebuiSolutionExplorer extends BaseCustomWebComponentConstru
199
200
  case 'screen':
200
201
  name = "Screens";
201
202
  break;
203
+ case '3dscreen':
204
+ name = "3D Screens";
205
+ break;
202
206
  //case '':
203
207
  // name='Additional Files';
204
208
  // break;
@@ -301,6 +305,11 @@ export class IobrokerWebuiSolutionExplorer extends BaseCustomWebComponentConstru
301
305
  else if (type == 'control') {
302
306
  window.appShell.openScreenEditor(nm, type, s.html, s.style, s.script, s.settings, s.properties);
303
307
  }
308
+ else if (type == '3dscreen') {
309
+ const editor = document.createElement('iobroker-webui-3dscreen-editor');
310
+ editor.setAttribute('scene-name', nm);
311
+ window.appShell.openDialog(editor, { x: 50, y: 50, width: 1200, height: 800 });
312
+ }
304
313
  });
305
314
  },
306
315
  data: { type, name: (dir ?? '') + '/' + x }