axisScaleGizmo.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. import { Observable } from "../Misc/observable.js";
  2. import { Vector3, Matrix, TmpVectors } from "../Maths/math.vector.js";
  3. import { Mesh } from "../Meshes/mesh.js";
  4. import { CreateBox } from "../Meshes/Builders/boxBuilder.js";
  5. import { CreateCylinder } from "../Meshes/Builders/cylinderBuilder.js";
  6. import { StandardMaterial } from "../Materials/standardMaterial.js";
  7. import { PointerDragBehavior } from "../Behaviors/Meshes/pointerDragBehavior.js";
  8. import { Gizmo } from "./gizmo.js";
  9. import { UtilityLayerRenderer } from "../Rendering/utilityLayerRenderer.js";
  10. import { Color3 } from "../Maths/math.color.js";
  11. import { Epsilon } from "../Maths/math.constants.js";
  12. /**
  13. * Single axis scale gizmo
  14. */
  15. export class AxisScaleGizmo extends Gizmo {
  16. /** Default material used to render when gizmo is not disabled or hovered */
  17. get coloredMaterial() {
  18. return this._coloredMaterial;
  19. }
  20. /** Material used to render when gizmo is hovered with mouse*/
  21. get hoverMaterial() {
  22. return this._hoverMaterial;
  23. }
  24. /** Material used to render when gizmo is disabled. typically grey.*/
  25. get disableMaterial() {
  26. return this._disableMaterial;
  27. }
  28. /**
  29. * Creates an AxisScaleGizmo
  30. * @param dragAxis The axis which the gizmo will be able to scale on
  31. * @param color The color of the gizmo
  32. * @param gizmoLayer The utility layer the gizmo will be added to
  33. * @param parent
  34. * @param thickness display gizmo axis thickness
  35. * @param hoverColor The color of the gizmo when hovering over and dragging
  36. * @param disableColor The Color of the gizmo when its disabled
  37. */
  38. constructor(dragAxis, color = Color3.Gray(), gizmoLayer = UtilityLayerRenderer.DefaultUtilityLayer, parent = null, thickness = 1, hoverColor = Color3.Yellow(), disableColor = Color3.Gray()) {
  39. super(gizmoLayer);
  40. this._pointerObserver = null;
  41. /**
  42. * Scale distance in babylon units that the gizmo will snap to when dragged (Default: 0)
  43. */
  44. this.snapDistance = 0;
  45. /**
  46. * Event that fires each time the gizmo snaps to a new location.
  47. * * snapDistance is the change in distance
  48. */
  49. this.onSnapObservable = new Observable();
  50. /**
  51. * If the scaling operation should be done on all axis (default: false)
  52. */
  53. this.uniformScaling = false;
  54. /**
  55. * Custom sensitivity value for the drag strength
  56. */
  57. this.sensitivity = 1;
  58. /**
  59. * The magnitude of the drag strength (scaling factor)
  60. */
  61. this.dragScale = 1;
  62. /**
  63. * Incremental snap scaling (default is false). When true, with a snapDistance of 0.1, scaling will be 1.1,1.2,1.3 instead of, when false: 1.1,1.21,1.33,...
  64. */
  65. this.incrementalSnap = false;
  66. this._isEnabled = true;
  67. this._parent = null;
  68. this._dragging = false;
  69. this._tmpVector = new Vector3(0, 0, 0);
  70. this._incrementalStartupValue = Vector3.Zero();
  71. this._parent = parent;
  72. // Create Material
  73. this._coloredMaterial = new StandardMaterial("", gizmoLayer.utilityLayerScene);
  74. this._coloredMaterial.diffuseColor = color;
  75. this._coloredMaterial.specularColor = color.subtract(new Color3(0.1, 0.1, 0.1));
  76. this._hoverMaterial = new StandardMaterial("", gizmoLayer.utilityLayerScene);
  77. this._hoverMaterial.diffuseColor = hoverColor;
  78. this._disableMaterial = new StandardMaterial("", gizmoLayer.utilityLayerScene);
  79. this._disableMaterial.diffuseColor = disableColor;
  80. this._disableMaterial.alpha = 0.4;
  81. // Build mesh + Collider
  82. this._gizmoMesh = new Mesh("axis", gizmoLayer.utilityLayerScene);
  83. const { arrowMesh, arrowTail } = this._createGizmoMesh(this._gizmoMesh, thickness);
  84. const collider = this._createGizmoMesh(this._gizmoMesh, thickness + 4, true);
  85. this._gizmoMesh.lookAt(this._rootMesh.position.add(dragAxis));
  86. this._rootMesh.addChild(this._gizmoMesh, Gizmo.PreserveScaling);
  87. this._gizmoMesh.scaling.scaleInPlace(1 / 3);
  88. // Closure of initial prop values for resetting
  89. const nodePosition = arrowMesh.position.clone();
  90. const linePosition = arrowTail.position.clone();
  91. const lineScale = arrowTail.scaling.clone();
  92. const increaseGizmoMesh = (dragDistance) => {
  93. const dragStrength = dragDistance * (3 / this._rootMesh.scaling.length()) * 6;
  94. arrowMesh.position.z += dragStrength / 3.5;
  95. arrowTail.scaling.y += dragStrength;
  96. this.dragScale = arrowTail.scaling.y;
  97. arrowTail.position.z = arrowMesh.position.z / 2;
  98. };
  99. const resetGizmoMesh = () => {
  100. arrowMesh.position.set(nodePosition.x, nodePosition.y, nodePosition.z);
  101. arrowTail.position.set(linePosition.x, linePosition.y, linePosition.z);
  102. arrowTail.scaling.set(lineScale.x, lineScale.y, lineScale.z);
  103. this.dragScale = arrowTail.scaling.y;
  104. this._dragging = false;
  105. };
  106. // Add drag behavior to handle events when the gizmo is dragged
  107. this.dragBehavior = new PointerDragBehavior({ dragAxis: dragAxis });
  108. this.dragBehavior.moveAttached = false;
  109. this.dragBehavior.updateDragPlane = false;
  110. this._rootMesh.addBehavior(this.dragBehavior);
  111. let currentSnapDragDistance = 0;
  112. let currentSnapDragDistanceIncremental = 0;
  113. const tmpSnapEvent = { snapDistance: 0 };
  114. this.dragBehavior.onDragObservable.add((event) => {
  115. if (this.attachedNode) {
  116. // Drag strength is modified by the scale of the gizmo (eg. for small objects like boombox the strength will be increased to match the behavior of larger objects)
  117. const dragStrength = this.sensitivity * event.dragDistance * ((this.scaleRatio * 3) / this._rootMesh.scaling.length());
  118. const tmpVector = this._tmpVector;
  119. // Snapping logic
  120. let snapped = false;
  121. let dragSteps = 0;
  122. if (this.uniformScaling) {
  123. tmpVector.setAll(0.57735); // 1 / sqrt(3)
  124. }
  125. else {
  126. tmpVector.copyFrom(dragAxis);
  127. }
  128. if (this.snapDistance == 0) {
  129. tmpVector.scaleToRef(dragStrength, tmpVector);
  130. }
  131. else {
  132. currentSnapDragDistance += dragStrength;
  133. currentSnapDragDistanceIncremental += dragStrength;
  134. const currentSnap = this.incrementalSnap ? currentSnapDragDistanceIncremental : currentSnapDragDistance;
  135. if (Math.abs(currentSnap) > this.snapDistance) {
  136. dragSteps = Math.floor(Math.abs(currentSnap) / this.snapDistance);
  137. if (currentSnap < 0) {
  138. dragSteps *= -1;
  139. }
  140. currentSnapDragDistance = currentSnapDragDistance % this.snapDistance;
  141. tmpVector.scaleToRef(this.snapDistance * dragSteps, tmpVector);
  142. snapped = true;
  143. }
  144. else {
  145. tmpVector.scaleInPlace(0);
  146. }
  147. }
  148. tmpVector.addInPlaceFromFloats(1, 1, 1);
  149. // can't use Math.sign here because Math.sign(0) is 0 and it needs to be positive
  150. tmpVector.x = Math.abs(tmpVector.x) < AxisScaleGizmo.MinimumAbsoluteScale ? AxisScaleGizmo.MinimumAbsoluteScale * (tmpVector.x < 0 ? -1 : 1) : tmpVector.x;
  151. tmpVector.y = Math.abs(tmpVector.y) < AxisScaleGizmo.MinimumAbsoluteScale ? AxisScaleGizmo.MinimumAbsoluteScale * (tmpVector.y < 0 ? -1 : 1) : tmpVector.y;
  152. tmpVector.z = Math.abs(tmpVector.z) < AxisScaleGizmo.MinimumAbsoluteScale ? AxisScaleGizmo.MinimumAbsoluteScale * (tmpVector.z < 0 ? -1 : 1) : tmpVector.z;
  153. const transformNode = this.attachedNode._isMesh ? this.attachedNode : undefined;
  154. if (Math.abs(this.snapDistance) > 0 && this.incrementalSnap) {
  155. // get current scaling
  156. this.attachedNode.getWorldMatrix().decompose(undefined, TmpVectors.Quaternion[0], TmpVectors.Vector3[2], Gizmo.PreserveScaling ? transformNode : undefined);
  157. // apply incrementaly, without taking care of current scaling value
  158. tmpVector.addInPlace(this._incrementalStartupValue);
  159. tmpVector.addInPlaceFromFloats(-1, -1, -1);
  160. // keep same sign or stretching close to 0 will change orientation at each drag and scaling will oscilate around 0
  161. tmpVector.x = Math.abs(tmpVector.x) * (this._incrementalStartupValue.x > 0 ? 1 : -1);
  162. tmpVector.y = Math.abs(tmpVector.y) * (this._incrementalStartupValue.y > 0 ? 1 : -1);
  163. tmpVector.z = Math.abs(tmpVector.z) * (this._incrementalStartupValue.z > 0 ? 1 : -1);
  164. Matrix.ComposeToRef(tmpVector, TmpVectors.Quaternion[0], TmpVectors.Vector3[2], TmpVectors.Matrix[1]);
  165. }
  166. else {
  167. Matrix.ScalingToRef(tmpVector.x, tmpVector.y, tmpVector.z, TmpVectors.Matrix[2]);
  168. TmpVectors.Matrix[2].multiplyToRef(this.attachedNode.getWorldMatrix(), TmpVectors.Matrix[1]);
  169. }
  170. // check scaling are not out of bounds. If not, copy resulting temp matrix to node world matrix
  171. TmpVectors.Matrix[1].decompose(TmpVectors.Vector3[1], undefined, undefined, Gizmo.PreserveScaling ? transformNode : undefined);
  172. const maxScale = 100000;
  173. if (Math.abs(TmpVectors.Vector3[1].x) < maxScale && Math.abs(TmpVectors.Vector3[1].y) < maxScale && Math.abs(TmpVectors.Vector3[1].z) < maxScale) {
  174. this.attachedNode.getWorldMatrix().copyFrom(TmpVectors.Matrix[1]);
  175. }
  176. // notify observers
  177. if (snapped) {
  178. tmpSnapEvent.snapDistance = this.snapDistance * dragSteps;
  179. this.onSnapObservable.notifyObservers(tmpSnapEvent);
  180. }
  181. this._matrixChanged();
  182. }
  183. });
  184. // On Drag Listener: to move gizmo mesh with user action
  185. this.dragBehavior.onDragStartObservable.add(() => {
  186. this._dragging = true;
  187. const transformNode = this.attachedNode._isMesh ? this.attachedNode : undefined;
  188. this.attachedNode?.getWorldMatrix().decompose(this._incrementalStartupValue, undefined, undefined, Gizmo.PreserveScaling ? transformNode : undefined);
  189. currentSnapDragDistance = 0;
  190. currentSnapDragDistanceIncremental = 0;
  191. });
  192. this.dragBehavior.onDragObservable.add((e) => increaseGizmoMesh(e.dragDistance));
  193. this.dragBehavior.onDragEndObservable.add(resetGizmoMesh);
  194. // Listeners for Universal Scalar
  195. parent?.uniformScaleGizmo?.dragBehavior?.onDragObservable?.add((e) => increaseGizmoMesh(e.delta.y));
  196. parent?.uniformScaleGizmo?.dragBehavior?.onDragEndObservable?.add(resetGizmoMesh);
  197. const cache = {
  198. gizmoMeshes: [arrowMesh, arrowTail],
  199. colliderMeshes: [collider.arrowMesh, collider.arrowTail],
  200. material: this._coloredMaterial,
  201. hoverMaterial: this._hoverMaterial,
  202. disableMaterial: this._disableMaterial,
  203. active: false,
  204. dragBehavior: this.dragBehavior,
  205. };
  206. this._parent?.addToAxisCache(this._gizmoMesh, cache);
  207. this._pointerObserver = gizmoLayer.utilityLayerScene.onPointerObservable.add((pointerInfo) => {
  208. if (this._customMeshSet) {
  209. return;
  210. }
  211. // axis mesh cache
  212. let meshCache = this._parent?.getAxisCache(this._gizmoMesh);
  213. this._isHovered = !!meshCache && !!(meshCache.colliderMeshes.indexOf(pointerInfo?.pickInfo?.pickedMesh) != -1);
  214. // uniform mesh cache
  215. meshCache = this._parent?.getAxisCache(this._rootMesh);
  216. this._isHovered || (this._isHovered = !!meshCache && !!(meshCache.colliderMeshes.indexOf(pointerInfo?.pickInfo?.pickedMesh) != -1));
  217. if (!this._parent) {
  218. const material = this.dragBehavior.enabled ? (this._isHovered || this._dragging ? this._hoverMaterial : this._coloredMaterial) : this._disableMaterial;
  219. this._setGizmoMeshMaterial(cache.gizmoMeshes, material);
  220. }
  221. });
  222. this.dragBehavior.onEnabledObservable.add((newState) => {
  223. this._setGizmoMeshMaterial(cache.gizmoMeshes, newState ? this._coloredMaterial : this._disableMaterial);
  224. });
  225. const light = gizmoLayer._getSharedGizmoLight();
  226. light.includedOnlyMeshes = light.includedOnlyMeshes.concat(this._rootMesh.getChildMeshes());
  227. }
  228. /**
  229. * @internal
  230. * Create Geometry for Gizmo
  231. * @param parentMesh
  232. * @param thickness
  233. * @param isCollider
  234. * @returns the gizmo mesh
  235. */
  236. _createGizmoMesh(parentMesh, thickness, isCollider = false) {
  237. const arrowMesh = CreateBox("yPosMesh", { size: 0.4 * (1 + (thickness - 1) / 4) }, this.gizmoLayer.utilityLayerScene);
  238. const arrowTail = CreateCylinder("cylinder", { diameterTop: 0.005 * thickness, height: 0.275, diameterBottom: 0.005 * thickness, tessellation: 96 }, this.gizmoLayer.utilityLayerScene);
  239. // Position arrow pointing in its drag axis
  240. arrowMesh.scaling.scaleInPlace(0.1);
  241. arrowMesh.material = this._coloredMaterial;
  242. arrowMesh.rotation.x = Math.PI / 2;
  243. arrowMesh.position.z += 0.3;
  244. arrowTail.material = this._coloredMaterial;
  245. arrowTail.position.z += 0.275 / 2;
  246. arrowTail.rotation.x = Math.PI / 2;
  247. if (isCollider) {
  248. arrowMesh.visibility = 0;
  249. arrowTail.visibility = 0;
  250. }
  251. parentMesh.addChild(arrowMesh);
  252. parentMesh.addChild(arrowTail);
  253. return { arrowMesh, arrowTail };
  254. }
  255. _attachedNodeChanged(value) {
  256. if (this.dragBehavior) {
  257. this.dragBehavior.enabled = value ? true : false;
  258. }
  259. }
  260. /**
  261. * If the gizmo is enabled
  262. */
  263. set isEnabled(value) {
  264. this._isEnabled = value;
  265. if (!value) {
  266. this.attachedMesh = null;
  267. this.attachedNode = null;
  268. }
  269. else {
  270. if (this._parent) {
  271. this.attachedMesh = this._parent.attachedMesh;
  272. this.attachedNode = this._parent.attachedNode;
  273. }
  274. }
  275. }
  276. get isEnabled() {
  277. return this._isEnabled;
  278. }
  279. /**
  280. * Disposes of the gizmo
  281. */
  282. dispose() {
  283. this.onSnapObservable.clear();
  284. this.gizmoLayer.utilityLayerScene.onPointerObservable.remove(this._pointerObserver);
  285. this.dragBehavior.detach();
  286. if (this._gizmoMesh) {
  287. this._gizmoMesh.dispose();
  288. }
  289. [this._coloredMaterial, this._hoverMaterial, this._disableMaterial].forEach((matl) => {
  290. if (matl) {
  291. matl.dispose();
  292. }
  293. });
  294. super.dispose();
  295. }
  296. /**
  297. * Disposes and replaces the current meshes in the gizmo with the specified mesh
  298. * @param mesh The mesh to replace the default mesh of the gizmo
  299. * @param useGizmoMaterial If the gizmo's default material should be used (default: false)
  300. */
  301. setCustomMesh(mesh, useGizmoMaterial = false) {
  302. super.setCustomMesh(mesh);
  303. if (useGizmoMaterial) {
  304. this._rootMesh.getChildMeshes().forEach((m) => {
  305. m.material = this._coloredMaterial;
  306. if (m.color) {
  307. m.color = this._coloredMaterial.diffuseColor;
  308. }
  309. });
  310. this._customMeshSet = false;
  311. }
  312. }
  313. }
  314. /**
  315. * The minimal absolute scale per component. can be positive or negative but never smaller.
  316. */
  317. AxisScaleGizmo.MinimumAbsoluteScale = Epsilon;
  318. //# sourceMappingURL=axisScaleGizmo.js.map