@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.
Files changed (47) hide show
  1. package/README.md +1 -0
  2. package/build/dive.cjs +333 -60
  3. package/build/dive.cjs.map +1 -1
  4. package/build/dive.d.cts +73 -6
  5. package/build/dive.d.ts +73 -6
  6. package/build/dive.js +335 -62
  7. package/build/dive.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/__test__/DIVE.test.ts +2 -0
  10. package/src/com/Communication.ts +21 -3
  11. package/src/com/__test__/Communication.test.ts +44 -6
  12. package/src/com/actions/index.ts +2 -0
  13. package/src/com/actions/object/getobjects.ts +2 -2
  14. package/src/com/actions/object/model/dropit.ts +4 -0
  15. package/src/dive.ts +7 -0
  16. package/src/gizmo/Gizmo.ts +130 -0
  17. package/src/gizmo/handles/AxisHandle.ts +124 -0
  18. package/src/gizmo/handles/RadialHandle.ts +119 -0
  19. package/src/gizmo/handles/ScaleHandle.ts +152 -0
  20. package/src/gizmo/plane/GizmoPlane.ts +85 -0
  21. package/src/gizmo/rotate/RotateGizmo.ts +95 -0
  22. package/src/gizmo/scale/ScaleGizmo.ts +97 -0
  23. package/src/gizmo/translate/TranslateGizmo.ts +88 -0
  24. package/src/helper/findSceneRecursive/__test__/findSceneRecursive.test.ts +40 -0
  25. package/src/helper/findSceneRecursive/findSceneRecursive.ts +16 -0
  26. package/src/interface/Draggable.ts +34 -0
  27. package/src/interface/Hoverable.ts +33 -0
  28. package/src/interface/Moveable.ts +0 -2
  29. package/src/interface/Selectable.ts +6 -0
  30. package/src/interface/__test__/Interfaces.test.ts +56 -0
  31. package/src/math/index.ts +3 -0
  32. package/src/math/signedAngleTo/__test__/signedAngleTo.test.ts +14 -0
  33. package/src/math/signedAngleTo/signedAngleTo.ts +13 -0
  34. package/src/model/Model.ts +35 -1
  35. package/src/model/__test__/Model.test.ts +141 -8
  36. package/src/scene/root/lightroot/LightRoot.ts +17 -3
  37. package/src/scene/root/lightroot/__test__/LightRoot.test.ts +12 -3
  38. package/src/scene/root/modelroot/ModelRoot.ts +17 -3
  39. package/src/scene/root/modelroot/__test__/ModelRoot.test.ts +13 -14
  40. package/src/toolbox/BaseTool.ts +254 -4
  41. package/src/toolbox/Toolbox.ts +6 -0
  42. package/src/toolbox/__test__/BaseTool.test.ts +389 -0
  43. package/src/toolbox/__test__/Toolbox.test.ts +8 -0
  44. package/src/toolbox/select/SelectTool.ts +29 -65
  45. package/src/toolbox/select/__test__/SelectTool.test.ts +57 -25
  46. package/src/toolbox/transform/TransformTool.ts +48 -0
  47. /package/src/helper/getObjectDelta/__test__/{getObjectDelta.spec.ts → getObjectDelta.test.ts} +0 -0
