@shopware-ag/dive 1.15.4 → 1.16.0

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": "@shopware-ag/dive",
3
- "version": "1.15.4",
3
+ "version": "1.16.0",
4
4
  "description": "Shopware Spatial Framework",
5
5
  "type": "module",
6
6
  "main": "./build/dive.cjs",
@@ -30,12 +30,14 @@
30
30
  ],
31
31
  "dependencies": {
32
32
  "@tweenjs/tween.js": "^23.1.1",
33
+ "lodash": "^4.17.21",
33
34
  "three": "^0.163.0",
34
35
  "three-spritetext": "^1.8.2"
35
36
  },
36
37
  "devDependencies": {
37
38
  "@eslint/js": "^9.1.1",
38
39
  "@types/jest": "^29.5.12",
40
+ "@types/lodash": "^4.17.12",
39
41
  "@types/node": "^20.12.7",
40
42
  "@types/three": "^0.163.0",
41
43
  "eslint": "^9.1.1",
@@ -1,6 +1,7 @@
1
1
  import { Actions } from "./actions/index.ts";
2
2
  import { generateUUID } from 'three/src/math/MathUtils';
3
3
  import { isSelectTool } from "../toolbox/select/SelectTool.ts";
4
+ import { merge } from 'lodash';
4
5
 
5
6
  // type imports
6
7
  import { type Color, type MeshStandardMaterial } from "three";
@@ -12,6 +13,8 @@ import { type DIVEModel } from "../model/Model.ts";
12
13
  import { type DIVEMediaCreator } from "../mediacreator/MediaCreator.ts";
13
14
  import { type DIVERenderer } from "../renderer/Renderer.ts";
14
15
  import { type DIVESelectable } from "../interface/Selectable.ts";
16
+ import { type DIVEIO } from "../io/IO.ts";
17
+
15
18
 
16
19
  type EventListener<Action extends keyof Actions> = (payload: Actions[Action]['PAYLOAD']) => void;
17
20
 
@@ -65,6 +68,15 @@ export class DIVECommunication {
65
68
  return this._mediaGenerator;
66
69
  }
67
70
 
71
+ private _io: DIVEIO | null;
72
+ private get io(): DIVEIO {
73
+ if (!this._io) {
74
+ const DIVEIO = require('../io/IO.ts').DIVEIO as typeof import('../io/IO.ts').DIVEIO;
75
+ this._io = new DIVEIO(this.scene);
76
+ }
77
+ return this._io;
78
+ }
79
+
68
80
  private registered: Map<string, COMEntity> = new Map();
69
81
 
70
82
  // private listeners: { [key: string]: EventListener[] } = {};
@@ -77,6 +89,7 @@ export class DIVECommunication {
77
89
  this.controller = controls;
78
90
  this.toolbox = toolbox;
79
91
  this._mediaGenerator = null;
92
+ this._io = null;
80
93
 
81
94
  DIVECommunication.__instances.push(this);
82
95
  }
@@ -192,6 +205,10 @@ export class DIVECommunication {
192
205
  returnValue = this.setParent(payload as Actions['SET_PARENT']['PAYLOAD']);
193
206
  break;
194
207
  }
208
+ case 'EXPORT_SCENE': {
209
+ returnValue = this.exportScene(payload as Actions['EXPORT_SCENE']['PAYLOAD']);
210
+ break;
211
+ }
195
212
  }
196
213
 
197
214
  this.dispatch(action, payload);
