gizmoManager.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. import { Observable } from "../Misc/observable.js";
  2. import { PointerEventTypes } from "../Events/pointerEvents.js";
  3. import { AbstractMesh } from "../Meshes/abstractMesh.js";
  4. import { UtilityLayerRenderer } from "../Rendering/utilityLayerRenderer.js";
  5. import { Color3 } from "../Maths/math.color.js";
  6. import { SixDofDragBehavior } from "../Behaviors/Meshes/sixDofDragBehavior.js";
  7. import { Gizmo, GizmoCoordinatesMode } from "./gizmo.js";
  8. import { RotationGizmo } from "./rotationGizmo.js";
  9. import { PositionGizmo } from "./positionGizmo.js";
  10. import { ScaleGizmo } from "./scaleGizmo.js";
  11. import { BoundingBoxGizmo } from "./boundingBoxGizmo.js";
  12. /**
  13. * Helps setup gizmo's in the scene to rotate/scale/position nodes
  14. */
  15. export class GizmoManager {
  16. /**
  17. * Utility layer that the bounding box gizmo belongs to
  18. */
  19. get keepDepthUtilityLayer() {
  20. return this._defaultKeepDepthUtilityLayer;
  21. }
  22. /**
  23. * Utility layer that all gizmos besides bounding box belong to
  24. */
  25. get utilityLayer() {
  26. return this._defaultUtilityLayer;
  27. }
  28. /**
  29. * True when the mouse pointer is hovering a gizmo mesh
  30. */
  31. get isHovered() {
  32. let hovered = false;
  33. for (const key in this.gizmos) {
  34. const gizmo = this.gizmos[key];
  35. if (gizmo && gizmo.isHovered) {
  36. hovered = true;
  37. break;
  38. }
  39. }
  40. return hovered;
  41. }
  42. /**
  43. * True when the mouse pointer is dragging a gizmo mesh
  44. */
  45. get isDragging() {
  46. let dragging = false;
  47. [this.gizmos.positionGizmo, this.gizmos.rotationGizmo, this.gizmos.scaleGizmo, this.gizmos.boundingBoxGizmo].forEach((gizmo) => {
  48. if (gizmo && gizmo.isDragging) {
  49. dragging = true;
  50. }
  51. });
  52. return dragging;
  53. }
  54. /**
  55. * Ratio for the scale of the gizmo (Default: 1)
  56. */
  57. set scaleRatio(value) {
  58. this._scaleRatio = value;
  59. [this.gizmos.positionGizmo, this.gizmos.rotationGizmo, this.gizmos.scaleGizmo].forEach((gizmo) => {
  60. if (gizmo) {
  61. gizmo.scaleRatio = value;
  62. }
  63. });
  64. }
  65. get scaleRatio() {
  66. return this._scaleRatio;
  67. }
  68. /**
  69. * Set the coordinate system to use. By default it's local.
  70. * But it's possible for a user to tweak so its local for translation and world for rotation.
  71. * In that case, setting the coordinate system will change `updateGizmoRotationToMatchAttachedMesh` and `updateGizmoPositionToMatchAttachedMesh`
  72. */
  73. set coordinatesMode(coordinatesMode) {
  74. this._coordinatesMode = coordinatesMode;
  75. [this.gizmos.positionGizmo, this.gizmos.rotationGizmo, this.gizmos.scaleGizmo].forEach((gizmo) => {
  76. if (gizmo) {
  77. gizmo.coordinatesMode = coordinatesMode;
  78. }
  79. });
  80. }
  81. get coordinatesMode() {
  82. return this._coordinatesMode;
  83. }
  84. /**
  85. * The mesh the gizmo's is attached to
  86. */
  87. get attachedMesh() {
  88. return this._attachedMesh;
  89. }
  90. /**
  91. * The node the gizmo's is attached to
  92. */
  93. get attachedNode() {
  94. return this._attachedNode;
  95. }
  96. /**
  97. * Additional transform node that will be used to transform all the gizmos
  98. */
  99. get additionalTransformNode() {
  100. return this._additionalTransformNode;
  101. }
  102. /**
  103. * Instantiates a gizmo manager
  104. * @param _scene the scene to overlay the gizmos on top of
  105. * @param thickness display gizmo axis thickness
  106. * @param utilityLayer the layer where gizmos are rendered
  107. * @param keepDepthUtilityLayer the layer where occluded gizmos are rendered
  108. */
  109. constructor(_scene, thickness = 1, utilityLayer = UtilityLayerRenderer.DefaultUtilityLayer, keepDepthUtilityLayer = UtilityLayerRenderer.DefaultKeepDepthUtilityLayer) {
  110. this._scene = _scene;
  111. /** When true, the gizmo will be detached from the current object when a pointer down occurs with an empty picked mesh */
  112. this.clearGizmoOnEmptyPointerEvent = false;
  113. /** When true (default), picking to attach a new mesh is enabled. This works in sync with inspector autopicking. */
  114. this.enableAutoPicking = true;
  115. /** Fires an event when the manager is attached to a mesh */
  116. this.onAttachedToMeshObservable = new Observable();
  117. /** Fires an event when the manager is attached to a node */
  118. this.onAttachedToNodeObservable = new Observable();
  119. this._gizmosEnabled = { positionGizmo: false, rotationGizmo: false, scaleGizmo: false, boundingBoxGizmo: false };
  120. this._pointerObservers = [];
  121. this._attachedMesh = null;
  122. this._attachedNode = null;
  123. this._boundingBoxColor = Color3.FromHexString("#0984e3");
  124. this._thickness = 1;
  125. this._scaleRatio = 1;
  126. this._coordinatesMode = GizmoCoordinatesMode.Local;
  127. /** Node Caching for quick lookup */
  128. this._gizmoAxisCache = new Map();
  129. /**
  130. * When bounding box gizmo is enabled, this can be used to track drag/end events
  131. */
  132. this.boundingBoxDragBehavior = new SixDofDragBehavior();
  133. /**
  134. * Array of meshes which will have the gizmo attached when a pointer selected them. If null, all meshes are attachable. (Default: null)
  135. */
  136. this.attachableMeshes = null;
  137. /**
  138. * Array of nodes which will have the gizmo attached when a pointer selected them. If null, all nodes are attachable. (Default: null)
  139. */
  140. this.attachableNodes = null;
  141. /**
  142. * If pointer events should perform attaching/detaching a gizmo, if false this can be done manually via attachToMesh/attachToNode. (Default: true)
  143. */
  144. this.usePointerToAttachGizmos = true;
  145. this._defaultUtilityLayer = utilityLayer;
  146. this._defaultKeepDepthUtilityLayer = keepDepthUtilityLayer;
  147. this._defaultKeepDepthUtilityLayer.utilityLayerScene.autoClearDepthAndStencil = false;
  148. this._thickness = thickness;
  149. this.gizmos = { positionGizmo: null, rotationGizmo: null, scaleGizmo: null, boundingBoxGizmo: null };
  150. const attachToMeshPointerObserver = this._attachToMeshPointerObserver(_scene);
  151. const gizmoAxisPointerObserver = Gizmo.GizmoAxisPointerObserver(this._defaultUtilityLayer, this._gizmoAxisCache);
  152. this._pointerObservers = [attachToMeshPointerObserver, gizmoAxisPointerObserver];
  153. }
  154. /**
  155. * @internal
  156. * Subscribes to pointer down events, for attaching and detaching mesh
  157. * @param scene The scene layer the observer will be added to
  158. * @returns the pointer observer
  159. */
  160. _attachToMeshPointerObserver(scene) {
  161. // Instantiate/dispose gizmos based on pointer actions
  162. const pointerObserver = scene.onPointerObservable.add((pointerInfo) => {
  163. if (!this.usePointerToAttachGizmos) {
  164. return;
  165. }
  166. if (pointerInfo.type == PointerEventTypes.POINTERDOWN) {
  167. if (pointerInfo.pickInfo && pointerInfo.pickInfo.pickedMesh) {
  168. if (this.enableAutoPicking) {
  169. let node = pointerInfo.pickInfo.pickedMesh;
  170. if (this.attachableMeshes == null) {
  171. // Attach to the most parent node
  172. while (node && node.parent != null) {
  173. node = node.parent;
  174. }
  175. }
  176. else {
  177. // Attach to the parent node that is an attachableMesh
  178. let found = false;
  179. this.attachableMeshes.forEach((mesh) => {
  180. if (node && (node == mesh || node.isDescendantOf(mesh))) {
  181. node = mesh;
  182. found = true;
  183. }
  184. });
  185. if (!found) {
  186. node = null;
  187. }
  188. }
  189. if (node instanceof AbstractMesh) {
  190. if (this._attachedMesh != node) {
  191. this.attachToMesh(node);
  192. }
  193. }
  194. else {
  195. if (this.clearGizmoOnEmptyPointerEvent) {
  196. this.attachToMesh(null);
  197. }
  198. }
  199. }
  200. }
  201. else {
  202. if (this.clearGizmoOnEmptyPointerEvent) {
  203. this.attachToMesh(null);
  204. }
  205. }
  206. }
  207. });
  208. return pointerObserver;
  209. }
  210. /**
  211. * Attaches a set of gizmos to the specified mesh
  212. * @param mesh The mesh the gizmo's should be attached to
  213. */
  214. attachToMesh(mesh) {
  215. if (this._attachedMesh) {
  216. this._attachedMesh.removeBehavior(this.boundingBoxDragBehavior);
  217. }
  218. if (this._attachedNode) {
  219. this._attachedNode.removeBehavior(this.boundingBoxDragBehavior);
  220. }
  221. this._attachedMesh = mesh;
  222. this._attachedNode = null;
  223. for (const key in this.gizmos) {
  224. const gizmo = this.gizmos[key];
  225. if (gizmo && this._gizmosEnabled[key]) {
  226. gizmo.attachedMesh = mesh;
  227. }
  228. }
  229. if (this.boundingBoxGizmoEnabled && this._attachedMesh) {
  230. this._attachedMesh.addBehavior(this.boundingBoxDragBehavior);
  231. }
  232. this.onAttachedToMeshObservable.notifyObservers(mesh);
  233. }
  234. /**
  235. * Attaches a set of gizmos to the specified node
  236. * @param node The node the gizmo's should be attached to
  237. */
  238. attachToNode(node) {
  239. if (this._attachedMesh) {
  240. this._attachedMesh.removeBehavior(this.boundingBoxDragBehavior);
  241. }
  242. if (this._attachedNode) {
  243. this._attachedNode.removeBehavior(this.boundingBoxDragBehavior);
  244. }
  245. this._attachedMesh = null;
  246. this._attachedNode = node;
  247. for (const key in this.gizmos) {
  248. const gizmo = this.gizmos[key];
  249. if (gizmo && this._gizmosEnabled[key]) {
  250. gizmo.attachedNode = node;
  251. }
  252. }
  253. if (this.boundingBoxGizmoEnabled && this._attachedNode) {
  254. this._attachedNode.addBehavior(this.boundingBoxDragBehavior);
  255. }
  256. this.onAttachedToNodeObservable.notifyObservers(node);
  257. }
  258. /**
  259. * If the position gizmo is enabled
  260. */
  261. set positionGizmoEnabled(value) {
  262. if (value) {
  263. if (!this.gizmos.positionGizmo) {
  264. this.gizmos.positionGizmo = new PositionGizmo(this._defaultUtilityLayer, this._thickness, this);
  265. }
  266. if (this._attachedNode) {
  267. this.gizmos.positionGizmo.attachedNode = this._attachedNode;
  268. }
  269. else {
  270. this.gizmos.positionGizmo.attachedMesh = this._attachedMesh;
  271. }
  272. }
  273. else if (this.gizmos.positionGizmo) {
  274. this.gizmos.positionGizmo.attachedNode = null;
  275. }
  276. this._gizmosEnabled.positionGizmo = value;
  277. this._setAdditionalTransformNode();
  278. }
  279. get positionGizmoEnabled() {
  280. return this._gizmosEnabled.positionGizmo;
  281. }
  282. /**
  283. * If the rotation gizmo is enabled
  284. */
  285. set rotationGizmoEnabled(value) {
  286. if (value) {
  287. if (!this.gizmos.rotationGizmo) {
  288. this.gizmos.rotationGizmo = new RotationGizmo(this._defaultUtilityLayer, 32, false, this._thickness, this);
  289. }
  290. if (this._attachedNode) {
  291. this.gizmos.rotationGizmo.attachedNode = this._attachedNode;
  292. }
  293. else {
  294. this.gizmos.rotationGizmo.attachedMesh = this._attachedMesh;
  295. }
  296. }
  297. else if (this.gizmos.rotationGizmo) {
  298. this.gizmos.rotationGizmo.attachedNode = null;
  299. }
  300. this._gizmosEnabled.rotationGizmo = value;
  301. this._setAdditionalTransformNode();
  302. }
  303. get rotationGizmoEnabled() {
  304. return this._gizmosEnabled.rotationGizmo;
  305. }
  306. /**
  307. * If the scale gizmo is enabled
  308. */
  309. set scaleGizmoEnabled(value) {
  310. if (value) {
  311. this.gizmos.scaleGizmo = this.gizmos.scaleGizmo || new ScaleGizmo(this._defaultUtilityLayer, this._thickness, this);
  312. if (this._attachedNode) {
  313. this.gizmos.scaleGizmo.attachedNode = this._attachedNode;
  314. }
  315. else {
  316. this.gizmos.scaleGizmo.attachedMesh = this._attachedMesh;
  317. }
  318. }
  319. else if (this.gizmos.scaleGizmo) {
  320. this.gizmos.scaleGizmo.attachedNode = null;
  321. }
  322. this._gizmosEnabled.scaleGizmo = value;
  323. this._setAdditionalTransformNode();
  324. }
  325. get scaleGizmoEnabled() {
  326. return this._gizmosEnabled.scaleGizmo;
  327. }
  328. /**
  329. * If the boundingBox gizmo is enabled
  330. */
  331. set boundingBoxGizmoEnabled(value) {
  332. if (value) {
  333. this.gizmos.boundingBoxGizmo = this.gizmos.boundingBoxGizmo || new BoundingBoxGizmo(this._boundingBoxColor, this._defaultKeepDepthUtilityLayer);
  334. if (this._attachedMesh) {
  335. this.gizmos.boundingBoxGizmo.attachedMesh = this._attachedMesh;
  336. }
  337. else {
  338. this.gizmos.boundingBoxGizmo.attachedNode = this._attachedNode;
  339. }
  340. if (this._attachedMesh) {
  341. this._attachedMesh.removeBehavior(this.boundingBoxDragBehavior);
  342. this._attachedMesh.addBehavior(this.boundingBoxDragBehavior);
  343. }
  344. else if (this._attachedNode) {
  345. this._attachedNode.removeBehavior(this.boundingBoxDragBehavior);
  346. this._attachedNode.addBehavior(this.boundingBoxDragBehavior);
  347. }
  348. }
  349. else if (this.gizmos.boundingBoxGizmo) {
  350. if (this._attachedMesh) {
  351. this._attachedMesh.removeBehavior(this.boundingBoxDragBehavior);
  352. }
  353. else if (this._attachedNode) {
  354. this._attachedNode.removeBehavior(this.boundingBoxDragBehavior);
  355. }
  356. this.gizmos.boundingBoxGizmo.attachedNode = null;
  357. }
  358. this._gizmosEnabled.boundingBoxGizmo = value;
  359. this._setAdditionalTransformNode();
  360. }
  361. get boundingBoxGizmoEnabled() {
  362. return this._gizmosEnabled.boundingBoxGizmo;
  363. }
  364. /**
  365. * Sets the additional transform applied to all the gizmos.
  366. * @See Gizmo.additionalTransformNode for more detail
  367. */
  368. set additionalTransformNode(node) {
  369. this._additionalTransformNode = node;
  370. this._setAdditionalTransformNode();
  371. }
  372. _setAdditionalTransformNode() {
  373. for (const key in this.gizmos) {
  374. const gizmo = this.gizmos[key];
  375. if (gizmo && this._gizmosEnabled[key]) {
  376. gizmo.additionalTransformNode = this._additionalTransformNode;
  377. }
  378. }
  379. }
  380. /**
  381. * Builds Gizmo Axis Cache to enable features such as hover state preservation and graying out other axis during manipulation
  382. * @param gizmoAxisCache Gizmo axis definition used for reactive gizmo UI
  383. */
  384. addToAxisCache(gizmoAxisCache) {
  385. if (gizmoAxisCache.size > 0) {
  386. gizmoAxisCache.forEach((v, k) => {
  387. this._gizmoAxisCache.set(k, v);
  388. });
  389. }
  390. }
  391. /**
  392. * Force release the drag action by code
  393. */
  394. releaseDrag() {
  395. [this.gizmos.positionGizmo, this.gizmos.rotationGizmo, this.gizmos.scaleGizmo, this.gizmos.boundingBoxGizmo].forEach((gizmo) => {
  396. gizmo?.releaseDrag();
  397. });
  398. }
  399. /**
  400. * Disposes of the gizmo manager
  401. */
  402. dispose() {
  403. this._pointerObservers.forEach((observer) => {
  404. this._scene.onPointerObservable.remove(observer);
  405. });
  406. for (const key in this.gizmos) {
  407. const gizmo = this.gizmos[key];
  408. if (gizmo) {
  409. gizmo.dispose();
  410. }
  411. }
  412. if (this._defaultKeepDepthUtilityLayer !== UtilityLayerRenderer._DefaultKeepDepthUtilityLayer) {
  413. this._defaultKeepDepthUtilityLayer?.dispose();
  414. }
  415. if (this._defaultUtilityLayer !== UtilityLayerRenderer._DefaultUtilityLayer) {
  416. this._defaultUtilityLayer?.dispose();
  417. }
  418. this.boundingBoxDragBehavior.detach();
  419. this.onAttachedToMeshObservable.clear();
  420. }
  421. }
  422. //# sourceMappingURL=gizmoManager.js.map