Browse Source

Gizmo Mesh Upgrade - Rotation Gizmo Update

Dave Gould 4 years ago
parent
commit
ef5cfc59c2
2 changed files with 283 additions and 36 deletions
  1. 195 35
      src/Gizmos/planeRotationGizmo.ts
  2. 88 1
      src/Gizmos/rotationGizmo.ts

+ 195 - 35
src/Gizmos/planeRotationGizmo.ts

@@ -6,14 +6,12 @@ import { Color3 } from '../Maths/math.color';
 import { AbstractMesh } from "../Meshes/abstractMesh";
 import { Mesh } from "../Meshes/mesh";
 import { Node } from "../node";
-import { LinesMesh } from "../Meshes/linesMesh";
 import { PointerDragBehavior } from "../Behaviors/Meshes/pointerDragBehavior";
 import { Gizmo } from "./gizmo";
 import { UtilityLayerRenderer } from "../Rendering/utilityLayerRenderer";
 import { StandardMaterial } from "../Materials/standardMaterial";
-
-import "../Meshes/Builders/linesBuilder";
 import { RotationGizmo } from "./rotationGizmo";
+import { Angle } from '../Maths/math.path';
 
 /**
  * Single plane rotation gizmo
@@ -37,6 +35,15 @@ export class PlaneRotationGizmo extends Gizmo {
 
     private _isEnabled: boolean = true;
     private _parent: Nullable<RotationGizmo> = null;
+    private _coloredMaterial: StandardMaterial;
+    private _hoverMaterial: StandardMaterial;
+    private _disableMaterial: StandardMaterial;
+
+    private circleConstants = {
+        radius: 0.3,
+        pi2: Math.PI * 2,
+        tessellation: 360
+    };
 
     /**
      * Creates a PlaneRotationGizmo
@@ -51,28 +58,27 @@ export class PlaneRotationGizmo extends Gizmo {
         super(gizmoLayer);
         this._parent = parent;
         // Create Material
-        var coloredMaterial = new StandardMaterial("", gizmoLayer.utilityLayerScene);
-        coloredMaterial.diffuseColor = color;
-        coloredMaterial.specularColor = color.subtract(new Color3(0.1, 0.1, 0.1));
+        this._coloredMaterial = new StandardMaterial("", gizmoLayer.utilityLayerScene);
+        this._coloredMaterial.diffuseColor = color;
+        this._coloredMaterial.specularColor = color.subtract(new Color3(0.1, 0.1, 0.1));
 
-        var hoverMaterial = new StandardMaterial("", gizmoLayer.utilityLayerScene);
-        hoverMaterial.diffuseColor = color.add(new Color3(0.3, 0.3, 0.3));
+        this._hoverMaterial = new StandardMaterial("", gizmoLayer.utilityLayerScene);
+        this._hoverMaterial.diffuseColor = Color3.Yellow();
+
+        this._disableMaterial = new StandardMaterial("", gizmoLayer.utilityLayerScene);
+        this._disableMaterial.diffuseColor = Color3.Gray();
+        this._disableMaterial.alpha = 0.4;
 
         // Build mesh on root node
         var parentMesh = new AbstractMesh("", gizmoLayer.utilityLayerScene);
+        this._createGizmoMesh(parentMesh, thickness, tessellation);
 
-        let drag = Mesh.CreateTorus("", 0.6, 0.03 * thickness, tessellation, gizmoLayer.utilityLayerScene);
-        drag.visibility = 0;
-        let rotationMesh = Mesh.CreateTorus("", 0.6, 0.005 * thickness, tessellation, gizmoLayer.utilityLayerScene);
-        rotationMesh.material = coloredMaterial;
+        // Axis Gizmo Closures
+        let dragDistance = 0;
+        const rotationCirclePaths: any[] = [];
+        const rotationCircle: Mesh = this.setupRotationCircle(rotationCirclePaths, parentMesh);
 
-        // Position arrow pointing in its drag axis
-        rotationMesh.rotation.x = Math.PI / 2;
-        drag.rotation.x = Math.PI / 2;
-        parentMesh.addChild(rotationMesh);
-        parentMesh.addChild(drag);
         parentMesh.lookAt(this._rootMesh.position.add(planeNormal));
-
         this._rootMesh.addChild(parentMesh);
         parentMesh.scaling.scaleInPlace(1 / 3);
         // Add drag behavior to handle events when the gizmo is dragged
@@ -82,17 +88,50 @@ export class PlaneRotationGizmo extends Gizmo {
         this.dragBehavior._useAlternatePickedPointAboveMaxDragAngle = true;
         this._rootMesh.addBehavior(this.dragBehavior);
 
-        var lastDragPosition = new Vector3();
+        // Closures for drag logic
+        const lastDragPosition = new Vector3();
+        let dragPlanePoint = new Vector3();
+        const rotationMatrix = new Matrix();
+        const planeNormalTowardsCamera = new Vector3();
+        let localPlaneNormalTowardsCamera = new Vector3();
 
         this.dragBehavior.onDragStartObservable.add((e) => {
             if (this.attachedNode) {
                 lastDragPosition.copyFrom(e.dragPlanePoint);
+
+                // This is for instantiation location of rotation circle
+                // Rotation Circle Forward Vector
+                const forward = new Vector3(0, 0, 1);		
+                const direction = rotationCircle.getDirection(forward);
+                direction.normalize();
+
+                // Remove Rotation Circle from parent mesh before drag interaction
+                parentMesh.removeChild(rotationCircle);
+                
+                lastDragPosition.copyFrom(e.dragPlanePoint);
+                dragPlanePoint = e.dragPlanePoint;
+                const origin = rotationCircle.getAbsolutePosition().clone();
+                const originalRotationVector = rotationCircle.getAbsolutePosition().clone().addInPlace(direction);
+                const dragStartVector = e.dragPlanePoint;
+                let angle = this.angleBetween3DCoords(origin, originalRotationVector, dragStartVector);
+
+                if (Vector3.Dot(rotationCircle.up, Vector3.Down()) > 0) {
+                    angle = -angle;
+                }
+
+                rotationCircle.addRotation(0, angle, 0);
             }
         });
 
-        var rotationMatrix = new Matrix();
-        var planeNormalTowardsCamera = new Vector3();
-        var localPlaneNormalTowardsCamera = new Vector3();
+        this.dragBehavior.onDragEndObservable.add(() => {
+            dragDistance = 0;
+            this.updateRotationCircle(rotationCircle, rotationCirclePaths, dragDistance, dragPlanePoint);
+            parentMesh.addChild(rotationCircle);    // Add rotation circle back to parent mesh after drag behavior
+        });
+
+        // var rotationMatrix = new Matrix();
+        // var planeNormalTowardsCamera = new Vector3();
+        // var localPlaneNormalTowardsCamera = new Vector3();
 
         var tmpSnapEvent = { snapDistance: 0 };
         var currentSnapDragDistance = 0;
@@ -118,11 +157,13 @@ export class PlaneRotationGizmo extends Gizmo {
                     localPlaneNormalTowardsCamera = Vector3.TransformCoordinates(planeNormalTowardsCamera, rotationMatrix);
                 }
                 // Flip up vector depending on which side the camera is on
+                let cameraFlipped = false;
                 if (gizmoLayer.utilityLayerScene.activeCamera) {
                     var camVec = gizmoLayer.utilityLayerScene.activeCamera.position.subtract(nodeTranslation);
                     if (Vector3.Dot(camVec, localPlaneNormalTowardsCamera) > 0) {
                         planeNormalTowardsCamera.scaleInPlace(-1);
                         localPlaneNormalTowardsCamera.scaleInPlace(-1);
+                        cameraFlipped = true;
                     }
                 }
                 var halfCircleSide = Vector3.Dot(localPlaneNormalTowardsCamera, cross) > 0.0;
@@ -145,6 +186,9 @@ export class PlaneRotationGizmo extends Gizmo {
                     }
                 }
 
+                dragDistance += cameraFlipped ? -angle: angle;
+                this.updateRotationCircle(rotationCircle, rotationCirclePaths, dragDistance, dragPlanePoint);
+
                 // Convert angle and axis to quaternion (http://www.euclideanspace.com/maths/geometry/rotations/conversions/angleToQuaternion/index.htm)
                 var quaternionCoefficient = Math.sin(angle / 2);
                 amountToRotate.set(planeNormalTowardsCamera.x * quaternionCoefficient, planeNormalTowardsCamera.y * quaternionCoefficient, planeNormalTowardsCamera.z * quaternionCoefficient, Math.cos(angle / 2));
@@ -177,22 +221,44 @@ export class PlaneRotationGizmo extends Gizmo {
             }
         });
 
-        this._pointerObserver = gizmoLayer.utilityLayerScene.onPointerObservable.add((pointerInfo) => {
-            if (this._customMeshSet) {
-                return;
-            }
-            this._isHovered = !!(pointerInfo.pickInfo && (this._rootMesh.getChildMeshes().indexOf(<Mesh>pointerInfo.pickInfo.pickedMesh) != -1));
-            var material = this._isHovered ? hoverMaterial : coloredMaterial;
-            this._rootMesh.getChildMeshes().forEach((m) => {
-                m.material = material;
-                if ((<LinesMesh>m).color) {
-                    (<LinesMesh>m).color = material.diffuseColor;
-                }
-            });
-        });
+        // this._pointerObserver = gizmoLayer.utilityLayerScene.onPointerObservable.add((pointerInfo) => {
+        //     if (this._customMeshSet) {
+        //         return;
+        //     }
+        //     this._isHovered = !!(pointerInfo.pickInfo && (this._rootMesh.getChildMeshes().indexOf(<Mesh>pointerInfo.pickInfo.pickedMesh) != -1));
+        //     var material = this._isHovered ? hoverMaterial : coloredMaterial;
+        //     this._rootMesh.getChildMeshes().forEach((m) => {
+        //         m.material = material;
+        //         if ((<LinesMesh>m).color) {
+        //             (<LinesMesh>m).color = material.diffuseColor;
+        //         }
+        //     });
+        // });
 
         var light = gizmoLayer._getSharedGizmoLight();
         light.includedOnlyMeshes = light.includedOnlyMeshes.concat(this._rootMesh.getChildMeshes(false));
+
+        const cache: any = {
+            material: this._coloredMaterial,
+            hoverMaterial: this._hoverMaterial,
+            disableMaterial: this._disableMaterial,
+            active: false
+        };
+        this._parent?.addToAxisCache((parentMesh as Mesh), cache);
+    }
+
+    private _createGizmoMesh(parentMesh: AbstractMesh, thickness: number, tessellation: number){
+        let drag = Mesh.CreateTorus("ignore", 0.6, 0.03 * thickness, tessellation, this.gizmoLayer.utilityLayerScene);
+        drag.visibility = 0;
+        let rotationMesh = Mesh.CreateTorus("", 0.6, 0.005 * thickness, tessellation, this.gizmoLayer.utilityLayerScene);
+        rotationMesh.material = this._coloredMaterial;
+
+        // Position arrow pointing in its drag axis
+        rotationMesh.rotation.x = Math.PI / 2;
+        drag.rotation.x = Math.PI / 2;
+
+        parentMesh.addChild(rotationMesh);
+        parentMesh.addChild(drag);
     }
 
     protected _attachedNodeChanged(value: Nullable<Node>) {
@@ -201,6 +267,100 @@ export class PlaneRotationGizmo extends Gizmo {
         }
     }
 
+    private angleBetween3DCoords(origin: Vector3, coord1: Vector3, coord2: Vector3): number {
+        // The dot product of vectors v1 & v2 is a function of the cosine of the angle between them scaled by the product of their magnitudes.
+        const v1 = new Vector3(coord1.x - origin.x, coord1.y - origin.y, coord1.z - origin.z);
+        const v2 = new Vector3(coord2.x - origin.x, coord2.y - origin.y, coord2.z - origin.z);
+
+        // Normalize v1
+        const v1mag = Math.sqrt(v1.x * v1.x + v1.y * v1.y + v1.z * v1.z);
+        const v1norm = new Vector3(v1.x / v1mag, v1.y / v1mag, v1.z / v1mag);
+
+        // Normalize v2
+        const v2mag = Math.sqrt(v2.x * v2.x + v2.y * v2.y + v2.z * v2.z);
+        const v2norm = new Vector3(v2.x / v2mag, v2.y / v2mag, v2.z / v2mag);
+
+        // Calculate the dot products of vectors v1 and v2
+        const dotProducts = v1norm.x * v2norm.x + v1norm.y * v2norm.y + v1norm.z * v2norm.z;
+        const cross = Vector3.Cross(v1norm as any, v2norm as any);
+
+        // Extract the angle from the dot products
+        let angle = (Math.acos(dotProducts) * 180.0) / Math.PI;
+        angle = Math.round(angle * 1000) / 1000;
+        angle = Angle.FromDegrees(angle).radians();
+
+        // Flip if its cross has negitive y orientation
+        if (cross.y < 0) { angle = -angle; }
+
+        return angle;
+    }
+
+    private setupRotationCircle(paths: any[], parentMesh: AbstractMesh): Mesh {
+        const fillRadians = 0;
+        const step = this.circleConstants.pi2 / this.circleConstants.tessellation;
+        for (let p = -Math.PI / 2; p < Math.PI / 2 - 1.5; p += step / 2) {
+            const path = [];
+            for (let i = 0; i < this.circleConstants.pi2; i += step ) {
+                if (i < fillRadians) {
+                    const x = this.circleConstants.radius * Math.sin(i) * Math.cos(p);
+                    const z = this.circleConstants.radius * Math.cos(i) * Math.cos(p);
+                    const y = 0;
+                    path.push(new Vector3(x, y, z));
+                } else {
+                    path.push(new Vector3(0, 0, 0));
+                }
+            }
+
+            paths.push(path);
+        }
+
+        const mat = new StandardMaterial("", this.gizmoLayer.utilityLayerScene);
+        mat.diffuseColor = Color3.Yellow();
+        mat.backFaceCulling = false;
+        const mesh = Mesh.CreateRibbon("ignore", paths, false, false, 0, this.gizmoLayer.utilityLayerScene, true);
+        mesh.material = mat;
+        mesh.material.alpha = .25;
+        mesh.rotation.x = Math.PI / 2;
+        parentMesh.addChild(mesh);
+        return mesh;
+    }
+
+    private updateRotationPath(pathArr: any[], newFill: number): void {
+        // To update the Ribbon, you have to mutate the pathArray in-place
+        const step = this.circleConstants.pi2 / this.circleConstants.tessellation;
+        let tessellationCounter = 0;
+        for (let p = -Math.PI / 2; p < Math.PI / 2 - 1.5; p += step / 2) {
+            const path = pathArr[tessellationCounter];
+            if (path) {
+                let radianCounter = 0;
+                for (let i = 0; i < this.circleConstants.pi2; i += step ) {
+                    if (path[radianCounter]) {
+                        if (i < Math.abs(newFill)) {
+                            const eie = (newFill > 0) ? i : i * -1;
+                            const pea = (newFill > 0) ? p : p * -1;
+                            path[radianCounter].x = this.circleConstants.radius * Math.sin(eie) * Math.cos(pea);
+                            path[radianCounter].z = this.circleConstants.radius * Math.cos(eie) * Math.cos(pea);
+                            path[radianCounter].y = 0;
+                        } else {
+                            path[radianCounter].x = 0;
+                            path[radianCounter].y = 0;
+                            path[radianCounter].z = 0;
+                        }
+                    }
+
+                    radianCounter++;
+                }
+            }
+
+            tessellationCounter ++;
+        }
+    }
+
+    private updateRotationCircle(mesh: Mesh, paths: any[], newFill: number, dragPlanePoint: Vector3): void {
+        this.updateRotationPath(paths, newFill);
+        mesh = Mesh.CreateRibbon("ribbon", paths, false, false, 0, this.gizmoLayer.utilityLayerScene, undefined, undefined, mesh);
+    }
+
     /**
          * If the gizmo is enabled
          */

