OriginGizmo.qml 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. // Copyright (C) 2025 The Qt Company Ltd.
  2. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
  3. import QtQuick
  4. import QtQuick3D
  5. Item {
  6. id: root
  7. required property Node targetNode
  8. enum Axis {
  9. PositiveZ = 0,
  10. NegativeZ = 1,
  11. PositiveY = 2,
  12. NegativeY = 3,
  13. PositiveX = 4,
  14. NegativeX = 5
  15. }
  16. // These are the 24 different rotations a rotation aligned on axes can have.
  17. // They are ordered in groups of 4 where the +Z,-Z,+Y,-Y,+X,-X axis is pointing
  18. // towards the screen (+Z). Inside this group the rotations are ordered to
  19. // rotate counter-clockwise.
  20. readonly property list<quaternion> rotations: [
  21. // +Z
  22. Qt.quaternion(1, 0, 0, 0),
  23. Qt.quaternion(Math.SQRT1_2, 0, 0, -Math.SQRT1_2),
  24. Qt.quaternion(0, 0, 0, -1),
  25. Qt.quaternion(Math.SQRT1_2, 0, 0, Math.SQRT1_2),
  26. // -Z
  27. Qt.quaternion(0, 0, -1, 0),
  28. Qt.quaternion(0, -Math.SQRT1_2, -Math.SQRT1_2, 0),
  29. Qt.quaternion(0, -1, 0, 0),
  30. Qt.quaternion(0, Math.SQRT1_2, -Math.SQRT1_2, 0),
  31. // +Y
  32. Qt.quaternion(0.5, 0.5, 0.5, 0.5),
  33. Qt.quaternion(Math.SQRT1_2, Math.SQRT1_2, 0, 0),
  34. Qt.quaternion(-0.5, -0.5, 0.5, 0.5),
  35. Qt.quaternion(0, 0, -Math.SQRT1_2, -Math.SQRT1_2),
  36. // -Y
  37. Qt.quaternion(0.5, -0.5, 0.5, -0.5),
  38. Qt.quaternion(0, 0, Math.SQRT1_2, -Math.SQRT1_2),
  39. Qt.quaternion(-0.5, 0.5, 0.5, -0.5),
  40. Qt.quaternion(-Math.SQRT1_2, Math.SQRT1_2, 0, 0),
  41. // +X
  42. Qt.quaternion(-0.5, -0.5, 0.5, -0.5),
  43. Qt.quaternion(-Math.SQRT1_2, 0, Math.SQRT1_2, 0),
  44. Qt.quaternion(-0.5, 0.5, 0.5, 0.5),
  45. Qt.quaternion(0, Math.SQRT1_2, 0, Math.SQRT1_2),
  46. // -X
  47. Qt.quaternion(0, Math.SQRT1_2, 0, -Math.SQRT1_2),
  48. Qt.quaternion(0.5, -0.5, 0.5, 0.5),
  49. Qt.quaternion(Math.SQRT1_2, 0, Math.SQRT1_2, 0),
  50. Qt.quaternion(0.5, 0.5, 0.5, -0.5),
  51. ]
  52. readonly property list<quaternion> xRotationGoals : [
  53. Qt.quaternion(0, 1, 0, 0),
  54. Qt.quaternion(0, 0, -1, 0),
  55. Qt.quaternion(0, -1, 0, 0),
  56. Qt.quaternion(0, 0, 1, 0),
  57. Qt.quaternion(0, -1, 0, 0),
  58. Qt.quaternion(0, 0, 1, 0),
  59. Qt.quaternion(0, 1, 0, 0),
  60. Qt.quaternion(0, 0, -1, 0),
  61. Qt.quaternion(0, 0, 1, 0),
  62. Qt.quaternion(0, 1, 0, 0),
  63. Qt.quaternion(0, 0, -1, 0),
  64. Qt.quaternion(-0, -1, -0, -0),
  65. Qt.quaternion(0, 0, -1, 0),
  66. Qt.quaternion(-0, -1, -0, -0),
  67. Qt.quaternion(0, 0, 1, 0),
  68. Qt.quaternion(-0, 1, -0, -0),
  69. Qt.quaternion(0, 0, 0, 1),
  70. Qt.quaternion(0, 0, 0, 1),
  71. Qt.quaternion(0, 0, 0, 1),
  72. Qt.quaternion(0, 0, 0, 1),
  73. Qt.quaternion(0, 0, 0, -1),
  74. Qt.quaternion(0, 0, 0, -1),
  75. Qt.quaternion(0, 0, 0, -1),
  76. Qt.quaternion(0, 0, 0, -1),
  77. ]
  78. readonly property list<quaternion> yRotationGoals : [
  79. Qt.quaternion(0, 0, 1, 0),
  80. Qt.quaternion(0, 1, 0, 0),
  81. Qt.quaternion(0, 0, -1, 0),
  82. Qt.quaternion(0, -1, 0, 0),
  83. Qt.quaternion(0, 0, 1, 0),
  84. Qt.quaternion(0, 1, 0, 0),
  85. Qt.quaternion(0, 0, -1, 0),
  86. Qt.quaternion(0, -1, 0, 0),
  87. Qt.quaternion(0, 0, 0, 1),
  88. Qt.quaternion(0, 0, 0, 1),
  89. Qt.quaternion(0, 0, 0, 1),
  90. Qt.quaternion(0, 0, 0, 1),
  91. Qt.quaternion(0, 0, 0, -1),
  92. Qt.quaternion(0, 0, 0, -1),
  93. Qt.quaternion(0, 0, 0, -1),
  94. Qt.quaternion(0, 0, 0, -1),
  95. Qt.quaternion(0, -1, 0, 0),
  96. Qt.quaternion(0, 0, 1, 0),
  97. Qt.quaternion(0, 1, 0, 0),
  98. Qt.quaternion(0, 0, -1, 0),
  99. Qt.quaternion(0, 0, -1, 0),
  100. Qt.quaternion(0, -1, 0, 0),
  101. Qt.quaternion(0, 0, 1, 0),
  102. Qt.quaternion(0, 1, 0, 0),
  103. ]
  104. readonly property list<quaternion> zRotationGoals : [
  105. Qt.quaternion(0, 0, 0, 1),
  106. Qt.quaternion(0, 0, 0, 1),
  107. Qt.quaternion(0, 0, 0, 1),
  108. Qt.quaternion(0, 0, 0, 1),
  109. Qt.quaternion(0, 0, 0, -1),
  110. Qt.quaternion(0, 0, 0, -1),
  111. Qt.quaternion(0, 0, 0, -1),
  112. Qt.quaternion(0, 0, 0, -1),
  113. Qt.quaternion(0, 1, 0, 0),
  114. Qt.quaternion(0, 0, -1, 0),
  115. Qt.quaternion(0, -1, 0, 0),
  116. Qt.quaternion(0, 0, 1, 0),
  117. Qt.quaternion(0, 1, 0, 0),
  118. Qt.quaternion(0, 0, -1, 0),
  119. Qt.quaternion(0, -1, 0, 0),
  120. Qt.quaternion(0, 0, 1, 0),
  121. Qt.quaternion(0, 0, -1, 0),
  122. Qt.quaternion(0, -1, 0, 0),
  123. Qt.quaternion(0, 0, 1, 0),
  124. Qt.quaternion(0, 1, 0, 0),
  125. Qt.quaternion(0, -1, 0, 0),
  126. Qt.quaternion(0, 0, 1, 0),
  127. Qt.quaternion(0, 1, 0, 0),
  128. Qt.quaternion(0, 0, -1, 0),
  129. ]
  130. // This function works by using a rotation to rotate x,y,z normal vectors
  131. // and see what axis-aligned rotation gives the closest distance to the
  132. // rotated normal vectors.
  133. function findClosestRotation(rotation, startI, stopI) {
  134. let rotationConjugated = rotation.conjugated();
  135. let xRotated = rotation.times(Qt.quaternion(0, 1, 0, 0)).times(rotationConjugated);
  136. let yRotated = rotation.times(Qt.quaternion(0, 0, 1, 0)).times(rotationConjugated);
  137. let zRotated = rotation.times(Qt.quaternion(0, 0, 0, 1)).times(rotationConjugated);
  138. var closestIndex = 0;
  139. var closestDistance = 123456789; // big number
  140. for (var i = startI; i < stopI ; i++) {
  141. let distance = xRotated.minus(xRotationGoals[i]).length() +
  142. yRotated.minus(yRotationGoals[i]).length() +
  143. zRotated.minus(zRotationGoals[i]).length();
  144. if (distance <= closestDistance) {
  145. closestDistance = distance;
  146. closestIndex = i;
  147. }
  148. }
  149. return closestIndex;
  150. }
  151. function quaternionAlign(rotation) {
  152. let closestIndex = findClosestRotation(rotation, 0, 24);
  153. return rotations[closestIndex];
  154. }
  155. function quaternionForAxis(axis, rotation) {
  156. let closestIndex = findClosestRotation(rotation, axis*4, (axis + 1)*4);
  157. return rotations[closestIndex];
  158. }
  159. function quaternionRotateLeft(rotation) {
  160. let closestIndex = findClosestRotation(rotation, 0, 24);
  161. let offset = (4 + closestIndex - 1) % 4;
  162. let group = Math.floor(closestIndex / 4);
  163. return rotations[offset + group * 4];
  164. }
  165. function quaternionRotateRight(rotation) {
  166. let closestIndex = findClosestRotation(rotation, 0, 24);
  167. let offset = (closestIndex + 1) % 4;
  168. let group = Math.floor(closestIndex / 4);
  169. return rotations[offset + group * 4];
  170. }
  171. signal axisClicked(int axis)
  172. signal ballMoved(vector2d velocity)
  173. QtObject {
  174. id: stylePalette
  175. property color white: "#fdf6e3"
  176. property color black: "#002b36"
  177. property color red: "#dc322f"
  178. property color green: "#859900"
  179. property color blue: "#268bd2"
  180. property color background: "#99002b36"
  181. }
  182. component LineRectangle : Rectangle {
  183. property vector2d startPoint: Qt.vector2d(0, 0)
  184. property vector2d endPoint: Qt.vector2d(0, 0)
  185. property real lineWidth: 5
  186. transformOrigin: Item.Left
  187. height: lineWidth
  188. readonly property vector2d offset: startPoint.plus(endPoint).times(0.5);
  189. width: startPoint.minus(endPoint).length()
  190. rotation: Math.atan2(endPoint.y - startPoint.y, endPoint.x - startPoint.x) * 180 / Math.PI
  191. }
  192. Rectangle {
  193. id: ballBackground
  194. anchors.centerIn: parent
  195. width: parent.width > parent.height ? parent.height : parent.width
  196. height: width
  197. radius: width / 2
  198. color: ballBackgroundHoverHandler.hovered ? stylePalette.background : "transparent"
  199. readonly property real subBallWidth: width / 5
  200. readonly property real subBallHalfWidth: subBallWidth * 0.5
  201. readonly property real subBallOffset: radius - subBallWidth / 2
  202. Item {
  203. anchors.centerIn: parent
  204. component SubBall : Rectangle {
  205. id: subBallRoot
  206. required property Node targetNode
  207. required property real offset
  208. property alias labelText: label.text
  209. property alias labelColor: label.color
  210. property alias labelVisible: label.visible
  211. property alias hovered: subBallHoverHandler.hovered
  212. property var initialPosition: Qt.vector3d(0, 0, 0)
  213. readonly property vector3d position: quaternionVectorMultiply(targetNode.rotation, initialPosition)
  214. signal tapped()
  215. function quaternionVectorMultiply(q, v) {
  216. var qv = Qt.vector3d(q.x, q.y, q.z)
  217. var uv = qv.crossProduct(v)
  218. var uuv = qv.crossProduct(uv)
  219. uv = uv.times(2.0 * q.scalar)
  220. uuv = uuv.times(2.0)
  221. return v.plus(uv).plus(uuv)
  222. }
  223. height: width
  224. radius: width / 2
  225. x: offset * position.x - width / 2
  226. y: offset * -position.y - height / 2
  227. z: position.z
  228. HoverHandler {
  229. id: subBallHoverHandler
  230. }
  231. TapHandler {
  232. acceptedButtons: Qt.LeftButton
  233. onTapped: (eventPoint, button)=>{
  234. subBallRoot.tapped()
  235. //eventPoint.accepted = true
  236. }
  237. }
  238. Text {
  239. id: label
  240. anchors.centerIn: parent
  241. }
  242. }
  243. SubBall {
  244. id: positiveX
  245. targetNode: root.targetNode
  246. width: ballBackground.subBallWidth
  247. offset: ballBackground.subBallOffset
  248. labelText: "X"
  249. labelColor: hovered ? stylePalette.white : stylePalette.black
  250. color: stylePalette.red
  251. initialPosition: Qt.vector3d(1, 0, 0)
  252. onTapped: {
  253. root.axisClicked(OriginGizmo.Axis.PositiveX)
  254. }
  255. }
  256. LineRectangle {
  257. endPoint: Qt.vector2d(positiveX.x + ballBackground.subBallHalfWidth, positiveX.y + ballBackground.subBallHalfWidth)
  258. color: stylePalette.red
  259. z: positiveX.z - 0.001
  260. }
  261. SubBall {
  262. id: positiveY
  263. targetNode: root.targetNode
  264. width: ballBackground.subBallWidth
  265. offset: ballBackground.subBallOffset
  266. labelText: "Y"
  267. labelColor: hovered ? stylePalette.white : stylePalette.black
  268. color: stylePalette.green
  269. initialPosition: Qt.vector3d(0, 1, 0)
  270. onTapped: {
  271. root.axisClicked(OriginGizmo.Axis.PositiveY)
  272. }
  273. }
  274. LineRectangle {
  275. endPoint: Qt.vector2d(positiveY.x + ballBackground.subBallHalfWidth, positiveY.y + ballBackground.subBallHalfWidth)
  276. color: stylePalette.green
  277. z: positiveY.z - 0.001
  278. }
  279. SubBall {
  280. id: positiveZ
  281. targetNode: root.targetNode
  282. width: ballBackground.subBallWidth
  283. offset: ballBackground.subBallOffset
  284. labelText: "Z"
  285. labelColor: hovered ? stylePalette.white : stylePalette.black
  286. color: stylePalette.blue
  287. initialPosition: Qt.vector3d(0, 0, 1)
  288. onTapped: {
  289. root.axisClicked(OriginGizmo.Axis.PositiveZ)
  290. }
  291. }
  292. LineRectangle {
  293. endPoint: Qt.vector2d(positiveZ.x + ballBackground.subBallHalfWidth, positiveZ.y + ballBackground.subBallHalfWidth)
  294. color: stylePalette.blue
  295. z: positiveZ.z - 0.001
  296. }
  297. SubBall {
  298. targetNode: root.targetNode
  299. width: ballBackground.subBallWidth
  300. offset: ballBackground.subBallOffset
  301. labelText: "-X"
  302. labelColor: stylePalette.white
  303. labelVisible: hovered
  304. color: Qt.rgba(stylePalette.red.r, stylePalette.red.g, stylePalette.red.b, z + 1 * 0.5)
  305. border.color: stylePalette.red
  306. border.width: 2
  307. initialPosition: Qt.vector3d(-1, 0, 0)
  308. onTapped: {
  309. root.axisClicked(OriginGizmo.Axis.NegativeX)
  310. }
  311. }
  312. SubBall {
  313. targetNode: root.targetNode
  314. width: ballBackground.subBallWidth
  315. offset: ballBackground.subBallOffset
  316. labelText: "-Y"
  317. labelColor: stylePalette.white
  318. labelVisible: hovered
  319. color: Qt.rgba(stylePalette.green.r, stylePalette.green.g, stylePalette.green.b, z + 1 * 0.5)
  320. border.color: stylePalette.green
  321. border.width: 2
  322. initialPosition: Qt.vector3d(0, -1, 0)
  323. onTapped: {
  324. root.axisClicked(OriginGizmo.Axis.NegativeY)
  325. }
  326. }
  327. SubBall {
  328. targetNode: root.targetNode
  329. width: ballBackground.subBallWidth
  330. offset: ballBackground.subBallOffset
  331. labelText: "-Z"
  332. labelColor: stylePalette.white
  333. labelVisible: hovered
  334. color: Qt.rgba(stylePalette.blue.r, stylePalette.blue.g, stylePalette.blue.b, z + 1 * 0.5)
  335. border.color: stylePalette.blue
  336. border.width: 2
  337. initialPosition: Qt.vector3d(0, 0, -1)
  338. onTapped: {
  339. root.axisClicked(OriginGizmo.Axis.NegativeZ)
  340. }
  341. }
  342. }
  343. HoverHandler {
  344. id: ballBackgroundHoverHandler
  345. acceptedDevices: PointerDevice.Mouse
  346. cursorShape: Qt.PointingHandCursor
  347. }
  348. DragHandler {
  349. id: dragHandler
  350. target: null
  351. enabled: ballBackground.visible
  352. onCentroidChanged: {
  353. if (centroid.velocity.x > 0 && centroid.velocity.y > 0) {
  354. root.ballMoved(centroid.velocity)
  355. }
  356. }
  357. }
  358. }
  359. }