planeRotationGizmo.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import { Observer, Observable } from "../Misc/observable";
  2. import { Nullable } from "../types";
  3. import { PointerInfo } from "../Events/pointerEvents";
  4. import { Quaternion, Matrix, Vector3, Color3 } from "../Maths/math";
  5. import { AbstractMesh } from "../Meshes/abstractMesh";
  6. import { Mesh } from "../Meshes/mesh";
  7. import { LinesMesh } from "../Meshes/linesMesh";
  8. import { PointerDragBehavior } from "../Behaviors/Meshes/pointerDragBehavior";
  9. import { _TimeToken } from "../Instrumentation/timeToken";
  10. import { _DepthCullingState, _StencilState, _AlphaState } from "../States/index";
  11. import { Gizmo } from "./gizmo";
  12. import { UtilityLayerRenderer } from "../Rendering/utilityLayerRenderer";
  13. import { StandardMaterial } from "../Materials/standardMaterial";
  14. import "../Meshes/Builders/linesBuilder";
  15. /**
  16. * Single plane rotation gizmo
  17. */
  18. export class PlaneRotationGizmo extends Gizmo {
  19. /**
  20. * Drag behavior responsible for the gizmos dragging interactions
  21. */
  22. public dragBehavior: PointerDragBehavior;
  23. private _pointerObserver: Nullable<Observer<PointerInfo>> = null;
  24. /**
  25. * Rotation distance in radians that the gizmo will snap to (Default: 0)
  26. */
  27. public snapDistance = 0;
  28. /**
  29. * Event that fires each time the gizmo snaps to a new location.
  30. * * snapDistance is the the change in distance
  31. */
  32. public onSnapObservable = new Observable<{ snapDistance: number }>();
  33. /**
  34. * Creates a PlaneRotationGizmo
  35. * @param gizmoLayer The utility layer the gizmo will be added to
  36. * @param planeNormal The normal of the plane which the gizmo will be able to rotate on
  37. * @param color The color of the gizmo
  38. * @param tessellation Amount of tessellation to be used when creating rotation circles
  39. */
  40. constructor(planeNormal: Vector3, color: Color3 = Color3.Gray(), gizmoLayer: UtilityLayerRenderer = UtilityLayerRenderer.DefaultUtilityLayer, tessellation = 32) {
  41. super(gizmoLayer);
  42. // Create Material
  43. var coloredMaterial = new StandardMaterial("", gizmoLayer.utilityLayerScene);
  44. coloredMaterial.diffuseColor = color;
  45. coloredMaterial.specularColor = color.subtract(new Color3(0.1, 0.1, 0.1));
  46. var hoverMaterial = new StandardMaterial("", gizmoLayer.utilityLayerScene);
  47. hoverMaterial.diffuseColor = color.add(new Color3(0.3, 0.3, 0.3));
  48. // Build mesh on root node
  49. var parentMesh = new AbstractMesh("", gizmoLayer.utilityLayerScene);
  50. let drag = Mesh.CreateTorus("", 0.6, 0.03, tessellation, gizmoLayer.utilityLayerScene);
  51. drag.visibility = 0;
  52. let rotationMesh = Mesh.CreateTorus("", 0.6, 0.005, tessellation, gizmoLayer.utilityLayerScene);
  53. rotationMesh.material = coloredMaterial;
  54. // Position arrow pointing in its drag axis
  55. rotationMesh.rotation.x = Math.PI / 2;
  56. drag.rotation.x = Math.PI / 2;
  57. parentMesh.addChild(rotationMesh);
  58. parentMesh.addChild(drag);
  59. parentMesh.lookAt(this._rootMesh.position.add(planeNormal));
  60. this._rootMesh.addChild(parentMesh);
  61. parentMesh.scaling.scaleInPlace(1 / 3);
  62. // Add drag behavior to handle events when the gizmo is dragged
  63. this.dragBehavior = new PointerDragBehavior({ dragPlaneNormal: planeNormal });
  64. this.dragBehavior.moveAttached = false;
  65. this.dragBehavior.maxDragAngle = Math.PI * 9 / 20;
  66. this.dragBehavior._useAlternatePickedPointAboveMaxDragAngle = true;
  67. this._rootMesh.addBehavior(this.dragBehavior);
  68. var lastDragPosition = new Vector3();
  69. this.dragBehavior.onDragStartObservable.add((e) => {
  70. if (this.attachedMesh) {
  71. lastDragPosition.copyFrom(e.dragPlanePoint);
  72. }
  73. });
  74. var rotationMatrix = new Matrix();
  75. var planeNormalTowardsCamera = new Vector3();
  76. var localPlaneNormalTowardsCamera = new Vector3();
  77. var tmpSnapEvent = { snapDistance: 0 };
  78. var currentSnapDragDistance = 0;
  79. var tmpMatrix = new Matrix();
  80. var tmpVector = new Vector3();
  81. var amountToRotate = new Quaternion();
  82. this.dragBehavior.onDragObservable.add((event) => {
  83. if (this.attachedMesh) {
  84. if (!this.attachedMesh.rotationQuaternion) {
  85. this.attachedMesh.rotationQuaternion = Quaternion.RotationYawPitchRoll(this.attachedMesh.rotation.y, this.attachedMesh.rotation.x, this.attachedMesh.rotation.z);
  86. }
  87. // Calc angle over full 360 degree (https://stackoverflow.com/questions/43493711/the-angle-between-two-3d-vectors-with-a-result-range-0-360)
  88. var newVector = event.dragPlanePoint.subtract(this.attachedMesh.absolutePosition).normalize();
  89. var originalVector = lastDragPosition.subtract(this.attachedMesh.absolutePosition).normalize();
  90. var cross = Vector3.Cross(newVector, originalVector);
  91. var dot = Vector3.Dot(newVector, originalVector);
  92. var angle = Math.atan2(cross.length(), dot);
  93. planeNormalTowardsCamera.copyFrom(planeNormal);
  94. localPlaneNormalTowardsCamera.copyFrom(planeNormal);
  95. if (this.updateGizmoRotationToMatchAttachedMesh) {
  96. this.attachedMesh.rotationQuaternion.toRotationMatrix(rotationMatrix);
  97. localPlaneNormalTowardsCamera = Vector3.TransformCoordinates(planeNormalTowardsCamera, rotationMatrix);
  98. }
  99. // Flip up vector depending on which side the camera is on
  100. if (gizmoLayer.utilityLayerScene.activeCamera) {
  101. var camVec = gizmoLayer.utilityLayerScene.activeCamera.position.subtract(this.attachedMesh.position);
  102. if (Vector3.Dot(camVec, localPlaneNormalTowardsCamera) > 0) {
  103. planeNormalTowardsCamera.scaleInPlace(-1);
  104. localPlaneNormalTowardsCamera.scaleInPlace(-1);
  105. }
  106. }
  107. var halfCircleSide = Vector3.Dot(localPlaneNormalTowardsCamera, cross) > 0.0;
  108. if (halfCircleSide) { angle = -angle; }
  109. // Snapping logic
  110. var snapped = false;
  111. if (this.snapDistance != 0) {
  112. currentSnapDragDistance += angle;
  113. if (Math.abs(currentSnapDragDistance) > this.snapDistance) {
  114. var dragSteps = Math.floor(Math.abs(currentSnapDragDistance) / this.snapDistance);
  115. if (currentSnapDragDistance < 0) {
  116. dragSteps *= -1;
  117. }
  118. currentSnapDragDistance = currentSnapDragDistance % this.snapDistance;
  119. angle = this.snapDistance * dragSteps;
  120. snapped = true;
  121. } else {
  122. angle = 0;
  123. }
  124. }
  125. // If the mesh has a parent, convert needed world rotation to local rotation
  126. tmpMatrix.reset();
  127. if (this.attachedMesh.parent) {
  128. this.attachedMesh.parent.computeWorldMatrix().invertToRef(tmpMatrix);
  129. tmpMatrix.getRotationMatrixToRef(tmpMatrix);
  130. Vector3.TransformCoordinatesToRef(planeNormalTowardsCamera, tmpMatrix, planeNormalTowardsCamera);
  131. }
  132. // Convert angle and axis to quaternion (http://www.euclideanspace.com/maths/geometry/rotations/conversions/angleToQuaternion/index.htm)
  133. var quaternionCoefficient = Math.sin(angle / 2);
  134. amountToRotate.set(planeNormalTowardsCamera.x * quaternionCoefficient, planeNormalTowardsCamera.y * quaternionCoefficient, planeNormalTowardsCamera.z * quaternionCoefficient, Math.cos(angle / 2));
  135. // If the meshes local scale is inverted (eg. loaded gltf file parent with z scale of -1) the rotation needs to be inverted on the y axis
  136. if (tmpMatrix.determinant() > 0) {
  137. amountToRotate.toEulerAnglesToRef(tmpVector);
  138. Quaternion.RotationYawPitchRollToRef(tmpVector.y, -tmpVector.x, -tmpVector.z, amountToRotate);
  139. }
  140. if (this.updateGizmoRotationToMatchAttachedMesh) {
  141. // Rotate selected mesh quaternion over fixed axis
  142. this.attachedMesh.rotationQuaternion.multiplyToRef(amountToRotate, this.attachedMesh.rotationQuaternion);
  143. } else {
  144. // Rotate selected mesh quaternion over rotated axis
  145. amountToRotate.multiplyToRef(this.attachedMesh.rotationQuaternion, this.attachedMesh.rotationQuaternion);
  146. }
  147. lastDragPosition.copyFrom(event.dragPlanePoint);
  148. if (snapped) {
  149. tmpSnapEvent.snapDistance = angle;
  150. this.onSnapObservable.notifyObservers(tmpSnapEvent);
  151. }
  152. }
  153. });
  154. this._pointerObserver = gizmoLayer.utilityLayerScene.onPointerObservable.add((pointerInfo) => {
  155. if (this._customMeshSet) {
  156. return;
  157. }
  158. var isHovered = pointerInfo.pickInfo && (this._rootMesh.getChildMeshes().indexOf(<Mesh>pointerInfo.pickInfo.pickedMesh) != -1);
  159. var material = isHovered ? hoverMaterial : coloredMaterial;
  160. this._rootMesh.getChildMeshes().forEach((m) => {
  161. m.material = material;
  162. if ((<LinesMesh>m).color) {
  163. (<LinesMesh>m).color = material.diffuseColor;
  164. }
  165. });
  166. });
  167. var light = gizmoLayer._getSharedGizmoLight();
  168. light.includedOnlyMeshes = light.includedOnlyMeshes.concat(this._rootMesh.getChildMeshes(false));
  169. }
  170. protected _attachedMeshChanged(value: Nullable<AbstractMesh>) {
  171. if (this.dragBehavior) {
  172. this.dragBehavior.enabled = value ? true : false;
  173. }
  174. }
  175. /**
  176. * Disposes of the gizmo
  177. */
  178. public dispose() {
  179. this.onSnapObservable.clear();
  180. this.gizmoLayer.utilityLayerScene.onPointerObservable.remove(this._pointerObserver);
  181. this.dragBehavior.detach();
  182. super.dispose();
  183. }
  184. }