+ 88 - 1
src/Gizmos/rotationGizmo.ts

@@ -1,5 +1,5 @@
 import { Logger } from "../Misc/logger";
-import { Observable } from "../Misc/observable";
+import { Observable, Observer } from "../Misc/observable";
 import { Nullable } from "../types";
 import { Vector3 } from "../Maths/math.vector";
 import { Color3 } from '../Maths/math.color';
@@ -9,6 +9,8 @@ import { Gizmo } from "./gizmo";
 import { PlaneRotationGizmo } from "./planeRotationGizmo";
 import { UtilityLayerRenderer } from "../Rendering/utilityLayerRenderer";
 import { Node } from "../node";
+import { PointerEventTypes, PointerInfo } from "../Events/pointerEvents";
+import { LinesMesh } from "../Meshes/linesMesh";
 /**
  * Gizmo that enables rotating a mesh along 3 axis
  */
@@ -33,6 +35,12 @@ export class RotationGizmo extends Gizmo {
 
     private _meshAttached: Nullable<AbstractMesh>;
     private _nodeAttached: Nullable<Node>;
+    private _observables: Nullable<Observer<PointerInfo>>[] = [];
+
+    /** Gizmo state variables used for UI behavior */
+    private dragging = false;
+    /** Node Caching for quick lookup */
+    private gizmoAxisCache: Map<Mesh, any> = new Map();
 
     public get attachedMesh() {
         return this._meshAttached;
@@ -102,6 +110,7 @@ export class RotationGizmo extends Gizmo {
 
         this.attachedMesh = null;
         this.attachedNode = null;
+        this.subscribeToPointerObserver();
     }
 
     public set updateGizmoRotationToMatchAttachedMesh(value: boolean) {
@@ -142,6 +151,81 @@ export class RotationGizmo extends Gizmo {
     public get scaleRatio() {
         return this.xGizmo.scaleRatio;
     }
+    /**
+     * Builds Gizmo Axis Cache to enable features such as hover state preservation and graying out other axis during manipulation
+     * @param mesh Axis gizmo mesh
+      @param cache display gizmo axis thickness
+     */
+    public addToAxisCache(mesh: Mesh, cache: any) {
+        this.gizmoAxisCache.set(mesh, cache);
+    }
+
+    public subscribeToPointerObserver(): void {
+        // Assumption, if user sets custom mesh, it will be disposed and pointerInfo will never hold reference to that mesh.
+        // Will be equivilent to if (this._customMeshSet) return;
+        const pointerObserver = this.gizmoLayer.utilityLayerScene.onPointerObservable.add((pointerInfo) => {
+            if (pointerInfo.pickInfo) {
+                // On Hover Logic
+                console.log(pointerInfo.pickInfo.pickedMesh?.id);
+                if (pointerInfo.type === PointerEventTypes.POINTERMOVE) {
+                    if (this.dragging) { return; }
+                    this.gizmoAxisCache.forEach((statusMap, parentMesh) => {
+                        const isHovered = pointerInfo.pickInfo && (parentMesh.getChildMeshes().indexOf((pointerInfo.pickInfo.pickedMesh as Mesh)) != -1);
+                        const material = isHovered || statusMap.active ? statusMap.hoverMaterial : statusMap.material;
+                        parentMesh.getChildMeshes().forEach((m) => {
+                            if (m.name !== 'ignore') {
+                                m.material = material;
+                                if ((m as LinesMesh).color) {
+                                    (m as LinesMesh).color = material.diffuseColor;
+                                }
+                            }
+                        });
+                    });
+                }
+
+                // On Mouse Down
+                if (pointerInfo.type === PointerEventTypes.POINTERDOWN) {
+                    // If user Clicked Gizmo
+                    if (this.gizmoAxisCache.has(pointerInfo.pickInfo.pickedMesh?.parent as Mesh)) {
+                        this.dragging = true;
+                        const statusMap = this.gizmoAxisCache.get(pointerInfo.pickInfo.pickedMesh?.parent as Mesh);
+                        statusMap!.active = true;
+                        console.log(this.gizmoAxisCache);
+                        this.gizmoAxisCache.forEach((statusMap, parentMesh) => {
+                            const isHovered = pointerInfo.pickInfo && (parentMesh.getChildMeshes().indexOf((pointerInfo.pickInfo.pickedMesh as Mesh)) != -1);
+                            const material = isHovered || statusMap.active ? statusMap.hoverMaterial : statusMap.disableMaterial;
+                            parentMesh.getChildMeshes().forEach((m) => {
+                                if (m.name !== 'ignore') {
+                                    m.material = material;
+                                    if ((m as LinesMesh).color) {
+                                        (m as LinesMesh).color = material.diffuseColor;
+                                    }
+                                }
+                            });
+                        });
+                    }
+                }
+
+                // On Mouse Up
+                if (pointerInfo.type === PointerEventTypes.POINTERUP) {
+                    this.gizmoAxisCache.forEach((statusMap, parentMesh) => {
+                        statusMap.active = false;
+                        this.dragging = false;
+                        parentMesh.getChildMeshes().forEach((m) => {
+                            if (m.name !== 'ignore') {
+                                m.material = statusMap.material;
+                                if ((m as LinesMesh).color) {
+                                    (m as LinesMesh).color = statusMap.material.diffuseColor;
+                                }
+                            }
+                        });
+                    });
+                }
+            }
+        });
+
+        this._observables = [pointerObserver];
+    }
 
     /**
      * Disposes of the gizmo
@@ -152,6 +236,9 @@ export class RotationGizmo extends Gizmo {
         this.zGizmo.dispose();
         this.onDragStartObservable.clear();
         this.onDragEndObservable.clear();
+        this._observables.forEach(obs => {
+            this.gizmoLayer.utilityLayerScene.onPointerObservable.remove(obs);
+        });
     }
 
     /**