@@ -0,0 +1,85 @@
1
+ import { Mesh, MeshBasicMaterial, Object3D, PlaneGeometry } from "three";
2
+ import { UI_LAYER_MASK } from "../../constant/VisibilityLayerMask";
3
+ import { DIVEGizmoAxis, DIVEGizmoMode } from "../Gizmo";
4
+
5
+ export class DIVEGizmoPlane extends Object3D {
6
+ private _meshX: Mesh;
7
+ public get XPlane(): Mesh {
8
+ return this._meshX;
9
+ }
10
+ private _meshY: Mesh;
11
+ public get YPlane(): Mesh {
12
+ return this._meshY;
13
+ }
14
+ private _meshZ: Mesh;
15
+ public get ZPlane(): Mesh {
16
+ return this._meshZ;
17
+ }
18
+
19
+ constructor() {
20
+ super();
21
+ this.name = "DIVEGizmoPlane";
22
+
23
+ const material = new MeshBasicMaterial({
24
+ transparent: true,
25
+ opacity: 0.15,
26
+ depthTest: false,
27
+ depthWrite: false,
28
+ side: 2,
29
+ });
30
+
31
+ const geoX = new PlaneGeometry(100, 100, 2, 2);
32
+ const matX = material.clone();
33
+ matX.color.set(0xff0000);
34
+ this._meshX = new Mesh(geoX, matX);
35
+ this._meshX.layers.mask = UI_LAYER_MASK;
36
+ this._meshX.rotateY(Math.PI / 2);
37
+
38
+ const geoY = new PlaneGeometry(100, 100, 2, 2);
39
+ const matY = material.clone();
40
+ matY.color.set(0x00ff00);
41
+ this._meshY = new Mesh(geoY, matY);
42
+ this._meshY.layers.mask = UI_LAYER_MASK;
43
+ this._meshY.rotateX(-Math.PI / 2);
44
+
45
+ const geoZ = new PlaneGeometry(100, 100, 2, 2);
46
+ const matZ = material.clone();
47
+ matZ.color.set(0x0000ff);
48
+ this._meshZ = new Mesh(geoZ, matZ);
49
+ this._meshZ.layers.mask = UI_LAYER_MASK;
50
+ }
51
+
52
+ public assemble(mode: DIVEGizmoMode, axis: DIVEGizmoAxis): void {
53
+ this.clear();
54
+
55
+ if (mode === 'translate' || mode === 'scale') {
56
+ switch (axis) {
57
+ case 'x':
58
+ this.add(this._meshY);
59
+ this.add(this._meshZ);
60
+ break;
61
+ case 'y':
62
+ this.add(this._meshX);
63
+ this.add(this._meshZ);
64
+ break;
65
+ case 'z':
66
+ this.add(this._meshX);
67
+ this.add(this._meshY);
68
+ break;
69
+ }
70
+ } else if (mode === 'rotate') {
71
+ switch (axis) {
72
+ case 'x':
73
+ this.add(this._meshX);
74
+ break;
75
+ case 'y':
76
+ this.add(this._meshY);
77
+ break;
78
+ case 'z':
79
+ this.add(this._meshZ);
80
+ break;
81
+ }
82
+ }
83
+
84
+ }
85
+ }
@@ -0,0 +1,95 @@
1
+ import { Euler, Object3D, Vector3 } from "three";
2
+ import { AxesColorBlue, AxesColorGreen, AxesColorRed } from "../../constant/AxisHelperColors";
3
+ import DIVEOrbitControls from "../../controls/OrbitControls";
4
+ import { DIVERadialHandle } from "../handles/RadialHandle";
5
+ import { DIVEGizmo, DIVEGizmoAxis } from "../Gizmo";
6
+ import { DraggableEvent } from "../../toolbox/BaseTool";
7
+ import { DIVEMath } from "../../math";
8
+
9
+ export class DIVERotateGizmo extends Object3D {
10
+ public children: DIVERadialHandle[];
11
+
12
+ private _controller: DIVEOrbitControls;
13
+
14
+ private _startRot: Euler | null;
15
+
16
+ constructor(controller: DIVEOrbitControls) {
17
+ super();
18
+
19
+ this.name = "DIVERotateGizmo";
20
+
21
+ this.children = [];
22
+
23
+ this._startRot = null;
24
+
25
+ this._controller = controller;
26
+
27
+ this.add(new DIVERadialHandle('x', 1, Math.PI / 2, new Vector3(1, 0, 0), AxesColorRed));
28
+ this.add(new DIVERadialHandle('y', 1, -Math.PI / 2, new Vector3(0, 1, 0), AxesColorGreen));
29
+ this.add(new DIVERadialHandle('z', 1, Math.PI / 2, new Vector3(0, 0, 1), AxesColorBlue));
30
+ }
31
+
32
+ public reset(): void {
33
+ this.children.forEach((child) => {
34
+ child.reset();
35
+ });
36
+ }
37
+
38
+ private handleHighlight(axis: DIVEGizmoAxis, value: boolean, dragged: boolean): void {
39
+ // Set highlight state for all handles.
40
+ this.children.forEach((child) => {
41
+ if (dragged) {
42
+ // Dragging has priority when it comes to highlighting.
43
+ child.highlight = child.axis === axis && dragged;
44
+ } else {
45
+ // If nothing is dragged, decide on hovered state.
46
+ child.highlight = child.axis === axis && value;
47
+ }
48
+ });
49
+ }
50
+
51
+ public onHandleHover(handle: DIVERadialHandle, value: boolean): void {
52
+ // If _startRot is set, it means there is a drag operation in progress.
53
+ // While dragging, we don't want to change the hover state.
54
+ if (this._startRot) return;
55
+
56
+ if (!this.parent) return;
57
+ if (!this.parent.parent) return;
58
+ (this.parent.parent as DIVEGizmo).onHover('rotate', handle.axis, value);
59
+
60
+ this.handleHighlight(handle.axis, value, false);
61
+ }
62
+
63
+ public onHandleDragStart(handle: DIVERadialHandle): void {
64
+ if (!this.parent) return;
65
+ if (!this.parent.parent) return;
66
+
67
+ const object = (this.parent.parent as DIVEGizmo).object;
68
+ if (!object) return;
69
+
70
+ this._startRot = object.rotation.clone();
71
+ this.handleHighlight(handle.axis, true, true);
72
+ }
73
+
74
+ public onHandleDrag(handle: DIVERadialHandle, e: DraggableEvent): void {
75
+ if (!this._startRot) return;
76
+ if (!this.parent) return;
77
+ if (!this.parent.parent) return;
78
+ if (!('onChange' in this.parent.parent)) return;
79
+
80
+ const currentVector = e.dragCurrent.clone().sub(this.parent.parent.position).normalize();
81
+ const startVector = e.dragStart.clone().sub(this.parent.parent.position).normalize();
82
+ const signedAngle = DIVEMath.signedAngleTo(startVector, currentVector, handle.forwardVector);
83
+ const euler = new Euler(
84
+ this._startRot.x + handle.forwardVector.x * signedAngle,
85
+ this._startRot.y + handle.forwardVector.y * signedAngle,
86
+ this._startRot.z + handle.forwardVector.z * signedAngle,
87
+ );
88
+ (this.parent.parent as DIVEGizmo).onChange(undefined, euler);
89
+ }
90
+
91
+ public onHandleDragEnd(handle: DIVERadialHandle): void {
92
+ this._startRot = null;
93
+ this.handleHighlight(handle.axis, false, false);
94
+ }
95
+ }
@@ -0,0 +1,97 @@
1
+ import { Object3D, Vector3 } from "three";
2
+ import { AxesColorBlue, AxesColorGreen, AxesColorRed } from "../../constant/AxisHelperColors";
3
+ import { DIVEHoverable } from "../../interface/Hoverable";
4
+ import DIVEOrbitControls from "../../controls/OrbitControls";
5
+ import { DIVEScaleHandle } from "../handles/ScaleHandle";
6
+ import { DraggableEvent } from "../../toolbox/BaseTool";
7
+ import { DIVEGizmoAxis, DIVEGizmo } from "../Gizmo";
8
+
9
+ export class DIVEScaleGizmo extends Object3D implements DIVEHoverable {
10
+ readonly isHoverable: true = true;
11
+
12
+ public children: DIVEScaleHandle[];
13
+
14
+ private _controller: DIVEOrbitControls;
15
+
16
+ private _startScale: Vector3 | null;
17
+
18
+ constructor(controller: DIVEOrbitControls) {
19
+ super();
20
+
21
+ this.name = "DIVEScaleGizmo";
22
+
23
+ this.children = [];
24
+
25
+ this._startScale = null;
26
+
27
+ this._controller = controller;
28
+
29
+ this.add(new DIVEScaleHandle('x', 1, new Vector3(1, 0, 0), AxesColorRed));
30
+ this.add(new DIVEScaleHandle('y', 1, new Vector3(0, 1, 0), AxesColorGreen));
31
+ this.add(new DIVEScaleHandle('z', 1, new Vector3(0, 0, 1), AxesColorBlue));
32
+ }
33
+
34
+ public reset(): void {
35
+ this.children.forEach((child) => {
36
+ child.reset();
37
+ });
38
+ }
39
+
40
+ public update(scale: Vector3): void {
41
+ this.children.forEach((child) => {
42
+ child.update(scale);
43
+ });
44
+ }
45
+
46
+ private handleHighlight(axis: DIVEGizmoAxis, value: boolean, dragged: boolean): void {
47
+ // Set highlight state for all handles.
48
+ this.children.forEach((child) => {
49
+ if (dragged) {
50
+ // Dragging has priority when it comes to highlighting.
51
+ child.highlight = child.axis === axis && dragged;
52
+ } else {
53
+ // If nothing is dragged, decide on hovered state.
54
+ child.highlight = child.axis === axis && value;
55
+ }
56
+ });
57
+ }
58
+
59
+ public onHoverAxis(handle: DIVEScaleHandle, value: boolean): void {
60
+ // If _startScale is set, it means there is a drag operation in progress.
61
+ // While dragging, we don't want to change the hover state.
62
+ if (this._startScale) return;
63
+
64
+ if (!this.parent) return;
65
+ if (!this.parent.parent) return;
66
+ (this.parent.parent as DIVEGizmo).onHover('translate', handle.axis, value);
67
+
68
+ this.handleHighlight(handle.axis, value, false);
69
+ }
70
+
71
+ public onAxisDragStart(handle: DIVEScaleHandle): void {
72
+ if (!this.parent) return;
73
+ if (!this.parent.parent) return;
74
+
75
+ const object = (this.parent.parent as DIVEGizmo).object;
76
+ if (!object) return;
77
+
78
+ this._startScale = object.scale.clone();
79
+ this.handleHighlight(handle.axis, true, true);
80
+ }
81
+
82
+ public onAxisDrag(axis: DIVEScaleHandle, e: DraggableEvent): void {
83
+ if (!this._startScale) return;
84
+
85
+ if (!this.parent) return;
86
+ if (!this.parent.parent) return;
87
+ if (!('onChange' in this.parent.parent)) return;
88
+
89
+ const delta = e.dragDelta.clone().projectOnVector(axis.forwardVector);
90
+ (this.parent.parent as DIVEGizmo).onChange(undefined, undefined, this._startScale.clone().add(delta));
91
+ }
92
+
93
+ public onAxisDragEnd(handle: DIVEScaleHandle): void {
94
+ this._startScale = null;
95
+ this.handleHighlight(handle.axis, false, false);
96
+ }
97
+ }
@@ -0,0 +1,88 @@
1
+ import { Object3D, Vector3 } from "three";
2
+ import { AxesColorBlue, AxesColorGreen, AxesColorRed } from "../../constant/AxisHelperColors";
3
+ import DIVEOrbitControls from "../../controls/OrbitControls";
4
+ import { DIVEAxisHandle } from "../handles/AxisHandle";
5
+ import { DIVEGizmo, DIVEGizmoAxis } from "../Gizmo";
6
+ import { DraggableEvent } from "../../toolbox/BaseTool";
7
+
8
+ export class DIVETranslateGizmo extends Object3D {
9
+ private _controller: DIVEOrbitControls;
10
+
11
+ public children: DIVEAxisHandle[];
12
+
13
+ private _startPos: Vector3 | null;
14
+
15
+ constructor(controller: DIVEOrbitControls) {
16
+ super();
17
+
18
+ this.name = "DIVETranslateGizmo";
19
+
20
+ this.children = [];
21
+
22
+ this._startPos = null;
23
+
24
+ this._controller = controller;
25
+
26
+ this.add(new DIVEAxisHandle('x', 1, new Vector3(1, 0, 0), AxesColorRed));
27
+ this.add(new DIVEAxisHandle('y', 1, new Vector3(0, 1, 0), AxesColorGreen));
28
+ this.add(new DIVEAxisHandle('z', 1, new Vector3(0, 0, 1), AxesColorBlue));
29
+ }
30
+
31
+ public reset(): void {
32
+ this.children.forEach((child) => {
33
+ child.reset();
34
+ });
35
+ }
36
+
37
+ private handleHighlight(axis: DIVEGizmoAxis, value: boolean, dragged: boolean): void {
38
+ // Set highlight state for all handles.
39
+ this.children.forEach((child) => {
40
+ if (dragged) {
41
+ // Dragging has priority when it comes to highlighting.
42
+ child.highlight = child.axis === axis && dragged;
43
+ } else {
44
+ // If nothing is dragged, decide on hovered state.
45
+ child.highlight = child.axis === axis && value;
46
+ }
47
+ });
48
+ }
49
+
50
+ public onHandleHover(handle: DIVEAxisHandle, value: boolean): void {
51
+ // If _startPos is set, it means there is a drag operation in progress.
52
+ // While dragging, we don't want to change the hover state.
53
+ if (this._startPos) return;
54
+
55
+ if (!this.parent) return;
56
+ if (!this.parent.parent) return;
57
+ (this.parent.parent as DIVEGizmo).onHover('translate', handle.axis, value);
58
+
59
+ this.handleHighlight(handle.axis, value, false);
60
+ }
61
+
62
+ public onHandleDragStart(handle: DIVEAxisHandle): void {
63
+ if (!this.parent) return;
64
+ if (!this.parent.parent) return;
65
+
66
+ const object = (this.parent.parent as DIVEGizmo).object;
67
+ if (!object) return;
68
+
69
+ this._startPos = object.position.clone();
70
+ this.handleHighlight(handle.axis, true, true);
71
+ }
72
+
73
+ public onHandleDrag(handle: DIVEAxisHandle, e: DraggableEvent): void {
74
+ if (!this._startPos) return;
75
+
76
+ if (!this.parent) return;
77
+ if (!this.parent.parent) return;
78
+ if (!('onChange' in this.parent.parent)) return;
79
+
80
+ const delta = e.dragDelta.clone().projectOnVector(handle.forwardVector);
81
+ (this.parent.parent as DIVEGizmo).onChange(this._startPos.clone().add(delta));
82
+ }
83
+
84
+ public onHandleDragEnd(handle: DIVEAxisHandle): void {
85
+ this._startPos = null;
86
+ this.handleHighlight(handle.axis, false, false);
87
+ }
88
+ }
@@ -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
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Interface for objects that can be hovered in the scene.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import { type Object3D } from "three";
8
+ import { type DraggableEvent } from "../toolbox/BaseTool";
9
+
10
+ export interface DIVEDraggable {
11
+ isDraggable: true;
12
+ onDragStart?: (e: DraggableEvent) => void;
13
+ onDrag?: (e: DraggableEvent) => void;
14
+ onDragEnd?: (e: DraggableEvent) => void;
15
+ }
16
+
17
+ export const isDraggable = (object: Object3D): object is Object3D & DIVEDraggable => {
18
+ return 'isDraggable' in object;
19
+ };
20
+
21
+ export const findDraggableInterface = (child: Object3D): (Object3D & DIVEDraggable) | undefined => {
22
+ if (child === undefined) return undefined;
23
+
24
+ if (child.parent === null) {
25
+ // in this case it is the scene itself
26
+ return undefined;
27
+ }
28
+
29
+ if (isDraggable(child)) {
30
+ return child as (Object3D & DIVEDraggable);
31
+ }
32
+
33
+ return findDraggableInterface(child.parent);
34
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Interface for objects that can be hovered in the scene.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import { type Object3D, type Intersection } from "three";
8
+
9
+ export interface DIVEHoverable {
10
+ isHoverable: true;
11
+ onPointerEnter?: (i: Intersection) => void;
12
+ onPointerOver?: (i: Intersection) => void;
13
+ onPointerLeave?: () => void;
14
+ }
15
+
16
+ export const isHoverable = (object: Object3D): object is Object3D & DIVEHoverable => {
17
+ return 'isHoverable' in object;
18
+ };
19
+
20
+ export const findHoverableInterface = (child: Object3D): (Object3D & DIVEHoverable) | undefined => {
21
+ if (child === undefined) return undefined;
22
+
23
+ if (child.parent === null) {
24
+ // in this case it is the scene itself
25
+ return undefined;
26
+ }
27
+
28
+ if (isHoverable(child)) {
29
+ return child as (Object3D & DIVEHoverable);
30
+ }
31
+
32
+ return findHoverableInterface(child.parent);
33
+ }
@@ -1,4 +1,3 @@
1
- import type { TransformControls } from "three/examples/jsm/Addons.js";
2
1
 
3
2
  /**
4
3
  * Interface for objects that can be moved in the scene.
@@ -8,6 +7,5 @@ import type { TransformControls } from "three/examples/jsm/Addons.js";
8
7
 
9
8
  export interface DIVEMoveable {
10
9
  isMoveable: true;
11
- gizmo: TransformControls | null;
12
10
  onMove?: () => void;
13
11
  }
@@ -4,8 +4,14 @@
4
4
  * @module
5
5
  */
6
6
 
7
+ import { Object3D } from "three";
8
+
7
9
  export interface DIVESelectable {
8
10
  isSelectable: true;
9
11
  onSelect?: () => void;
10
12
  onDeselect?: () => void;
13
+ }
14
+
15
+ export function isSelectable(object: Object3D): object is Object3D & DIVESelectable {
16
+ return 'isSelectable' in object;
11
17
  }
@@ -1,7 +1,10 @@
1
+ import { type Object3D } from 'three';
1
2
  import * as Moveable_DEF from '../Moveable';
2
3
  import * as Rotatable_DEF from '../Rotatable';
3
4
  import * as Scalable_DEF from '../Scalable';
4
5
  import * as Selectable_DEF from '../Selectable';
6
+ import * as Draggable_DEF from '../Draggable';
7
+ import * as Hoverable_DEF from '../Hoverable';
5
8
 
6
9
  describe('interfaces', () => {
7
10
  it('should be defined', () => {
@@ -9,5 +12,58 @@ describe('interfaces', () => {
9
12
  expect(Rotatable_DEF).toBeDefined();
10
13
  expect(Scalable_DEF).toBeDefined();
11
14
  expect(Selectable_DEF).toBeDefined();
15
+ expect(Draggable_DEF).toBeDefined();
16
+ expect(Hoverable_DEF).toBeDefined();
17
+ });
18
+
19
+ it('should identify Selectable', () => {
20
+ const Selectable = { isSelectable: true };
21
+ expect(Selectable_DEF.isSelectable(Selectable as unknown as Object3D)).toBe(true);
22
+ });
23
+
24
+ it('should identify Draggable', () => {
25
+ const Draggable = { isDraggable: true };
26
+ expect(Draggable_DEF.isDraggable(Draggable as unknown as Object3D)).toBe(true);
27
+ });
28
+
29
+ it('should find Draggable', () => {
30
+ let Draggable = {
31
+ isDraggable: true,
32
+ } as unknown as Object3D & Draggable_DEF.DIVEDraggable;
33
+ expect(Draggable_DEF.findDraggableInterface(Draggable as unknown as Object3D)).toBe(Draggable);
34
+
35
+ let parent = {
36
+ isDraggable: true,
37
+ }
38
+ Draggable = {
39
+ parent: parent,
40
+ } as unknown as Object3D & Draggable_DEF.DIVEDraggable;
41
+ expect(Draggable_DEF.findDraggableInterface(Draggable as unknown as Object3D)).toBe(parent);
42
+
43
+ Draggable = { isDraggable: true, parent: null } as unknown as Object3D & Draggable_DEF.DIVEDraggable;
44
+ expect(Draggable_DEF.findDraggableInterface(Draggable as unknown as Object3D)).toBe(undefined);
45
+ });
46
+
47
+ it('should identify Hoverable', () => {
48
+ const Hoverable = { isHoverable: true };
49
+ expect(Hoverable_DEF.isHoverable(Hoverable as unknown as Object3D)).toBe(true);
50
+ });
51
+
52
+ it('should find Hoverable', () => {
53
+ let Hoverable = {
54
+ isHoverable: true,
55
+ } as unknown as Object3D & Hoverable_DEF.DIVEHoverable;
56
+ expect(Hoverable_DEF.findHoverableInterface(Hoverable as unknown as Object3D)).toBe(Hoverable);
57
+
58
+ let parent = {
59
+ isHoverable: true,
60
+ }
61
+ Hoverable = {
62
+ parent: parent,
63
+ } as unknown as Object3D & Hoverable_DEF.DIVEHoverable;
64
+ expect(Hoverable_DEF.findHoverableInterface(Hoverable as unknown as Object3D)).toBe(parent);
65
+
66
+ Hoverable = { isHoverable: true, parent: null } as unknown as Object3D & Hoverable_DEF.DIVEHoverable;
67
+ expect(Hoverable_DEF.findHoverableInterface(Hoverable as unknown as Object3D)).toBe(undefined);
12
68
  });
13
69
  });
package/src/math/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import ceilExp from "./ceil/ceilExp.ts";
2
2
  import floorExp from "./floor/floorExp.ts";
3
3
  import roundExp from "./round/roundExp.ts";
4
+ import signedAngleTo from "./signedAngleTo/signedAngleTo.ts";
4
5
  import toFixedExp from "./toFixed/toFixedExp.ts";
5
6
  import truncateExp from "./truncate/truncateExp.ts";
6
7
 
@@ -10,10 +11,12 @@ export const DIVEMath: {
10
11
  roundExp: typeof roundExp;
11
12
  toFixedExp: typeof toFixedExp;
12
13
  truncateExp: typeof truncateExp;
14
+ signedAngleTo: typeof signedAngleTo;
13
15
  } = {
14
16
  ceilExp,
15
17
  floorExp,
16
18
  roundExp,
17
19
  toFixedExp,
18
20
  truncateExp,
21
+ signedAngleTo,
19
22
  }
@@ -0,0 +1,14 @@
1
+ import { Vector3 } from 'three';
2
+ import signedAngleTo from '../signedAngleTo';
3
+
4
+ describe('dive/math/signedAngleTo', () => {
5
+ it('should signedAngleTo', () => {
6
+ const planeNormal = new Vector3(0, 0, 1);
7
+ const a = new Vector3(1, 0, 0);
8
+ expect(signedAngleTo(a, new Vector3(1, 0, 0), planeNormal)).toBe(0);
9
+ expect(signedAngleTo(a, new Vector3(0, 1, 0), planeNormal)).toBe(Math.PI / 2);
10
+ expect(signedAngleTo(a, new Vector3(-1, 0, 0), planeNormal)).toBe(Math.PI);
11
+ expect(signedAngleTo(a, new Vector3(0, -1, 0), planeNormal)).toBe(-Math.PI / 2);
12
+ expect(signedAngleTo(a, planeNormal, planeNormal)).toBe(0);
13
+ });
14
+ });
@@ -0,0 +1,13 @@
1
+ import { Vector3 } from "three";
2
+
3
+ /**
4
+ * Calculate the signed angle between two vectors. Only works when the vectors are on the same plane.
5
+ * @param vecB Start Vector
6
+ * @param vecA Target Vector
7
+ * @param planeNormal The vector's plane normal
8
+ * @returns Signed angle in radians
9
+ */
10
+
11
+ export default function signedAngleTo(vecA: Vector3, vecB: Vector3, planeNormal: Vector3): number {
12
+ return Math.atan2(vecA.clone().cross(vecB).dot(planeNormal), vecB.clone().dot(vecA));
13
+ }