RevolveTool.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. /*
  2. * RevolveTool.js
  3. *
  4. * @author realor
  5. */
  6. import { Tool } from './Tool.js'
  7. import { Solid } from '../core/Solid.js'
  8. import { Profile } from '../core/Profile.js'
  9. import { ObjectBuilder } from '../builders/ObjectBuilder.js'
  10. import { CircleBuilder } from '../builders/CircleBuilder.js'
  11. import { Revolver } from '../builders/Revolver.js'
  12. import { Controls } from '../ui/Controls.js'
  13. import { MessageDialog } from '../ui/MessageDialog.js'
  14. import { I18N } from '../i18n/I18N.js'
  15. import * as THREE from '../lib/three.module.js'
  16. class RevolveTool extends Tool {
  17. constructor(application, options) {
  18. super(application)
  19. this.name = 'revolve'
  20. this.label = 'tool.revolve.label'
  21. this.help = 'tool.revolve.help'
  22. this.className = 'revolve'
  23. this.angleStep = 1
  24. this.setOptions(options)
  25. this.stage = 0
  26. this.solid = null
  27. this.startPoint = new THREE.Vector3()
  28. this.endPoint = new THREE.Vector3()
  29. this.angle = 0
  30. this.axisMatrixWorld = new THREE.Matrix4()
  31. this.revolutionAxisMatrixWorld = new THREE.Matrix4()
  32. this.revolutionAxisMatrixWorldInverse = new THREE.Matrix4()
  33. this.solidMatrixWorldInverse = new THREE.Matrix4()
  34. this._onPointerDown = this.onPointerDown.bind(this)
  35. this._onPointerUp = this.onPointerUp.bind(this)
  36. this._onPointerMove = this.onPointerMove.bind(this)
  37. this._onSelection = this.onSelection.bind(this)
  38. this._onContextMenu = this.onContextMenu.bind(this)
  39. this.extrudeMaterial = new THREE.LineBasicMaterial({
  40. color: new THREE.Color(1, 0, 0),
  41. depthTest: false,
  42. linewidth: 1.5
  43. })
  44. this.wheelPoints = []
  45. this.wheel = this.createWheel()
  46. this.createPanel()
  47. }
  48. activate() {
  49. this.panel.visible = true
  50. const application = this.application
  51. const container = application.container
  52. container.addEventListener('contextmenu', this._onContextMenu, false)
  53. container.addEventListener('pointerdown', this._onPointerDown, false)
  54. container.addEventListener('pointerup', this._onPointerUp, false)
  55. container.addEventListener('pointermove', this._onPointerMove, false)
  56. application.addEventListener('selection', this._onSelection, false)
  57. this.prepareRevolution()
  58. }
  59. deactivate() {
  60. this.panel.visible = false
  61. const application = this.application
  62. const container = application.container
  63. container.removeEventListener('contextmenu', this._onContextMenu, false)
  64. container.removeEventListener('pointerdown', this._onPointerDown, false)
  65. container.removeEventListener('pointerup', this._onPointerUp, false)
  66. container.removeEventListener('pointermove', this._onPointerMove, false)
  67. application.removeEventListener('selection', this._onSelection, false)
  68. application.pointSelector.deactivate()
  69. this.removeWheel()
  70. }
  71. createPanel() {
  72. this.panel = this.application.createPanel(this.label, 'left', 'panel_revolve')
  73. this.panel.preferredHeight = 140
  74. this.helpElem = document.createElement('div')
  75. this.panel.bodyElem.appendChild(this.helpElem)
  76. this.angleInputElem = Controls.addNumberField(this.panel.bodyElem, 'revolve_angle', 'label.angle', 0)
  77. this.angleInputElem.step = this.angleStep
  78. this.angleInputElem.addEventListener(
  79. 'change',
  80. event => {
  81. this.angle = parseFloat(this.angleInputElem.value)
  82. this.updateRevolution()
  83. },
  84. false
  85. )
  86. this.angleLabelElem = this.angleInputElem.parentElement.firstChild
  87. this.buttonsElem = document.createElement('div')
  88. this.panel.bodyElem.appendChild(this.buttonsElem)
  89. this.applyButton = Controls.addButton(this.buttonsElem, 'revolve_apply', 'button.apply', event => {
  90. this.angle = parseFloat(this.angleInputElem.value)
  91. this.updateRevolution()
  92. })
  93. this.changeAxisButton = Controls.addButton(this.buttonsElem, 'revolve_changeAxis', 'button.change_axis', event => {
  94. this.angle = 0
  95. this.updateRevolution()
  96. this.setStage(0)
  97. })
  98. this.finishButton = Controls.addButton(this.buttonsElem, 'revolve_finish', 'button.finish', event => {
  99. this.application.selection.clear()
  100. })
  101. }
  102. onPointerDown(event) {
  103. const application = this.application
  104. const pointSelector = application.pointSelector
  105. if (!pointSelector.isPointSelectionEvent(event)) return
  106. if (this.stage === 2) {
  107. this.setStage(3)
  108. }
  109. }
  110. onPointerMove(event) {
  111. const pointSelector = this.application.pointSelector
  112. if (!pointSelector.isPointSelectionEvent(event)) return
  113. let snap = pointSelector.snap
  114. if (snap) {
  115. if (this.stage === 3) {
  116. this.updateAngle(snap.positionWorld)
  117. }
  118. }
  119. }
  120. onPointerUp(event) {
  121. const pointSelector = this.application.pointSelector
  122. if (!pointSelector.isPointSelectionEvent(event)) return
  123. const snap = pointSelector.snap
  124. if (snap) {
  125. if (this.stage === 0) {
  126. this.solidMatrixWorldInverse.copy(this.solid.matrixWorld).invert()
  127. this.startPoint.copy(snap.positionWorld)
  128. this.startPoint.applyMatrix4(this.solidMatrixWorldInverse)
  129. this.startPoint.z = 0
  130. if (snap.object) {
  131. this.axisMatrixWorld.copy(snap.object.matrixWorld)
  132. } else {
  133. this.axisMatrixWorld.identity()
  134. }
  135. this.axisMatrixWorld.setPosition(snap.positionWorld)
  136. this.setStage(1)
  137. } else if (this.stage === 1) {
  138. this.solidMatrixWorldInverse.copy(this.solid.matrixWorld).invert()
  139. this.endPoint.copy(snap.positionWorld)
  140. this.endPoint.applyMatrix4(this.solidMatrixWorldInverse)
  141. this.endPoint.z = 0
  142. const builder = this.solid.builder
  143. builder.location.copy(this.startPoint)
  144. let axis = new THREE.Vector3()
  145. axis.subVectors(this.endPoint, this.startPoint).normalize()
  146. if (this.isValidAxis(axis)) {
  147. builder.axis.copy(axis)
  148. this.updateRevolution(true)
  149. this.setStage(2)
  150. } else {
  151. this.setStage(0)
  152. MessageDialog.create('ERROR', 'message.invalid_revolution_axis')
  153. .setClassName('error')
  154. .setI18N(this.application.i18n)
  155. .show()
  156. }
  157. } else if (this.stage === 3) {
  158. this.updateAngle(snap.positionWorld)
  159. this.setStage(2)
  160. }
  161. } else {
  162. if (this.stage === 3) {
  163. this.setStage(2)
  164. }
  165. }
  166. }
  167. onSelection(event) {
  168. this.prepareRevolution()
  169. }
  170. onContextMenu(event) {
  171. const pointSelector = this.application.pointSelector
  172. if (!pointSelector.isPointSelectionEvent(event)) return
  173. event.preventDefault()
  174. }
  175. setStage(stage) {
  176. this.stage = stage
  177. const application = this.application
  178. switch (stage) {
  179. case 0: // set revolve axis start point
  180. application.pointSelector.auxiliaryPoints = []
  181. application.pointSelector.auxiliaryLines = []
  182. application.pointSelector.excludeSelection = false
  183. application.pointSelector.clearAxisGuides()
  184. application.pointSelector.activate()
  185. this.angleLabelElem.style.display = 'none'
  186. this.angleInputElem.disabled = false
  187. this.angleInputElem.style.display = 'none'
  188. this.applyButton.style.display = 'none'
  189. this.changeAxisButton.style.display = 'none'
  190. this.finishButton.style.display = ''
  191. I18N.set(this.helpElem, 'innerHTML', 'tool.revolve.set_axis_first_point')
  192. application.i18n.update(this.helpElem)
  193. this.removeWheel()
  194. break
  195. case 1: // set revolve axis end point
  196. application.pointSelector.auxiliaryPoints = []
  197. application.pointSelector.auxiliaryLines = []
  198. application.pointSelector.excludeSelection = false
  199. application.pointSelector.setAxisGuides(this.axisMatrixWorld, true)
  200. application.pointSelector.activate()
  201. this.angleLabelElem.style.display = 'none'
  202. this.angleInputElem.disabled = false
  203. this.angleInputElem.style.display = 'none'
  204. this.applyButton.style.display = 'none'
  205. this.finishButton.style.display = ''
  206. this.changeAxisButton.style.display = 'none'
  207. I18N.set(this.helpElem, 'innerHTML', 'tool.revolve.set_axis_second_point')
  208. application.i18n.update(this.helpElem)
  209. this.removeWheel()
  210. break
  211. case 2: // dynamic revolve: pointer up
  212. application.pointSelector.clearAxisGuides()
  213. application.pointSelector.excludeSelection = true
  214. application.pointSelector.auxiliaryPoints = this.wheelPoints
  215. application.pointSelector.auxiliaryLines = []
  216. application.pointSelector.activate()
  217. this.angleLabelElem.style.display = ''
  218. this.angleInputElem.disabled = false
  219. this.angleInputElem.style.display = ''
  220. this.applyButton.style.display = ''
  221. this.finishButton.style.display = ''
  222. this.changeAxisButton.style.display = ''
  223. I18N.set(this.helpElem, 'innerHTML', 'tool.revolve.drag_pointer')
  224. application.i18n.update(this.helpElem)
  225. this.addWheel()
  226. break
  227. case 3: // dynamic revolve: pointer down
  228. application.pointSelector.clearAxisGuides()
  229. application.pointSelector.excludeSelection = true
  230. application.pointSelector.auxiliaryPoints = this.wheelPoints
  231. application.pointSelector.auxiliaryLines = []
  232. application.pointSelector.activate()
  233. this.angleLabelElem.style.display = ''
  234. this.angleInputElem.disabled = true
  235. this.angleInputElem.style.display = ''
  236. this.applyButton.style.display = 'none'
  237. this.changeAxisButton.style.display = 'none'
  238. this.finishButton.style.display = ''
  239. I18N.set(this.helpElem, 'innerHTML', 'tool.revolve.drag_pointer')
  240. application.i18n.update(this.helpElem)
  241. this.addWheel()
  242. break
  243. case 4: // no object selected
  244. application.pointSelector.auxiliaryPoints = []
  245. application.pointSelector.auxiliaryLines = []
  246. application.pointSelector.deactivate()
  247. this.angleLabelElem.style.display = 'none'
  248. this.angleInputElem.style.display = 'none'
  249. this.applyButton.style.display = 'none'
  250. this.changeAxisButton.style.display = 'none'
  251. this.finishButton.style.display = 'none'
  252. I18N.set(this.helpElem, 'innerHTML', 'tool.revolve.select_object')
  253. application.i18n.update(this.helpElem)
  254. this.removeWheel()
  255. break
  256. }
  257. }
  258. prepareRevolution() {
  259. const application = this.application
  260. const object = application.selection.object
  261. let solid
  262. let firstRevolve = false
  263. let solidChanged = false
  264. if (object instanceof Profile && !(object.parent instanceof Solid)) {
  265. solid = this.revolveProfile(object)
  266. firstRevolve = true
  267. } else if (object instanceof Profile && object.parent instanceof Solid && object.parent.builder instanceof Revolver && object.parent.children.length === 3) {
  268. solid = object.parent
  269. } else if (object instanceof Solid && object.builder instanceof Revolver && object.children.length === 3 && object.children[2] instanceof Profile) {
  270. solid = object
  271. } else {
  272. solid = null
  273. }
  274. solidChanged = this.solid !== solid
  275. this.solid = solid
  276. if (solid) {
  277. this.angle = solid.builder.angle
  278. this.updateAngleInPanel()
  279. if (firstRevolve) {
  280. this.setStage(0) // set revolve axis
  281. } else if (solidChanged) {
  282. this.setStage(2)
  283. } else {
  284. this.setStage(this.stage) // set to last stage
  285. }
  286. if (!application.selection.contains(solid)) {
  287. application.selection.set(solid)
  288. }
  289. } else {
  290. this.angle = 0
  291. this.updateAngleInPanel()
  292. this.setStage(4)
  293. }
  294. }
  295. revolveProfile(profile) {
  296. const application = this.application
  297. const parent = profile.parent
  298. application.removeObject(profile)
  299. const solid = new Solid()
  300. solid.name = 'Revolve'
  301. const builder = new Revolver(0)
  302. solid.builder = builder
  303. builder.optimize = !(profile.builder instanceof CircleBuilder)
  304. profile.matrix.decompose(solid.position, solid.rotation, solid.scale)
  305. solid.matrix.copy(profile.matrix)
  306. profile.visible = false
  307. profile.matrix.identity()
  308. profile.matrix.decompose(profile.position, profile.rotation, profile.scale)
  309. solid.add(profile)
  310. ObjectBuilder.build(solid)
  311. application.addObject(solid, parent, false, true)
  312. return solid
  313. }
  314. updateAngle(positionWorld) {
  315. const solid = this.solid
  316. const builder = solid.builder
  317. let position = new THREE.Vector3()
  318. position.copy(positionWorld)
  319. position.applyMatrix4(this.revolutionAxisMatrixWorldInverse)
  320. let angleRad = Math.atan2(-position.z, position.x)
  321. let angle = THREE.MathUtils.radToDeg(angleRad)
  322. if (builder.axis.y < 0) angle += 180
  323. if (angle < 0) angle += 360
  324. if (this.angle > 180 && angle === 0) angle = 360
  325. this.angle = angle
  326. console.info('angle', angle)
  327. this.updateAngleInPanel()
  328. this.updateRevolution()
  329. }
  330. updateRevolution(force = false) {
  331. const solid = this.solid
  332. if (solid) {
  333. let revolver = solid.builder
  334. if (force || this.angle !== revolver.angle) {
  335. revolver.angle = this.angle
  336. solid.needsRebuild = true
  337. ObjectBuilder.build(solid)
  338. this.application.notifyObjectsChanged(solid)
  339. }
  340. }
  341. }
  342. updateAngleInPanel() {
  343. const k = 10000000
  344. this.angleInputElem.value = Math.round(this.angle * k) / k
  345. }
  346. isValidAxis(axis) {
  347. // TODO: also check axis does not intersect profile
  348. return axis.length() > 0.001
  349. }
  350. addWheel() {
  351. const application = this.application
  352. const solid = this.solid
  353. const builder = solid.builder
  354. const yAxis = new THREE.Vector3()
  355. yAxis.copy(builder.axis).normalize()
  356. const xAxis = builder.location.x > 0 ? new THREE.Vector3(-yAxis.y, yAxis.x, 0) : new THREE.Vector3(yAxis.y, -yAxis.x, 0)
  357. const zAxis = new THREE.Vector3()
  358. zAxis.crossVectors(xAxis, yAxis)
  359. const revolutionAxisMatrixWorld = this.revolutionAxisMatrixWorld
  360. const revolutionAxisMatrixWorldInverse = this.revolutionAxisMatrixWorldInverse
  361. revolutionAxisMatrixWorld.makeBasis(xAxis, yAxis, zAxis)
  362. revolutionAxisMatrixWorld.setPosition(builder.location)
  363. revolutionAxisMatrixWorld.premultiply(solid.matrixWorld)
  364. revolutionAxisMatrixWorldInverse.copy(revolutionAxisMatrixWorld).invert()
  365. const wheel = this.wheel
  366. const rotationMatrix = new THREE.Matrix4()
  367. rotationMatrix.makeRotationX(-Math.PI / 2)
  368. let wheelMatrixWorld = wheel.matrixWorld
  369. wheelMatrixWorld.copy(revolutionAxisMatrixWorld)
  370. wheelMatrixWorld.multiply(rotationMatrix)
  371. wheelMatrixWorld.decompose(wheel.position, wheel.quaternion, wheel.scale)
  372. wheel.updateMatrix()
  373. const divisions = 72
  374. const angleIncr = (2 * Math.PI) / divisions
  375. for (let i = 0; i < divisions; i++) {
  376. let angle = i * angleIncr
  377. let x = 0.5 * Math.cos(angle)
  378. let y = 0.5 * Math.sin(angle)
  379. this.wheelPoints[i].set(x, y, 0).applyMatrix4(wheelMatrixWorld)
  380. }
  381. application.overlays.add(wheel)
  382. application.repaint()
  383. }
  384. removeWheel() {
  385. this.application.overlays.remove(this.wheel)
  386. this.application.repaint()
  387. }
  388. createWheel() {
  389. const geometry = new THREE.BufferGeometry()
  390. const points = []
  391. points.push(new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, 1))
  392. points.push(new THREE.Vector3(-0.1, 0, 0), new THREE.Vector3(0.1, 0, 0))
  393. points.push(new THREE.Vector3(0, -0.1, 0), new THREE.Vector3(0, 0.1, 0))
  394. const divisions = 72
  395. const angleIncr = (2 * Math.PI) / divisions
  396. const wheelPoints = this.wheelPoints
  397. for (let i = 0; i < divisions; i++) {
  398. let angle = i * angleIncr
  399. let x = 0.5 * Math.cos(angle)
  400. let y = 0.5 * Math.sin(angle)
  401. wheelPoints.push(new THREE.Vector3(x, y, 0))
  402. }
  403. for (let i = 0; i < divisions; i++) {
  404. let p1 = wheelPoints[i]
  405. let p2 = wheelPoints[(i + 1) % divisions]
  406. let p3 = new THREE.Vector3()
  407. let angle = i * angleIncr
  408. let r
  409. if (i % 18 === 0) r = 0.35
  410. else if (i % 9 === 0) r = 0.4
  411. else r = 0.45
  412. p3.x = r * Math.cos(angle)
  413. p3.y = r * Math.sin(angle)
  414. points.push(p1, p2)
  415. points.push(p1, p3)
  416. }
  417. geometry.setFromPoints(points)
  418. const lines = new THREE.LineSegments(geometry, new THREE.LineBasicMaterial({ color: 0xff0000, linewidth: 1, transparent: true, opacity: 0.8, depthTest: false }))
  419. lines.name = 'RotationWheel'
  420. lines.raycast = function() {}
  421. lines.renderOrder = 10000
  422. return lines
  423. }
  424. }
  425. export { RevolveTool }