Browse Source

Merge pull request #9077 from davesauce14/master

Upgrade Gizmo Meshes
Raanan Weber 4 years ago
parent
commit
a03a6e5807

+ 2 - 1
dist/preview release/what's new.md

@@ -23,7 +23,8 @@
 - Refactored React refs from old string API to React.createRef() API ([belfortk](https://github.com/belfortk))
 - Scale on one axis for `BoundingBoxGizmo` ([cedricguillemet](https://github.com/cedricguillemet))
 - Camera gizmo ([cedricguillemet](https://github.com/cedricguillemet))
-- gizmo isHovered boolean ([cedricguillemet](https://github.com/cedricguillemet))
+- Upgraded gizmo meshes ([davesauce14](https://github.com/davesauce14))
+- Gizmo isHovered boolean ([cedricguillemet](https://github.com/cedricguillemet))
 - Node support (Transform, Bone) for gizmos ([cedricguillemet](https://github.com/cedricguillemet))
 - Simplified code contributions by fully automating the dev setup with gitpod ([nisarhassan12](https://github.com/nisarhassan12))
 - Add a `CascadedShadowMap.IsSupported` method and log an error instead of throwing an exception when CSM is not supported ([Popov72](https://github.com/Popov72))

+ 59 - 26
src/Gizmos/axisDragGizmo.ts

@@ -8,7 +8,7 @@ import { Mesh } from "../Meshes/mesh";
 import { LinesMesh } from "../Meshes/linesMesh";
 import { CylinderBuilder } from "../Meshes/Builders/cylinderBuilder";
 import { PointerDragBehavior } from "../Behaviors/Meshes/pointerDragBehavior";
-import { Gizmo } from "./gizmo";
+import { Gizmo, GizmoAxisCache } from "./gizmo";
 import { UtilityLayerRenderer } from "../Rendering/utilityLayerRenderer";
 import { StandardMaterial } from "../Materials/standardMaterial";
 import { Scene } from "../scene";
@@ -36,25 +36,33 @@ export class AxisDragGizmo extends Gizmo {
     private _isEnabled: boolean = true;
     private _parent: Nullable<PositionGizmo> = null;
 
-    private _arrow: TransformNode;
+    private _gizmoMesh: Mesh;
     private _coloredMaterial: StandardMaterial;
     private _hoverMaterial: StandardMaterial;
+    private _disableMaterial: StandardMaterial;
+    private _dragging: boolean = false;
 
     /** @hidden */
-    public static _CreateArrow(scene: Scene, material: StandardMaterial, thickness: number = 1): TransformNode {
+    public static _CreateArrow(scene: Scene, material: StandardMaterial, thickness: number = 1, isCollider = false): TransformNode {
         var arrow = new TransformNode("arrow", scene);
         var cylinder = CylinderBuilder.CreateCylinder("cylinder", { diameterTop: 0, height: 0.075, diameterBottom: 0.0375 * (1 + (thickness - 1) / 4), tessellation: 96 }, scene);
         var line = CylinderBuilder.CreateCylinder("cylinder", { diameterTop: 0.005 * thickness, height: 0.275, diameterBottom: 0.005 * thickness, tessellation: 96 }, scene);
-        line.material = material;
-        cylinder.parent = arrow;
-        line.parent = arrow;
 
         // Position arrow pointing in its drag axis
+        cylinder.parent = arrow;
         cylinder.material = material;
         cylinder.rotation.x = Math.PI / 2;
         cylinder.position.z += 0.3;
+
+        line.parent = arrow;
+        line.material = material;
         line.position.z += 0.275 / 2;
         line.rotation.x = Math.PI / 2;
+
+        if (isCollider) {
+            line.visibility = 0;
+            cylinder.visibility = 0;
+        }
         return arrow;
     }
 
@@ -78,20 +86,31 @@ export class AxisDragGizmo extends Gizmo {
     constructor(dragAxis: Vector3, color: Color3 = Color3.Gray(), gizmoLayer: UtilityLayerRenderer = UtilityLayerRenderer.DefaultUtilityLayer, parent: Nullable<PositionGizmo> = null, thickness: number = 1) {
         super(gizmoLayer);
         this._parent = parent;
+
         // Create Material
         this._coloredMaterial = new StandardMaterial("", gizmoLayer.utilityLayerScene);
         this._coloredMaterial.diffuseColor = color;
         this._coloredMaterial.specularColor = color.subtract(new Color3(0.1, 0.1, 0.1));
 
         this._hoverMaterial = new StandardMaterial("", gizmoLayer.utilityLayerScene);
-        this._hoverMaterial.diffuseColor = color.add(new Color3(0.3, 0.3, 0.3));
+        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
-        this._arrow = AxisDragGizmo._CreateArrow(gizmoLayer.utilityLayerScene, this._coloredMaterial, thickness);
+        // Build Mesh + Collider
+        const arrow = AxisDragGizmo._CreateArrow(gizmoLayer.utilityLayerScene, this._coloredMaterial, thickness);
+        const collider = AxisDragGizmo._CreateArrow(gizmoLayer.utilityLayerScene, this._coloredMaterial, thickness + 4, true);
 
-        this._arrow.lookAt(this._rootMesh.position.add(dragAxis));
-        this._arrow.scaling.scaleInPlace(1 / 3);
-        this._arrow.parent = this._rootMesh;
+        // Add to Root Node
+        this._gizmoMesh = new Mesh("", gizmoLayer.utilityLayerScene);
+        this._gizmoMesh.addChild((arrow as Mesh));
+        this._gizmoMesh.addChild((collider as Mesh));
+
+        this._gizmoMesh.lookAt(this._rootMesh.position.add(dragAxis));
+        this._gizmoMesh.scaling.scaleInPlace(1 / 3);
+        this._gizmoMesh.parent = this._rootMesh;
 
         var currentSnapDragDistance = 0;
         var tmpVector = new Vector3();
@@ -132,23 +151,37 @@ export class AxisDragGizmo extends Gizmo {
                 this._matrixChanged();
             }
         });
+        this.dragBehavior.onDragStartObservable.add(() => { this._dragging = true; });
+        this.dragBehavior.onDragEndObservable.add(() => { this._dragging = false; });
+
+        var light = gizmoLayer._getSharedGizmoLight();
+        light.includedOnlyMeshes = light.includedOnlyMeshes.concat(this._rootMesh.getChildMeshes(false));
+
+        const cache: GizmoAxisCache = {
+            gizmoMeshes: arrow.getChildMeshes() as Mesh[],
+            colliderMeshes: collider.getChildMeshes() as Mesh[],
+            material: this._coloredMaterial,
+            hoverMaterial: this._hoverMaterial,
+            disableMaterial: this._disableMaterial,
+            active: false
+        };
+        this._parent?.addToAxisCache(collider as Mesh, cache);
 
         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 ? this._hoverMaterial : this._coloredMaterial;
-            this._rootMesh.getChildMeshes().forEach((m) => {
-                m.material = material;
-                if ((<LinesMesh>m).color) {
-                    (<LinesMesh>m).color = material.diffuseColor;
-                }
-            });
+            this._isHovered = !!(cache.colliderMeshes.indexOf(<Mesh>pointerInfo?.pickInfo?.pickedMesh) != -1);
+            if (!this._parent) {
+                var material = this._isHovered || this._dragging ? this._hoverMaterial : this._coloredMaterial;
+                cache.gizmoMeshes.forEach((m: Mesh) => {
+                    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));
     }
     protected _attachedNodeChanged(value: Nullable<Node>) {
         if (this.dragBehavior) {
@@ -183,10 +216,10 @@ export class AxisDragGizmo extends Gizmo {
         this.onSnapObservable.clear();
         this.gizmoLayer.utilityLayerScene.onPointerObservable.remove(this._pointerObserver);
         this.dragBehavior.detach();
-        if (this._arrow) {
-            this._arrow.dispose();
+        if (this._gizmoMesh) {
+            this._gizmoMesh.dispose();
         }
-        [this._coloredMaterial, this._hoverMaterial].forEach((matl) => {
+        [this._coloredMaterial, this._hoverMaterial, this._disableMaterial].forEach((matl) => {
             if (matl) {
                 matl.dispose();
             }

+ 100 - 33
src/Gizmos/axisScaleGizmo.ts

@@ -10,7 +10,7 @@ import { BoxBuilder } from "../Meshes/Builders/boxBuilder";
 import { CylinderBuilder } from "../Meshes/Builders/cylinderBuilder";
 import { StandardMaterial } from "../Materials/standardMaterial";
 import { PointerDragBehavior } from "../Behaviors/Meshes/pointerDragBehavior";
-import { Gizmo } from "./gizmo";
+import { Gizmo, GizmoAxisCache } from "./gizmo";
 import { UtilityLayerRenderer } from "../Rendering/utilityLayerRenderer";
 import { ScaleGizmo } from "./scaleGizmo";
 import { Color3 } from '../Maths/math.color';
@@ -45,9 +45,11 @@ export class AxisScaleGizmo extends Gizmo {
     private _isEnabled: boolean = true;
     private _parent: Nullable<ScaleGizmo> = null;
 
-    private _arrow: AbstractMesh;
+    private _gizmoMesh: Mesh;
     private _coloredMaterial: StandardMaterial;
     private _hoverMaterial: StandardMaterial;
+    private _disableMaterial: StandardMaterial;
+    private _dragging: boolean = false;
 
     /**
      * Creates an AxisScaleGizmo
@@ -65,26 +67,45 @@ export class AxisScaleGizmo extends Gizmo {
         this._coloredMaterial.specularColor = color.subtract(new Color3(0.1, 0.1, 0.1));
 
         this._hoverMaterial = new StandardMaterial("", gizmoLayer.utilityLayerScene);
-        this._hoverMaterial.diffuseColor = color.add(new Color3(0.3, 0.3, 0.3));
+        this._hoverMaterial.diffuseColor = Color3.Yellow();
 
-        // Build mesh on root node
-        this._arrow = new AbstractMesh("", gizmoLayer.utilityLayerScene);
-        var arrowMesh = BoxBuilder.CreateBox("yPosMesh", { size: 0.4 * (1 + (thickness - 1) / 4) }, gizmoLayer.utilityLayerScene);
-        var arrowTail = CylinderBuilder.CreateCylinder("cylinder", { diameterTop: 0.005 * thickness, height: 0.275, diameterBottom: 0.005 * thickness, tessellation: 96 }, gizmoLayer.utilityLayerScene);
-        arrowTail.material = this._coloredMaterial;
-        this._arrow.addChild(arrowMesh);
-        this._arrow.addChild(arrowTail);
+        this._disableMaterial = new StandardMaterial("", gizmoLayer.utilityLayerScene);
+        this._disableMaterial.diffuseColor = Color3.Gray();
+        this._disableMaterial.alpha = 0.4;
 
-        // Position arrow pointing in its drag axis
-        arrowMesh.scaling.scaleInPlace(0.1);
-        arrowMesh.material = this._coloredMaterial;
-        arrowMesh.rotation.x = Math.PI / 2;
-        arrowMesh.position.z += 0.3;
-        arrowTail.position.z += 0.275 / 2;
-        arrowTail.rotation.x = Math.PI / 2;
-        this._arrow.lookAt(this._rootMesh.position.add(dragAxis));
-        this._rootMesh.addChild(this._arrow);
-        this._arrow.scaling.scaleInPlace(1 / 3);
+        // Build mesh + Collider
+        this._gizmoMesh = new Mesh("axis", gizmoLayer.utilityLayerScene);
+        const { arrowMesh, arrowTail } = this._createGizmoMesh(this._gizmoMesh, thickness);
+        const collider = this._createGizmoMesh(this._gizmoMesh, thickness + 4, true);
+
+        this._gizmoMesh.lookAt(this._rootMesh.position.add(dragAxis));
+        this._rootMesh.addChild(this._gizmoMesh);
+        this._gizmoMesh.scaling.scaleInPlace(1 / 3);
+
+        // Closure of inital prop values for resetting
+        const nodePosition = arrowMesh.position.clone();
+        const linePosition = arrowTail.position.clone();
+        const lineScale = arrowTail.scaling.clone();
+
+        const increaseGizmoMesh = (dragDistance: number) => {
+            const dragStrength = this.sensitivity * dragDistance * ((this.scaleRatio * 3) / this._rootMesh.scaling.length());
+            const scalar = 1; // This will increase the rate of gizmo size on drag
+            const originalScale = arrowTail.scaling.y;
+            const newScale = originalScale + dragStrength * scalar;
+            const newMeshPosition = arrowMesh.position.z + ((newScale - originalScale) / 4);
+            if (newMeshPosition >= 0) {
+                arrowMesh.position.z = newMeshPosition;
+                arrowTail.scaling.y = newScale;
+                arrowTail.position.z = arrowMesh.position.z / 2;
+            }
+        };
+
+        const resetGizmoMesh = () => {
+            arrowMesh.position.set(nodePosition.x, nodePosition.y, nodePosition.z);
+            arrowTail.position.set(linePosition.x, linePosition.y, linePosition.z);
+            arrowTail.scaling.set(lineScale.x, lineScale.y, lineScale.z);
+            this._dragging = false;
+        };
 
         // Add drag behavior to handle events when the gizmo is dragged
         this.dragBehavior = new PointerDragBehavior({ dragAxis: dragAxis });
@@ -139,25 +160,71 @@ export class AxisScaleGizmo extends Gizmo {
                 this._matrixChanged();
             }
         });
+        // On Drag Listener: to move gizmo mesh with user action
+        this.dragBehavior.onDragStartObservable.add(() => { this._dragging = true; });
+        this.dragBehavior.onDragObservable.add((e) => increaseGizmoMesh(e.dragDistance));
+        this.dragBehavior.onDragEndObservable.add(resetGizmoMesh);
+
+        // Listeners for Universal Scalar
+        parent?.uniformScaleGizmo?.dragBehavior?.onDragObservable?.add((e) => increaseGizmoMesh(e.delta.y));
+        parent?.uniformScaleGizmo?.dragBehavior?.onDragEndObservable?.add(resetGizmoMesh);
+
+        const cache: GizmoAxisCache = {
+            gizmoMeshes: [arrowMesh, arrowTail],
+            colliderMeshes: [collider.arrowMesh, collider.arrowTail],
+            material: this._coloredMaterial,
+            hoverMaterial: this._hoverMaterial,
+            disableMaterial: this._disableMaterial,
+            active: false
+        };
+        this._parent?.addToAxisCache(this._gizmoMesh, cache);
 
         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 ? this._hoverMaterial : this._coloredMaterial;
-            this._rootMesh.getChildMeshes().forEach((m) => {
-                m.material = material;
-                if ((<LinesMesh>m).color) {
-                    (<LinesMesh>m).color = material.diffuseColor;
-                }
-            });
+            this._isHovered = !!(cache.colliderMeshes.indexOf(<Mesh>pointerInfo?.pickInfo?.pickedMesh) != -1);
+            if (!this._parent) {
+                var material = this._isHovered || this._dragging ? this._hoverMaterial : this._coloredMaterial;
+                cache.gizmoMeshes.forEach((m: Mesh) => {
+                    m.material = material;
+                    if ((<LinesMesh>m).color) {
+                        (<LinesMesh>m).color = material.diffuseColor;
+                    }
+                });
+            }
         });
 
         var light = gizmoLayer._getSharedGizmoLight();
         light.includedOnlyMeshes = light.includedOnlyMeshes.concat(this._rootMesh.getChildMeshes());
     }
 
+    /** Create Geometry for Gizmo */
+    private _createGizmoMesh(parentMesh: AbstractMesh, thickness: number, isCollider = false) {
+        var arrowMesh = BoxBuilder.CreateBox("yPosMesh", { size: 0.4 * (1 + (thickness - 1) / 4) }, this.gizmoLayer.utilityLayerScene);
+        var arrowTail = CylinderBuilder.CreateCylinder("cylinder", { diameterTop: 0.005 * thickness, height: 0.275, diameterBottom: 0.005 * thickness, tessellation: 96 }, this.gizmoLayer.utilityLayerScene);
+
+        // Position arrow pointing in its drag axis
+        arrowMesh.scaling.scaleInPlace(0.1);
+        arrowMesh.material = this._coloredMaterial;
+        arrowMesh.rotation.x = Math.PI / 2;
+        arrowMesh.position.z += 0.3;
+
+        arrowTail.material = this._coloredMaterial;
+        arrowTail.position.z += 0.275 / 2;
+        arrowTail.rotation.x = Math.PI / 2;
+
+        if (isCollider) {
+            arrowMesh.visibility = 0;
+            arrowTail.visibility = 0;
+        }
+
+        parentMesh.addChild(arrowMesh);
+        parentMesh.addChild(arrowTail);
+
+        return { arrowMesh, arrowTail };
+    }
+
     protected _attachedNodeChanged(value: Nullable<Node>) {
         if (this.dragBehavior) {
             this.dragBehavior.enabled = value ? true : false;
@@ -165,8 +232,8 @@ export class AxisScaleGizmo extends Gizmo {
     }
 
     /**
- * If the gizmo is enabled
- */
+     * If the gizmo is enabled
+     */
     public set isEnabled(value: boolean) {
         this._isEnabled = value;
         if (!value) {
@@ -191,10 +258,10 @@ export class AxisScaleGizmo extends Gizmo {
         this.onSnapObservable.clear();
         this.gizmoLayer.utilityLayerScene.onPointerObservable.remove(this._pointerObserver);
         this.dragBehavior.detach();
-        if (this._arrow) {
-            this._arrow.dispose();
+        if (this._gizmoMesh) {
+            this._gizmoMesh.dispose();
         }
-        [this._coloredMaterial, this._hoverMaterial].forEach((matl) => {
+        [this._coloredMaterial, this._hoverMaterial, this._disableMaterial].forEach((matl) => {
             if (matl) {
                 matl.dispose();
             }

+ 89 - 0
src/Gizmos/gizmo.ts

@@ -11,6 +11,27 @@ import { Node } from "../node";
 import { Bone } from "../Bones/bone";
 import { UtilityLayerRenderer } from "../Rendering/utilityLayerRenderer";
 import { TransformNode } from '../Meshes/transformNode';
+import { StandardMaterial } from '../Materials/standardMaterial';
+import { PointerEventTypes, PointerInfo } from '../Events/pointerEvents';
+import { LinesMesh } from '../Meshes/linesMesh';
+
+/**
+ * Cache built by each axis. Used for managing state between all elements of gizmo for enhanced UI
+ */
+export interface GizmoAxisCache {
+    /** Mesh used to runder the Gizmo */
+    gizmoMeshes: Mesh[];
+    /** Mesh used to detect user interaction with Gizmo */
+    colliderMeshes: Mesh[];
+    /** Material used to inicate color of gizmo mesh */
+    material: StandardMaterial;
+    /** Material used to inicate hover state of the Gizmo */
+    hoverMaterial: StandardMaterial;
+    /** Material used to inicate disabled state of the Gizmo */
+    disableMaterial: StandardMaterial;
+    /** Used to indicate Active state of the Gizmo */
+    active: boolean;
+}
 /**
  * Renders gizmos on top of an existing scene which provide controls for position, rotation, etc.
  */
@@ -276,6 +297,74 @@ export class Gizmo implements IDisposable {
     }
 
     /**
+     * Subscribes to pointer up, down, and hover events. Used for responsive gizmos.
+     * @param gizmoLayer The utility layer the gizmo will be added to
+     * @param gizmoAxisCache Gizmo axis definition used for reactive gizmo UI
+     * @returns {Observer<PointerInfo>} pointerObserver
+     */
+    public static GizmoAxisPointerObserver(gizmoLayer: UtilityLayerRenderer, gizmoAxisCache: Map<Mesh, GizmoAxisCache>): Observer<PointerInfo> {
+
+        let dragging = false;
+
+        const pointerObserver = gizmoLayer.utilityLayerScene.onPointerObservable.add((pointerInfo) => {
+            if (pointerInfo.pickInfo) {
+                // On Hover Logic
+                if (pointerInfo.type === PointerEventTypes.POINTERMOVE) {
+                    if (dragging) { return; }
+                    gizmoAxisCache.forEach((cache) => {
+                        if (cache.colliderMeshes && cache.gizmoMeshes) {
+                            const isHovered = (cache.colliderMeshes?.indexOf((pointerInfo?.pickInfo?.pickedMesh as Mesh)) != -1);
+                            const material = isHovered || cache.active ? cache.hoverMaterial : cache.material;
+                            cache.gizmoMeshes.forEach((m: Mesh) => {
+                                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 (gizmoAxisCache.has(pointerInfo.pickInfo.pickedMesh?.parent as Mesh)) {
+                        dragging = true;
+                        const statusMap = gizmoAxisCache.get(pointerInfo.pickInfo.pickedMesh?.parent as Mesh);
+                        statusMap!.active = true;
+                        gizmoAxisCache.forEach((cache) => {
+                            const isHovered = (cache.colliderMeshes?.indexOf((pointerInfo?.pickInfo?.pickedMesh as Mesh)) != -1);
+                            const material = isHovered || cache.active ? cache.hoverMaterial : cache.disableMaterial;
+                            cache.gizmoMeshes.forEach((m: Mesh) => {
+                                m.material = material;
+                                if ((m as LinesMesh).color) {
+                                    (m as LinesMesh).color = material.diffuseColor;
+                                }
+                            });
+                        });
+                    }
+                }
+
+                // On Mouse Up
+                if (pointerInfo.type === PointerEventTypes.POINTERUP) {
+                    gizmoAxisCache.forEach((cache) => {
+                        cache.active = false;
+                        dragging = false;
+                        cache.gizmoMeshes.forEach((m: Mesh) => {
+                            m.material = cache.material;
+                            if ((m as LinesMesh).color) {
+                                (m as LinesMesh).color = cache.material.diffuseColor;
+                            }
+                        });
+                    });
+                }
+            }
+        });
+
+        return pointerObserver!;
+    }
+
+    /**
      * Disposes of the gizmo
      */
     public dispose() {

+ 36 - 8
src/Gizmos/gizmoManager.ts

@@ -4,11 +4,11 @@ import { PointerInfo, PointerEventTypes } from "../Events/pointerEvents";
 import { Scene, IDisposable } from "../scene";
 import { Node } from "../node";
 import { AbstractMesh } from "../Meshes/abstractMesh";
+import { Mesh } from '../Meshes/mesh';
 import { UtilityLayerRenderer } from "../Rendering/utilityLayerRenderer";
 import { Color3 } from '../Maths/math.color';
 import { SixDofDragBehavior } from "../Behaviors/Meshes/sixDofDragBehavior";
-
-import { Gizmo } from "./gizmo";
+import { Gizmo, GizmoAxisCache } from "./gizmo";
 import { RotationGizmo } from "./rotationGizmo";
 import { PositionGizmo } from "./positionGizmo";
 import { ScaleGizmo } from "./scaleGizmo";
@@ -33,13 +33,15 @@ export class GizmoManager implements IDisposable {
     public onAttachedToNodeObservable = new Observable<Nullable<Node>>();
 
     private _gizmosEnabled = { positionGizmo: false, rotationGizmo: false, scaleGizmo: false, boundingBoxGizmo: false };
-    private _pointerObserver: Nullable<Observer<PointerInfo>> = null;
+    private _pointerObservers: Observer<PointerInfo>[] = [];
     private _attachedMesh: Nullable<AbstractMesh> = null;
     private _attachedNode: Nullable<Node> = null;
     private _boundingBoxColor = Color3.FromHexString("#0984e3");
     private _defaultUtilityLayer: UtilityLayerRenderer;
     private _defaultKeepDepthUtilityLayer: UtilityLayerRenderer;
     private _thickness: number = 1;
+    /** Node Caching for quick lookup */
+    private _gizmoAxisCache: Map<Mesh, GizmoAxisCache> = new Map();
     /**
      * When bounding box gizmo is enabled, this can be used to track drag/end events
      */
@@ -98,8 +100,18 @@ export class GizmoManager implements IDisposable {
         this._thickness = thickness;
         this.gizmos = { positionGizmo: null, rotationGizmo: null, scaleGizmo: null, boundingBoxGizmo: null };
 
+        const attachToMeshPointerObserver = this._attachToMeshPointerObserver(scene);
+        const gizmoAxisPointerObserver = Gizmo.GizmoAxisPointerObserver(this._defaultUtilityLayer, this._gizmoAxisCache);
+        this._pointerObservers = [attachToMeshPointerObserver, gizmoAxisPointerObserver];
+    }
+
+    /**
+     * Subscribes to pointer down events, for attaching and detaching mesh
+     * @param scene The sceme layer the observer will be added to
+     */
+    private _attachToMeshPointerObserver(scene: Scene): Observer<PointerInfo> {
         // Instatiate/dispose gizmos based on pointer actions
-        this._pointerObserver = scene.onPointerObservable.add((pointerInfo) => {
+        const pointerObserver = scene.onPointerObservable.add((pointerInfo) => {
             if (!this.usePointerToAttachGizmos) {
                 return;
             }
@@ -140,6 +152,7 @@ export class GizmoManager implements IDisposable {
                 }
             }
         });
+        return pointerObserver!;
     }
 
     /**
@@ -198,7 +211,7 @@ export class GizmoManager implements IDisposable {
     public set positionGizmoEnabled(value: boolean) {
         if (value) {
             if (!this.gizmos.positionGizmo) {
-                this.gizmos.positionGizmo = new PositionGizmo(this._defaultUtilityLayer, this._thickness);
+                this.gizmos.positionGizmo = new PositionGizmo(this._defaultUtilityLayer, this._thickness, this);
             }
             if (this._attachedNode) {
                 this.gizmos.positionGizmo.attachedNode = this._attachedNode;
@@ -219,7 +232,7 @@ export class GizmoManager implements IDisposable {
     public set rotationGizmoEnabled(value: boolean) {
         if (value) {
             if (!this.gizmos.rotationGizmo) {
-                this.gizmos.rotationGizmo = new RotationGizmo(this._defaultUtilityLayer, 32, false, this._thickness);
+                this.gizmos.rotationGizmo = new RotationGizmo(this._defaultUtilityLayer, 32, false, this._thickness, this);
             }
             if (this._attachedNode) {
                 this.gizmos.rotationGizmo.attachedNode = this._attachedNode;
@@ -239,7 +252,7 @@ export class GizmoManager implements IDisposable {
      */
     public set scaleGizmoEnabled(value: boolean) {
         if (value) {
-            this.gizmos.scaleGizmo = this.gizmos.scaleGizmo || new ScaleGizmo(this._defaultUtilityLayer, this._thickness);
+            this.gizmos.scaleGizmo = this.gizmos.scaleGizmo || new ScaleGizmo(this._defaultUtilityLayer, this._thickness, this);
             if (this._attachedNode) {
                 this.gizmos.scaleGizmo.attachedNode = this._attachedNode;
             } else {
@@ -287,10 +300,25 @@ export class GizmoManager implements IDisposable {
     }
 
     /**
+     * Builds Gizmo Axis Cache to enable features such as hover state preservation and graying out other axis during manipulation
+     * @param gizmoAxisCache Gizmo axis definition used for reactive gizmo UI
+     */
+    public addToAxisCache(gizmoAxisCache: Map<Mesh, GizmoAxisCache>) {
+        if (gizmoAxisCache.size > 0) {
+            gizmoAxisCache.forEach((v, k) => {
+                this._gizmoAxisCache.set(k, v);
+            });
+        }
+    }
+
+    /**
      * Disposes of the gizmo manager
      */
     public dispose() {
-        this.scene.onPointerObservable.remove(this._pointerObserver);
+
+        this._pointerObservers.forEach((observer) => {
+            this.scene.onPointerObservable.remove(observer);
+        });
         for (var key in this.gizmos) {
             var gizmo = <Nullable<Gizmo>>((<any>this.gizmos)[key]);
             if (gizmo) {

+ 38 - 28
src/Gizmos/planeDragGizmo.ts

@@ -8,7 +8,7 @@ import { Node } from "../node";
 import { Mesh } from "../Meshes/mesh";
 import { PlaneBuilder } from "../Meshes/Builders/planeBuilder";
 import { PointerDragBehavior } from "../Behaviors/Meshes/pointerDragBehavior";
-import { Gizmo } from "./gizmo";
+import { Gizmo, GizmoAxisCache } from "./gizmo";
 import { UtilityLayerRenderer } from "../Rendering/utilityLayerRenderer";
 import { StandardMaterial } from "../Materials/standardMaterial";
 import { Scene } from "../scene";
@@ -32,12 +32,14 @@ export class PlaneDragGizmo extends Gizmo {
      */
     public onSnapObservable = new Observable<{ snapDistance: number }>();
 
-    private _plane: TransformNode;
+    private _gizmoMesh: TransformNode;
     private _coloredMaterial: StandardMaterial;
     private _hoverMaterial: StandardMaterial;
+    private _disableMaterial: StandardMaterial;
 
     private _isEnabled: boolean = false;
     private _parent: Nullable<PositionGizmo> = null;
+    private _dragging: boolean = false;
 
     /** @hidden */
     public static _CreatePlane(scene: Scene, material: StandardMaterial): TransformNode {
@@ -50,16 +52,6 @@ export class PlaneDragGizmo extends Gizmo {
         return plane;
     }
 
-    /** @hidden */
-    public static _CreateArrowInstance(scene: Scene, arrow: TransformNode): TransformNode {
-        const instance = new TransformNode("arrow", scene);
-        for (const mesh of arrow.getChildMeshes()) {
-            const childInstance = (mesh as Mesh).createInstance(mesh.name);
-            childInstance.parent = instance;
-        }
-        return instance;
-    }
-
     /**
      * Creates a PlaneDragGizmo
      * @param gizmoLayer The utility layer the gizmo will be added to
@@ -75,14 +67,18 @@ export class PlaneDragGizmo extends Gizmo {
         this._coloredMaterial.specularColor = color.subtract(new Color3(0.1, 0.1, 0.1));
 
         this._hoverMaterial = new StandardMaterial("", gizmoLayer.utilityLayerScene);
-        this._hoverMaterial.diffuseColor = color.add(new Color3(0.3, 0.3, 0.3));
+        this._hoverMaterial.diffuseColor = Color3.Yellow();
+
+        this._disableMaterial = new StandardMaterial("", gizmoLayer.utilityLayerScene);
+        this._disableMaterial.diffuseColor = Color3.Gray();
+        this._disableMaterial.alpha = 0.4;
 
         // Build plane mesh on root node
-        this._plane = PlaneDragGizmo._CreatePlane(gizmoLayer.utilityLayerScene, this._coloredMaterial);
+        this._gizmoMesh = PlaneDragGizmo._CreatePlane(gizmoLayer.utilityLayerScene, this._coloredMaterial);
 
-        this._plane.lookAt(this._rootMesh.position.add(dragPlaneNormal));
-        this._plane.scaling.scaleInPlace(1 / 3);
-        this._plane.parent = this._rootMesh;
+        this._gizmoMesh.lookAt(this._rootMesh.position.add(dragPlaneNormal));
+        this._gizmoMesh.scaling.scaleInPlace(1 / 3);
+        this._gizmoMesh.parent = this._rootMesh;
 
         var currentSnapDragDistance = 0;
         var tmpVector = new Vector3();
@@ -116,20 +112,34 @@ export class PlaneDragGizmo extends Gizmo {
                 this._matrixChanged();
             }
         });
+        this.dragBehavior.onDragStartObservable.add(() => { this._dragging = true; });
+        this.dragBehavior.onDragEndObservable.add(() => { this._dragging = false; });
+
+        var light = gizmoLayer._getSharedGizmoLight();
+        light.includedOnlyMeshes = light.includedOnlyMeshes.concat(this._rootMesh.getChildMeshes(false));
+
+        const cache: GizmoAxisCache = {
+            gizmoMeshes: this._gizmoMesh.getChildMeshes() as Mesh[],
+            colliderMeshes: this._gizmoMesh.getChildMeshes() as Mesh[],
+            material: this._coloredMaterial,
+            hoverMaterial: this._hoverMaterial,
+            disableMaterial: this._disableMaterial,
+            active: false
+        };
+        this._parent?.addToAxisCache((this._gizmoMesh as Mesh), cache);
 
         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 ? this._hoverMaterial : this._coloredMaterial;
-            this._rootMesh.getChildMeshes().forEach((m) => {
-                m.material = material;
-            });
+            this._isHovered = !!(cache.colliderMeshes.indexOf(<Mesh>pointerInfo?.pickInfo?.pickedMesh) != -1);
+            if (!this._parent) {
+                var material = this._isHovered || this._dragging ? this._hoverMaterial : this._coloredMaterial;
+                cache.gizmoMeshes.forEach((m: Mesh) => {
+                    m.material = material;
+                });
+            }
         });
-
-        var light = gizmoLayer._getSharedGizmoLight();
-        light.includedOnlyMeshes = light.includedOnlyMeshes.concat(this._rootMesh.getChildMeshes(false));
     }
     protected _attachedNodeChanged(value: Nullable<Node>) {
         if (this.dragBehavior) {
@@ -162,10 +172,10 @@ export class PlaneDragGizmo extends Gizmo {
         this.gizmoLayer.utilityLayerScene.onPointerObservable.remove(this._pointerObserver);
         this.dragBehavior.detach();
         super.dispose();
-        if (this._plane) {
-            this._plane.dispose();
+        if (this._gizmoMesh) {
+            this._gizmoMesh.dispose();
         }
-        [this._coloredMaterial, this._hoverMaterial].forEach((matl) => {
+        [this._coloredMaterial, this._hoverMaterial, this._disableMaterial].forEach((matl) => {
             if (matl) {
                 matl.dispose();
             }

+ 184 - 37
src/Gizmos/planeRotationGizmo.ts

@@ -3,16 +3,15 @@ import { Nullable } from "../types";
 import { PointerInfo } from "../Events/pointerEvents";
 import { Quaternion, Matrix, Vector3 } from "../Maths/math.vector";
 import { Color3 } from '../Maths/math.color';
+import "../Meshes/Builders/linesBuilder";
 import { AbstractMesh } from "../Meshes/abstractMesh";
+import { LinesMesh } from '../Meshes/linesMesh';
 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 { Gizmo, GizmoAxisCache } from "./gizmo";
 import { UtilityLayerRenderer } from "../Rendering/utilityLayerRenderer";
 import { StandardMaterial } from "../Materials/standardMaterial";
-
-import "../Meshes/Builders/linesBuilder";
 import { RotationGizmo } from "./rotationGizmo";
 
 /**
@@ -37,6 +36,19 @@ export class PlaneRotationGizmo extends Gizmo {
 
     private _isEnabled: boolean = true;
     private _parent: Nullable<RotationGizmo> = null;
+    private _coloredMaterial: StandardMaterial;
+    private _hoverMaterial: StandardMaterial;
+    private _disableMaterial: StandardMaterial;
+    private _gizmoMesh: Mesh;
+    private _rotationCircle: Mesh;
+    private _dragging: boolean = false;
+
+    private static _CircleConstants = {
+        radius: 0.3,
+        pi2: Math.PI * 2,
+        tessellation: 70,
+        rotationCircleRange: 4
+    };
 
     /**
      * Creates a PlaneRotationGizmo
@@ -51,30 +63,28 @@ 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();
 
-        // Build mesh on root node
-        var parentMesh = new AbstractMesh("", gizmoLayer.utilityLayerScene);
+        this._disableMaterial = new StandardMaterial("", gizmoLayer.utilityLayerScene);
+        this._disableMaterial.diffuseColor = Color3.Gray();
+        this._disableMaterial.alpha = 0.4;
 
-        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;
+        // Build mesh on root node
+        this._gizmoMesh = new Mesh("", gizmoLayer.utilityLayerScene);
+        const { rotationMesh, collider } = this._createGizmoMesh(this._gizmoMesh, thickness, tessellation);
 
-        // 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));
+        // Setup Rotation Circle
+        const rotationCirclePaths: any[] = [];
+        this._rotationCircle = this.setupRotationCircle(rotationCirclePaths, this._gizmoMesh);
 
-        this._rootMesh.addChild(parentMesh);
-        parentMesh.scaling.scaleInPlace(1 / 3);
+        this._gizmoMesh.lookAt(this._rootMesh.position.add(planeNormal));
+        this._rootMesh.addChild(this._gizmoMesh);
+        this._gizmoMesh.scaling.scaleInPlace(1 / 3);
         // Add drag behavior to handle events when the gizmo is dragged
         this.dragBehavior = new PointerDragBehavior({ dragPlaneNormal: planeNormal });
         this.dragBehavior.moveAttached = false;
@@ -82,17 +92,44 @@ export class PlaneRotationGizmo extends Gizmo {
         this.dragBehavior._useAlternatePickedPointAboveMaxDragAngle = true;
         this._rootMesh.addBehavior(this.dragBehavior);
 
-        var lastDragPosition = new Vector3();
+        // Closures for drag logic
+        let dragDistance = 0;
+        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
+                const forward = new Vector3(0, 0, 1);
+                const direction = this._rotationCircle.getDirection(forward);
+                direction.normalize();
+
+                // Remove Rotation Circle from parent mesh before drag interaction
+                this._gizmoMesh.removeChild(this._rotationCircle);
+
+                lastDragPosition.copyFrom(e.dragPlanePoint);
+                dragPlanePoint = e.dragPlanePoint;
+                const origin = this._rotationCircle.getAbsolutePosition().clone();
+                const originalRotationPoint = this._rotationCircle.getAbsolutePosition().clone().addInPlace(direction);
+                const dragStartPoint = e.dragPlanePoint;
+                const angle = Vector3.GetAngleBetweenVectors(originalRotationPoint.subtract(origin), dragStartPoint.subtract(origin), this._rotationCircle.up);
+
+                this._rotationCircle.addRotation(0, angle, 0);
+                this._dragging = true;
             }
         });
 
-        var rotationMatrix = new Matrix();
-        var planeNormalTowardsCamera = new Vector3();
-        var localPlaneNormalTowardsCamera = new Vector3();
+        this.dragBehavior.onDragEndObservable.add(() => {
+            dragDistance = 0;
+            this.updateRotationCircle(this._rotationCircle, rotationCirclePaths, dragDistance, dragPlanePoint);
+            this._gizmoMesh.addChild(this._rotationCircle);    // Add rotation circle back to parent mesh after drag behavior
+            this._dragging = false;
+        });
 
         var tmpSnapEvent = { snapDistance: 0 };
         var currentSnapDragDistance = 0;
@@ -118,11 +155,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 +184,9 @@ export class PlaneRotationGizmo extends Gizmo {
                     }
                 }
 
+                dragDistance += cameraFlipped ? -angle : angle;
+                this.updateRotationCircle(this._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 +219,50 @@ export class PlaneRotationGizmo extends Gizmo {
             }
         });
 
+        var light = gizmoLayer._getSharedGizmoLight();
+        light.includedOnlyMeshes = light.includedOnlyMeshes.concat(this._rootMesh.getChildMeshes(false));
+
+        const cache: GizmoAxisCache = {
+            colliderMeshes: [ collider ],
+            gizmoMeshes: [ rotationMesh ],
+            material: this._coloredMaterial,
+            hoverMaterial: this._hoverMaterial,
+            disableMaterial: this._disableMaterial,
+            active: false
+        };
+        this._parent?.addToAxisCache(this._gizmoMesh, cache);
+
         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._isHovered = !!(cache.colliderMeshes.indexOf(<Mesh>pointerInfo?.pickInfo?.pickedMesh) != -1);
+            if (!this._parent) {
+                var material = this._isHovered || this._dragging ? this._hoverMaterial : this._coloredMaterial;
+                cache.gizmoMeshes.forEach((m: Mesh) => {
+                    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));
+    /** Create Geometry for Gizmo */
+    private _createGizmoMesh(parentMesh: AbstractMesh, thickness: number, tessellation: number) {
+        let collider = Mesh.CreateTorus("ignore", 0.6, 0.03 * thickness, tessellation, this.gizmoLayer.utilityLayerScene);
+        collider.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;
+        collider.rotation.x = Math.PI / 2;
+
+        parentMesh.addChild(rotationMesh);
+        parentMesh.addChild(collider);
+        return { rotationMesh, collider };
     }
 
     protected _attachedNodeChanged(value: Nullable<Node>) {
@@ -201,6 +271,72 @@ export class PlaneRotationGizmo extends Gizmo {
         }
     }
 
+    private setupRotationCircle(paths: Vector3[][], parentMesh: AbstractMesh): Mesh {
+        const fillRadians = 0;
+        const step = PlaneRotationGizmo._CircleConstants.pi2 / PlaneRotationGizmo._CircleConstants.tessellation;
+        for (let p = -Math.PI / 2; p < Math.PI / 2 - 1.5; p += step / 2) {
+            const path: Vector3[] = [];
+            for (let i = 0; i < PlaneRotationGizmo._CircleConstants.pi2 * PlaneRotationGizmo._CircleConstants.rotationCircleRange + 0.01; i += step) {
+                if (i < fillRadians) {
+                    const x = PlaneRotationGizmo._CircleConstants.radius * Math.sin(i) * Math.cos(p);
+                    const z = PlaneRotationGizmo._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("rotationCircle", 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: Vector3[][], newFill: number): void {
+        // To update the Ribbon, you have to mutate the pathArray in-place
+        const step = PlaneRotationGizmo._CircleConstants.pi2 / PlaneRotationGizmo._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 < PlaneRotationGizmo._CircleConstants.pi2 * PlaneRotationGizmo._CircleConstants.rotationCircleRange + 0.01; i += step) {
+                    if (path[radianCounter]) {
+                        if (i < Math.abs(newFill)) {
+                            const absI = (newFill > 0) ? i : i * -1;
+                            const absP = (newFill > 0) ? p : p * -1;
+                            path[radianCounter].set(
+                                PlaneRotationGizmo._CircleConstants.radius * Math.sin(absI) * Math.cos(absP),
+                                0,
+                                PlaneRotationGizmo._CircleConstants.radius * Math.cos(absI) * Math.cos(absP)
+                            );
+                        } else {
+                            path[radianCounter].set(0, 0, 0);
+                        }
+                    }
+
+                    radianCounter++;
+                }
+            }
+
+            tessellationCounter ++;
+        }
+    }
+
+    private updateRotationCircle(mesh: Mesh, paths: any[], newFill: number, dragPlanePoint: Vector3): void {
+        this.updateRotationPath(paths, newFill);
+        Mesh.CreateRibbon("rotationCircle", paths, false, false, 0, this.gizmoLayer.utilityLayerScene, undefined, undefined, mesh);
+    }
+
     /**
          * If the gizmo is enabled
          */
@@ -225,6 +361,17 @@ export class PlaneRotationGizmo extends Gizmo {
         this.onSnapObservable.clear();
         this.gizmoLayer.utilityLayerScene.onPointerObservable.remove(this._pointerObserver);
         this.dragBehavior.detach();
+        if (this._gizmoMesh) {
+            this._gizmoMesh.dispose();
+        }
+        if (this._rotationCircle) {
+            this._rotationCircle.dispose();
+        }
+        [this._coloredMaterial, this._hoverMaterial, this._disableMaterial].forEach((matl) => {
+            if (matl) {
+                matl.dispose();
+            }
+        });
         super.dispose();
     }
 }

+ 28 - 3
src/Gizmos/positionGizmo.ts

@@ -1,15 +1,17 @@
 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';
 import { AbstractMesh } from "../Meshes/abstractMesh";
 import { Node } from "../node";
 import { Mesh } from "../Meshes/mesh";
-import { Gizmo } from "./gizmo";
+import { Gizmo, GizmoAxisCache } from "./gizmo";
 import { AxisDragGizmo } from "./axisDragGizmo";
 import { PlaneDragGizmo } from "./planeDragGizmo";
 import { UtilityLayerRenderer } from "../Rendering/utilityLayerRenderer";
+import { PointerInfo } from "../Events/pointerEvents";
+import { GizmoManager } from './gizmoManager';
 /**
  * Gizmo that enables dragging a mesh along 3 axis
  */
@@ -45,6 +47,10 @@ export class PositionGizmo extends Gizmo {
     private _meshAttached: Nullable<AbstractMesh> = null;
     private _nodeAttached: Nullable<Node> = null;
     private _snapDistance: number;
+    private _observables: Observer<PointerInfo>[] = [];
+
+    /** Node Caching for quick lookup */
+    private _gizmoAxisCache: Map<Mesh, GizmoAxisCache> = new Map();
 
     /** Fires an event when any of it's sub gizmos are dragged */
     public onDragStartObservable = new Observable();
@@ -104,7 +110,7 @@ export class PositionGizmo extends Gizmo {
      * @param gizmoLayer The utility layer the gizmo will be added to
       @param thickness display gizmo axis thickness
      */
-    constructor(gizmoLayer: UtilityLayerRenderer = UtilityLayerRenderer.DefaultUtilityLayer, thickness: number = 1) {
+    constructor(gizmoLayer: UtilityLayerRenderer = UtilityLayerRenderer.DefaultUtilityLayer, thickness: number = 1, gizmoManager?: GizmoManager) {
         super(gizmoLayer);
         this.xGizmo = new AxisDragGizmo(new Vector3(1, 0, 0), Color3.Red().scale(0.5), gizmoLayer, this, thickness);
         this.yGizmo = new AxisDragGizmo(new Vector3(0, 1, 0), Color3.Green().scale(0.5), gizmoLayer, this, thickness);
@@ -124,6 +130,13 @@ export class PositionGizmo extends Gizmo {
         });
 
         this.attachedMesh = null;
+
+        if (gizmoManager) {
+            gizmoManager.addToAxisCache(this._gizmoAxisCache);
+        } else {
+            // Only subscribe to pointer event if gizmoManager isnt
+            Gizmo.GizmoAxisPointerObserver(gizmoLayer, this._gizmoAxisCache);
+        }
     }
 
     /**
@@ -193,6 +206,15 @@ export class PositionGizmo extends Gizmo {
     }
 
     /**
+     * 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 Gizmo axis definition used for reactive gizmo UI
+     */
+    public addToAxisCache(mesh: Mesh, cache: GizmoAxisCache) {
+        this._gizmoAxisCache.set(mesh, cache);
+    }
+
+    /**
      * Disposes of the gizmo
      */
     public dispose() {
@@ -201,6 +223,9 @@ export class PositionGizmo extends Gizmo {
                 gizmo.dispose();
             }
         });
+        this._observables.forEach((obs) => {
+            this.gizmoLayer.utilityLayerScene.onPointerObservable.remove(obs);
+        });
         this.onDragStartObservable.clear();
         this.onDragEndObservable.clear();
     }

+ 28 - 4
src/Gizmos/rotationGizmo.ts

@@ -1,16 +1,17 @@
 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';
 import { AbstractMesh } from "../Meshes/abstractMesh";
 import { Mesh } from "../Meshes/mesh";
-import { Gizmo } from "./gizmo";
+import { Gizmo, GizmoAxisCache } from "./gizmo";
 import { PlaneRotationGizmo } from "./planeRotationGizmo";
 import { UtilityLayerRenderer } from "../Rendering/utilityLayerRenderer";
 import { Node } from "../node";
+import { PointerInfo } from "../Events/pointerEvents";
 import { TransformNode } from "../Meshes/transformNode";
-
+import { GizmoManager } from './gizmoManager';
 /**
  * Gizmo that enables rotating a mesh along 3 axis
  */
@@ -35,6 +36,10 @@ export class RotationGizmo extends Gizmo {
 
     private _meshAttached: Nullable<AbstractMesh>;
     private _nodeAttached: Nullable<Node>;
+    private _observables: Observer<PointerInfo>[] = [];
+
+    /** Node Caching for quick lookup */
+    private _gizmoAxisCache: Map<Mesh, GizmoAxisCache> = new Map();
 
     public get attachedMesh() {
         return this._meshAttached;
@@ -94,7 +99,7 @@ export class RotationGizmo extends Gizmo {
      * @param useEulerRotation Use and update Euler angle instead of quaternion
      * @param thickness display gizmo axis thickness
      */
-    constructor(gizmoLayer: UtilityLayerRenderer = UtilityLayerRenderer.DefaultUtilityLayer, tessellation = 32, useEulerRotation = false, thickness: number = 1) {
+    constructor(gizmoLayer: UtilityLayerRenderer = UtilityLayerRenderer.DefaultUtilityLayer, tessellation = 32, useEulerRotation = false, thickness: number = 1, gizmoManager?: GizmoManager) {
         super(gizmoLayer);
         this.xGizmo = new PlaneRotationGizmo(new Vector3(1, 0, 0), Color3.Red().scale(0.5), gizmoLayer, tessellation, this, useEulerRotation, thickness);
         this.yGizmo = new PlaneRotationGizmo(new Vector3(0, 1, 0), Color3.Green().scale(0.5), gizmoLayer, tessellation, this, useEulerRotation, thickness);
@@ -112,6 +117,13 @@ export class RotationGizmo extends Gizmo {
 
         this.attachedMesh = null;
         this.attachedNode = null;
+
+        if (gizmoManager) {
+            gizmoManager.addToAxisCache(this._gizmoAxisCache);
+        } else {
+            // Only subscribe to pointer event if gizmoManager isnt
+            Gizmo.GizmoAxisPointerObserver(gizmoLayer, this._gizmoAxisCache);
+        }
     }
 
     public set updateGizmoRotationToMatchAttachedMesh(value: boolean) {
@@ -154,6 +166,15 @@ export class RotationGizmo extends Gizmo {
     }
 
     /**
+     * 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 Gizmo axis definition used for reactive gizmo UI
+     */
+    public addToAxisCache(mesh: Mesh, cache: GizmoAxisCache) {
+        this._gizmoAxisCache.set(mesh, cache);
+    }
+
+    /**
      * Disposes of the gizmo
      */
     public dispose() {
@@ -162,6 +183,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);
+        });
     }
 
     /**

+ 77 - 18
src/Gizmos/scaleGizmo.ts

@@ -1,15 +1,18 @@
 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';
 import { AbstractMesh } from "../Meshes/abstractMesh";
 import { PolyhedronBuilder } from "../Meshes/Builders/polyhedronBuilder";
-import { Gizmo } from "./gizmo";
+import { Gizmo, GizmoAxisCache } from "./gizmo";
 import { AxisScaleGizmo } from "./axisScaleGizmo";
 import { UtilityLayerRenderer } from "../Rendering/utilityLayerRenderer";
 import { Mesh } from "../Meshes/mesh";
 import { Node } from "../node";
+import { PointerInfo } from "../Events/pointerEvents";
+import { StandardMaterial } from "../Materials/standardMaterial";
+import { GizmoManager } from './gizmoManager';
 /**
  * Gizmo that enables scaling a mesh along 3 axis
  */
@@ -38,6 +41,13 @@ export class ScaleGizmo extends Gizmo {
     private _uniformScalingMesh: Mesh;
     private _octahedron: Mesh;
     private _sensitivity: number = 1;
+    private _coloredMaterial: StandardMaterial;
+    private _hoverMaterial: StandardMaterial;
+    private _disableMaterial: StandardMaterial;
+    private _observables: Observer<PointerInfo>[] = [];
+
+    /** Node Caching for quick lookup */
+    private _gizmoAxisCache: Map<Mesh, GizmoAxisCache> = new Map();
 
     /** Fires an event when any of it's sub gizmos are dragged */
     public onDragStartObservable = new Observable();
@@ -92,26 +102,13 @@ export class ScaleGizmo extends Gizmo {
      * @param gizmoLayer The utility layer the gizmo will be added to
      * @param thickness display gizmo axis thickness
      */
-    constructor(gizmoLayer: UtilityLayerRenderer = UtilityLayerRenderer.DefaultUtilityLayer, thickness: number = 1) {
+    constructor(gizmoLayer: UtilityLayerRenderer = UtilityLayerRenderer.DefaultUtilityLayer, thickness: number = 1, gizmoManager?: GizmoManager) {
         super(gizmoLayer);
+        this.uniformScaleGizmo = this._createUniformScaleMesh();
         this.xGizmo = new AxisScaleGizmo(new Vector3(1, 0, 0), Color3.Red().scale(0.5), gizmoLayer, this, thickness);
         this.yGizmo = new AxisScaleGizmo(new Vector3(0, 1, 0), Color3.Green().scale(0.5), gizmoLayer, this, thickness);
         this.zGizmo = new AxisScaleGizmo(new Vector3(0, 0, 1), Color3.Blue().scale(0.5), gizmoLayer, this, thickness);
 
-        // Create uniform scale gizmo
-        this.uniformScaleGizmo = new AxisScaleGizmo(new Vector3(0, 1, 0), Color3.Yellow().scale(0.5), gizmoLayer, this);
-        this.uniformScaleGizmo.updateGizmoRotationToMatchAttachedMesh = false;
-        this.uniformScaleGizmo.uniformScaling = true;
-        this._uniformScalingMesh = PolyhedronBuilder.CreatePolyhedron("", { type: 1 }, this.uniformScaleGizmo.gizmoLayer.utilityLayerScene);
-        this._uniformScalingMesh.scaling.scaleInPlace(0.02);
-        this._uniformScalingMesh.visibility = 0;
-        this._octahedron = PolyhedronBuilder.CreatePolyhedron("", { type: 1 }, this.uniformScaleGizmo.gizmoLayer.utilityLayerScene);
-        this._octahedron.scaling.scaleInPlace(0.007);
-        this._uniformScalingMesh.addChild(this._octahedron);
-        this.uniformScaleGizmo.setCustomMesh(this._uniformScalingMesh, true);
-        var light = gizmoLayer._getSharedGizmoLight();
-        light.includedOnlyMeshes = light.includedOnlyMeshes.concat(this._octahedron);
-
         // Relay drag events
         [this.xGizmo, this.yGizmo, this.zGizmo, this.uniformScaleGizmo].forEach((gizmo) => {
             gizmo.dragBehavior.onDragStartObservable.add(() => {
@@ -124,6 +121,52 @@ export class ScaleGizmo extends Gizmo {
 
         this.attachedMesh = null;
         this.attachedNode = null;
+
+        if (gizmoManager) {
+            gizmoManager.addToAxisCache(this._gizmoAxisCache);
+        } else {
+            // Only subscribe to pointer event if gizmoManager isnt
+            Gizmo.GizmoAxisPointerObserver(gizmoLayer, this._gizmoAxisCache);
+        }
+    }
+
+    /** Create Geometry for Gizmo */
+    private _createUniformScaleMesh(): AxisScaleGizmo {
+        this._coloredMaterial = new StandardMaterial("", this.gizmoLayer.utilityLayerScene);
+        this._coloredMaterial.diffuseColor = Color3.Gray();
+
+        this._hoverMaterial = new StandardMaterial("", this.gizmoLayer.utilityLayerScene);
+        this._hoverMaterial.diffuseColor = Color3.Yellow();
+
+        this._disableMaterial = new StandardMaterial("", this.gizmoLayer.utilityLayerScene);
+        this._disableMaterial.diffuseColor = Color3.Gray();
+        this._disableMaterial.alpha = 0.4;
+
+        const uniformScaleGizmo = new AxisScaleGizmo(new Vector3(0, 1, 0), Color3.Gray().scale(0.5), this.gizmoLayer, this);
+        uniformScaleGizmo.updateGizmoRotationToMatchAttachedMesh = false;
+        uniformScaleGizmo.uniformScaling = true;
+        this._uniformScalingMesh = PolyhedronBuilder.CreatePolyhedron("uniform", { type: 1 }, uniformScaleGizmo.gizmoLayer.utilityLayerScene);
+        this._uniformScalingMesh.scaling.scaleInPlace(0.01);
+        this._uniformScalingMesh.visibility = 0;
+        this._octahedron = PolyhedronBuilder.CreatePolyhedron("", { type: 1 }, uniformScaleGizmo.gizmoLayer.utilityLayerScene);
+        this._octahedron.scaling.scaleInPlace(0.007);
+        this._uniformScalingMesh.addChild(this._octahedron);
+        uniformScaleGizmo.setCustomMesh(this._uniformScalingMesh, true);
+        var light = this.gizmoLayer._getSharedGizmoLight();
+        light.includedOnlyMeshes = light.includedOnlyMeshes.concat(this._octahedron);
+
+        const cache: GizmoAxisCache = {
+            gizmoMeshes: [this._octahedron, this._uniformScalingMesh],
+            colliderMeshes: [this._uniformScalingMesh],
+            material: this._coloredMaterial,
+            hoverMaterial: this._hoverMaterial,
+            disableMaterial: this._disableMaterial,
+            active: false
+        };
+
+        this.addToAxisCache(uniformScaleGizmo._rootMesh, cache);
+
+        return uniformScaleGizmo;
     }
 
     public set updateGizmoRotationToMatchAttachedMesh(value: boolean) {
@@ -189,6 +232,15 @@ export class ScaleGizmo extends Gizmo {
     }
 
     /**
+     * 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 Gizmo axis definition used for reactive gizmo UI
+     */
+    public addToAxisCache(mesh: Mesh, cache: GizmoAxisCache) {
+        this._gizmoAxisCache.set(mesh, cache);
+    }
+
+    /**
      * Disposes of the gizmo
      */
     public dispose() {
@@ -197,13 +249,20 @@ export class ScaleGizmo extends Gizmo {
                 gizmo.dispose();
             }
         });
+        this._observables.forEach((obs) => {
+            this.gizmoLayer.utilityLayerScene.onPointerObservable.remove(obs);
+        });
         this.onDragStartObservable.clear();
         this.onDragEndObservable.clear();
-
         [this._uniformScalingMesh, this._octahedron].forEach((msh) => {
             if (msh) {
                 msh.dispose();
             }
         });
+        [this._coloredMaterial, this._hoverMaterial, this._disableMaterial].forEach((matl) => {
+            if (matl) {
+                matl.dispose();
+            }
+        });
     }
 }