@@ -266,7 +283,7 @@ export class DIVECommunication {
266
283
  private addObject(payload: Actions['ADD_OBJECT']['PAYLOAD']): Actions['ADD_OBJECT']['RETURN'] {
267
284
  if (this.registered.get(payload.id)) return false;
268
285
 
269
- if (payload.parent === undefined) payload.parent = null;
286
+ if (payload.parentId === undefined) payload.parentId = null;
270
287
 
271
288
  this.registered.set(payload.id, payload);
272
289
 
@@ -279,7 +296,7 @@ export class DIVECommunication {
279
296
  const objectToUpdate = this.registered.get(payload.id);
280
297
  if (!objectToUpdate) return false;
281
298
 
282
- this.registered.set(payload.id, { ...objectToUpdate, ...payload });
299
+ this.registered.set(payload.id, merge(objectToUpdate, payload));
283
300
 
284
301
  const updatedObject = this.registered.get(payload.id)!;
285
302
  this.scene.UpdateSceneObject({ ...payload, id: updatedObject.id, entityType: updatedObject.entityType });
@@ -522,6 +539,10 @@ export class DIVECommunication {
522
539
  parentObject.attach(sceneObject);
523
540
  return true;
524
541
  }
542
+
543
+ private exportScene(payload: Actions['EXPORT_SCENE']['PAYLOAD']): Actions['EXPORT_SCENE']['RETURN'] {
544
+ return this.io.Export(payload.type);
545
+ }
525
546
  }
526
547
 
527
548
  export type { Actions } from './actions/index.ts';
@@ -44,6 +44,16 @@ jest.mock('../../mediacreator/MediaCreator', () => {
44
44
  }
45
45
  });
46
46
 
47
+ jest.mock('../../io/IO', () => {
48
+ return {
49
+ DIVEIO: jest.fn(function () {
50
+ this.Import = jest.fn();
51
+ this.Export = jest.fn();
52
+ return this;
53
+ }),
54
+ }
55
+ });
56
+
47
57
  jest.mock('../../toolbox/select/SelectTool', () => {
48
58
  return {
49
59
  isSelectTool: jest.fn().mockReturnValue(true),
@@ -521,7 +531,7 @@ describe('dive/communication/DIVECommunication', () => {
521
531
  id: "group1",
522
532
  position: { x: 0, y: 0, z: 0 },
523
533
  rotation: { x: 0, y: 0, z: 0 },
524
- parent: null,
534
+ parentId: null,
525
535
  } as COMGroup);
526
536
 
527
537
  const success = testCom.PerformAction('GET_ALL_SCENE_DATA', {});
@@ -532,7 +542,7 @@ describe('dive/communication/DIVECommunication', () => {
532
542
  id: "pov",
533
543
  position: { x: 0, y: 0, z: 0 },
534
544
  target: { x: 0, y: 0, z: 0 },
535
- parent: null,
545
+ parentId: null,
536
546
  }],
537
547
  floorColor: "#ffffff",
538
548
  floorEnabled: true,
@@ -542,7 +552,7 @@ describe('dive/communication/DIVECommunication', () => {
542
552
  type: "ambient",
543
553
  intensity: 0.5,
544
554
  color: 'white',
545
- parent: null,
555
+ parentId: null,
546
556
  }],
547
557
  mediaItem: null,
548
558
  name: undefined,
@@ -552,7 +562,7 @@ describe('dive/communication/DIVECommunication', () => {
552
562
  position: { x: 0, y: 0, z: 0 },
553
563
  rotation: { x: 0, y: 0, z: 0 },
554
564
  scale: { x: 0.01, y: 0.01, z: 0.01 },
555
- parent: null,
565
+ parentId: null,
556
566
  uri: "https://threejs.org/examples/models/gltf/LittlestTokyo.glb",
557
567
  }],
558
568
  primitives: [],
@@ -566,7 +576,7 @@ describe('dive/communication/DIVECommunication', () => {
566
576
  id: "group1",
567
577
  position: { x: 0, y: 0, z: 0 },
568
578
  rotation: { x: 0, y: 0, z: 0 },
569
- parent: null,
579
+ parentId: null,
570
580
  }],
571
581
  });
572
582
  });
@@ -592,7 +602,7 @@ describe('dive/communication/DIVECommunication', () => {
592
602
  expect(Array.from(successWithoutIds.values())).toStrictEqual([]);
593
603
 
594
604
  const successWithIds = testCom.PerformAction('GET_OBJECTS', { ids: ['test1'] });
595
- expect(Array.from(successWithIds.values())).toStrictEqual([{ entityType: "pov", id: "test1", position: { x: 0, y: 0, z: 0 }, target: { x: 0, y: 0, z: 0 }, parent: null }]);
605
+ expect(Array.from(successWithIds.values())).toStrictEqual([{ entityType: "pov", id: "test1", position: { x: 0, y: 0, z: 0 }, target: { x: 0, y: 0, z: 0 }, parentId: null }]);
596
606
  });
