iobroker.mywebui 1.42.0 → 1.42.2
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 +1 -1
- package/www/dist/frontend/common/IobrokerHandler.js +20 -0
- package/www/dist/frontend/config/3d-scene-store.ts +304 -0
- package/www/dist/frontend/config/3d-scene-types.ts +277 -0
- package/www/dist/frontend/config/IobrokerWebui3DScreenEditor.js +582 -0
- package/www/dist/frontend/config/IobrokerWebui3DScreenViewer.js +251 -0
- package/www/dist/frontend/config/IobrokerWebuiSolutionExplorer.js +9 -0
package/package.json
CHANGED
|
@@ -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
|
+
}
|