planeRotationGizmo.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. import { Observer, Observable } from "../Misc/observable";
  2. import { Nullable } from "../types";
  3. import { PointerInfo } from "../Events/pointerEvents";
  4. import { Quaternion, Matrix, Vector3 } from "../Maths/math.vector";
  5. import { Color3 } from '../Maths/math.color';
  6. import { AbstractMesh } from "../Meshes/abstractMesh";
  7. import { Mesh } from "../Meshes/mesh";
  8. import { LinesMesh } from "../Meshes/linesMesh";
  9. import { PointerDragBehavior } from "../Behaviors/Meshes/pointerDragBehavior";
  10. import { Gizmo } from "./gizmo";
  11. import { UtilityLayerRenderer } from "../Rendering/utilityLayerRenderer";
  12. import { StandardMaterial } from "../Materials/standardMaterial";
  13. import "../Meshes/Builders/linesBuilder";
  14. import { RotationGizmo } from "./rotationGizmo";
  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. private _isEnabled: boolean = true;
  34. private _parent: Nullable<RotationGizmo> = null;
  35. /**
  36. * Creates a PlaneRotationGizmo
  37. * @param gizmoLayer The utility layer the gizmo will be added to
  38. * @param planeNormal The normal of the plane which the gizmo will be able to rotate on
  39. * @param color The color of the gizmo
  40. * @param tessellation Amount of tessellation to be used when creating rotation circles
  41. * @param useEulerRotation Use and update Euler angle instead of quaternion
  42. */
  43. constructor(planeNormal: Vector3, color: Color3 = Color3.Gray(), gizmoLayer: UtilityLayerRenderer = UtilityLayerRenderer.DefaultUtilityLayer, tessellation = 32, parent: Nullable<RotationGizmo> = null, useEulerRotation = false) {
  44. super(gizmoLayer);
  45. this._parent = parent;
  46. // Create Material
  47. var coloredMaterial = new StandardMaterial("", gizmoLayer.utilityLayerScene);
  48. coloredMaterial.diffuseColor = color;
  49. coloredMaterial.specularColor = color.subtract(new Color3(0.1, 0.1, 0.1));
  50. var hoverMaterial = new StandardMaterial("", gizmoLayer.utilityLayerScene);
  51. hoverMaterial.diffuseColor = color.add(new Color3(0.3, 0.3, 0.3));
  52. // Build mesh on root node
  53. var parentMesh = new AbstractMesh("", gizmoLayer.utilityLayerScene);
  54. let drag = Mesh.CreateTorus("", 0.6, 0.03, tessellation, gizmoLayer.utilityLayerScene);
  55. drag.visibility = 0;
  56. let rotationMesh = Mesh.CreateTorus("", 0.6, 0.005, tessellation, gizmoLayer.utilityLayerScene);
  57. rotationMesh.material = coloredMaterial;
  58. // Position arrow pointing in its drag axis
  59. rotationMesh.rotation.x = Math.PI / 2;
  60. drag.rotation.x = Math.PI / 2;
  61. parentMesh.addChild(rotationMesh);
  62. parentMesh.addChild(drag);
  63. parentMesh.lookAt(this._rootMesh.position.add(planeNormal));
  64. this._rootMesh.addChild(parentMesh);
  65. parentMesh.scaling.scaleInPlace(1 / 3);
  66. // Add drag behavior to handle events when the gizmo is dragged
  67. this.dragBehavior = new PointerDragBehavior({ dragPlaneNormal: planeNormal });
  68. this.dragBehavior.moveAttached = false;
  69. this.dragBehavior.maxDragAngle = Math.PI * 9 / 20;
  70. this.dragBehavior._useAlternatePickedPointAboveMaxDragAngle = true;
  71. this._rootMesh.addBehavior(this.dragBehavior);
  72. var lastDragPosition = new Vector3();
  73. this.dragBehavior.onDragStartObservable.add((e) => {
  74. if (this.attachedMesh) {
  75. lastDragPosition.copyFrom(e.dragPlanePoint);
  76. }
  77. });
  78. var rotationMatrix = new Matrix();
  79. var planeNormalTowardsCamera = new Vector3();
  80. var localPlaneNormalTowardsCamera = new Vector3();
  81. var tmpSnapEvent = { snapDistance: 0 };
  82. var currentSnapDragDistance = 0;
  83. var tmpMatrix = new Matrix();
  84. var tmpVector = new Vector3();
  85. var amountToRotate = new Quaternion();
  86. this.dragBehavior.onDragObservable.add((event) => {
  87. if (this.attachedMesh) {
  88. if (!this.attachedMesh.rotationQuaternion || useEulerRotation) {
  89. this.attachedMesh.rotationQuaternion = Quaternion.RotationYawPitchRoll(this.attachedMesh.rotation.y, this.attachedMesh.rotation.x, this.attachedMesh.rotation.z);
  90. }
  91. // Remove parent priort to rotating
  92. var attachedMeshParent = this.attachedMesh.parent;
  93. if (attachedMeshParent) {
  94. this.attachedMesh.setParent(null);
  95. }
  96. // Calc angle over full 360 degree (https://stackoverflow.com/questions/43493711/the-angle-between-two-3d-vectors-with-a-result-range-0-360)
  97. var newVector = event.dragPlanePoint.subtract(this.attachedMesh.absolutePosition).normalize();
  98. var originalVector = lastDragPosition.subtract(this.attachedMesh.absolutePosition).normalize();
  99. var cross = Vector3.Cross(newVector, originalVector);
  100. var dot = Vector3.Dot(newVector, originalVector);
  101. var angle = Math.atan2(cross.length(), dot);
  102. planeNormalTowardsCamera.copyFrom(planeNormal);
  103. localPlaneNormalTowardsCamera.copyFrom(planeNormal);
  104. if (this.updateGizmoRotationToMatchAttachedMesh) {
  105. this.attachedMesh.rotationQuaternion.toRotationMatrix(rotationMatrix);
  106. localPlaneNormalTowardsCamera = Vector3.TransformCoordinates(planeNormalTowardsCamera, rotationMatrix);
  107. }
  108. // Flip up vector depending on which side the camera is on
  109. if (gizmoLayer.utilityLayerScene.activeCamera) {
  110. var camVec = gizmoLayer.utilityLayerScene.activeCamera.position.subtract(this.attachedMesh.position);
  111. if (Vector3.Dot(camVec, localPlaneNormalTowardsCamera) > 0) {
  112. planeNormalTowardsCamera.scaleInPlace(-1);
  113. localPlaneNormalTowardsCamera.scaleInPlace(-1);
  114. }
  115. }
  116. var halfCircleSide = Vector3.Dot(localPlaneNormalTowardsCamera, cross) > 0.0;
  117. if (halfCircleSide) { angle = -angle; }
  118. // Snapping logic
  119. var snapped = false;
  120. if (this.snapDistance != 0) {
  121. currentSnapDragDistance += angle;
  122. if (Math.abs(currentSnapDragDistance) > this.snapDistance) {
  123. var dragSteps = Math.floor(Math.abs(currentSnapDragDistance) / this.snapDistance);
  124. if (currentSnapDragDistance < 0) {
  125. dragSteps *= -1;
  126. }
  127. currentSnapDragDistance = currentSnapDragDistance % this.snapDistance;
  128. angle = this.snapDistance * dragSteps;
  129. snapped = true;
  130. } else {
  131. angle = 0;
  132. }
  133. }
  134. // If the mesh has a parent, convert needed world rotation to local rotation
  135. tmpMatrix.reset();
  136. if (this.attachedMesh.parent) {
  137. this.attachedMesh.parent.computeWorldMatrix().invertToRef(tmpMatrix);
  138. tmpMatrix.getRotationMatrixToRef(tmpMatrix);
  139. Vector3.TransformCoordinatesToRef(planeNormalTowardsCamera, tmpMatrix, planeNormalTowardsCamera);
  140. }
  141. // Convert angle and axis to quaternion (http://www.euclideanspace.com/maths/geometry/rotations/conversions/angleToQuaternion/index.htm)
  142. var quaternionCoefficient = Math.sin(angle / 2);
  143. amountToRotate.set(planeNormalTowardsCamera.x * quaternionCoefficient, planeNormalTowardsCamera.y * quaternionCoefficient, planeNormalTowardsCamera.z * quaternionCoefficient, Math.cos(angle / 2));
  144. // 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
  145. if (tmpMatrix.determinant() > 0) {
  146. amountToRotate.toEulerAnglesToRef(tmpVector);
  147. Quaternion.RotationYawPitchRollToRef(tmpVector.y, -tmpVector.x, -tmpVector.z, amountToRotate);
  148. }
  149. if (this.updateGizmoRotationToMatchAttachedMesh) {
  150. // Rotate selected mesh quaternion over fixed axis
  151. this.attachedMesh.rotationQuaternion.multiplyToRef(amountToRotate, this.attachedMesh.rotationQuaternion);
  152. } else {
  153. // Rotate selected mesh quaternion over rotated axis
  154. amountToRotate.multiplyToRef(this.attachedMesh.rotationQuaternion, this.attachedMesh.rotationQuaternion);
  155. }
  156. if (useEulerRotation) {
  157. this.attachedMesh.rotationQuaternion.toEulerAnglesToRef(tmpVector);
  158. this.attachedMesh.rotationQuaternion = null;
  159. this.attachedMesh.rotation.copyFrom(tmpVector);
  160. }
  161. lastDragPosition.copyFrom(event.dragPlanePoint);
  162. if (snapped) {
  163. tmpSnapEvent.snapDistance = angle;
  164. this.onSnapObservable.notifyObservers(tmpSnapEvent);
  165. }
  166. // Restore parent
  167. if (attachedMeshParent) {
  168. this.attachedMesh.setParent(attachedMeshParent);
  169. }
  170. }
  171. });
  172. this._pointerObserver = gizmoLayer.utilityLayerScene.onPointerObservable.add((pointerInfo) => {
  173. if (this._customMeshSet) {
  174. return;
  175. }
  176. var isHovered = pointerInfo.pickInfo && (this._rootMesh.getChildMeshes().indexOf(<Mesh>pointerInfo.pickInfo.pickedMesh) != -1);
  177. var material = isHovered ? hoverMaterial : coloredMaterial;
  178. this._rootMesh.getChildMeshes().forEach((m) => {
  179. m.material = material;
  180. if ((<LinesMesh>m).color) {
  181. (<LinesMesh>m).color = material.diffuseColor;
  182. }
  183. });
  184. });
  185. var light = gizmoLayer._getSharedGizmoLight();
  186. light.includedOnlyMeshes = light.includedOnlyMeshes.concat(this._rootMesh.getChildMeshes(false));
  187. }
  188. protected _attachedMeshChanged(value: Nullable<AbstractMesh>) {
  189. if (this.dragBehavior) {
  190. this.dragBehavior.enabled = value ? true : false;
  191. }
  192. }
  193. /**
  194. * If the gizmo is enabled
  195. */
  196. public set isEnabled(value: boolean) {
  197. this._isEnabled = value;
  198. if (!value) {
  199. this.attachedMesh = null;
  200. }
  201. else {
  202. if (this._parent) {
  203. this.attachedMesh = this._parent.attachedMesh;
  204. }
  205. }
  206. }
  207. public get isEnabled(): boolean {
  208. return this._isEnabled;
  209. }
  210. /**
  211. * Disposes of the gizmo
  212. */
  213. public dispose() {
  214. this.onSnapObservable.clear();
  215. this.gizmoLayer.utilityLayerScene.onPointerObservable.remove(this._pointerObserver);
  216. this.dragBehavior.detach();
  217. super.dispose();
  218. }
  219. }