597
607
 
598
608
  it('should perform action SELECT_OBJECT', () => {
@@ -846,4 +856,15 @@ describe('dive/communication/DIVECommunication', () => {
846
856
  });
847
857
  expect(attachToValidParent).toBe(true);
848
858
  });
859
+
860
+ it('should perform action EXPORT_SCENE', async () => {
861
+ const url = 'https://example.com';
862
+
863
+ jest.spyOn(testCom['io'], 'Export').mockResolvedValueOnce(url);
864
+
865
+ const result = await testCom.PerformAction('EXPORT_SCENE', {
866
+ type: 'glb',
867
+ });
868
+ expect(result).toBe(url);
869
+ });
849
870
  });
@@ -23,6 +23,7 @@ import SET_GIZMO_VISIBILITY from "./toolbox/transform/setgizmovisible.js";
23
23
  import COMPUTE_ENCOMPASSING_VIEW from "./camera/computeencompassingview.ts";
24
24
  import USE_TOOL from "./toolbox/usetool.ts";
25
25
  import SET_PARENT from "./object/setparent.ts";
26
+ import EXPORT_SCENE from "./scene/exportscene.ts";
26
27
 
27
28
  export type Actions = {
28
29
  GET_ALL_SCENE_DATA: GET_ALL_SCENE_DATA,
@@ -50,4 +51,5 @@ export type Actions = {
50
51
  UPDATE_SCENE: UPDATE_SCENE,
51
52
  GENERATE_MEDIA: GENERATE_MEDIA,
52
53
  SET_PARENT: SET_PARENT,
54
+ EXPORT_SCENE: EXPORT_SCENE,
53
55
  };
@@ -0,0 +1,6 @@
1
+ import { type DIVESceneFileType } from "../../../types";
2
+
3
+ export default interface EXPORT_SCENE {
4
+ 'PAYLOAD': { type: keyof DIVESceneFileType },
5
+ 'RETURN': Promise<string | null>,
6
+ };
@@ -5,5 +5,5 @@ export type COMBaseEntity = {
5
5
  name: string;
6
6
  entityType: COMEntityType;
7
7
  visible: boolean;
8
- parent?: Partial<COMBaseEntity> & { id: string } | null;
8
+ parentId?: string | null;
9
9
  }
package/src/dive.ts CHANGED
@@ -10,6 +10,7 @@ import { getObjectDelta } from "./helper/getObjectDelta/getObjectDelta.ts";
10
10
 
11
11
  import { generateUUID } from "three/src/math/MathUtils";
12
12
  import { DIVEInfo } from "./info/Info.ts";
13
+ import pkgjson from '../package.json';
13
14
 
14
15
  export type DIVESettings = {
15
16
  autoResize: boolean;
@@ -227,7 +228,7 @@ export default class DIVE {
227
228
  },
228
229
  }
229
230
 
230
- console.log('DIVE initialized');
231
+ console.log(`DIVE ${pkgjson.version} initialized`);
231
232
  }
232
233
 
