boundingBoxGizmo.js 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785
  1. import { Observable } from "../Misc/observable.js";
  2. import { Logger } from "../Misc/logger.js";
  3. import { Quaternion, Matrix, Vector3 } from "../Maths/math.vector.js";
  4. import { AbstractMesh } from "../Meshes/abstractMesh.js";
  5. import { CreateSphere } from "../Meshes/Builders/sphereBuilder.js";
  6. import { CreateBox } from "../Meshes/Builders/boxBuilder.js";
  7. import { CreateLines } from "../Meshes/Builders/linesBuilder.js";
  8. import { PointerDragBehavior } from "../Behaviors/Meshes/pointerDragBehavior.js";
  9. import { Gizmo } from "./gizmo.js";
  10. import { UtilityLayerRenderer } from "../Rendering/utilityLayerRenderer.js";
  11. import { StandardMaterial } from "../Materials/standardMaterial.js";
  12. import { PivotTools } from "../Misc/pivotTools.js";
  13. import { Color3 } from "../Maths/math.color.js";
  14. import { Epsilon } from "../Maths/math.constants.js";
  15. /**
  16. * Bounding box gizmo
  17. */
  18. export class BoundingBoxGizmo extends Gizmo {
  19. /**
  20. * Sets the axis factor
  21. * @param factor the Vector3 value
  22. */
  23. set axisFactor(factor) {
  24. this._axisFactor = factor;
  25. // update scale cube visibility
  26. const scaleBoxes = this._scaleBoxesParent.getChildMeshes();
  27. let index = 0;
  28. for (let i = 0; i < 3; i++) {
  29. for (let j = 0; j < 3; j++) {
  30. for (let k = 0; k < 3; k++) {
  31. const zeroAxisCount = (i === 1 ? 1 : 0) + (j === 1 ? 1 : 0) + (k === 1 ? 1 : 0);
  32. if (zeroAxisCount === 1 || zeroAxisCount === 3) {
  33. continue;
  34. }
  35. if (scaleBoxes[index]) {
  36. const dragAxis = new Vector3(i - 1, j - 1, k - 1);
  37. dragAxis.multiplyInPlace(this._axisFactor);
  38. scaleBoxes[index].setEnabled(dragAxis.lengthSquared() > Epsilon);
  39. }
  40. index++;
  41. }
  42. }
  43. }
  44. }
  45. /**
  46. * Gets the axis factor
  47. * @returns the Vector3 factor value
  48. */
  49. get axisFactor() {
  50. return this._axisFactor;
  51. }
  52. /**
  53. * Sets scale drag speed value
  54. * @param value the new speed value
  55. */
  56. set scaleDragSpeed(value) {
  57. this._scaleDragSpeed = value;
  58. }
  59. /**
  60. * Gets scale drag speed
  61. * @returns the scale speed number
  62. */
  63. get scaleDragSpeed() {
  64. return this._scaleDragSpeed;
  65. }
  66. /** Default material used to render when gizmo is not disabled or hovered */
  67. get coloredMaterial() {
  68. return this._coloredMaterial;
  69. }
  70. /** Material used to render when gizmo is hovered with mouse*/
  71. get hoverMaterial() {
  72. return this._hoverColoredMaterial;
  73. }
  74. /**
  75. * Get the pointerDragBehavior
  76. */
  77. get pointerDragBehavior() {
  78. return this._pointerDragBehavior;
  79. }
  80. /** True when a rotation sphere or scale box or a attached mesh is dragged */
  81. get isDragging() {
  82. return this._dragging || this._pointerDragBehavior.dragging;
  83. }
  84. /**
  85. * Sets the color of the bounding box gizmo
  86. * @param color the color to set
  87. */
  88. setColor(color) {
  89. this._coloredMaterial.emissiveColor = color;
  90. this._hoverColoredMaterial.emissiveColor = color.clone().add(new Color3(0.3, 0.3, 0.3));
  91. this._lineBoundingBox.getChildren().forEach((l) => {
  92. if (l.color) {
  93. l.color = color;
  94. }
  95. });
  96. }
  97. /**
  98. * Creates an BoundingBoxGizmo
  99. * @param color The color of the gizmo
  100. * @param gizmoLayer The utility layer the gizmo will be added to
  101. */
  102. constructor(color = Color3.Gray(), gizmoLayer = UtilityLayerRenderer.DefaultKeepDepthUtilityLayer) {
  103. super(gizmoLayer);
  104. this._boundingDimensions = new Vector3(1, 1, 1);
  105. this._renderObserver = null;
  106. this._pointerObserver = null;
  107. this._scaleDragSpeed = 0.2;
  108. this._rotateSpheresDragBehaviors = [];
  109. this._scaleBoxesDragBehaviors = [];
  110. /**
  111. * boolean updated when a rotation sphere or scale box is dragged
  112. */
  113. this._dragging = false;
  114. this._tmpQuaternion = new Quaternion();
  115. this._tmpVector = new Vector3(0, 0, 0);
  116. this._tmpRotationMatrix = new Matrix();
  117. this._incrementalStartupValue = Vector3.Zero();
  118. this._incrementalAnchorStartupValue = Vector3.Zero();
  119. /**
  120. * If child meshes should be ignored when calculating the bounding box. This should be set to true to avoid perf hits with heavily nested meshes (Default: false)
  121. */
  122. this.ignoreChildren = false;
  123. /**
  124. * Returns true if a descendant should be included when computing the bounding box. When null, all descendants are included. If ignoreChildren is set this will be ignored. (Default: null)
  125. */
  126. this.includeChildPredicate = null;
  127. /**
  128. * The size of the rotation spheres attached to the bounding box (Default: 0.1)
  129. */
  130. this.rotationSphereSize = 0.1;
  131. /**
  132. * The size of the scale boxes attached to the bounding box (Default: 0.1)
  133. */
  134. this.scaleBoxSize = 0.1;
  135. /**
  136. * If set, the rotation spheres and scale boxes will increase in size based on the distance away from the camera to have a consistent screen size (Default: false)
  137. * Note : fixedDragMeshScreenSize takes precedence over fixedDragMeshBoundsSize if both are true
  138. */
  139. this.fixedDragMeshScreenSize = false;
  140. /**
  141. * If set, the rotation spheres and scale boxes will increase in size based on the size of the bounding box
  142. * Note : fixedDragMeshScreenSize takes precedence over fixedDragMeshBoundsSize if both are true
  143. */
  144. this.fixedDragMeshBoundsSize = false;
  145. /**
  146. * The distance away from the object which the draggable meshes should appear world sized when fixedDragMeshScreenSize is set to true (default: 10)
  147. */
  148. this.fixedDragMeshScreenSizeDistanceFactor = 10;
  149. /**
  150. * Drag distance in babylon units that the gizmo will snap scaling to when dragged
  151. */
  152. this.scalingSnapDistance = 0;
  153. /**
  154. * Drag distance in babylon units that the gizmo will snap rotation to when dragged
  155. */
  156. this.rotationSnapDistance = 0;
  157. /**
  158. * Fired when a rotation sphere or scale box is dragged
  159. */
  160. this.onDragStartObservable = new Observable();
  161. /**
  162. * Fired when a scale box is dragged
  163. */
  164. this.onScaleBoxDragObservable = new Observable();
  165. /**
  166. * Fired when a scale box drag is ended
  167. */
  168. this.onScaleBoxDragEndObservable = new Observable();
  169. /**
  170. * Fired when a rotation sphere is dragged
  171. */
  172. this.onRotationSphereDragObservable = new Observable();
  173. /**
  174. * Fired when a rotation sphere drag is ended
  175. */
  176. this.onRotationSphereDragEndObservable = new Observable();
  177. /**
  178. * Relative bounding box pivot used when scaling the attached node. When null object with scale from the opposite corner. 0.5,0.5,0.5 for center and 0.5,0,0.5 for bottom (Default: null)
  179. */
  180. this.scalePivot = null;
  181. /**
  182. * Scale factor used for masking some axis
  183. */
  184. this._axisFactor = new Vector3(1, 1, 1);
  185. /**
  186. * 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,...
  187. */
  188. this.incrementalSnap = false;
  189. this._existingMeshScale = new Vector3();
  190. // Dragging
  191. this._dragMesh = null;
  192. this._pointerDragBehavior = new PointerDragBehavior();
  193. // Do not update the gizmo's scale so it has a fixed size to the object its attached to
  194. this.updateScale = false;
  195. this._anchorMesh = new AbstractMesh("anchor", gizmoLayer.utilityLayerScene);
  196. // Create Materials
  197. this._coloredMaterial = new StandardMaterial("", gizmoLayer.utilityLayerScene);
  198. this._coloredMaterial.disableLighting = true;
  199. this._hoverColoredMaterial = new StandardMaterial("", gizmoLayer.utilityLayerScene);
  200. this._hoverColoredMaterial.disableLighting = true;
  201. // Build bounding box out of lines
  202. this._lineBoundingBox = new AbstractMesh("", gizmoLayer.utilityLayerScene);
  203. this._lineBoundingBox.rotationQuaternion = new Quaternion();
  204. const lines = [];
  205. lines.push(CreateLines("lines", { points: [new Vector3(0, 0, 0), new Vector3(this._boundingDimensions.x, 0, 0)] }, gizmoLayer.utilityLayerScene));
  206. lines.push(CreateLines("lines", { points: [new Vector3(0, 0, 0), new Vector3(0, this._boundingDimensions.y, 0)] }, gizmoLayer.utilityLayerScene));
  207. lines.push(CreateLines("lines", { points: [new Vector3(0, 0, 0), new Vector3(0, 0, this._boundingDimensions.z)] }, gizmoLayer.utilityLayerScene));
  208. lines.push(CreateLines("lines", { points: [new Vector3(this._boundingDimensions.x, 0, 0), new Vector3(this._boundingDimensions.x, this._boundingDimensions.y, 0)] }, gizmoLayer.utilityLayerScene));
  209. lines.push(CreateLines("lines", { points: [new Vector3(this._boundingDimensions.x, 0, 0), new Vector3(this._boundingDimensions.x, 0, this._boundingDimensions.z)] }, gizmoLayer.utilityLayerScene));
  210. lines.push(CreateLines("lines", { points: [new Vector3(0, this._boundingDimensions.y, 0), new Vector3(this._boundingDimensions.x, this._boundingDimensions.y, 0)] }, gizmoLayer.utilityLayerScene));
  211. lines.push(CreateLines("lines", { points: [new Vector3(0, this._boundingDimensions.y, 0), new Vector3(0, this._boundingDimensions.y, this._boundingDimensions.z)] }, gizmoLayer.utilityLayerScene));
  212. lines.push(CreateLines("lines", { points: [new Vector3(0, 0, this._boundingDimensions.z), new Vector3(this._boundingDimensions.x, 0, this._boundingDimensions.z)] }, gizmoLayer.utilityLayerScene));
  213. lines.push(CreateLines("lines", { points: [new Vector3(0, 0, this._boundingDimensions.z), new Vector3(0, this._boundingDimensions.y, this._boundingDimensions.z)] }, gizmoLayer.utilityLayerScene));
  214. lines.push(CreateLines("lines", {
  215. points: [
  216. new Vector3(this._boundingDimensions.x, this._boundingDimensions.y, this._boundingDimensions.z),
  217. new Vector3(0, this._boundingDimensions.y, this._boundingDimensions.z),
  218. ],
  219. }, gizmoLayer.utilityLayerScene));
  220. lines.push(CreateLines("lines", {
  221. points: [
  222. new Vector3(this._boundingDimensions.x, this._boundingDimensions.y, this._boundingDimensions.z),
  223. new Vector3(this._boundingDimensions.x, 0, this._boundingDimensions.z),
  224. ],
  225. }, gizmoLayer.utilityLayerScene));
  226. lines.push(CreateLines("lines", {
  227. points: [
  228. new Vector3(this._boundingDimensions.x, this._boundingDimensions.y, this._boundingDimensions.z),
  229. new Vector3(this._boundingDimensions.x, this._boundingDimensions.y, 0),
  230. ],
  231. }, gizmoLayer.utilityLayerScene));
  232. lines.forEach((l) => {
  233. l.color = color;
  234. l.position.addInPlace(new Vector3(-this._boundingDimensions.x / 2, -this._boundingDimensions.y / 2, -this._boundingDimensions.z / 2));
  235. l.isPickable = false;
  236. this._lineBoundingBox.addChild(l);
  237. });
  238. this._rootMesh.addChild(this._lineBoundingBox);
  239. this.setColor(color);
  240. // Create rotation spheres
  241. this._rotateSpheresParent = new AbstractMesh("", gizmoLayer.utilityLayerScene);
  242. this._rotateSpheresParent.rotationQuaternion = new Quaternion();
  243. for (let i = 0; i < 12; i++) {
  244. const sphere = CreateSphere("", { diameter: 1 }, gizmoLayer.utilityLayerScene);
  245. sphere.rotationQuaternion = new Quaternion();
  246. sphere.material = this._coloredMaterial;
  247. sphere.isNearGrabbable = true;
  248. // Drag behavior
  249. const rotateSpheresDragBehavior = new PointerDragBehavior({});
  250. rotateSpheresDragBehavior.moveAttached = false;
  251. rotateSpheresDragBehavior.updateDragPlane = false;
  252. sphere.addBehavior(rotateSpheresDragBehavior);
  253. const startingTurnDirection = new Vector3(1, 0, 0);
  254. let totalTurnAmountOfDrag = 0;
  255. let previousProjectDist = 0;
  256. rotateSpheresDragBehavior.onDragStartObservable.add(() => {
  257. startingTurnDirection.copyFrom(sphere.forward);
  258. totalTurnAmountOfDrag = 0;
  259. previousProjectDist = 0;
  260. });
  261. rotateSpheresDragBehavior.onDragObservable.add((event) => {
  262. this.onRotationSphereDragObservable.notifyObservers({});
  263. if (this.attachedMesh) {
  264. const originalParent = this.attachedMesh.parent;
  265. if (originalParent && originalParent.scaling && originalParent.scaling.isNonUniformWithinEpsilon(0.001)) {
  266. Logger.Warn("BoundingBoxGizmo controls are not supported on child meshes with non-uniform parent scaling");
  267. return;
  268. }
  269. PivotTools._RemoveAndStorePivotPoint(this.attachedMesh);
  270. const worldDragDirection = startingTurnDirection;
  271. // Project the world right on to the drag plane
  272. const toSub = event.dragPlaneNormal.scale(Vector3.Dot(event.dragPlaneNormal, worldDragDirection));
  273. const dragAxis = worldDragDirection.subtract(toSub).normalizeToNew();
  274. // project drag delta on to the resulting drag axis and rotate based on that
  275. let projectDist = Vector3.Dot(dragAxis, event.delta) < 0 ? Math.abs(event.delta.length()) : -Math.abs(event.delta.length());
  276. // Make rotation relative to size of mesh.
  277. projectDist = (projectDist / this._boundingDimensions.length()) * this._anchorMesh.scaling.length();
  278. // Rotate based on axis
  279. if (!this.attachedMesh.rotationQuaternion) {
  280. this.attachedMesh.rotationQuaternion = Quaternion.RotationYawPitchRoll(this.attachedMesh.rotation.y, this.attachedMesh.rotation.x, this.attachedMesh.rotation.z);
  281. }
  282. if (!this._anchorMesh.rotationQuaternion) {
  283. this._anchorMesh.rotationQuaternion = Quaternion.RotationYawPitchRoll(this._anchorMesh.rotation.y, this._anchorMesh.rotation.x, this._anchorMesh.rotation.z);
  284. }
  285. // Do not allow the object to turn more than a full circle
  286. totalTurnAmountOfDrag += projectDist;
  287. if (Math.abs(totalTurnAmountOfDrag) <= 2 * Math.PI) {
  288. if (this.rotationSnapDistance > 0) {
  289. const dragSteps = Math.floor(Math.abs(totalTurnAmountOfDrag) / this.rotationSnapDistance) * (totalTurnAmountOfDrag < 0 ? -1 : 1);
  290. const angle = this.rotationSnapDistance * dragSteps;
  291. projectDist = angle - previousProjectDist;
  292. previousProjectDist = angle;
  293. }
  294. if (i >= 8) {
  295. Quaternion.RotationYawPitchRollToRef(0, 0, projectDist, this._tmpQuaternion);
  296. }
  297. else if (i >= 4) {
  298. Quaternion.RotationYawPitchRollToRef(projectDist, 0, 0, this._tmpQuaternion);
  299. }
  300. else {
  301. Quaternion.RotationYawPitchRollToRef(0, projectDist, 0, this._tmpQuaternion);
  302. }
  303. // if using pivot, move anchor so mesh will be at relative (0,0,0) when parented
  304. if (this.attachedMesh.isUsingPivotMatrix()) {
  305. this._anchorMesh.position.copyFrom(this.attachedMesh.position);
  306. }
  307. // Rotate around center of bounding box
  308. this._anchorMesh.addChild(this.attachedMesh);
  309. if (this._anchorMesh.getScene().useRightHandedSystem) {
  310. this._tmpQuaternion.conjugateInPlace();
  311. }
  312. this._tmpQuaternion.normalize();
  313. this._anchorMesh.rotationQuaternion.multiplyToRef(this._tmpQuaternion, this._anchorMesh.rotationQuaternion);
  314. this._anchorMesh.rotationQuaternion.normalize();
  315. this._anchorMesh.removeChild(this.attachedMesh);
  316. this.attachedMesh.setParent(originalParent);
  317. }
  318. this.updateBoundingBox();
  319. PivotTools._RestorePivotPoint(this.attachedMesh);
  320. }
  321. this._updateDummy();
  322. });
  323. // Selection/deselection
  324. rotateSpheresDragBehavior.onDragStartObservable.add(() => {
  325. this.onDragStartObservable.notifyObservers({});
  326. this._dragging = true;
  327. this._selectNode(sphere);
  328. });
  329. rotateSpheresDragBehavior.onDragEndObservable.add((event) => {
  330. this.onRotationSphereDragEndObservable.notifyObservers({});
  331. this._dragging = false;
  332. this._selectNode(null);
  333. this._updateDummy();
  334. this._unhoverMeshOnTouchUp(event.pointerInfo, sphere);
  335. });
  336. this._rotateSpheresDragBehaviors.push(rotateSpheresDragBehavior);
  337. this._rotateSpheresParent.addChild(sphere);
  338. }
  339. this._rootMesh.addChild(this._rotateSpheresParent);
  340. // Create scale cubes
  341. this._scaleBoxesParent = new AbstractMesh("", gizmoLayer.utilityLayerScene);
  342. this._scaleBoxesParent.rotationQuaternion = new Quaternion();
  343. for (let i = 0; i < 3; i++) {
  344. for (let j = 0; j < 3; j++) {
  345. for (let k = 0; k < 3; k++) {
  346. // create box for relevant axis
  347. const zeroAxisCount = (i === 1 ? 1 : 0) + (j === 1 ? 1 : 0) + (k === 1 ? 1 : 0);
  348. if (zeroAxisCount === 1 || zeroAxisCount === 3) {
  349. continue;
  350. }
  351. const box = CreateBox("", { size: 1 }, gizmoLayer.utilityLayerScene);
  352. box.material = this._coloredMaterial;
  353. box._internalMetadata = zeroAxisCount === 2; // None homogenous scale handle
  354. box.isNearGrabbable = true;
  355. // Dragging logic
  356. const dragAxis = new Vector3(i - 1, j - 1, k - 1).normalize();
  357. const scaleBoxesDragBehavior = new PointerDragBehavior({ dragAxis: dragAxis });
  358. scaleBoxesDragBehavior.updateDragPlane = false;
  359. scaleBoxesDragBehavior.moveAttached = false;
  360. let totalRelativeDragDistance = 0;
  361. let previousScale = 0;
  362. box.addBehavior(scaleBoxesDragBehavior);
  363. scaleBoxesDragBehavior.onDragObservable.add((event) => {
  364. this.onScaleBoxDragObservable.notifyObservers({});
  365. if (this.attachedMesh) {
  366. const originalParent = this.attachedMesh.parent;
  367. if (originalParent && originalParent.scaling && originalParent.scaling.isNonUniformWithinEpsilon(0.001)) {
  368. Logger.Warn("BoundingBoxGizmo controls are not supported on child meshes with non-uniform parent scaling");
  369. return;
  370. }
  371. PivotTools._RemoveAndStorePivotPoint(this.attachedMesh);
  372. let relativeDragDistance = (event.dragDistance / this._boundingDimensions.length()) * this._anchorMesh.scaling.length();
  373. totalRelativeDragDistance += relativeDragDistance;
  374. if (this.scalingSnapDistance > 0) {
  375. const dragSteps = Math.floor(Math.abs(totalRelativeDragDistance) / this.scalingSnapDistance) * (totalRelativeDragDistance < 0 ? -1 : 1);
  376. const scale = this.scalingSnapDistance * dragSteps;
  377. relativeDragDistance = scale - previousScale;
  378. previousScale = scale;
  379. }
  380. const deltaScale = new Vector3(relativeDragDistance, relativeDragDistance, relativeDragDistance);
  381. const fullScale = new Vector3(previousScale, previousScale, previousScale);
  382. if (zeroAxisCount === 2) {
  383. // scale on 1 axis when using the anchor box in the face middle
  384. deltaScale.x *= Math.abs(dragAxis.x);
  385. deltaScale.y *= Math.abs(dragAxis.y);
  386. deltaScale.z *= Math.abs(dragAxis.z);
  387. }
  388. deltaScale.scaleInPlace(this._scaleDragSpeed);
  389. deltaScale.multiplyInPlace(this._axisFactor);
  390. fullScale.scaleInPlace(this._scaleDragSpeed);
  391. fullScale.multiplyInPlace(this._axisFactor);
  392. fullScale.addInPlace(this._incrementalStartupValue);
  393. this.updateBoundingBox();
  394. if (this.scalePivot) {
  395. this.attachedMesh.getWorldMatrix().getRotationMatrixToRef(this._tmpRotationMatrix);
  396. // Move anchor to desired pivot point (Bottom left corner + dimension/2)
  397. this._boundingDimensions.scaleToRef(0.5, this._tmpVector);
  398. Vector3.TransformCoordinatesToRef(this._tmpVector, this._tmpRotationMatrix, this._tmpVector);
  399. this._anchorMesh.position.subtractInPlace(this._tmpVector);
  400. this._boundingDimensions.multiplyToRef(this.scalePivot, this._tmpVector);
  401. Vector3.TransformCoordinatesToRef(this._tmpVector, this._tmpRotationMatrix, this._tmpVector);
  402. this._anchorMesh.position.addInPlace(this._tmpVector);
  403. }
  404. else {
  405. // Scale from the position of the opposite corner
  406. box.absolutePosition.subtractToRef(this._anchorMesh.position, this._tmpVector);
  407. this._anchorMesh.position.subtractInPlace(this._tmpVector);
  408. if (this.attachedMesh.isUsingPivotMatrix()) {
  409. this._anchorMesh.position.subtractInPlace(this.attachedMesh.getPivotPoint());
  410. }
  411. }
  412. this._anchorMesh.addChild(this.attachedMesh);
  413. if (this.incrementalSnap) {
  414. fullScale.x /= Math.abs(this._incrementalStartupValue.x) < Epsilon ? 1 : this._incrementalStartupValue.x;
  415. fullScale.y /= Math.abs(this._incrementalStartupValue.y) < Epsilon ? 1 : this._incrementalStartupValue.y;
  416. fullScale.z /= Math.abs(this._incrementalStartupValue.z) < Epsilon ? 1 : this._incrementalStartupValue.z;
  417. fullScale.x = Math.max(this._incrementalAnchorStartupValue.x * fullScale.x, this.scalingSnapDistance);
  418. fullScale.y = Math.max(this._incrementalAnchorStartupValue.y * fullScale.y, this.scalingSnapDistance);
  419. fullScale.z = Math.max(this._incrementalAnchorStartupValue.z * fullScale.z, this.scalingSnapDistance);
  420. this._anchorMesh.scaling.x += (fullScale.x - this._anchorMesh.scaling.x) * Math.abs(dragAxis.x);
  421. this._anchorMesh.scaling.y += (fullScale.y - this._anchorMesh.scaling.y) * Math.abs(dragAxis.y);
  422. this._anchorMesh.scaling.z += (fullScale.z - this._anchorMesh.scaling.z) * Math.abs(dragAxis.z);
  423. }
  424. else {
  425. this._anchorMesh.scaling.addInPlace(deltaScale);
  426. if (this._anchorMesh.scaling.x < 0 || this._anchorMesh.scaling.y < 0 || this._anchorMesh.scaling.z < 0) {
  427. this._anchorMesh.scaling.subtractInPlace(deltaScale);
  428. }
  429. }
  430. this._anchorMesh.removeChild(this.attachedMesh);
  431. this.attachedMesh.setParent(originalParent);
  432. PivotTools._RestorePivotPoint(this.attachedMesh);
  433. }
  434. this._updateDummy();
  435. });
  436. // Selection/deselection
  437. scaleBoxesDragBehavior.onDragStartObservable.add(() => {
  438. this.onDragStartObservable.notifyObservers({});
  439. this._dragging = true;
  440. this._selectNode(box);
  441. totalRelativeDragDistance = 0;
  442. previousScale = 0;
  443. this._incrementalStartupValue.copyFrom(this.attachedMesh.scaling);
  444. this._incrementalAnchorStartupValue.copyFrom(this._anchorMesh.scaling);
  445. });
  446. scaleBoxesDragBehavior.onDragEndObservable.add((event) => {
  447. this.onScaleBoxDragEndObservable.notifyObservers({});
  448. this._dragging = false;
  449. this._selectNode(null);
  450. this._updateDummy();
  451. this._unhoverMeshOnTouchUp(event.pointerInfo, box);
  452. });
  453. this._scaleBoxesParent.addChild(box);
  454. this._scaleBoxesDragBehaviors.push(scaleBoxesDragBehavior);
  455. }
  456. }
  457. }
  458. this._rootMesh.addChild(this._scaleBoxesParent);
  459. // Hover color change
  460. const pointerIds = [];
  461. this._pointerObserver = gizmoLayer.utilityLayerScene.onPointerObservable.add((pointerInfo) => {
  462. if (!pointerIds[pointerInfo.event.pointerId]) {
  463. this._rotateSpheresParent
  464. .getChildMeshes()
  465. .concat(this._scaleBoxesParent.getChildMeshes())
  466. .forEach((mesh) => {
  467. if (pointerInfo.pickInfo && pointerInfo.pickInfo.pickedMesh == mesh) {
  468. pointerIds[pointerInfo.event.pointerId] = mesh;
  469. mesh.material = this._hoverColoredMaterial;
  470. this._isHovered = true;
  471. }
  472. });
  473. }
  474. else {
  475. if (pointerInfo.pickInfo && pointerInfo.pickInfo.pickedMesh != pointerIds[pointerInfo.event.pointerId]) {
  476. pointerIds[pointerInfo.event.pointerId].material = this._coloredMaterial;
  477. delete pointerIds[pointerInfo.event.pointerId];
  478. this._isHovered = false;
  479. }
  480. }
  481. });
  482. // Update bounding box positions
  483. this._renderObserver = this.gizmoLayer.originalScene.onBeforeRenderObservable.add(() => {
  484. // Only update the bounding box if scaling has changed
  485. if (this.attachedMesh && !this._existingMeshScale.equals(this.attachedMesh.scaling)) {
  486. this.updateBoundingBox();
  487. }
  488. else if (this.fixedDragMeshScreenSize || this.fixedDragMeshBoundsSize) {
  489. this._updateRotationSpheres();
  490. this._updateScaleBoxes();
  491. }
  492. // If drag mesh is enabled and dragging, update the attached mesh pose to match the drag mesh
  493. if (this._dragMesh && this.attachedMesh && this._pointerDragBehavior.dragging) {
  494. this._lineBoundingBox.position.rotateByQuaternionToRef(this._rootMesh.rotationQuaternion, this._tmpVector);
  495. this.attachedMesh.setAbsolutePosition(this._dragMesh.position.add(this._tmpVector.scale(-1)));
  496. }
  497. });
  498. this.updateBoundingBox();
  499. }
  500. _attachedNodeChanged(value) {
  501. if (value) {
  502. // Reset anchor mesh to match attached mesh's scale
  503. // This is needed to avoid invalid box/sphere position on first drag
  504. this._anchorMesh.scaling.setAll(1);
  505. PivotTools._RemoveAndStorePivotPoint(value);
  506. const originalParent = value.parent;
  507. this._anchorMesh.addChild(value);
  508. this._anchorMesh.removeChild(value);
  509. value.setParent(originalParent);
  510. PivotTools._RestorePivotPoint(value);
  511. this.updateBoundingBox();
  512. value.getChildMeshes(false).forEach((m) => {
  513. m.markAsDirty("scaling");
  514. });
  515. this.gizmoLayer.utilityLayerScene.onAfterRenderObservable.addOnce(() => {
  516. this._updateDummy();
  517. });
  518. }
  519. }
  520. _selectNode(selectedMesh) {
  521. this._rotateSpheresParent
  522. .getChildMeshes()
  523. .concat(this._scaleBoxesParent.getChildMeshes())
  524. .forEach((m) => {
  525. m.isVisible = !selectedMesh || m == selectedMesh;
  526. });
  527. }
  528. _unhoverMeshOnTouchUp(pointerInfo, selectedMesh) {
  529. // force unhover mesh if not a mouse event
  530. if (pointerInfo?.event instanceof PointerEvent && pointerInfo?.event.pointerType === "touch") {
  531. selectedMesh.material = this._coloredMaterial;
  532. }
  533. }
  534. /**
  535. * returns an array containing all boxes used for scaling (in increasing x, y and z orders)
  536. * @returns array of scaling boxes
  537. */
  538. getScaleBoxes() {
  539. return this._scaleBoxesParent.getChildMeshes();
  540. }
  541. /**
  542. * Updates the bounding box information for the Gizmo
  543. */
  544. updateBoundingBox() {
  545. if (this.attachedMesh) {
  546. PivotTools._RemoveAndStorePivotPoint(this.attachedMesh);
  547. // Store original parent
  548. const originalParent = this.attachedMesh.parent;
  549. this.attachedMesh.setParent(null);
  550. this._update();
  551. // Rotate based on axis
  552. if (!this.attachedMesh.rotationQuaternion) {
  553. this.attachedMesh.rotationQuaternion = Quaternion.RotationYawPitchRoll(this.attachedMesh.rotation.y, this.attachedMesh.rotation.x, this.attachedMesh.rotation.z);
  554. }
  555. if (!this._anchorMesh.rotationQuaternion) {
  556. this._anchorMesh.rotationQuaternion = Quaternion.RotationYawPitchRoll(this._anchorMesh.rotation.y, this._anchorMesh.rotation.x, this._anchorMesh.rotation.z);
  557. }
  558. this._anchorMesh.rotationQuaternion.copyFrom(this.attachedMesh.rotationQuaternion);
  559. // Store original position and reset mesh to origin before computing the bounding box
  560. this._tmpQuaternion.copyFrom(this.attachedMesh.rotationQuaternion);
  561. this._tmpVector.copyFrom(this.attachedMesh.position);
  562. this.attachedMesh.rotationQuaternion.set(0, 0, 0, 1);
  563. this.attachedMesh.position.set(0, 0, 0);
  564. // Update bounding dimensions/positions
  565. const boundingMinMax = this.attachedMesh.getHierarchyBoundingVectors(!this.ignoreChildren, this.includeChildPredicate);
  566. boundingMinMax.max.subtractToRef(boundingMinMax.min, this._boundingDimensions);
  567. // Update gizmo to match bounding box scaling and rotation
  568. // The position set here is the offset from the origin for the boundingbox when the attached mesh is at the origin
  569. // The position of the gizmo is then set to the attachedMesh in gizmo._update
  570. this._lineBoundingBox.scaling.copyFrom(this._boundingDimensions);
  571. this._lineBoundingBox.position.set((boundingMinMax.max.x + boundingMinMax.min.x) / 2, (boundingMinMax.max.y + boundingMinMax.min.y) / 2, (boundingMinMax.max.z + boundingMinMax.min.z) / 2);
  572. this._rotateSpheresParent.position.copyFrom(this._lineBoundingBox.position);
  573. this._scaleBoxesParent.position.copyFrom(this._lineBoundingBox.position);
  574. this._lineBoundingBox.computeWorldMatrix();
  575. this._anchorMesh.position.copyFrom(this._lineBoundingBox.absolutePosition);
  576. // Restore position/rotation values
  577. this.attachedMesh.rotationQuaternion.copyFrom(this._tmpQuaternion);
  578. this.attachedMesh.position.copyFrom(this._tmpVector);
  579. // Restore original parent
  580. this.attachedMesh.setParent(originalParent);
  581. }
  582. this._updateRotationSpheres();
  583. this._updateScaleBoxes();
  584. if (this.attachedMesh) {
  585. this._existingMeshScale.copyFrom(this.attachedMesh.scaling);
  586. PivotTools._RestorePivotPoint(this.attachedMesh);
  587. }
  588. }
  589. _updateRotationSpheres() {
  590. const rotateSpheres = this._rotateSpheresParent.getChildMeshes();
  591. for (let i = 0; i < 3; i++) {
  592. for (let j = 0; j < 2; j++) {
  593. for (let k = 0; k < 2; k++) {
  594. const index = i * 4 + j * 2 + k;
  595. if (i == 0) {
  596. rotateSpheres[index].position.set(this._boundingDimensions.x / 2, this._boundingDimensions.y * j, this._boundingDimensions.z * k);
  597. rotateSpheres[index].position.addInPlace(new Vector3(-this._boundingDimensions.x / 2, -this._boundingDimensions.y / 2, -this._boundingDimensions.z / 2));
  598. rotateSpheres[index].lookAt(Vector3.Cross(rotateSpheres[index].position.normalizeToNew(), Vector3.Right()).normalizeToNew().add(rotateSpheres[index].position));
  599. }
  600. if (i == 1) {
  601. rotateSpheres[index].position.set(this._boundingDimensions.x * j, this._boundingDimensions.y / 2, this._boundingDimensions.z * k);
  602. rotateSpheres[index].position.addInPlace(new Vector3(-this._boundingDimensions.x / 2, -this._boundingDimensions.y / 2, -this._boundingDimensions.z / 2));
  603. rotateSpheres[index].lookAt(Vector3.Cross(rotateSpheres[index].position.normalizeToNew(), Vector3.Up()).normalizeToNew().add(rotateSpheres[index].position));
  604. }
  605. if (i == 2) {
  606. rotateSpheres[index].position.set(this._boundingDimensions.x * j, this._boundingDimensions.y * k, this._boundingDimensions.z / 2);
  607. rotateSpheres[index].position.addInPlace(new Vector3(-this._boundingDimensions.x / 2, -this._boundingDimensions.y / 2, -this._boundingDimensions.z / 2));
  608. rotateSpheres[index].lookAt(Vector3.Cross(rotateSpheres[index].position.normalizeToNew(), Vector3.Forward()).normalizeToNew().add(rotateSpheres[index].position));
  609. }
  610. if (this.fixedDragMeshScreenSize && this.gizmoLayer.utilityLayerScene.activeCamera) {
  611. rotateSpheres[index].absolutePosition.subtractToRef(this.gizmoLayer.utilityLayerScene.activeCamera.position, this._tmpVector);
  612. const distanceFromCamera = (this.rotationSphereSize * this._tmpVector.length()) / this.fixedDragMeshScreenSizeDistanceFactor;
  613. rotateSpheres[index].scaling.set(distanceFromCamera, distanceFromCamera, distanceFromCamera);
  614. }
  615. else if (this.fixedDragMeshBoundsSize) {
  616. rotateSpheres[index].scaling.set(this.rotationSphereSize * this._boundingDimensions.x, this.rotationSphereSize * this._boundingDimensions.y, this.rotationSphereSize * this._boundingDimensions.z);
  617. }
  618. else {
  619. rotateSpheres[index].scaling.set(this.rotationSphereSize, this.rotationSphereSize, this.rotationSphereSize);
  620. }
  621. }
  622. }
  623. }
  624. }
  625. _updateScaleBoxes() {
  626. const scaleBoxes = this._scaleBoxesParent.getChildMeshes();
  627. let index = 0;
  628. for (let i = 0; i < 3; i++) {
  629. for (let j = 0; j < 3; j++) {
  630. for (let k = 0; k < 3; k++) {
  631. const zeroAxisCount = (i === 1 ? 1 : 0) + (j === 1 ? 1 : 0) + (k === 1 ? 1 : 0);
  632. if (zeroAxisCount === 1 || zeroAxisCount === 3) {
  633. continue;
  634. }
  635. if (scaleBoxes[index]) {
  636. scaleBoxes[index].position.set(this._boundingDimensions.x * (i / 2), this._boundingDimensions.y * (j / 2), this._boundingDimensions.z * (k / 2));
  637. scaleBoxes[index].position.addInPlace(new Vector3(-this._boundingDimensions.x / 2, -this._boundingDimensions.y / 2, -this._boundingDimensions.z / 2));
  638. if (this.fixedDragMeshScreenSize && this.gizmoLayer.utilityLayerScene.activeCamera) {
  639. scaleBoxes[index].absolutePosition.subtractToRef(this.gizmoLayer.utilityLayerScene.activeCamera.globalPosition, this._tmpVector);
  640. const distanceFromCamera = (this.scaleBoxSize * this._tmpVector.length()) / this.fixedDragMeshScreenSizeDistanceFactor;
  641. scaleBoxes[index].scaling.set(distanceFromCamera, distanceFromCamera, distanceFromCamera);
  642. }
  643. else if (this.fixedDragMeshBoundsSize) {
  644. scaleBoxes[index].scaling.set(this.scaleBoxSize * this._boundingDimensions.x, this.scaleBoxSize * this._boundingDimensions.y, this.scaleBoxSize * this._boundingDimensions.z);
  645. }
  646. else {
  647. scaleBoxes[index].scaling.set(this.scaleBoxSize, this.scaleBoxSize, this.scaleBoxSize);
  648. }
  649. }
  650. index++;
  651. }
  652. }
  653. }
  654. }
  655. /**
  656. * Enables rotation on the specified axis and disables rotation on the others
  657. * @param axis The list of axis that should be enabled (eg. "xy" or "xyz")
  658. */
  659. setEnabledRotationAxis(axis) {
  660. this._rotateSpheresParent.getChildMeshes().forEach((m, i) => {
  661. if (i < 4) {
  662. m.setEnabled(axis.indexOf("x") != -1);
  663. }
  664. else if (i < 8) {
  665. m.setEnabled(axis.indexOf("y") != -1);
  666. }
  667. else {
  668. m.setEnabled(axis.indexOf("z") != -1);
  669. }
  670. });
  671. }
  672. /**
  673. * Enables/disables scaling
  674. * @param enable if scaling should be enabled
  675. * @param homogeneousScaling defines if scaling should only be homogeneous
  676. */
  677. setEnabledScaling(enable, homogeneousScaling = false) {
  678. this._scaleBoxesParent.getChildMeshes().forEach((m) => {
  679. let enableMesh = enable;
  680. // Disable heterogeneous scale handles if requested.
  681. if (homogeneousScaling && m._internalMetadata === true) {
  682. enableMesh = false;
  683. }
  684. m.setEnabled(enableMesh);
  685. });
  686. }
  687. _updateDummy() {
  688. if (this._dragMesh) {
  689. this._dragMesh.position.copyFrom(this._lineBoundingBox.getAbsolutePosition());
  690. this._dragMesh.scaling.copyFrom(this._lineBoundingBox.scaling);
  691. this._dragMesh.rotationQuaternion.copyFrom(this._rootMesh.rotationQuaternion);
  692. }
  693. }
  694. /**
  695. * Enables a pointer drag behavior on the bounding box of the gizmo
  696. */
  697. enableDragBehavior() {
  698. this._dragMesh = CreateBox("dummy", { size: 1 }, this.gizmoLayer.utilityLayerScene);
  699. this._dragMesh.visibility = 0;
  700. this._dragMesh.rotationQuaternion = new Quaternion();
  701. this._pointerDragBehavior.useObjectOrientationForDragging = false;
  702. this._dragMesh.addBehavior(this._pointerDragBehavior);
  703. }
  704. /**
  705. * Force release the drag action by code
  706. */
  707. releaseDrag() {
  708. this._scaleBoxesDragBehaviors.forEach((dragBehavior) => {
  709. dragBehavior.releaseDrag();
  710. });
  711. this._rotateSpheresDragBehaviors.forEach((dragBehavior) => {
  712. dragBehavior.releaseDrag();
  713. });
  714. this._pointerDragBehavior.releaseDrag();
  715. }
  716. /**
  717. * Disposes of the gizmo
  718. */
  719. dispose() {
  720. this.gizmoLayer.utilityLayerScene.onPointerObservable.remove(this._pointerObserver);
  721. this.gizmoLayer.originalScene.onBeforeRenderObservable.remove(this._renderObserver);
  722. this._lineBoundingBox.dispose();
  723. this._rotateSpheresParent.dispose();
  724. this._scaleBoxesParent.dispose();
  725. if (this._dragMesh) {
  726. this._dragMesh.dispose();
  727. }
  728. this._scaleBoxesDragBehaviors.length = 0;
  729. this._rotateSpheresDragBehaviors.length = 0;
  730. super.dispose();
  731. }
  732. /**
  733. * Makes a mesh not pickable and wraps the mesh inside of a bounding box mesh that is pickable. (This is useful to avoid picking within complex geometry)
  734. * @param mesh the mesh to wrap in the bounding box mesh and make not pickable
  735. * @returns the bounding box mesh with the passed in mesh as a child
  736. */
  737. static MakeNotPickableAndWrapInBoundingBox(mesh) {
  738. const makeNotPickable = (root) => {
  739. root.isPickable = false;
  740. root.getChildMeshes().forEach((c) => {
  741. makeNotPickable(c);
  742. });
  743. };
  744. makeNotPickable(mesh);
  745. // Reset position to get bounding box from origin with no rotation
  746. if (!mesh.rotationQuaternion) {
  747. mesh.rotationQuaternion = Quaternion.RotationYawPitchRoll(mesh.rotation.y, mesh.rotation.x, mesh.rotation.z);
  748. }
  749. const oldPos = mesh.position.clone();
  750. const oldRot = mesh.rotationQuaternion.clone();
  751. mesh.rotationQuaternion.set(0, 0, 0, 1);
  752. mesh.position.set(0, 0, 0);
  753. // Update bounding dimensions/positions
  754. const box = CreateBox("box", { size: 1 }, mesh.getScene());
  755. const boundingMinMax = mesh.getHierarchyBoundingVectors();
  756. boundingMinMax.max.subtractToRef(boundingMinMax.min, box.scaling);
  757. // Adjust scale to avoid undefined behavior when adding child
  758. if (box.scaling.y === 0) {
  759. box.scaling.y = Epsilon;
  760. }
  761. if (box.scaling.x === 0) {
  762. box.scaling.x = Epsilon;
  763. }
  764. if (box.scaling.z === 0) {
  765. box.scaling.z = Epsilon;
  766. }
  767. box.position.set((boundingMinMax.max.x + boundingMinMax.min.x) / 2, (boundingMinMax.max.y + boundingMinMax.min.y) / 2, (boundingMinMax.max.z + boundingMinMax.min.z) / 2);
  768. // Restore original positions
  769. mesh.addChild(box);
  770. mesh.rotationQuaternion.copyFrom(oldRot);
  771. mesh.position.copyFrom(oldPos);
  772. // Reverse parenting
  773. mesh.removeChild(box);
  774. box.addChild(mesh);
  775. box.visibility = 0;
  776. return box;
  777. }
  778. /**
  779. * CustomMeshes are not supported by this gizmo
  780. */
  781. setCustomMesh() {
  782. Logger.Error("Custom meshes are not supported on this gizmo");
  783. }
  784. }
  785. //# sourceMappingURL=boundingBoxGizmo.js.map