@shopware-ag/dive 1.16.7 → 1.16.9

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/src/node/Node.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Box3, Object3D, type Vector3Like } from 'three';
1
+ import { Box3, Object3D, Vector3, type Vector3Like } from 'three';
2
2
  import { PRODUCT_LAYER_MASK } from '../constant/VisibilityLayerMask';
3
3
  import { DIVECommunication } from '../com/Communication';
4
4
 
@@ -14,6 +14,7 @@ export class DIVENode extends Object3D implements DIVESelectable, DIVEMovable {
14
14
 
15
15
  public gizmo: TransformControls | null = null;
16
16
 
17
+ protected _positionWorldBuffer: Vector3;
17
18
  protected _boundingBox: Box3;
18
19
 
19
20
  constructor() {
@@ -21,11 +22,24 @@ export class DIVENode extends Object3D implements DIVESelectable, DIVEMovable {
21
22
 
22
23
  this.layers.mask = PRODUCT_LAYER_MASK;
23
24
 
25
+ this._positionWorldBuffer = new Vector3();
24
26
  this._boundingBox = new Box3();
25
27
  }
26
28
 
27
29
  public SetPosition(position: Vector3Like): void {
28
- this.position.set(position.x, position.y, position.z);
30
+ // if there is no parent, the object will be attached later and keep it's world position
31
+ if (!this.parent) {
32
+ this.position.set(position.x, position.y, position.z);
33
+ return;
34
+ }
35
+
36
+ // if we have a parent, we have to calculate the position in the parent's coordinate system to keep the world position
37
+ const newPosition = new Vector3(position.x, position.y, position.z);
38
+ this.position.copy(this.parent.worldToLocal(newPosition));
39
+
40
+ if ('isDIVEGroup' in this.parent) {
41
+ (this.parent as unknown as DIVEGroup).UpdateLineTo(this);
42
+ }
29
43
  }
30
44
 
31
45
  public SetRotation(rotation: Vector3Like): void {
@@ -46,27 +60,26 @@ export class DIVENode extends Object3D implements DIVESelectable, DIVEMovable {
46
60
  'UPDATE_OBJECT',
47
61
  {
48
62
  id: this.userData.id,
49
- position: this.position,
63
+ position: this.getWorldPosition(this._positionWorldBuffer),
50
64
  rotation: this.rotation,
51
65
  scale: this.scale,
52
66
  },
53
67
  );
54
68
  }
55
69
 
70
+ /**
71
+ * Can be called when the object is moved from a foreign object (gizmo, parent, etc.) to update the object's position.
72
+ */
56
73
  public onMove(): void {
57
74
  DIVECommunication.get(this.userData.id)?.PerformAction(
58
75
  'UPDATE_OBJECT',
59
76
  {
60
77
  id: this.userData.id,
61
- position: this.position,
78
+ position: this.getWorldPosition(this._positionWorldBuffer),
62
79
  rotation: this.rotation,
63
80
  scale: this.scale,
64
81
  },
65
82
  );
66
-
67
- if (this.parent && 'isDIVEGroup' in this.parent) {
68
- (this.parent as unknown as DIVEGroup).UpdateLineTo(this);
69
- }
70
83
  }
71
84
 
72
85
  public onSelect(): void {
@@ -1,13 +1,17 @@
1
1
  import { DIVENode } from '../Node';
2
2
  import { DIVECommunication } from '../../com/Communication';
3
- import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader';
4
3
  import { Vector3, Box3, Mesh, Color, Euler } from 'three';
4
+ import { type DIVEGroup } from '../../group/Group';
5
5
 
6
6
  const intersectObjectsMock = jest.fn();
7
7
 
8
8
  jest.mock('three', () => {
9
9
  return {
10
- Vector3: jest.fn(function (x: number, y: number, z: number) {
10
+ Vector3: jest.fn(function (
11
+ x: number = 0,
12
+ y: number = 0,
13
+ z: number = 0,
14
+ ) {
11
15
  this.x = x;
12
16
  this.y = y;
13
17
  this.z = z;
@@ -92,6 +96,9 @@ jest.mock('three', () => {
92
96
  this.traverse = jest.fn((callback) => {
93
97
  callback(this.children[0]);
94
98
  });
99
+ this.getWorldPosition = jest.fn(() => {
100
+ return new Vector3();
101
+ });
95
102
  return this;
96
103
  }),
97
104
  Box3: jest.fn(function () {
@@ -177,7 +184,32 @@ describe('dive/node/DIVENode', () => {
177
184
  });
178
185
 
179
186
  it('should set position', () => {
180
- expect(() => node.SetPosition({ x: 0, y: 0, z: 0 })).not.toThrow();
187
+ const spySet = jest.spyOn(node.position, 'set');
188
+ const spyCopy = jest.spyOn(node.position, 'copy');
189
+
190
+ // without a parent, the node should only set it's local position
191
+ node.parent = null;
192
+ expect(() => node.SetPosition({ x: 1, y: 2, z: 3 })).not.toThrow();
193
+ expect(spySet).toHaveBeenCalledWith(1, 2, 3);
194
+ expect(spyCopy).not.toHaveBeenCalled();
195
+
196
+ // with a parent, the node should set it's position relative to the parent
197
+ spySet.mockClear();
198
+ spyCopy.mockClear();
199
+ node.parent = {
200
+ worldToLocal: jest.fn(() => new Vector3(4, 5, 6)),
201
+ isDIVEGroup: true,
202
+ UpdateLineTo: jest.fn(),
203
+ } as unknown as DIVENode;
204
+ const spyUpdateLineTo = jest.spyOn(
205
+ node.parent as DIVEGroup,
206
+ 'UpdateLineTo',
207
+ );
208
+ expect(() => node.SetPosition({ x: 4, y: 5, z: 6 })).not.toThrow();
209
+ expect(spySet).not.toHaveBeenCalled();
210
+ expect(spyCopy).toHaveBeenCalledWith(
211
+ expect.objectContaining({ x: 4, y: 5, z: 6 }),
212
+ );
181
213
  });
182
214
 
183
215
  it('should set rotation', () => {
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  BoxGeometry,
3
+ BufferAttribute,
3
4
  BufferGeometry,
4
5
  Color,
5
6
  ConeGeometry,
@@ -14,6 +15,7 @@ import { PRODUCT_LAYER_MASK } from '../constant/VisibilityLayerMask';
14
15
  import { findSceneRecursive } from '../helper/findSceneRecursive/findSceneRecursive';
15
16
  import { DIVENode } from '../node/Node';
16
17
  import { type COMGeometry, type COMMaterial } from '../com/types';
18
+ import { DIVECommunication } from '../com/Communication';
17
19
 
18
20
  /**
19
21
  * A basic model class.
@@ -24,7 +26,6 @@ import { type COMGeometry, type COMMaterial } from '../com/types';
24
26
  *
25
27
  * @module
26
28
  */
27
-
28
29
  export class DIVEPrimitive extends DIVENode {
29
30
  readonly isDIVEPrimitive: true = true;
30
31
 
@@ -106,11 +107,25 @@ export class DIVEPrimitive extends DIVENode {
106
107
  }
107
108
 
108
109
  public PlaceOnFloor(): void {
109
- const oldPos = this.position.clone();
110
- this.position.y = -this._boundingBox.min.y * this.scale.y;
111
- if (this.position.y === oldPos.y) return;
112
-
113
- this.onMove();
110
+ // calculate and temporary save world position
111
+ const worldPos = this.getWorldPosition(this._positionWorldBuffer);
112
+ const oldWorldPos = worldPos.clone();
113
+
114
+ // calculate the bottom center of the bounding box and set it to world posion
115
+ worldPos.y = -this._boundingBox.min.y * this.scale.y;
116
+
117
+ // skip any action when the position did not change
118
+ if (worldPos.y === oldWorldPos.y) return;
119
+
120
+ DIVECommunication.get(this.userData.id)?.PerformAction(
121
+ 'UPDATE_OBJECT',
122
+ {
123
+ id: this.userData.id,
124
+ position: worldPos,
125
+ rotation: this.rotation,
126
+ scale: this.scale,
127
+ },
128
+ );
114
129
  }
115
130
 
116
131
  public DropIt(): void {
@@ -159,12 +174,18 @@ export class DIVEPrimitive extends DIVENode {
159
174
  }
160
175
 
161
176
  private assembleGeometry(geometry: COMGeometry): BufferGeometry | null {
177
+ // reset material to smooth shading
178
+ (this._mesh.material as MeshStandardMaterial).flatShading = false;
179
+
162
180
  switch (geometry.name.toLowerCase()) {
163
181
  case 'cylinder':
164
182
  return this.createCylinderGeometry(geometry);
165
183
  case 'sphere':
166
184
  return this.createSphereGeometry(geometry);
167
185
  case 'pyramid':
186
+ // set material to flat shading for pyramid
187
+ (this._mesh.material as MeshStandardMaterial).flatShading =
188
+ true;
168
189
  return this.createPyramidGeometry(geometry);
169
190
  case 'cube':
170
191
  case 'box':
@@ -202,16 +223,36 @@ export class DIVEPrimitive extends DIVENode {
202
223
  }
203
224
 
204
225
  private createPyramidGeometry(geometry: COMGeometry): BufferGeometry {
205
- const geo = new ConeGeometry(
206
- geometry.width / 2,
207
- geometry.height,
208
- 4,
209
- 1,
210
- true,
226
+ // prettier-multiline-arrays-next-line-pattern: 3
227
+ const vertices = new Float32Array([
228
+ -geometry.width / 2, 0, -geometry.depth / 2, // 0
229
+ geometry.width / 2, 0, -geometry.depth / 2, // 1
230
+ geometry.width / 2, 0, geometry.depth / 2, // 2
231
+ -geometry.width / 2, 0, geometry.depth / 2, // 3
232
+ 0, geometry.height, 0,
233
+ ]);
234
+
235
+ // prettier-multiline-arrays-next-line-pattern: 3
236
+ const indices = new Uint16Array([
237
+ 0, 1, 2,
238
+ 0, 2, 3,
239
+ 0, 4, 1,
240
+ 1, 4, 2,
241
+ 2, 4, 3,
242
+ 3, 4, 0,
243
+ ]);
244
+
245
+ const geometryBuffer = new BufferGeometry();
246
+ geometryBuffer.setAttribute(
247
+ 'position',
248
+ new BufferAttribute(vertices, 3),
211
249
  );
212
- geo.rotateY(Math.PI / 4);
213
- geo.translate(0, geometry.height / 2, 0);
214
- return geo;
250
+ geometryBuffer.setIndex(new BufferAttribute(indices, 1));
251
+ geometryBuffer.computeVertexNormals();
252
+
253
+ geometryBuffer.computeBoundingBox();
254
+ geometryBuffer.computeBoundingSphere();
255
+ return geometryBuffer;
215
256
  }
216
257
 
217
258
  private createBoxGeometry(geometry: COMGeometry): BufferGeometry {
@@ -18,7 +18,11 @@ const intersectObjectsMock = jest.fn();
18
18
 
19
19
  jest.mock('three', () => {
20
20
  return {
21
- Vector3: jest.fn(function (x: number, y: number, z: number) {
21
+ Vector3: jest.fn(function (
22
+ x: number = 0,
23
+ y: number = 0,
24
+ z: number = 0,
25
+ ) {
22
26
  this.x = x;
23
27
  this.y = y;
24
28
  this.z = z;
@@ -109,6 +113,9 @@ jest.mock('three', () => {
109
113
  this.traverse = jest.fn((callback) => {
110
114
  callback(this.children[0]);
111
115
  });
116
+ this.getWorldPosition = jest.fn(() => {
117
+ return this.position.clone();
118
+ });
112
119
  return this;
113
120
  }),
114
121
  Box3: jest.fn(function () {
@@ -152,6 +159,12 @@ jest.mock('three', () => {
152
159
  this.setAttribute = jest.fn();
153
160
  this.setIndex = jest.fn();
154
161
  this.translate = jest.fn();
162
+ this.computeVertexNormals = jest.fn();
163
+ this.computeBoundingBox = jest.fn();
164
+ this.computeBoundingSphere = jest.fn();
165
+ return this;
166
+ }),
167
+ BufferAttribute: jest.fn(function () {
155
168
  return this;
156
169
  }),
157
170
  CylinderGeometry: jest.fn(function () {
@@ -241,12 +254,37 @@ describe('dive/primitive/DIVEPrimitive', () => {
241
254
  });
242
255
 
243
256
  it('should place on floor', () => {
257
+ const com = DIVECommunication.get('id')!;
258
+ const spyPerformAction = jest.spyOn(com, 'PerformAction');
259
+
244
260
  primitive.userData.id = 'something';
245
261
 
262
+ primitive['_boundingBox'] = {
263
+ min: new Vector3(0, -1, 0),
264
+ } as unknown as Box3;
265
+
266
+ expect(() => primitive.PlaceOnFloor()).not.toThrow();
267
+ expect(spyPerformAction).toHaveBeenCalledWith(
268
+ 'UPDATE_OBJECT',
269
+ expect.objectContaining({
270
+ position: expect.objectContaining({
271
+ y: 1,
272
+ }),
273
+ }),
274
+ );
275
+
276
+ // skip any action when the position did not change
277
+ spyPerformAction.mockClear();
278
+ primitive.position.y = 1;
246
279
  expect(() => primitive.PlaceOnFloor()).not.toThrow();
280
+ expect(spyPerformAction).not.toHaveBeenCalled();
247
281
 
282
+ // mock that the communication is not available
283
+ spyPerformAction.mockClear();
248
284
  jest.spyOn(DIVECommunication, 'get').mockReturnValueOnce(undefined);
285
+ primitive.position.y = 0;
249
286
  expect(() => primitive.PlaceOnFloor()).not.toThrow();
287
+ expect(spyPerformAction).not.toHaveBeenCalled();
250
288
  });
251
289
 
252
290
  it('should drop it', () => {
@@ -413,6 +413,8 @@ jest.mock('../../../group/Group.ts', () => {
413
413
 
414
414
  let root: DIVERoot;
415
415
 
416
+ let spyConsoleWarn = jest.spyOn(console, 'warn').mockImplementation(() => {});
417
+
416
418
  describe('DIVE/scene/root/DIVERoot', () => {
417
419
  beforeEach(() => {
418
420
  root = new DIVERoot();
@@ -422,6 +424,10 @@ describe('DIVE/scene/root/DIVERoot', () => {
422
424
  jest.clearAllMocks();
423
425
  });
424
426
 
427
+ afterAll(() => {
428
+ spyConsoleWarn.mockRestore();
429
+ });
430
+
425
431
  it('should instantiate', () => {
426
432
  expect(root).toBeDefined();
427
433
  });
@@ -474,6 +480,7 @@ describe('DIVE/scene/root/DIVERoot', () => {
474
480
  } as COMPov),
475
481
  ).not.toThrow();
476
482
 
483
+ spyConsoleWarn.mockClear();
477
484
  expect(() =>
478
485
  root.AddSceneObject({
479
486
  id: 'id',
@@ -482,6 +489,9 @@ describe('DIVE/scene/root/DIVERoot', () => {
482
489
  visible: true,
483
490
  } as COMLight),
484
491
  ).not.toThrow();
492
+ expect(spyConsoleWarn).toHaveBeenCalled();
493
+
494
+ spyConsoleWarn.mockClear();
485
495
  expect(() =>
486
496
  root.AddSceneObject({
487
497
  id: 'id_scene',
@@ -491,6 +501,9 @@ describe('DIVE/scene/root/DIVERoot', () => {
491
501
  type: 'scene',
492
502
  } as COMLight),
493
503
  ).not.toThrow();
504
+ expect(spyConsoleWarn).not.toHaveBeenCalled();
505
+
506
+ spyConsoleWarn.mockClear();
494
507
  expect(() =>
495
508
  root.AddSceneObject({
496
509
  id: 'id_ambient',
@@ -500,6 +513,9 @@ describe('DIVE/scene/root/DIVERoot', () => {
500
513
  type: 'ambient',
501
514
  } as COMLight),
502
515
  ).not.toThrow();
516
+ expect(spyConsoleWarn).not.toHaveBeenCalled();
517
+
518
+ spyConsoleWarn.mockClear();
503
519
  expect(() =>
504
520
  root.AddSceneObject({
505
521
  id: 'id_point',
@@ -514,6 +530,7 @@ describe('DIVE/scene/root/DIVERoot', () => {
514
530
  parentId: 'id',
515
531
  } as COMLight),
516
532
  ).not.toThrow();
533
+ expect(spyConsoleWarn).not.toHaveBeenCalled();
517
534
 
518
535
  expect(() =>
519
536
  root.AddSceneObject({
@@ -754,6 +771,7 @@ describe('DIVE/scene/root/DIVERoot', () => {
754
771
  } as unknown as Object3D,
755
772
  ];
756
773
 
774
+ spyConsoleWarn.mockClear();
757
775
  expect(() =>
758
776
  root.DeleteSceneObject({
759
777
  id: 'does_not_exist',
@@ -762,6 +780,8 @@ describe('DIVE/scene/root/DIVERoot', () => {
762
780
  visible: true,
763
781
  } as COMPov),
764
782
  ).not.toThrow();
783
+
784
+ spyConsoleWarn.mockClear();
765
785
  expect(() =>
766
786
  root.DeleteSceneObject({
767
787
  id: 'id',
@@ -770,7 +790,9 @@ describe('DIVE/scene/root/DIVERoot', () => {
770
790
  visible: true,
771
791
  } as COMPov),
772
792
  ).not.toThrow();
793
+ expect(spyConsoleWarn).not.toHaveBeenCalled();
773
794
 
795
+ spyConsoleWarn.mockClear();
774
796
  expect(() =>
775
797
  root.DeleteSceneObject({
776
798
  id: 'does_not_exist',
@@ -780,6 +802,9 @@ describe('DIVE/scene/root/DIVERoot', () => {
780
802
  type: 'scene',
781
803
  } as COMLight),
782
804
  ).not.toThrow();
805
+ expect(spyConsoleWarn).toHaveBeenCalled();
806
+
807
+ spyConsoleWarn.mockClear();
783
808
  expect(() =>
784
809
  root.DeleteSceneObject({
785
810
  id: 'id',
@@ -789,7 +814,9 @@ describe('DIVE/scene/root/DIVERoot', () => {
789
814
  type: 'scene',
790
815
  } as COMLight),
791
816
  ).not.toThrow();
817
+ expect(spyConsoleWarn).not.toHaveBeenCalled();
792
818
 
819
+ spyConsoleWarn.mockClear();
793
820
  expect(() =>
794
821
  root.DeleteSceneObject({
795
822
  id: 'does_not_exist',
@@ -798,6 +825,9 @@ describe('DIVE/scene/root/DIVERoot', () => {
798
825
  visible: true,
799
826
  } as COMModel),
800
827
  ).not.toThrow();
828
+ expect(spyConsoleWarn).toHaveBeenCalled();
829
+
830
+ spyConsoleWarn.mockClear();
801
831
  expect(() =>
802
832
  root.DeleteSceneObject({
803
833
  id: 'id',
@@ -815,6 +845,9 @@ describe('DIVE/scene/root/DIVERoot', () => {
815
845
  visible: true,
816
846
  } as COMPrimitive),
817
847
  ).not.toThrow();
848
+ expect(spyConsoleWarn).toHaveBeenCalled();
849
+
850
+ spyConsoleWarn.mockClear();
818
851
  expect(() =>
819
852
  root.DeleteSceneObject({
820
853
  id: 'id',
@@ -823,7 +856,9 @@ describe('DIVE/scene/root/DIVERoot', () => {
823
856
  visible: true,
824
857
  } as COMPrimitive),
825
858
  ).not.toThrow();
859
+ expect(spyConsoleWarn).not.toHaveBeenCalled();
826
860
 
861
+ spyConsoleWarn.mockClear();
827
862
  expect(() =>
828
863
  root.DeleteSceneObject({
829
864
  id: 'does_not_exist',
@@ -832,6 +867,9 @@ describe('DIVE/scene/root/DIVERoot', () => {
832
867
  visible: true,
833
868
  } as COMGroup),
834
869
  ).not.toThrow();
870
+ expect(spyConsoleWarn).toHaveBeenCalled();
871
+
872
+ spyConsoleWarn.mockClear();
835
873
  expect(() =>
836
874
  root.DeleteSceneObject({
837
875
  id: 'id',
@@ -840,6 +878,7 @@ describe('DIVE/scene/root/DIVERoot', () => {
840
878
  visible: true,
841
879
  } as COMGroup),
842
880
  ).not.toThrow();
881
+ expect(spyConsoleWarn).not.toHaveBeenCalled();
843
882
 
844
883
  const firstFind = root.GetSceneObject({ id: 'id' });
845
884
  jest.spyOn(root, 'GetSceneObject').mockReturnValueOnce({
@@ -0,0 +1,26 @@
1
+ import type { Vector3Like } from 'three';
2
+ import type {
3
+ COMGroup,
4
+ COMLight,
5
+ COMModel,
6
+ COMPov,
7
+ COMPrimitive,
8
+ } from '../dive';
9
+
10
+ export type DIVESceneData = {
11
+ name: string;
12
+ mediaItem: null;
13
+ backgroundColor: string;
14
+ floorEnabled: boolean;
15
+ floorColor: string;
16
+ userCamera: {
17
+ position: Vector3Like;
18
+ target: Vector3Like;
19
+ };
20
+ spotmarks: object[];
21
+ lights: COMLight[];
22
+ objects: COMModel[];
23
+ cameras: COMPov[];
24
+ primitives: COMPrimitive[];
25
+ groups: COMGroup[];
26
+ };
@@ -1,4 +1,5 @@
1
1
  import { type DIVESceneObject } from './SceneObjects';
2
2
  import { type DIVESceneFileType } from './SceneType';
3
+ import { type DIVESceneData } from './SceneData';
3
4
 
4
- export { type DIVESceneObject, type DIVESceneFileType };
5
+ export { type DIVESceneObject, type DIVESceneFileType, type DIVESceneData };