RotateTool.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. /*
  2. * RotateTool.js
  3. *
  4. * @author realor
  5. */
  6. import { TransformationTool } from './TransformationTool.js'
  7. import { Controls } from '../ui/Controls.js'
  8. import { I18N } from '../i18n/I18N.js'
  9. import { PointSelector } from '../utils/PointSelector.js'
  10. import { GeometryUtils } from '../utils/GeometryUtils.js'
  11. import * as THREE from '../lib/three.module.js'
  12. class RotateTool extends TransformationTool {
  13. static CHANGED_PROPERTIES = ['position', 'rotation']
  14. constructor(application, options) {
  15. super(application)
  16. this.name = 'rotate'
  17. this.label = 'tool.rotate.label'
  18. this.className = 'rotate'
  19. this.setOptions(options)
  20. // internals
  21. this.firstPointWorld = new THREE.Vector3()
  22. this.secondPointWorld = new THREE.Vector3()
  23. this.anchorPointWorld = new THREE.Vector3()
  24. this.rotation = 0
  25. this.baseMatrixWorld = new THREE.Matrix4()
  26. this.baseMatrixWorldInverse = new THREE.Matrix4()
  27. this._vector1 = new THREE.Vector3()
  28. this._vector2 = new THREE.Vector3()
  29. this.wheelPoints = []
  30. this.wheel = this.createWheel()
  31. this.createPanel()
  32. }
  33. createPanel() {
  34. this.panel = this.application.createPanel(this.label, 'left', 'panel_rotate')
  35. this.panel.preferredHeight = 140
  36. this.helpElem = document.createElement('div')
  37. this.panel.bodyElem.appendChild(this.helpElem)
  38. this.rotationInputElem = Controls.addNumberField(this.panel.bodyElem, 'rotate_angle', 'label.rotation', 0)
  39. this.rotationInputElem.step = 1
  40. this.rotationInputElem.addEventListener(
  41. 'change',
  42. event => {
  43. this.rotation = this.rotationInputElem.value
  44. this.rotateObjects()
  45. },
  46. false
  47. )
  48. this.buttonsPanel = document.createElement('div')
  49. this.panel.bodyElem.appendChild(this.buttonsPanel)
  50. this.acceptButton = Controls.addButton(this.buttonsPanel, 'cancel_section', 'button.accept', event => {
  51. this.rotation = this.rotationInputElem.value
  52. this.rotateObjects()
  53. this.setStage(0)
  54. })
  55. this.cancelButton = Controls.addButton(this.buttonsPanel, 'cancel_section', 'button.cancel', event => {
  56. this.rotation = 0
  57. this.rotateObjects()
  58. this.setStage(0)
  59. })
  60. }
  61. onPointerMove(event) {
  62. if (this.stage === 3) {
  63. const application = this.application
  64. const pointSelector = application.pointSelector
  65. if (!pointSelector.isPointSelectionEvent(event)) return
  66. event.preventDefault()
  67. const snap = pointSelector.snap
  68. if (snap) {
  69. let anchorPoint = this._vector1
  70. anchorPoint.copy(this.anchorPointWorld)
  71. anchorPoint.applyMatrix4(this.baseMatrixWorldInverse)
  72. anchorPoint.z = 0
  73. let destinationPoint = this._vector2
  74. destinationPoint.copy(snap.positionWorld)
  75. destinationPoint.applyMatrix4(this.baseMatrixWorldInverse)
  76. destinationPoint.z = 0
  77. let angleRad1 = Math.atan2(anchorPoint.y, anchorPoint.x)
  78. let angleRad2 = Math.atan2(destinationPoint.y, destinationPoint.x)
  79. let angleRad = angleRad2 - angleRad1
  80. let angleDeg = THREE.MathUtils.radToDeg(angleRad)
  81. const k = 100000
  82. angleDeg = Math.round(k * angleDeg) / k
  83. if (angleDeg > 180) angleDeg -= 360
  84. else if (angleDeg < -180) angleDeg += 360
  85. this.rotation = angleDeg
  86. this.rotateObjects()
  87. this.rotationInputElem.value = this.rotation
  88. }
  89. }
  90. }
  91. onPointerUp(event) {
  92. const application = this.application
  93. const pointSelector = application.pointSelector
  94. if (!pointSelector.isPointSelectionEvent(event)) return
  95. const snap = pointSelector.snap
  96. if (this.stage === 0) {
  97. // first axis point
  98. if (snap) {
  99. this.firstPointWorld.copy(snap.positionWorld)
  100. if (snap.object) {
  101. let axisMatrixWorld = this.axisMatrixWorld
  102. if (snap.object) {
  103. axisMatrixWorld.copy(snap.object.matrixWorld)
  104. } else {
  105. axisMatrixWorld.identity()
  106. }
  107. axisMatrixWorld.setPosition(this.firstPointWorld)
  108. if (application.selection.isEmpty()) {
  109. application.selection.set(snap.object)
  110. }
  111. }
  112. this.setStage(1)
  113. } else {
  114. this.setStage(0)
  115. }
  116. application.overlays.remove(this.wheel)
  117. } else if (this.stage === 1) {
  118. // second axis point
  119. if (snap) {
  120. if (!snap.positionWorld.equals(this.firstPointWorld)) {
  121. this.secondPointWorld.copy(snap.positionWorld)
  122. this.createBaseMatrix()
  123. this.rotationInputElem.value = 0
  124. this.setStage(2)
  125. }
  126. } else {
  127. this.setStage(0)
  128. }
  129. } else if (this.stage === 2 || this.stage === 4) {
  130. // anchor point
  131. this.objectMatrices.clear()
  132. if (snap) {
  133. if (!snap.positionWorld.equals(this.firstPointWorld) && !snap.positionWorld.equals(this.secondPointWorld)) {
  134. this.anchorPointWorld.copy(snap.positionWorld)
  135. this.rotationInputElem.value = 0
  136. this.setStage(3)
  137. }
  138. } else {
  139. this.setStage(0)
  140. }
  141. } else if (this.stage === 3) {
  142. // destination point
  143. this.setStage(4)
  144. }
  145. }
  146. setStage(stage) {
  147. this.stage = stage
  148. const application = this.application
  149. switch (stage) {
  150. case 0: // set first point of rotation axis
  151. this.rotation = 0
  152. application.pointSelector.clearAxisGuides()
  153. application.pointSelector.excludeSelection = false
  154. application.pointSelector.auxiliaryPoints = []
  155. application.pointSelector.activate()
  156. this.rotationInputElem.parentElement.style.display = 'none'
  157. this.buttonsPanel.style.display = 'none'
  158. I18N.set(this.helpElem, 'innerHTML', 'tool.rotate.select_first_point')
  159. application.i18n.update(this.helpElem)
  160. application.overlays.remove(this.wheel)
  161. application.repaint()
  162. break
  163. case 1: // set second point of rotation axis
  164. application.pointSelector.setAxisGuides(this.axisMatrixWorld, true)
  165. application.pointSelector.excludeSelection = false
  166. application.pointSelector.auxiliaryPoints = []
  167. application.pointSelector.activate()
  168. this.rotationInputElem.parentElement.style.display = 'none'
  169. this.buttonsPanel.style.display = 'none'
  170. I18N.set(this.helpElem, 'innerHTML', 'tool.rotate.select_second_point')
  171. application.i18n.update(this.helpElem)
  172. break
  173. case 2: // set anchor point
  174. application.pointSelector.clearAxisGuides()
  175. application.pointSelector.excludeSelection = false
  176. application.pointSelector.auxiliaryPoints = this.wheelPoints
  177. application.pointSelector.activate()
  178. this.rotationInputElem.parentElement.style.display = 'none'
  179. this.buttonsPanel.style.display = 'none'
  180. I18N.set(this.helpElem, 'innerHTML', 'tool.rotate.select_anchor_point')
  181. application.i18n.update(this.helpElem)
  182. break
  183. case 3: // set destination point
  184. application.pointSelector.clearAxisGuides()
  185. application.pointSelector.excludeSelection = true
  186. application.pointSelector.auxiliaryPoints = this.wheelPoints
  187. application.pointSelector.activate()
  188. this.rotationInputElem.parentElement.style.display = ''
  189. this.rotationInputElem.disabled = true
  190. this.buttonsPanel.style.display = 'none'
  191. I18N.set(this.helpElem, 'innerHTML', 'tool.rotate.select_destination_point')
  192. application.i18n.update(this.helpElem)
  193. break
  194. case 4: // edit rotation angle
  195. application.pointSelector.clearAxisGuides()
  196. application.pointSelector.excludeSelection = false
  197. application.pointSelector.auxiliaryPoints = this.wheelPoints
  198. application.pointSelector.activate()
  199. this.rotationInputElem.parentElement.style.display = ''
  200. this.rotationInputElem.disabled = false
  201. this.buttonsPanel.style.display = ''
  202. I18N.set(this.helpElem, 'innerHTML', 'tool.rotate.edit_rotation')
  203. application.i18n.update(this.helpElem)
  204. break
  205. }
  206. }
  207. createBaseMatrix() {
  208. const rotationAxisWorld = this._vector1
  209. rotationAxisWorld.subVectors(this.secondPointWorld, this.firstPointWorld)
  210. rotationAxisWorld.normalize()
  211. let yVector = GeometryUtils.orthogonalVector(rotationAxisWorld).normalize()
  212. let xVector = new THREE.Vector3()
  213. xVector.crossVectors(yVector, rotationAxisWorld)
  214. const baseMatrixWorld = this.baseMatrixWorld
  215. baseMatrixWorld.makeBasis(xVector, yVector, rotationAxisWorld)
  216. baseMatrixWorld.setPosition(this.firstPointWorld)
  217. const baseMatrixWorldInverse = this.baseMatrixWorldInverse
  218. baseMatrixWorldInverse.copy(baseMatrixWorld).invert()
  219. const wheel = this.wheel
  220. baseMatrixWorld.decompose(wheel.position, wheel.quaternion, wheel.scale)
  221. wheel.updateMatrix()
  222. const application = this.application
  223. application.overlays.add(wheel)
  224. application.repaint()
  225. const divisions = 72
  226. const angleIncr = (2 * Math.PI) / divisions
  227. for (let i = 0; i < divisions; i++) {
  228. let angle = i * angleIncr
  229. let x = 0.5 * Math.cos(angle)
  230. let y = 0.5 * Math.sin(angle)
  231. this.wheelPoints[i].set(x, y, 0).applyMatrix4(baseMatrixWorld)
  232. }
  233. }
  234. rotateObjects() {
  235. const application = this.application
  236. const baseMatrixWorld = this.baseMatrixWorld
  237. const baseMatrixWorldInverse = this.baseMatrixWorldInverse
  238. const angleRad = THREE.MathUtils.degToRad(this.rotation)
  239. const rotationMatrixWorld = this._matrix1.makeRotationZ(angleRad)
  240. rotationMatrixWorld.multiply(baseMatrixWorldInverse)
  241. rotationMatrixWorld.premultiply(baseMatrixWorld)
  242. this.transformObjects(rotationMatrixWorld, RotateTool.CHANGED_PROPERTIES)
  243. }
  244. resetTool() {
  245. super.resetTool()
  246. this.rotation = 0
  247. const application = this.application
  248. application.overlays.remove(this.wheel)
  249. application.repaint()
  250. }
  251. createWheel() {
  252. const geometry = new THREE.BufferGeometry()
  253. const points = []
  254. points.push(new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, 1))
  255. points.push(new THREE.Vector3(-0.1, 0, 0), new THREE.Vector3(0.1, 0, 0))
  256. points.push(new THREE.Vector3(0, -0.1, 0), new THREE.Vector3(0, 0.1, 0))
  257. const divisions = 72
  258. const angleIncr = (2 * Math.PI) / divisions
  259. const wheelPoints = this.wheelPoints
  260. for (let i = 0; i < divisions; i++) {
  261. let angle = i * angleIncr
  262. let x = 0.5 * Math.cos(angle)
  263. let y = 0.5 * Math.sin(angle)
  264. wheelPoints.push(new THREE.Vector3(x, y, 0))
  265. }
  266. for (let i = 0; i < divisions; i++) {
  267. let p1 = wheelPoints[i]
  268. let p2 = wheelPoints[(i + 1) % divisions]
  269. let p3 = new THREE.Vector3()
  270. let angle = i * angleIncr
  271. let r
  272. if (i % 18 === 0) r = 0.35
  273. else if (i % 9 === 0) r = 0.4
  274. else r = 0.45
  275. p3.x = r * Math.cos(angle)
  276. p3.y = r * Math.sin(angle)
  277. points.push(p1, p2)
  278. points.push(p1, p3)
  279. }
  280. geometry.setFromPoints(points)
  281. const lines = new THREE.LineSegments(geometry, new THREE.LineBasicMaterial({ color: 0xff0000, linewidth: 1, transparent: true, opacity: 0.8, depthTest: false }))
  282. lines.name = 'RotationWheel'
  283. lines.raycast = function() {}
  284. lines.renderOrder = 10000
  285. return lines
  286. }
  287. }
  288. export { RotateTool }