@shopware-ag/dive 1.17.0 → 1.17.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shopware-ag/dive",
3
- "version": "1.17.0",
3
+ "version": "1.17.2",
4
4
  "description": "Shopware Spatial Framework",
5
5
  "type": "module",
6
6
  "main": "./build/dive.cjs",
@@ -44,6 +44,7 @@
44
44
  "globals": "^15.0.0",
45
45
  "jest": "^29.7.0",
46
46
  "jest-environment-jsdom": "^29.7.0",
47
+ "jest-junit": "^16.0.0",
47
48
  "jsdom": "^24.0.0",
48
49
  "prettier": "^3.3.3",
49
50
  "prettier-plugin-multiline-arrays": "^3.0.6",
@@ -457,6 +457,27 @@ export class DIVECommunication {
457
457
  const deletedObject = this.registered.get(payload.id);
458
458
  if (!deletedObject) return false;
459
459
 
460
+ // If the object has a parent, detach it first
461
+ if (deletedObject.parentId) {
462
+ // First detach from parent group
463
+ this.setParent({
464
+ object: { id: deletedObject.id },
465
+ parent: null,
466
+ });
467
+ }
468
+
469
+ // If deleting a group, update all children to have no parent
470
+ if (deletedObject.entityType === 'group') {
471
+ this.registered.forEach((object) => {
472
+ if (object.parentId === deletedObject.id) {
473
+ this.updateObject({
474
+ id: object.id,
475
+ parentId: null,
476
+ });
477
+ }
478
+ });
479
+ }
480
+
460
481
  // copy object to payload to use later
461
482
  Object.assign(payload, deletedObject);
462
483
 
@@ -723,6 +744,11 @@ export class DIVECommunication {
723
744
  if (payload.parent === null) {
724
745
  // detach from current parent
725
746
  this.scene.Root.attach(sceneObject);
747
+ // Update registration to reflect no parent
748
+ this.updateObject({
749
+ id: object.id,
750
+ parentId: null,
751
+ });
726
752
  return true;
727
753
  }
728
754
 
@@ -735,6 +761,11 @@ export class DIVECommunication {
735
761
  if (!parent) {
736
762
  // detach from current parent
737
763
  this.scene.Root.attach(sceneObject);
764
+ // Update registration to reflect no parent
765
+ this.updateObject({
766
+ id: object.id,
767
+ parentId: null,
768
+ });
738
769
  return true;
739
770
  }
740
771
 
@@ -743,11 +774,21 @@ export class DIVECommunication {
743
774
  if (!parentObject) {
744
775
  // detach from current parent
745
776
  this.scene.Root.attach(sceneObject);
777
+ // Update registration to reflect no parent
778
+ this.updateObject({
779
+ id: object.id,
780
+ parentId: null,
781
+ });
746
782
  return true;
747
783
  }
748
784
 
749
785
  // attach to new parent
750
786
  parentObject.attach(sceneObject);
787
+ // Update registration to reflect new parent
788
+ this.updateObject({
789
+ id: object.id,
790
+ parentId: parent.id,
791
+ });
751
792
  return true;
752
793
  }
753
794
 
@@ -20,6 +20,12 @@ export class DIVEGizmo extends Object3D {
20
20
  this.assemble();
21
21
  }
22
22
 
23
+ public set debug(value: boolean) {
24
+ this._translateGizmo.debug = value;
25
+ this._rotateGizmo.debug = value;
26
+ this._scaleGizmo.debug = value;
27
+ }
28
+
23
29
  private _gizmoNode: Object3D;
24
30
  public get gizmoNode(): Object3D {
25
31
  return this._gizmoNode;
@@ -20,6 +20,10 @@ export class DIVEAxisHandle
20
20
  readonly isHoverable: true = true;
21
21
  readonly isDraggable: true = true;
22
22
 
23
+ public set debug(value: boolean) {
24
+ this._colliderMesh.visible = value;
25
+ }
26
+
23
27
  public parent: DIVETranslateGizmo | null = null;
24
28
 
25
29
  public axis: 'x' | 'y' | 'z';
@@ -39,6 +43,8 @@ export class DIVEAxisHandle
39
43
 
40
44
  private _lineMaterial: MeshBasicMaterial;
41
45
 
46
+ private _colliderMesh: Mesh;
47
+
42
48
  public get forwardVector(): Vector3 {
43
49
  return new Vector3(0, 0, 1)
44
50
  .applyQuaternion(this.quaternion)
@@ -89,7 +95,7 @@ export class DIVEAxisHandle
89
95
  this.add(lineMesh);
90
96
 
91
97
  // create collider
92
- const collider = new CylinderGeometry(0.1, 0.1, length, 3);
98
+ const colliderGeo = new CylinderGeometry(0.1, 0.1, length, 3);
93
99
  const colliderMaterial = new MeshBasicMaterial({
94
100
  color: 0xff00ff,
95
101
  transparent: true,
@@ -97,13 +103,13 @@ export class DIVEAxisHandle
97
103
  depthTest: false,
98
104
  depthWrite: false,
99
105
  });
100
- const colliderMesh = new Mesh(collider, colliderMaterial);
101
- colliderMesh.visible = false;
102
- colliderMesh.layers.mask = UI_LAYER_MASK;
103
- colliderMesh.renderOrder = Infinity;
104
- colliderMesh.rotateX(Math.PI / 2);
105
- colliderMesh.translateY(length / 2);
106
- this.add(colliderMesh);
106
+ this._colliderMesh = new Mesh(colliderGeo, colliderMaterial);
107
+ this._colliderMesh.visible = false;
108
+ this._colliderMesh.layers.mask = UI_LAYER_MASK;
109
+ this._colliderMesh.renderOrder = Infinity;
110
+ this._colliderMesh.rotateX(Math.PI / 2);
111
+ this._colliderMesh.translateY(length / 2);
112
+ this.add(this._colliderMesh);
107
113
 
108
114
  this.rotateX((direction.y * -Math.PI) / 2);
109
115
  this.rotateY((direction.x * Math.PI) / 2);
@@ -20,6 +20,10 @@ export class DIVERadialHandle
20
20
  readonly isHoverable: true = true;
21
21
  readonly isDraggable: true = true;
22
22
 
23
+ public set debug(value: boolean) {
24
+ this._colliderMesh.visible = value;
25
+ }
26
+
23
27
  public parent: DIVERotateGizmo | null = null;
24
28
 
25
29
  public axis: 'x' | 'y' | 'z';
@@ -39,6 +43,8 @@ export class DIVERadialHandle
39
43
 
40
44
  private _lineMaterial: MeshBasicMaterial;
41
45
 
46
+ private _colliderMesh: Mesh;
47
+
42
48
  public get forwardVector(): Vector3 {
43
49
  return new Vector3(0, 0, 1)
44
50
  .applyQuaternion(this.quaternion)
@@ -87,7 +93,7 @@ export class DIVERadialHandle
87
93
  this.add(lineMesh);
88
94
 
89
95
  // create collider
90
- const collider = new TorusGeometry(radius, 0.1, 3, 48, arc);
96
+ const colliderGeo = new TorusGeometry(radius, 0.1, 3, 48, arc);
91
97
  const colliderMaterial = new MeshBasicMaterial({
92
98
  color: 0xff00ff,
93
99
  transparent: true,
@@ -95,12 +101,12 @@ export class DIVERadialHandle
95
101
  depthTest: false,
96
102
  depthWrite: false,
97
103
  });
98
- const colliderMesh = new Mesh(collider, colliderMaterial);
99
- colliderMesh.visible = false;
100
- colliderMesh.layers.mask = UI_LAYER_MASK;
101
- colliderMesh.renderOrder = Infinity;
104
+ this._colliderMesh = new Mesh(colliderGeo, colliderMaterial);
105
+ this._colliderMesh.visible = false;
106
+ this._colliderMesh.layers.mask = UI_LAYER_MASK;
107
+ this._colliderMesh.renderOrder = Infinity;
102
108
 
103
- this.add(colliderMesh);
109
+ this.add(this._colliderMesh);
104
110
 
105
111
  this.lookAt(direction);
106
112
  }
@@ -21,6 +21,10 @@ export class DIVEScaleHandle
21
21
  readonly isHoverable: true = true;
22
22
  readonly isDraggable: true = true;
23
23
 
24
+ public set debug(value: boolean) {
25
+ this._colliderMesh.visible = value;
26
+ }
27
+
24
28
  public parent: DIVEScaleGizmo | null = null;
25
29
 
26
30
  public axis: 'x' | 'y' | 'z';
@@ -40,6 +44,8 @@ export class DIVEScaleHandle
40
44
 
41
45
  private _lineMaterial: MeshBasicMaterial;
42
46
 
47
+ private _colliderMesh: Mesh;
48
+
43
49
  private _box: Mesh;
44
50
  private _boxSize: number;
45
51
 
@@ -113,7 +119,7 @@ export class DIVEScaleHandle
113
119
  this.add(this._box);
114
120
 
115
121
  // create collider
116
- const collider = new CylinderGeometry(
122
+ const colliderGeo = new CylinderGeometry(
117
123
  0.1,
118
124
  0.1,
119
125
  length + boxSize / 2,
@@ -126,13 +132,13 @@ export class DIVEScaleHandle
126
132
  depthTest: false,
127
133
  depthWrite: false,
128
134
  });
129
- const colliderMesh = new Mesh(collider, colliderMaterial);
130
- colliderMesh.visible = false;
131
- colliderMesh.layers.mask = UI_LAYER_MASK;
132
- colliderMesh.renderOrder = Infinity;
133
- colliderMesh.rotateX(Math.PI / 2);
134
- colliderMesh.translateY(length / 2);
135
- this.add(colliderMesh);
135
+ this._colliderMesh = new Mesh(colliderGeo, colliderMaterial);
136
+ this._colliderMesh.visible = false;
137
+ this._colliderMesh.layers.mask = UI_LAYER_MASK;
138
+ this._colliderMesh.renderOrder = Infinity;
139
+ this._colliderMesh.rotateX(Math.PI / 2);
140
+ this._colliderMesh.translateY(length / 2);
141
+ this.add(this._colliderMesh);
136
142
 
137
143
  this.rotateX((direction.y * -Math.PI) / 2);
138
144
  this.rotateY((direction.x * Math.PI) / 2);
@@ -15,6 +15,12 @@ export class DIVERotateGizmo extends Object3D {
15
15
 
16
16
  private _controller: DIVEOrbitControls;
17
17
 
18
+ public set debug(value: boolean) {
19
+ this.children.forEach((child) => {
20
+ child.debug = value;
21
+ });
22
+ }
23
+
18
24
  private _startRot: Euler | null;
19
25
 
20
26
  constructor(controller: DIVEOrbitControls) {
@@ -17,6 +17,12 @@ export class DIVEScaleGizmo extends Object3D implements DIVEHoverable {
17
17
 
18
18
  private _controller: DIVEOrbitControls;
19
19
 
20
+ public set debug(value: boolean) {
21
+ this.children.forEach((child) => {
22
+ child.debug = value;
23
+ });
24
+ }
25
+
20
26
  private _startScale: Vector3 | null;
21
27
 
22
28
  constructor(controller: DIVEOrbitControls) {
@@ -12,6 +12,12 @@ import { DraggableEvent } from '../../toolbox/BaseTool';
12
12
  export class DIVETranslateGizmo extends Object3D {
13
13
  private _controller: DIVEOrbitControls;
14
14
 
15
+ public set debug(value: boolean) {
16
+ this.children.forEach((child) => {
17
+ child.debug = value;
18
+ });
19
+ }
20
+
15
21
  public children: DIVEAxisHandle[];
16
22
 
17
23
  private _startPos: Vector3 | null;
@@ -116,8 +116,13 @@ export class DIVEModel extends DIVENode {
116
116
  const worldPos = this.getWorldPosition(this._positionWorldBuffer);
117
117
  const oldWorldPos = worldPos.clone();
118
118
 
119
- // calculate the bottom center of the bounding box and set it to world posion
120
- worldPos.y = (this._boundingBox.max.y - this._boundingBox.min.y) / 2;
119
+ // compute the bounding box
120
+ this._mesh?.geometry?.computeBoundingBox();
121
+ const meshBB = this._mesh?.geometry?.boundingBox;
122
+
123
+ // subtract the bounding box min y axis value from the world position y value
124
+ if (!meshBB || !this._mesh) return;
125
+ worldPos.y = worldPos.y - this._mesh.localToWorld(meshBB.min.clone()).y;
121
126
 
122
127
  // skip any action when the position did not change
123
128
  if (worldPos.y === oldWorldPos.y) return;
@@ -9,9 +9,9 @@ import {
9
9
  MeshStandardMaterial,
10
10
  type Texture,
11
11
  Color,
12
+ Object3D,
12
13
  } from 'three';
13
14
  import { type COMMaterial } from '../../com/types';
14
- import { isTypeOnlyImportOrExportDeclaration } from 'typescript';
15
15
 
16
16
  const intersectObjectsMock = jest.fn();
17
17
 
@@ -25,43 +25,43 @@ jest.mock('three', () => {
25
25
  this.x = x;
26
26
  this.y = y;
27
27
  this.z = z;
28
- this.copy = (vec3: Vector3) => {
28
+ this.copy = jest.fn((vec3: Vector3) => {
29
29
  this.x = vec3.x;
30
30
  this.y = vec3.y;
31
31
  this.z = vec3.z;
32
32
  return this;
33
- };
34
- this.set = (x: number, y: number, z: number) => {
33
+ });
34
+ this.set = jest.fn((x: number, y: number, z: number) => {
35
35
  this.x = x;
36
36
  this.y = y;
37
37
  this.z = z;
38
38
  return this;
39
- };
40
- this.multiply = (vec3: Vector3) => {
39
+ });
40
+ this.multiply = jest.fn((vec3: Vector3) => {
41
41
  this.x *= vec3.x;
42
42
  this.y *= vec3.y;
43
43
  this.z *= vec3.z;
44
44
  return this;
45
- };
46
- this.clone = () => {
45
+ });
46
+ this.clone = jest.fn(() => {
47
47
  return new Vector3(this.x, this.y, this.z);
48
- };
49
- this.setY = (y: number) => {
48
+ });
49
+ this.setY = jest.fn((y: number) => {
50
50
  this.y = y;
51
51
  return this;
52
- };
53
- this.add = (vec3: Vector3) => {
52
+ });
53
+ this.add = jest.fn((vec3: Vector3) => {
54
54
  this.x += vec3.x;
55
55
  this.y += vec3.y;
56
56
  this.z += vec3.z;
57
57
  return this;
58
- };
59
- this.sub = (vec3: Vector3) => {
58
+ });
59
+ this.sub = jest.fn((vec3: Vector3) => {
60
60
  this.x -= vec3.x;
61
61
  this.y -= vec3.y;
62
62
  this.z -= vec3.z;
63
63
  return this;
64
- };
64
+ });
65
65
  return this;
66
66
  }),
67
67
  Object3D: jest.fn(function () {
@@ -83,14 +83,7 @@ jest.mock('three', () => {
83
83
  };
84
84
  this.add = jest.fn();
85
85
  this.sub = jest.fn();
86
- this.children = [
87
- {
88
- visible: true,
89
- material: {
90
- color: {},
91
- },
92
- },
93
- ];
86
+ this.children = [];
94
87
  this.userData = {};
95
88
  this.position = new Vector3();
96
89
  this.rotation = {
@@ -105,12 +98,14 @@ jest.mock('three', () => {
105
98
  z: 1,
106
99
  set: jest.fn(),
107
100
  };
108
- this.localToWorld = (vec3: Vector3) => {
101
+ this.localToWorld = jest.fn((vec3: Vector3) => {
109
102
  return vec3;
110
- };
111
- this.mesh = new Mesh();
103
+ });
112
104
  this.traverse = jest.fn((callback) => {
113
- callback(this.children[0]);
105
+ callback(this);
106
+ this.children.forEach((child: Object3D) => {
107
+ callback(child);
108
+ });
114
109
  });
115
110
  this.getWorldPosition = jest.fn(() => {
116
111
  return this.position.clone();
@@ -136,11 +131,12 @@ jest.mock('three', () => {
136
131
  return this;
137
132
  }),
138
133
  Mesh: jest.fn(function () {
134
+ this.isMesh = true;
139
135
  this.geometry = {
140
136
  computeBoundingBox: jest.fn(),
141
137
  boundingBox: new Box3(),
142
138
  };
143
- this.material = {};
139
+ this.material = new MeshStandardMaterial();
144
140
  this.castShadow = true;
145
141
  this.receiveShadow = true;
146
142
  this.layers = {
@@ -149,9 +145,9 @@ jest.mock('three', () => {
149
145
  this.updateWorldMatrix = jest.fn();
150
146
  this.traverse = jest.fn();
151
147
  this.removeFromParent = jest.fn();
152
- this.localToWorld = (vec3: Vector3) => {
148
+ this.localToWorld = jest.fn((vec3: Vector3) => {
153
149
  return vec3;
154
- };
150
+ });
155
151
  return this;
156
152
  }),
157
153
  MeshStandardMaterial: jest.fn(function () {
@@ -181,33 +177,12 @@ jest.mock('../../com/Communication.ts', () => {
181
177
  };
182
178
  });
183
179
 
180
+ const object = new Object3D();
181
+ object.children.push(new Mesh());
182
+
184
183
  const gltf = {
185
184
  scene: {
186
- isMesh: true,
187
- isObject3D: true,
188
- parent: null,
189
- dispatchEvent: jest.fn(),
190
- layers: {
191
- mask: 0,
192
- },
193
- material: {},
194
- updateWorldMatrix: jest.fn(),
195
- children: [
196
- {
197
- castShadow: false,
198
- receiveShadow: false,
199
- layers: {
200
- mask: 0,
201
- },
202
- children: [],
203
- updateWorldMatrix: jest.fn(),
204
- isMesh: true,
205
- },
206
- ],
207
- traverse: function (callback: (object: object) => void) {
208
- callback(this);
209
- },
210
- removeFromParent: jest.fn(),
185
+ ...object,
211
186
  },
212
187
  } as unknown as GLTF;
213
188
 
@@ -236,38 +211,38 @@ describe('dive/model/DIVEModel', () => {
236
211
  });
237
212
 
238
213
  it('should place on floor', () => {
214
+ model.SetModel(gltf);
215
+
239
216
  const com = DIVECommunication.get('id')!;
240
217
  const spyPerformAction = jest.spyOn(com, 'PerformAction');
241
218
 
242
219
  model.userData.id = 'something';
220
+ model.position.set(0, 4, 0);
243
221
 
244
- model['_boundingBox'] = {
245
- min: new Vector3(0, 1, 0),
246
- max: new Vector3(1, 4, 1),
247
- } as unknown as Box3;
222
+ jest.spyOn(model['_mesh']!, 'localToWorld').mockReturnValueOnce(
223
+ new Vector3(0, 2, 0),
224
+ );
225
+
226
+ const scene = {
227
+ parent: null,
228
+ Root: {
229
+ children: [
230
+ model,
231
+ ],
232
+ },
233
+ } as unknown as DIVEScene;
234
+ scene.Root.parent = scene;
235
+ model.parent = scene.Root;
248
236
 
249
237
  expect(() => model.PlaceOnFloor()).not.toThrow();
250
238
  expect(spyPerformAction).toHaveBeenCalledWith(
251
239
  'UPDATE_OBJECT',
252
240
  expect.objectContaining({
253
241
  position: expect.objectContaining({
254
- y: 1.5,
242
+ y: 2,
255
243
  }),
256
244
  }),
257
245
  );
258
-
259
- // skip any action when the position did not change
260
- spyPerformAction.mockClear();
261
- model.position.y = 1.5;
262
- expect(() => model.PlaceOnFloor()).not.toThrow();
263
- expect(spyPerformAction).not.toHaveBeenCalled();
264
-
265
- // mock that the communication is not available
266
- spyPerformAction.mockClear();
267
- jest.spyOn(DIVECommunication, 'get').mockReturnValueOnce(undefined);
268
- model.position.y = 0;
269
- expect(() => model.PlaceOnFloor()).not.toThrow();
270
- expect(spyPerformAction).not.toHaveBeenCalled();
271
246
  });
272
247
 
273
248
  it('should drop it', () => {
@@ -111,8 +111,13 @@ export class DIVEPrimitive extends DIVENode {
111
111
  const worldPos = this.getWorldPosition(this._positionWorldBuffer);
112
112
  const oldWorldPos = worldPos.clone();
113
113
 
114
- // calculate the bottom center of the bounding box and set it to world posion
115
- worldPos.y = (this._boundingBox.max.y - this._boundingBox.min.y) / 2;
114
+ // compute the bounding box
115
+ this._mesh?.geometry?.computeBoundingBox();
116
+ const meshBB = this._mesh?.geometry?.boundingBox;
117
+
118
+ // subtract the bounding box min y axis value from the world position y value
119
+ if (!meshBB || !this._mesh) return;
120
+ worldPos.y = worldPos.y - this._mesh.localToWorld(meshBB.min.clone()).y;
116
121
 
117
122
  // skip any action when the position did not change
118
123
  if (worldPos.y === oldWorldPos.y) return;
@@ -139,7 +139,15 @@ jest.mock('three', () => {
139
139
  Mesh: jest.fn(function () {
140
140
  this.geometry = {
141
141
  computeBoundingBox: jest.fn(),
142
- boundingBox: new Box3(),
142
+ boundingBox: {
143
+ min: new Vector3(0, 2, 0),
144
+ max: new Vector3(2, 4, 2),
145
+ getCenter: jest.fn(() => {
146
+ return new Vector3(0, 0, 0);
147
+ }),
148
+ expandByObject: jest.fn(),
149
+ setFromObject: jest.fn(),
150
+ },
143
151
  };
144
152
  this.material = {};
145
153
  this.castShadow = true;
@@ -258,34 +266,33 @@ describe('dive/primitive/DIVEPrimitive', () => {
258
266
  const spyPerformAction = jest.spyOn(com, 'PerformAction');
259
267
 
260
268
  primitive.userData.id = 'something';
261
-
269
+ primitive.position.set(0, 2, 0);
262
270
  primitive['_boundingBox'] = {
263
- min: new Vector3(0, 1, 0),
264
- max: new Vector3(0, 4, 0),
271
+ min: new Vector3(0, -2, 0),
272
+ setFromObject: jest.fn(),
265
273
  } as unknown as Box3;
266
274
 
275
+ const scene = {
276
+ parent: null,
277
+ Root: {
278
+ children: [
279
+ primitive,
280
+ ],
281
+ },
282
+ } as unknown as DIVEScene;
283
+ scene.Root.parent = scene;
284
+
285
+ primitive.parent = scene.Root;
286
+
267
287
  expect(() => primitive.PlaceOnFloor()).not.toThrow();
268
288
  expect(spyPerformAction).toHaveBeenCalledWith(
269
289
  'UPDATE_OBJECT',
270
290
  expect.objectContaining({
271
291
  position: expect.objectContaining({
272
- y: 1.5,
292
+ y: 0,
273
293
  }),
274
294
  }),
275
295
  );
276
-
277
- // skip any action when the position did not change
278
- spyPerformAction.mockClear();
279
- primitive.position.y = 1.5;
280
- expect(() => primitive.PlaceOnFloor()).not.toThrow();
281
- expect(spyPerformAction).not.toHaveBeenCalled();
282
-
283
- // mock that the communication is not available
284
- spyPerformAction.mockClear();
285
- jest.spyOn(DIVECommunication, 'get').mockReturnValueOnce(undefined);
286
- primitive.position.y = 0;
287
- expect(() => primitive.PlaceOnFloor()).not.toThrow();
288
- expect(spyPerformAction).not.toHaveBeenCalled();
289
296
  });
290
297
 
291
298
  it('should drop it', () => {