233
234
  public Dispose(): void {
package/src/io/IO.ts ADDED
@@ -0,0 +1,107 @@
1
+ import { DIVEGLTFIO } from "./gltf/GLTFIO";
2
+
3
+ import { type DIVESceneFileType } from "../types";
4
+ import { type DIVEScene } from "../scene/Scene";
5
+
6
+ export class DIVEIO {
7
+
8
+ private _scene: DIVEScene;
9
+
10
+ private _gltfIO: DIVEGLTFIO;
11
+
12
+ constructor(scene: DIVEScene) {
13
+
14
+ this._scene = scene;
15
+
16
+ this._gltfIO = new DIVEGLTFIO();
17
+
18
+ }
19
+
20
+ public Import<FileType extends keyof DIVESceneFileType>(type: FileType, url: string): Promise<DIVESceneFileType[FileType] | null> {
21
+
22
+ return this._importFromURL(type, url)
23
+
24
+ .catch((error) => {
25
+
26
+ console.error(error);
27
+
28
+ return null;
29
+
30
+ });
31
+
32
+ }
33
+
34
+ public Export<FileType extends keyof DIVESceneFileType>(type: FileType): Promise<string | null> {
35
+
36
+ return this._exportToURL(type)
37
+
38
+ .catch((error) => {
39
+
40
+ console.error(error);
41
+
42
+ return null;
43
+
44
+ });
45
+
46
+ }
47
+
48
+ private _importFromURL<FileType extends keyof DIVESceneFileType>(type: FileType, url: string): Promise<DIVESceneFileType[FileType]> {
49
+
50
+ switch (type) {
51
+
52
+ case 'glb': {
53
+
54
+ return this._gltfIO.Import(url);
55
+
56
+ }
57
+
58
+ default: {
59
+
60
+ return Promise.reject('Unsupported file type: ' + type);
61
+
62
+ }
63
+
64
+ }
65
+ }
66
+
67
+ private _exportToURL<FileType extends keyof DIVESceneFileType>(type: FileType): Promise<string> {
68
+
69
+ switch (type) {
70
+
71
+ case 'glb': {
72
+
73
+ return new Promise((resolve, reject) => {
74
+
75
+ this._gltfIO.Export(this._scene, true, true)
76
+
77
+ .then((data) => {
78
+
79
+ resolve(this._createBlobURL(data))
80
+
81
+ })
82
+
83
+ .catch((error) => {
84
+
85
+ reject(error);
86
+
87
+ });
88
+ });
89
+
90
+ }
91
+
92
+ default: {
93
+
94
+ return Promise.reject('Unsupported file type: ' + type);
95
+
96
+ }
97
+
98
+ }
99
+ }
100
+
101
+ private _createBlobURL(data: ArrayBuffer): string {
102
+
103
+ return URL.createObjectURL(new Blob([data]));
104
+
105
+ }
106
+
107
+ }
@@ -0,0 +1,145 @@
1
+ import { DIVEIO } from '../IO';
2
+ import { DIVEScene } from '../../scene/Scene';
3
+
4
+ import { type GLTF } from 'three/examples/jsm/loaders/GLTFLoader';
5
+
6
+ jest.mock('../gltf/GLTFIO', () => {
7
+
8
+ return {
9
+
10
+ DIVEGLTFIO: jest.fn(function () {
11
+
12
+ this.Import = jest.fn();
13
+
14
+ this.Export = jest.fn();
15
+
16
+ return this;
17
+
18
+ })
19
+
20
+ };
21
+
22
+ });
23
+
24
+ jest.mock('../../scene/Scene.ts', () => {
25
+
26
+ return {
27
+
28
+ DIVEScene: jest.fn(function () {
29
+
30
+ this.add = jest.fn();
31
+
32
+ this.isObject3D = true;
33
+
34
+ this.parent = null;
35
+
36
+ this.dispatchEvent = jest.fn();
37
+
38
+ this.position = {
39
+
40
+ set: jest.fn(),
41
+
42
+ }
43
+
44
+ this.SetIntensity = jest.fn();
45
+
46
+ this.SetEnabled = jest.fn();
47
+
48
+ this.SetColor = jest.fn();
49
+
50
+ this.userData = {
51
+
52
+ id: undefined,
53
+
54
+ }
55
+
56
+ this.removeFromParent = jest.fn();
57
+
58
+ return this;
59
+ })
60
+
61
+ };
62
+
63
+ });
64
+
65
+ let testIO: DIVEIO;
66
+
67
+ describe('dive/io/DIVEIO', () => {
68
+ beforeEach(() => {
69
+
70
+ testIO = new DIVEIO(new DIVEScene());
71
+
72
+ });
73
+
74
+ afterEach(() => {
75
+
76
+ jest.clearAllMocks();
77
+
78
+ });
79
+
80
+ it('should instantiate', () => {
81
+
82
+ expect(testIO).toBeDefined();
83
+
84
+ });
85
+
86
+ it('should import from URL', async () => {
87
+
88
+ const mockGLTF = {} as GLTF;
89
+
90
+ jest.spyOn(testIO['_gltfIO'], 'Import').mockResolvedValueOnce(mockGLTF);
91
+
92
+ const result = await testIO.Import('glb', 'test.glb');
93
+
94
+ expect(result).toStrictEqual(mockGLTF);
95
+
96
+ });
97
+
98
+ it('should reject when importing with unsupported file type', async () => {
99
+
100
+ jest.spyOn(console, 'error').mockImplementationOnce(() => { });
101
+
102
+ const result = await testIO.Import('unsupported file type' as "glb", 'test.glb');
103
+
104
+ expect(result).toStrictEqual(null);
105
+
106
+ });
107
+
108
+ it('should export to URL', async () => {
109
+
110
+ const mockObject = {};
111
+
112
+ const mockURL = 'blob://mockURL';
113
+
114
+ jest.spyOn(testIO['_gltfIO'], 'Export').mockResolvedValueOnce(mockObject);
115
+
116
+ URL.createObjectURL = jest.fn().mockReturnValueOnce(mockURL);
117
+
118
+ const result = await testIO.Export('glb');
119
+
120
+ expect(result).toBeDefined();
121
+
122
+ });
123
+
124
+ it('should handle rejection from gltf io', async () => {
125
+
126
+ jest.spyOn(console, 'error').mockImplementationOnce(() => { });
127
+
128
+ jest.spyOn(testIO['_gltfIO'], 'Export').mockRejectedValueOnce('Error');
129
+
130
+ const result = await testIO.Export('glb');
131
+
132
+ expect(result).toBeDefined();
133
+
134
+ });
135
+
136
+ it('should reject when exporting with unsupported file type', async () => {
137
+
138
+ jest.spyOn(console, 'error').mockImplementationOnce(() => { });
139
+
140
+ const result = await testIO.Export('unsupported file type' as "glb");
141
+
142
+ expect(result).toStrictEqual(null);
143
+
144
+ });
145
+ });
@@ -0,0 +1,53 @@
1
+ import { GLTFLoader, type GLTF } from "three/examples/jsm/loaders/GLTFLoader";
2
+ import { GLTFExporter } from "three/examples/jsm/exporters/GLTFExporter";
3
+ import { Object3D } from "three";
4
+
5
+ export class DIVEGLTFIO {
6
+
7
+ private _importer: GLTFLoader;
8
+
9
+ private _exporter: GLTFExporter;
10
+
11
+ constructor() {
12
+
13
+ this._importer = new GLTFLoader();
14
+
15
+ this._exporter = new GLTFExporter();
16
+
17
+ }
18
+
19
+ public Import(url: string, onProgress?: (progress: number) => void): Promise<GLTF> {
20
+
21
+ return this._importer.loadAsync(url, (progress) => {
22
+
23
+ if (!onProgress) return;
24
+
25
+ onProgress(progress.loaded / progress.total);
26
+
27
+ });
28
+
29
+ }
30
+
31
+ public Export(object: Object3D, binary: true, onlyVisible: boolean): Promise<ArrayBuffer>;
32
+
33
+ public Export(object: Object3D, binary: false, onlyVisible: boolean): Promise<{ [key: string]: unknown }>;
34
+
35
+ public Export(object: Object3D, binary: boolean, onlyVisible: boolean): Promise<ArrayBuffer | { [key: string]: unknown }> {
36
+
37
+ if (binary) {
38
+
39
+ // export as binary ArrayBuffer
40
+
41
+ return this._exporter.parseAsync(object, { binary, onlyVisible }) as unknown as Promise<ArrayBuffer>;
42
+
43
+ } else {
44
+
45
+ // export as JSON object
46
+
47
+ return this._exporter.parseAsync(object, { binary, onlyVisible }) as unknown as Promise<{ [key: string]: unknown; }>;
48
+
49
+ }
50
+
51
+ };
52
+
53
+ }