@shopware-ag/dive 1.1.2 → 1.2.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/README.md +1 -0
- package/build/dive.cjs +45 -0
- package/build/dive.cjs.map +1 -1
- package/build/dive.d.cts +9 -0
- package/build/dive.d.ts +9 -0
- package/build/dive.js +48 -3
- package/build/dive.js.map +1 -1
- package/package.json +1 -1
- package/src/com/Communication.ts +15 -0
- package/src/com/__test__/Communication.test.ts +41 -0
- package/src/com/actions/index.ts +2 -0
- package/src/com/actions/object/model/dropit.ts +4 -0
- package/src/helper/findSceneRecursive/__test__/findSceneRecursive.test.ts +40 -0
- package/src/helper/findSceneRecursive/findSceneRecursive.ts +16 -0
- package/src/model/Model.ts +35 -1
- package/src/model/__test__/Model.test.ts +141 -8
- /package/src/helper/getObjectDelta/__test__/{getObjectDelta.spec.ts → getObjectDelta.test.ts} +0 -0
|
@@ -289,6 +289,47 @@ describe('dive/communication/DIVECommunication', () => {
|
|
|
289
289
|
expect(successSet).toBe(true);
|
|
290
290
|
});
|
|
291
291
|
|
|
292
|
+
it('should perform action DROP_IT with existing model', () => {
|
|
293
|
+
const payload = {
|
|
294
|
+
entityType: "model",
|
|
295
|
+
id: "model",
|
|
296
|
+
position: { x: 0, y: 0, z: 0 },
|
|
297
|
+
rotation: { x: 0, y: 0, z: 0 },
|
|
298
|
+
scale: { x: 0.01, y: 0.01, z: 0.01 },
|
|
299
|
+
|
|
300
|
+
uri: "https://threejs.org/examples/models/gltf/LittlestTokyo.glb",
|
|
301
|
+
} as COMModel;
|
|
302
|
+
|
|
303
|
+
testCom.PerformAction('ADD_OBJECT', payload);
|
|
304
|
+
|
|
305
|
+
const placeSpy = jest.spyOn(mockScene, 'GetSceneObject').mockReturnValue({
|
|
306
|
+
DropIt: jest.fn(),
|
|
307
|
+
} as unknown as Object3D);
|
|
308
|
+
|
|
309
|
+
const successPlace = testCom.PerformAction('DROP_IT', payload);
|
|
310
|
+
expect(successPlace).toBe(true);
|
|
311
|
+
expect(placeSpy).toHaveBeenCalledTimes(1);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should perform action DROP_IT without existing model', () => {
|
|
315
|
+
const payload = {
|
|
316
|
+
entityType: "model",
|
|
317
|
+
id: "model",
|
|
318
|
+
position: { x: 0, y: 0, z: 0 },
|
|
319
|
+
rotation: { x: 0, y: 0, z: 0 },
|
|
320
|
+
scale: { x: 0.01, y: 0.01, z: 0.01 },
|
|
321
|
+
|
|
322
|
+
uri: "https://threejs.org/examples/models/gltf/LittlestTokyo.glb",
|
|
323
|
+
};
|
|
324
|
+
const placeSpy = jest.spyOn(mockScene, 'GetSceneObject').mockReturnValue({
|
|
325
|
+
DropIt: jest.fn(),
|
|
326
|
+
} as unknown as Object3D);
|
|
327
|
+
|
|
328
|
+
const successPlace = testCom.PerformAction('DROP_IT', payload);
|
|
329
|
+
expect(successPlace).toBe(false);
|
|
330
|
+
expect(placeSpy).toHaveBeenCalledTimes(0);
|
|
331
|
+
});
|
|
332
|
+
|
|
292
333
|
it('should perform action PLACE_ON_FLOOR with existing model', () => {
|
|
293
334
|
const payload = {
|
|
294
335
|
entityType: "model",
|
package/src/com/actions/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import GENERATE_MEDIA from "./media/generatemedia.ts";
|
|
|
17
17
|
import GET_ALL_SCENE_DATA from "./scene/getallscenedata.ts";
|
|
18
18
|
import SELECT_OBJECT from "./object/selectobject.ts";
|
|
19
19
|
import GET_CAMERA_TRANSFORM from "./camera/getcameratransform.ts";
|
|
20
|
+
import DROP_IT from "./object/model/dropit.ts";
|
|
20
21
|
|
|
21
22
|
export type Actions = {
|
|
22
23
|
GET_ALL_SCENE_DATA: GET_ALL_SCENE_DATA,
|
|
@@ -27,6 +28,7 @@ export type Actions = {
|
|
|
27
28
|
DELETE_OBJECT: DELETE_OBJECT,
|
|
28
29
|
SELECT_OBJECT: SELECT_OBJECT,
|
|
29
30
|
SET_BACKGROUND: SET_BACKGROUND,
|
|
31
|
+
DROP_IT: DROP_IT,
|
|
30
32
|
PLACE_ON_FLOOR: PLACE_ON_FLOOR,
|
|
31
33
|
SET_CAMERA_TRANSFORM: SET_CAMERA_TRANSFORM,
|
|
32
34
|
GET_CAMERA_TRANSFORM: GET_CAMERA_TRANSFORM,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Object3D } from 'three';
|
|
2
|
+
import { findSceneRecursive } from '../findSceneRecursive.ts';
|
|
3
|
+
|
|
4
|
+
describe('dive/helper/findSceneRecursive', () => {
|
|
5
|
+
it('should find itself if parent is not set', () => {
|
|
6
|
+
const obj = {} as Object3D;
|
|
7
|
+
|
|
8
|
+
const found = findSceneRecursive(obj);
|
|
9
|
+
|
|
10
|
+
expect(found).toStrictEqual(obj);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should find itself if it has no parent', () => {
|
|
14
|
+
const obj = {
|
|
15
|
+
parent: null,
|
|
16
|
+
} as Object3D;
|
|
17
|
+
|
|
18
|
+
const found = findSceneRecursive(obj);
|
|
19
|
+
|
|
20
|
+
expect(found).toStrictEqual(obj);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should find itself if it has no parent', () => {
|
|
24
|
+
const scene = {
|
|
25
|
+
parent: null,
|
|
26
|
+
} as Object3D;
|
|
27
|
+
|
|
28
|
+
const objparent = {
|
|
29
|
+
parent: scene,
|
|
30
|
+
} as Object3D;
|
|
31
|
+
|
|
32
|
+
const obj = {
|
|
33
|
+
parent: objparent,
|
|
34
|
+
} as Object3D;
|
|
35
|
+
|
|
36
|
+
const found = findSceneRecursive(obj);
|
|
37
|
+
|
|
38
|
+
expect(found).toStrictEqual(scene);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Object3D } from 'three';
|
|
2
|
+
import type DIVEScene from '../../scene/Scene';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Find the scene object of an object.
|
|
6
|
+
*
|
|
7
|
+
* @param object - The object to find the scene of.
|
|
8
|
+
* @returns The scene object.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const findSceneRecursive = (object: Object3D): DIVEScene => {
|
|
12
|
+
if (object.parent) {
|
|
13
|
+
return findSceneRecursive(object.parent);
|
|
14
|
+
}
|
|
15
|
+
return object as DIVEScene;
|
|
16
|
+
}
|
package/src/model/Model.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { Box3, Object3D, Vector3, Vector3Like } from 'three';
|
|
1
|
+
import { Box3, type Mesh, Object3D, Raycaster, Vector3, Vector3Like } from 'three';
|
|
2
2
|
import { DIVESelectable } from '../interface/Selectable';
|
|
3
3
|
import { PRODUCT_LAYER_MASK } from '../constant/VisibilityLayerMask';
|
|
4
4
|
import { DIVEMoveable } from '../interface/Moveable';
|
|
5
5
|
import DIVECommunication from '../com/Communication';
|
|
6
6
|
import type { GLTF, TransformControls } from 'three/examples/jsm/Addons.js';
|
|
7
|
+
import { findSceneRecursive } from '../helper/findSceneRecursive/findSceneRecursive';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* A basic model class.
|
|
@@ -66,6 +67,39 @@ export default class DIVEModel extends Object3D implements DIVESelectable, DIVEM
|
|
|
66
67
|
DIVECommunication.get(this.userData.id)?.PerformAction('UPDATE_OBJECT', { id: this.userData.id, position: this.position, rotation: this.rotation, scale: this.scale });
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
public DropIt(): void {
|
|
71
|
+
if (!this.parent) {
|
|
72
|
+
console.warn('DIVEModel: DropIt() called on a model that is not in the scene.', this);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// calculate the bottom center of the bounding box
|
|
77
|
+
const bottomY = this.boundingBox.min.y * this.scale.y;
|
|
78
|
+
const bbBottomCenter = this.localToWorld(this.boundingBox.getCenter(new Vector3()).multiply(this.scale));
|
|
79
|
+
bbBottomCenter.y = bottomY + this.position.y;
|
|
80
|
+
|
|
81
|
+
// set up raycaster and raycast all scene objects (product layer)
|
|
82
|
+
const raycaster = new Raycaster(bbBottomCenter, new Vector3(0, -1, 0));
|
|
83
|
+
raycaster.layers.mask = PRODUCT_LAYER_MASK;
|
|
84
|
+
const intersections = raycaster.intersectObjects(findSceneRecursive(this).Root.children, true);
|
|
85
|
+
|
|
86
|
+
// if we hit something, move the model to the top on the hit object's bounding box
|
|
87
|
+
if (intersections.length > 0) {
|
|
88
|
+
const mesh = intersections[0].object as Mesh;
|
|
89
|
+
mesh.geometry.computeBoundingBox();
|
|
90
|
+
const meshBB = mesh.geometry.boundingBox!;
|
|
91
|
+
const worldPos = mesh.localToWorld(meshBB.max.clone());
|
|
92
|
+
|
|
93
|
+
const oldPos = this.position.clone();
|
|
94
|
+
const newPos = this.position.clone().setY(worldPos.y).add(new Vector3(0, bottomY, 0));
|
|
95
|
+
this.position.copy(newPos);
|
|
96
|
+
|
|
97
|
+
// if the position changed, update the object in communication
|
|
98
|
+
if (this.position.y === oldPos.y) return;
|
|
99
|
+
DIVECommunication.get(this.userData.id)?.PerformAction('UPDATE_OBJECT', { id: this.userData.id, position: this.position, rotation: this.rotation, scale: this.scale });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
69
103
|
public onMove(): void {
|
|
70
104
|
DIVECommunication.get(this.userData.id)?.PerformAction('UPDATE_OBJECT', { id: this.userData.id, position: this.position, rotation: this.rotation, scale: this.scale });
|
|
71
105
|
}
|
|
@@ -1,11 +1,49 @@
|
|
|
1
1
|
import Model from '../Model';
|
|
2
2
|
import DIVECommunication from '../../com/Communication';
|
|
3
3
|
import { GLTF } from 'three/examples/jsm/Addons';
|
|
4
|
+
import DIVEScene from '../../scene/Scene';
|
|
5
|
+
import { Vector3, Box3, Mesh } from 'three';
|
|
6
|
+
|
|
7
|
+
const intersectObjectsMock = jest.fn();
|
|
4
8
|
|
|
5
9
|
jest.mock('three', () => {
|
|
6
10
|
return {
|
|
7
11
|
Vector3: jest.fn(function (x: number, y: number, z: number) {
|
|
8
|
-
|
|
12
|
+
this.x = x;
|
|
13
|
+
this.y = y;
|
|
14
|
+
this.z = z;
|
|
15
|
+
this.copy = (vec3: Vector3) => {
|
|
16
|
+
this.x = vec3.x;
|
|
17
|
+
this.y = vec3.y;
|
|
18
|
+
this.z = vec3.z;
|
|
19
|
+
return this;
|
|
20
|
+
};
|
|
21
|
+
this.set = (x: number, y: number, z: number) => {
|
|
22
|
+
this.x = x;
|
|
23
|
+
this.y = y;
|
|
24
|
+
this.z = z;
|
|
25
|
+
return this;
|
|
26
|
+
};
|
|
27
|
+
this.multiply = (vec3: Vector3) => {
|
|
28
|
+
this.x *= vec3.x;
|
|
29
|
+
this.y *= vec3.y;
|
|
30
|
+
this.z *= vec3.z;
|
|
31
|
+
return this;
|
|
32
|
+
};
|
|
33
|
+
this.clone = () => {
|
|
34
|
+
return new Vector3(this.x, this.y, this.z);
|
|
35
|
+
};
|
|
36
|
+
this.setY = (y: number) => {
|
|
37
|
+
this.y = y;
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
this.add = (vec3: Vector3) => {
|
|
41
|
+
this.x += vec3.x;
|
|
42
|
+
this.y += vec3.y;
|
|
43
|
+
this.z += vec3.z;
|
|
44
|
+
return this;
|
|
45
|
+
};
|
|
46
|
+
return this;
|
|
9
47
|
}),
|
|
10
48
|
Object3D: jest.fn(function () {
|
|
11
49
|
this.clear = jest.fn();
|
|
@@ -31,12 +69,7 @@ jest.mock('three', () => {
|
|
|
31
69
|
},
|
|
32
70
|
}];
|
|
33
71
|
this.userData = {};
|
|
34
|
-
this.position =
|
|
35
|
-
x: 0,
|
|
36
|
-
y: 0,
|
|
37
|
-
z: 0,
|
|
38
|
-
set: jest.fn(),
|
|
39
|
-
};
|
|
72
|
+
this.position = new Vector3();
|
|
40
73
|
this.rotation = {
|
|
41
74
|
x: 0,
|
|
42
75
|
y: 0,
|
|
@@ -49,11 +82,46 @@ jest.mock('three', () => {
|
|
|
49
82
|
z: 1,
|
|
50
83
|
set: jest.fn(),
|
|
51
84
|
};
|
|
85
|
+
this.localToWorld = (vec3: Vector3) => {
|
|
86
|
+
return vec3;
|
|
87
|
+
};
|
|
88
|
+
this.mesh = new Mesh();
|
|
52
89
|
return this;
|
|
53
90
|
}),
|
|
54
91
|
Box3: jest.fn(function () {
|
|
55
|
-
this.min =
|
|
92
|
+
this.min = new Vector3(Infinity, Infinity, Infinity);
|
|
93
|
+
this.max = new Vector3(-Infinity, -Infinity, -Infinity);
|
|
94
|
+
this.getCenter = jest.fn(() => {
|
|
95
|
+
return new Vector3(0, 0, 0);
|
|
96
|
+
});
|
|
56
97
|
this.expandByObject = jest.fn();
|
|
98
|
+
|
|
99
|
+
return this;
|
|
100
|
+
}),
|
|
101
|
+
Raycaster: jest.fn(function () {
|
|
102
|
+
this.intersectObjects = intersectObjectsMock;
|
|
103
|
+
this.layers = {
|
|
104
|
+
mask: 0,
|
|
105
|
+
};
|
|
106
|
+
return this;
|
|
107
|
+
}),
|
|
108
|
+
Mesh: jest.fn(function () {
|
|
109
|
+
this.geometry = {
|
|
110
|
+
computeBoundingBox: jest.fn(),
|
|
111
|
+
boundingBox: new Box3(),
|
|
112
|
+
};
|
|
113
|
+
this.material = {};
|
|
114
|
+
this.castShadow = true;
|
|
115
|
+
this.receiveShadow = true;
|
|
116
|
+
this.layers = {
|
|
117
|
+
mask: 0,
|
|
118
|
+
};
|
|
119
|
+
this.updateWorldMatrix = jest.fn();
|
|
120
|
+
this.traverse = jest.fn();
|
|
121
|
+
this.removeFromParent = jest.fn();
|
|
122
|
+
this.localToWorld = (vec3: Vector3) => {
|
|
123
|
+
return vec3;
|
|
124
|
+
};
|
|
57
125
|
return this;
|
|
58
126
|
}),
|
|
59
127
|
}
|
|
@@ -151,6 +219,71 @@ describe('dive/model/DIVEModel', () => {
|
|
|
151
219
|
expect(() => model.PlaceOnFloor()).not.toThrow();
|
|
152
220
|
});
|
|
153
221
|
|
|
222
|
+
it('should drop it', () => {
|
|
223
|
+
const comMock = {
|
|
224
|
+
PerformAction: jest.fn(),
|
|
225
|
+
} as unknown as DIVECommunication;
|
|
226
|
+
jest.spyOn(DIVECommunication, 'get').mockReturnValue(comMock);
|
|
227
|
+
|
|
228
|
+
const size = {
|
|
229
|
+
x: 1,
|
|
230
|
+
y: 1,
|
|
231
|
+
z: 1,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const model = new Model();
|
|
235
|
+
model.userData.id = 'something';
|
|
236
|
+
model.position.set(0, 4, 0);
|
|
237
|
+
model['boundingBox'] = {
|
|
238
|
+
min: new Vector3(-size.x / 2, -size.y / 2, -size.z / 2),
|
|
239
|
+
max: new Vector3(size.x / 2, size.y / 2, size.z / 2),
|
|
240
|
+
getCenter: jest.fn(() => {
|
|
241
|
+
return new Vector3(0, 0, 0);
|
|
242
|
+
}),
|
|
243
|
+
} as unknown as Box3;
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
const hitObject = new Mesh();
|
|
247
|
+
hitObject.geometry.boundingBox = new Box3();
|
|
248
|
+
hitObject.geometry.boundingBox.max = new Vector3(0, 2, 0);
|
|
249
|
+
intersectObjectsMock.mockReturnValue([{
|
|
250
|
+
object: hitObject,
|
|
251
|
+
|
|
252
|
+
}]);
|
|
253
|
+
|
|
254
|
+
const scene = {
|
|
255
|
+
parent: null,
|
|
256
|
+
Root: {
|
|
257
|
+
children: [
|
|
258
|
+
model,
|
|
259
|
+
],
|
|
260
|
+
},
|
|
261
|
+
} as unknown as DIVEScene;
|
|
262
|
+
scene.Root.parent = scene;
|
|
263
|
+
|
|
264
|
+
// test when parent is not set
|
|
265
|
+
console.warn = jest.fn();
|
|
266
|
+
expect(() => model.DropIt()).not.toThrow();
|
|
267
|
+
expect(console.warn).toHaveBeenCalledTimes(1);
|
|
268
|
+
|
|
269
|
+
model.parent = scene.Root;
|
|
270
|
+
|
|
271
|
+
expect(() => model.DropIt()).not.toThrow();
|
|
272
|
+
expect(model.position.y).toBe(1.5);
|
|
273
|
+
expect(comMock.PerformAction).toHaveBeenCalledTimes(1);
|
|
274
|
+
|
|
275
|
+
expect(() => model.DropIt()).not.toThrow();
|
|
276
|
+
expect(comMock.PerformAction).toHaveBeenCalledTimes(1);
|
|
277
|
+
|
|
278
|
+
// reset for PerformAction to be called again
|
|
279
|
+
model.position.y = 2;
|
|
280
|
+
jest.spyOn(DIVECommunication, 'get').mockReturnValueOnce(undefined);
|
|
281
|
+
expect(() => model.DropIt()).not.toThrow();
|
|
282
|
+
expect(comMock.PerformAction).toHaveBeenCalledTimes(1);
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
});
|
|
286
|
+
|
|
154
287
|
it('should onMove', () => {
|
|
155
288
|
const model = new Model();
|
|
156
289
|
model.userData.id = 'something';
|
/package/src/helper/getObjectDelta/__test__/{getObjectDelta.spec.ts → getObjectDelta.test.ts}
RENAMED
|
File without changes
|