@shopware-ag/dive 1.1.2 → 1.3.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 +333 -60
- package/build/dive.cjs.map +1 -1
- package/build/dive.d.cts +73 -6
- package/build/dive.d.ts +73 -6
- package/build/dive.js +335 -62
- package/build/dive.js.map +1 -1
- package/package.json +1 -1
- package/src/__test__/DIVE.test.ts +2 -0
- package/src/com/Communication.ts +21 -3
- package/src/com/__test__/Communication.test.ts +44 -6
- package/src/com/actions/index.ts +2 -0
- package/src/com/actions/object/getobjects.ts +2 -2
- package/src/com/actions/object/model/dropit.ts +4 -0
- package/src/dive.ts +7 -0
- package/src/gizmo/Gizmo.ts +130 -0
- package/src/gizmo/handles/AxisHandle.ts +124 -0
- package/src/gizmo/handles/RadialHandle.ts +119 -0
- package/src/gizmo/handles/ScaleHandle.ts +152 -0
- package/src/gizmo/plane/GizmoPlane.ts +85 -0
- package/src/gizmo/rotate/RotateGizmo.ts +95 -0
- package/src/gizmo/scale/ScaleGizmo.ts +97 -0
- package/src/gizmo/translate/TranslateGizmo.ts +88 -0
- package/src/helper/findSceneRecursive/__test__/findSceneRecursive.test.ts +40 -0
- package/src/helper/findSceneRecursive/findSceneRecursive.ts +16 -0
- package/src/interface/Draggable.ts +34 -0
- package/src/interface/Hoverable.ts +33 -0
- package/src/interface/Moveable.ts +0 -2
- package/src/interface/Selectable.ts +6 -0
- package/src/interface/__test__/Interfaces.test.ts +56 -0
- package/src/math/index.ts +3 -0
- package/src/math/signedAngleTo/__test__/signedAngleTo.test.ts +14 -0
- package/src/math/signedAngleTo/signedAngleTo.ts +13 -0
- package/src/model/Model.ts +35 -1
- package/src/model/__test__/Model.test.ts +141 -8
- package/src/scene/root/lightroot/LightRoot.ts +17 -3
- package/src/scene/root/lightroot/__test__/LightRoot.test.ts +12 -3
- package/src/scene/root/modelroot/ModelRoot.ts +17 -3
- package/src/scene/root/modelroot/__test__/ModelRoot.test.ts +13 -14
- package/src/toolbox/BaseTool.ts +254 -4
- package/src/toolbox/Toolbox.ts +6 -0
- package/src/toolbox/__test__/BaseTool.test.ts +389 -0
- package/src/toolbox/__test__/Toolbox.test.ts +8 -0
- package/src/toolbox/select/SelectTool.ts +29 -65
- package/src/toolbox/select/__test__/SelectTool.test.ts +57 -25
- package/src/toolbox/transform/TransformTool.ts +48 -0
- /package/src/helper/getObjectDelta/__test__/{getObjectDelta.spec.ts → getObjectDelta.test.ts} +0 -0
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';
|
|
@@ -2,8 +2,9 @@ import { Color, Object3D } from "three";
|
|
|
2
2
|
import { type COMLight } from "../../../com/types.ts";
|
|
3
3
|
import DIVEAmbientLight from "../../../light/AmbientLight.ts";
|
|
4
4
|
import DIVEPointLight from "../../../light/PointLight.ts";
|
|
5
|
-
import type { DIVEMoveable } from "../../../interface/Moveable.ts";
|
|
6
5
|
import DIVESceneLight from "../../../light/SceneLight.ts";
|
|
6
|
+
import { type TransformControls } from "three/examples/jsm/Addons";
|
|
7
|
+
import type DIVEScene from "../../Scene.ts";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* A basic scene node to hold all lights.
|
|
@@ -76,10 +77,23 @@ export default class DIVELightRoot extends Object3D {
|
|
|
76
77
|
return;
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
// _______________________________________________________
|
|
81
|
+
// this is only neccessary due to using the old TransformControls instead of the new DIVEGizmo
|
|
82
|
+
const findScene = (object: Object3D): DIVEScene => {
|
|
83
|
+
if (object.parent !== null) {
|
|
84
|
+
return findScene(object.parent);
|
|
85
|
+
};
|
|
86
|
+
return object as DIVEScene;
|
|
81
87
|
}
|
|
82
88
|
|
|
89
|
+
const scene = findScene(sceneObject);
|
|
90
|
+
scene.children.find((object) => {
|
|
91
|
+
if ('isTransformControls' in object) {
|
|
92
|
+
(object as TransformControls).detach();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
// _______________________________________________________
|
|
96
|
+
|
|
83
97
|
this.remove(sceneObject);
|
|
84
98
|
}
|
|
85
99
|
}
|
|
@@ -2,6 +2,7 @@ import { TransformControls } from 'three/examples/jsm/Addons';
|
|
|
2
2
|
import { COMLight } from '../../../../com';
|
|
3
3
|
import { DIVEMoveable } from '../../../../interface/Moveable';
|
|
4
4
|
import DIVELightRoot from '../LightRoot';
|
|
5
|
+
import type DIVEScene from '../../../Scene';
|
|
5
6
|
|
|
6
7
|
const mock_SetPosition = jest.fn();
|
|
7
8
|
const mock_SetIntensity = jest.fn();
|
|
@@ -115,6 +116,17 @@ describe('dive/scene/root/lightroot/DIVELightRoot', () => {
|
|
|
115
116
|
|
|
116
117
|
it('should delete light', () => {
|
|
117
118
|
const lightRoot = new DIVELightRoot();
|
|
119
|
+
|
|
120
|
+
const sceneParent = {
|
|
121
|
+
parent: null,
|
|
122
|
+
children: [
|
|
123
|
+
{
|
|
124
|
+
isTransformControls: true,
|
|
125
|
+
detach: jest.fn(),
|
|
126
|
+
}
|
|
127
|
+
],
|
|
128
|
+
}
|
|
129
|
+
lightRoot.parent = sceneParent as unknown as DIVEScene;
|
|
118
130
|
expect(() => lightRoot.DeleteLight({ id: undefined })).not.toThrow();
|
|
119
131
|
|
|
120
132
|
expect(() => lightRoot.DeleteLight({ id: 'test_id' })).not.toThrow();
|
|
@@ -128,9 +140,6 @@ describe('dive/scene/root/lightroot/DIVELightRoot', () => {
|
|
|
128
140
|
|
|
129
141
|
lightRoot.UpdateLight({ id: 'test_id', type: 'point', position: { x: 1, y: 2, z: 3 }, intensity: 0.5, color: 0x123456 });
|
|
130
142
|
(lightRoot.children[0] as unknown as DIVEMoveable).isMoveable = true;
|
|
131
|
-
(lightRoot.children[0] as unknown as DIVEMoveable).gizmo = {
|
|
132
|
-
detach: jest.fn(),
|
|
133
|
-
} as unknown as TransformControls;
|
|
134
143
|
expect(() => lightRoot.DeleteLight({ id: 'test_id' })).not.toThrow();
|
|
135
144
|
expect(lightRoot.children).toHaveLength(0);
|
|
136
145
|
});
|
|
@@ -3,7 +3,8 @@ import { COMModel } from "../../../com/types.ts";
|
|
|
3
3
|
import Model from "../../../model/Model.ts";
|
|
4
4
|
import DIVELoadingManager from "../../../loadingmanager/LoadingManager.ts";
|
|
5
5
|
import DIVECommunication from "../../../com/Communication.ts";
|
|
6
|
-
import type
|
|
6
|
+
import { type TransformControls } from "three/examples/jsm/Addons";
|
|
7
|
+
import type DIVEScene from "../../Scene.ts";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* A basic scene node to hold all models.
|
|
@@ -65,10 +66,23 @@ export default class DIVEModelRoot extends Object3D {
|
|
|
65
66
|
return;
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
// _______________________________________________________
|
|
70
|
+
// this is only neccessary due to using the old TransformControls instead of the new DIVEGizmo
|
|
71
|
+
const findScene = (object: Object3D): DIVEScene => {
|
|
72
|
+
if (object.parent !== null) {
|
|
73
|
+
return findScene(object.parent);
|
|
74
|
+
};
|
|
75
|
+
return object as DIVEScene;
|
|
70
76
|
}
|
|
71
77
|
|
|
78
|
+
const scene = findScene(sceneObject);
|
|
79
|
+
scene.children.find((object) => {
|
|
80
|
+
if ('isTransformControls' in object) {
|
|
81
|
+
(object as TransformControls).detach();
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
// _______________________________________________________
|
|
85
|
+
|
|
72
86
|
this.remove(sceneObject);
|
|
73
87
|
}
|
|
74
88
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import DIVEModelRoot from '../ModelRoot';
|
|
2
2
|
import DIVECommunication from '../../../../com/Communication';
|
|
3
|
-
import { TransformControls } from 'three/examples/jsm/Addons';
|
|
4
3
|
import { DIVEMoveable } from '../../../../interface/Moveable';
|
|
4
|
+
import type DIVEScene from '../../../Scene';
|
|
5
5
|
|
|
6
6
|
const mock_LoadGLTF = jest.fn().mockResolvedValue({});
|
|
7
7
|
const mock_SetPosition = jest.fn();
|
|
@@ -154,6 +154,18 @@ describe('dive/scene/root/modelroot/DIVEModelRoot', () => {
|
|
|
154
154
|
|
|
155
155
|
it('should delete model', async () => {
|
|
156
156
|
const modelRoot = new DIVEModelRoot();
|
|
157
|
+
|
|
158
|
+
const sceneParent = {
|
|
159
|
+
parent: null,
|
|
160
|
+
children: [
|
|
161
|
+
{
|
|
162
|
+
isTransformControls: true,
|
|
163
|
+
detach: jest.fn(),
|
|
164
|
+
}
|
|
165
|
+
],
|
|
166
|
+
}
|
|
167
|
+
modelRoot.parent = sceneParent as unknown as DIVEScene;
|
|
168
|
+
|
|
157
169
|
expect(() => modelRoot.DeleteModel({ id: undefined })).not.toThrow();
|
|
158
170
|
expect(consoleWarnSpy).toHaveBeenCalled();
|
|
159
171
|
consoleWarnSpy.mockClear();
|
|
@@ -168,19 +180,6 @@ describe('dive/scene/root/modelroot/DIVEModelRoot', () => {
|
|
|
168
180
|
})).not.toThrow();
|
|
169
181
|
(modelRoot.children[0] as unknown as DIVEMoveable).isMoveable = true;
|
|
170
182
|
expect(() => modelRoot.DeleteModel({ id: 'test_id' })).not.toThrow();
|
|
171
|
-
|
|
172
|
-
await expect(() => modelRoot.UpdateModel({
|
|
173
|
-
id: 'test_id',
|
|
174
|
-
uri: 'not a real uri',
|
|
175
|
-
position: { x: 1, y: 2, z: 3 },
|
|
176
|
-
rotation: { x: 1, y: 2, z: 3 },
|
|
177
|
-
scale: { x: 1, y: 2, z: 3 },
|
|
178
|
-
})).not.toThrow();
|
|
179
|
-
(modelRoot.children[0] as unknown as DIVEMoveable).isMoveable = true;
|
|
180
|
-
(modelRoot.children[0] as unknown as DIVEMoveable).gizmo = {
|
|
181
|
-
detach: jest.fn(),
|
|
182
|
-
} as unknown as TransformControls;
|
|
183
|
-
expect(() => modelRoot.DeleteModel({ id: 'test_id' })).not.toThrow();
|
|
184
183
|
expect(modelRoot.children).toHaveLength(0);
|
|
185
184
|
});
|
|
186
185
|
});
|
package/src/toolbox/BaseTool.ts
CHANGED
|
@@ -1,18 +1,268 @@
|
|
|
1
|
+
import { Intersection, Object3D, Raycaster, Vector2, Vector3 } from "three";
|
|
2
|
+
import { PRODUCT_LAYER_MASK, UI_LAYER_MASK } from "../constant/VisibilityLayerMask";
|
|
3
|
+
import DIVEScene from "../scene/Scene";
|
|
4
|
+
import DIVEOrbitControls from "../controls/OrbitControls";
|
|
5
|
+
import { DIVEDraggable, findDraggableInterface } from "../interface/Draggable";
|
|
6
|
+
import { DIVEHoverable, findHoverableInterface } from "../interface/Hoverable";
|
|
7
|
+
|
|
8
|
+
export type DraggableEvent = {
|
|
9
|
+
dragStart: Vector3;
|
|
10
|
+
dragCurrent: Vector3;
|
|
11
|
+
dragEnd: Vector3;
|
|
12
|
+
dragDelta: Vector3;
|
|
13
|
+
}
|
|
14
|
+
|
|
1
15
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
2
16
|
export default abstract class DIVEBaseTool {
|
|
3
|
-
|
|
17
|
+
readonly POINTER_DRAG_THRESHOLD: number = 0.001;
|
|
18
|
+
|
|
19
|
+
public name: string;
|
|
20
|
+
|
|
21
|
+
protected _canvas: HTMLElement;
|
|
22
|
+
protected _scene: DIVEScene;
|
|
23
|
+
protected _controller: DIVEOrbitControls;
|
|
24
|
+
|
|
25
|
+
// general pointer members
|
|
26
|
+
protected _pointer: Vector2;
|
|
27
|
+
|
|
28
|
+
protected get _pointerAnyDown(): boolean {
|
|
29
|
+
return this._pointerPrimaryDown
|
|
30
|
+
|| this._pointerMiddleDown
|
|
31
|
+
|| this._pointerSecondaryDown;
|
|
32
|
+
};
|
|
33
|
+
protected _pointerPrimaryDown: boolean;
|
|
34
|
+
protected _pointerMiddleDown: boolean;
|
|
35
|
+
protected _pointerSecondaryDown: boolean;
|
|
36
|
+
protected _lastPointerDown: Vector2;
|
|
37
|
+
protected _lastPointerUp: Vector2;
|
|
38
|
+
|
|
39
|
+
// raycast members
|
|
40
|
+
protected _raycaster: Raycaster;
|
|
41
|
+
protected _intersects: Intersection[];
|
|
4
42
|
|
|
5
|
-
|
|
43
|
+
// hovering members
|
|
44
|
+
protected _hovered: (Object3D & DIVEHoverable) | null;
|
|
45
|
+
|
|
46
|
+
// dragging members
|
|
47
|
+
protected _dragging: boolean;
|
|
48
|
+
protected _dragStart: Vector3;
|
|
49
|
+
protected _dragCurrent: Vector3;
|
|
50
|
+
protected _dragEnd: Vector3;
|
|
51
|
+
protected _dragDelta: Vector3;
|
|
52
|
+
protected _draggable: DIVEDraggable | null;
|
|
53
|
+
protected _dragRaycastOnObjects: Object3D[] | null;
|
|
54
|
+
|
|
55
|
+
protected constructor(scene: DIVEScene, controller: DIVEOrbitControls) {
|
|
6
56
|
this.name = "BaseTool";
|
|
57
|
+
|
|
58
|
+
this._canvas = controller.domElement;
|
|
59
|
+
this._scene = scene;
|
|
60
|
+
this._controller = controller;
|
|
61
|
+
|
|
62
|
+
this._pointer = new Vector2();
|
|
63
|
+
|
|
64
|
+
this._pointerPrimaryDown = false;
|
|
65
|
+
this._pointerMiddleDown = false;
|
|
66
|
+
this._pointerSecondaryDown = false;
|
|
67
|
+
|
|
68
|
+
this._lastPointerDown = new Vector2();
|
|
69
|
+
this._lastPointerUp = new Vector2();
|
|
70
|
+
|
|
71
|
+
this._raycaster = new Raycaster();
|
|
72
|
+
this._raycaster.layers.mask = PRODUCT_LAYER_MASK | UI_LAYER_MASK;
|
|
73
|
+
this._intersects = [];
|
|
74
|
+
|
|
75
|
+
this._hovered = null;
|
|
76
|
+
|
|
77
|
+
this._dragging = false;
|
|
78
|
+
this._dragStart = new Vector3();
|
|
79
|
+
this._dragCurrent = new Vector3();
|
|
80
|
+
this._dragEnd = new Vector3();
|
|
81
|
+
this._dragDelta = new Vector3();
|
|
82
|
+
this._draggable = null;
|
|
83
|
+
this._dragRaycastOnObjects = null;
|
|
7
84
|
}
|
|
8
85
|
|
|
9
86
|
public Activate(): void { }
|
|
10
87
|
|
|
11
88
|
public Deactivate(): void { }
|
|
12
89
|
|
|
13
|
-
public onPointerDown(e: PointerEvent): void {
|
|
90
|
+
public onPointerDown(e: PointerEvent): void {
|
|
91
|
+
switch (e.button) {
|
|
92
|
+
case 0:
|
|
93
|
+
this._pointerPrimaryDown = true;
|
|
94
|
+
break;
|
|
95
|
+
case 1:
|
|
96
|
+
this._pointerMiddleDown = true;
|
|
97
|
+
break;
|
|
98
|
+
case 2:
|
|
99
|
+
this._pointerSecondaryDown = true;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this._lastPointerDown.copy(this._pointer);
|
|
104
|
+
|
|
105
|
+
this._draggable = findDraggableInterface(this._intersects[0]?.object) || null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public onDragStart(e: PointerEvent): void {
|
|
109
|
+
if (!this._draggable) return;
|
|
110
|
+
|
|
111
|
+
if (this._dragRaycastOnObjects !== null) {
|
|
112
|
+
this._intersects = this._raycaster.intersectObjects(this._dragRaycastOnObjects, true);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (this._intersects.length === 0) return;
|
|
116
|
+
|
|
117
|
+
this._dragStart.copy(this._intersects[0].point.clone());
|
|
118
|
+
this._dragCurrent.copy(this._intersects[0].point.clone());
|
|
119
|
+
this._dragEnd.copy(this._dragStart.clone());
|
|
120
|
+
this._dragDelta.set(0, 0, 0);
|
|
121
|
+
|
|
122
|
+
if (this._draggable && this._draggable.onDragStart) {
|
|
123
|
+
this._draggable.onDragStart({
|
|
124
|
+
dragStart: this._dragStart,
|
|
125
|
+
dragCurrent: this._dragCurrent,
|
|
126
|
+
dragEnd: this._dragEnd,
|
|
127
|
+
dragDelta: this._dragDelta,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
this._dragging = true;
|
|
131
|
+
this._controller.enabled = false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
public onPointerMove(e: PointerEvent): void {
|
|
136
|
+
// update pointer
|
|
137
|
+
this._pointer.x = (e.offsetX / this._canvas.clientWidth) * 2 - 1;
|
|
138
|
+
this._pointer.y = -(e.offsetY / this._canvas.clientHeight) * 2 + 1;
|
|
139
|
+
|
|
140
|
+
// set raycaster
|
|
141
|
+
this._raycaster.setFromCamera(this._pointer, this._controller.object);
|
|
14
142
|
|
|
15
|
-
|
|
143
|
+
// refresh intersects
|
|
144
|
+
this._intersects = this.raycast(this._scene.children);
|
|
145
|
+
|
|
146
|
+
// handle hover
|
|
147
|
+
const hoverable = findHoverableInterface(this._intersects[0]?.object);
|
|
148
|
+
if (this._intersects[0] && hoverable) {
|
|
149
|
+
if (!this._hovered) {
|
|
150
|
+
if (hoverable.onPointerEnter) hoverable.onPointerEnter(this._intersects[0]);
|
|
151
|
+
this._hovered = hoverable;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (this._hovered.uuid !== hoverable.uuid) {
|
|
156
|
+
if (this._hovered.onPointerLeave) this._hovered.onPointerLeave();
|
|
157
|
+
if (hoverable.onPointerEnter) hoverable.onPointerEnter(this._intersects[0]);
|
|
158
|
+
this._hovered = hoverable;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (hoverable.onPointerOver) hoverable.onPointerOver(this._intersects[0]);
|
|
163
|
+
this._hovered = hoverable;
|
|
164
|
+
|
|
165
|
+
} else {
|
|
166
|
+
if (this._hovered) {
|
|
167
|
+
if (this._hovered.onPointerLeave) this._hovered.onPointerLeave();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this._hovered = null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// handle drag
|
|
174
|
+
if (this._pointerAnyDown) {
|
|
175
|
+
if (!this._dragging) {
|
|
176
|
+
this.onDragStart(e);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
this.onDrag(e);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
public onDrag(e: PointerEvent): void {
|
|
184
|
+
if (this._dragRaycastOnObjects !== null) {
|
|
185
|
+
this._intersects = this._raycaster.intersectObjects(this._dragRaycastOnObjects, true);
|
|
186
|
+
}
|
|
187
|
+
const intersect = this._intersects[0];
|
|
188
|
+
if (!intersect) return;
|
|
189
|
+
|
|
190
|
+
this._dragCurrent.copy(intersect.point.clone());
|
|
191
|
+
this._dragEnd.copy(intersect.point.clone());
|
|
192
|
+
this._dragDelta.subVectors(this._dragCurrent.clone(), this._dragStart.clone());
|
|
193
|
+
|
|
194
|
+
if (this._draggable && this._draggable.onDrag) {
|
|
195
|
+
this._draggable.onDrag({
|
|
196
|
+
dragStart: this._dragStart,
|
|
197
|
+
dragCurrent: this._dragCurrent,
|
|
198
|
+
dragEnd: this._dragEnd,
|
|
199
|
+
dragDelta: this._dragDelta,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
public onPointerUp(e: PointerEvent): void {
|
|
205
|
+
if (this.pointerWasDragged() || this._dragging) {
|
|
206
|
+
if (this._draggable) {
|
|
207
|
+
this.onDragEnd(e);
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
this.onClick(e);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
switch (e.button) {
|
|
214
|
+
case 0:
|
|
215
|
+
this._pointerPrimaryDown = false;
|
|
216
|
+
break;
|
|
217
|
+
case 1:
|
|
218
|
+
this._pointerMiddleDown = false;
|
|
219
|
+
break;
|
|
220
|
+
case 2:
|
|
221
|
+
this._pointerSecondaryDown = false;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
this._lastPointerUp.copy(this._pointer);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
public onClick(e: PointerEvent): void { }
|
|
229
|
+
|
|
230
|
+
public onDragEnd(e: PointerEvent): void {
|
|
231
|
+
const intersect = this._intersects[0];
|
|
232
|
+
if (intersect) {
|
|
233
|
+
this._dragEnd.copy(intersect.point.clone());
|
|
234
|
+
this._dragCurrent.copy(intersect.point.clone());
|
|
235
|
+
this._dragDelta.subVectors(this._dragCurrent.clone(), this._dragStart.clone());
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (this._draggable && this._draggable.onDragEnd) {
|
|
239
|
+
this._draggable.onDragEnd({
|
|
240
|
+
dragStart: this._dragStart,
|
|
241
|
+
dragCurrent: this._dragCurrent,
|
|
242
|
+
dragEnd: this._dragEnd,
|
|
243
|
+
dragDelta: this._dragDelta,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
this._draggable = null;
|
|
248
|
+
this._dragging = false;
|
|
249
|
+
|
|
250
|
+
this._dragStart.set(0, 0, 0);
|
|
251
|
+
this._dragCurrent.set(0, 0, 0);
|
|
252
|
+
this._dragEnd.set(0, 0, 0);
|
|
253
|
+
this._dragDelta.set(0, 0, 0);
|
|
254
|
+
|
|
255
|
+
this._controller.enabled = true;
|
|
256
|
+
}
|
|
16
257
|
|
|
17
258
|
public onWheel(e: WheelEvent): void { }
|
|
259
|
+
|
|
260
|
+
protected raycast(objects?: Object3D[]): Intersection[] {
|
|
261
|
+
if (objects !== undefined) return this._raycaster.intersectObjects(objects, true);
|
|
262
|
+
return this._raycaster.intersectObjects(this._scene.children, true);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private pointerWasDragged(): boolean {
|
|
266
|
+
return this._lastPointerDown.distanceTo(this._pointer) > this.POINTER_DRAG_THRESHOLD;
|
|
267
|
+
}
|
|
18
268
|
}
|
package/src/toolbox/Toolbox.ts
CHANGED
|
@@ -21,11 +21,13 @@ export default class DIVEToolbox {
|
|
|
21
21
|
constructor(scene: DIVEScene, controller: DIVEOrbitControls) {
|
|
22
22
|
this.selectTool = new DIVESelectTool(scene, controller);
|
|
23
23
|
|
|
24
|
+
controller.domElement.addEventListener('pointermove', this.onPointerMove.bind(this));
|
|
24
25
|
controller.domElement.addEventListener('pointerdown', this.onPointerDown.bind(this));
|
|
25
26
|
controller.domElement.addEventListener('pointerup', this.onPointerUp.bind(this));
|
|
26
27
|
controller.domElement.addEventListener('wheel', this.onWheel.bind(this));
|
|
27
28
|
|
|
28
29
|
this.removeListenersCallback = () => {
|
|
30
|
+
controller.domElement.removeEventListener('pointermove', this.onPointerMove.bind(this));
|
|
29
31
|
controller.domElement.removeEventListener('pointerdown', this.onPointerDown.bind(this));
|
|
30
32
|
controller.domElement.removeEventListener('pointerup', this.onPointerUp.bind(this));
|
|
31
33
|
controller.domElement.removeEventListener('wheel', this.onWheel.bind(this));
|
|
@@ -62,6 +64,10 @@ export default class DIVEToolbox {
|
|
|
62
64
|
this.selectTool.SetGizmoMode(mode);
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
public onPointerMove(e: PointerEvent): void {
|
|
68
|
+
this.activeTool.onPointerMove(e);
|
|
69
|
+
}
|
|
70
|
+
|
|
65
71
|
public onPointerDown(e: PointerEvent): void {
|
|
66
72
|
this.activeTool.onPointerDown(e);
|
|
67
73
|
}
|