iobroker.mywebui 1.41.7 → 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/io-package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "mywebui",
4
- "version": "1.41.7",
4
+ "version": "1.42.0",
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.41.7",
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",
@@ -16,7 +16,11 @@
16
16
  "resolveJsonModule": true,
17
17
  "allowSyntheticDefaultImports": true,
18
18
  "removeComments": false,
19
- "baseUrl": "./src"
19
+ "baseUrl": "./",
20
+ "paths": {
21
+ "three": ["../node_modules/three/build/three.module.js"],
22
+ "three/*": ["../node_modules/three/*"]
23
+ }
20
24
  },
21
25
  "include": ["src/**/*"],
22
26
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
@@ -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
+ }