DrawTool.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. /*
  2. * DrawTool.js
  3. *
  4. * @author realor
  5. */
  6. import { Tool } from './Tool.js'
  7. import { Cord } from '../core/Cord.js'
  8. import { Profile } from '../core/Profile.js'
  9. import { Controls } from '../ui/Controls.js'
  10. import { CordGeometry } from '../core/CordGeometry.js'
  11. import { ProfileGeometry } from '../core/ProfileGeometry.js'
  12. import { PointSelector } from '../utils/PointSelector.js'
  13. import { GeometryUtils } from '../utils/GeometryUtils.js'
  14. import { ObjectUtils } from '../utils/ObjectUtils.js'
  15. import { I18N } from '../i18n/I18N.js'
  16. import * as THREE from '../lib/three.module.js'
  17. class DrawTool extends Tool {
  18. constructor(application, options) {
  19. super(application)
  20. this.name = 'draw'
  21. this.label = 'tool.draw.label'
  22. this.help = 'tool.draw.help'
  23. this.className = 'draw'
  24. this.setOptions(options)
  25. this.mode = 0 // 0: add, 1: edit
  26. this.vertices = [] // Vector3[]
  27. this.index = -1
  28. this.object = null // Cord or Profile
  29. this.points = null // THREE.Points
  30. this.position1World = null // THREE.Vector3
  31. this.position2World = null // THREE.Vector3
  32. this.inverseMatrixWorld = new THREE.Matrix4() // object inverse matrixWorld
  33. this.addPointsMaterial = new THREE.PointsMaterial({ color: 0x00ff00, size: 4, sizeAttenuation: false, depthTest: false, transparent: true })
  34. this.editPointsMaterial = new THREE.PointsMaterial({ color: 0, size: 4, sizeAttenuation: false, depthTest: false, transparent: true })
  35. this._onPointerUp = this.onPointerUp.bind(this)
  36. this._onSelection = this.onSelection.bind(this)
  37. this.createPanel()
  38. }
  39. createPanel() {
  40. this.panel = this.application.createPanel(this.label, 'left', 'panel_draw')
  41. this.panel.preferredHeight = 140
  42. // this.helpElem = document.createElement('div')
  43. // this.panel.bodyElem.appendChild(this.helpElem)
  44. // I18N.set(this.helpElem, 'innerHTML', 'tool.draw.help')
  45. this.offsetInputElem = Controls.addNumberField(this.panel.bodyElem, 'draw_offset', 'label.offset', 0)
  46. this.offsetInputElem.step = 0.1
  47. this.offsetInputElem.addEventListener(
  48. 'change',
  49. event => {
  50. this.applyOffset()
  51. },
  52. false
  53. )
  54. this.buttonsElem = document.createElement('div')
  55. this.panel.bodyElem.appendChild(this.buttonsElem)
  56. this.finishButton = Controls.addButton(this.buttonsElem, 'draw_finish', 'button.finish', event => this.finish())
  57. this.profileButton = Controls.addButton(this.buttonsElem, 'draw_make_profile', 'button.make_profile', event => this.makeProfile())
  58. this.updatePanel()
  59. }
  60. activate() {
  61. this.panel.visible = true
  62. const application = this.application
  63. const container = application.container
  64. container.addEventListener('pointerup', this._onPointerUp, false)
  65. application.addEventListener('selection', this._onSelection, false)
  66. application.pointSelector.excludeSelection = false
  67. application.pointSelector.activate()
  68. this.setObject(application.selection.object)
  69. }
  70. deactivate() {
  71. this.panel.visible = false
  72. const application = this.application
  73. const container = application.container
  74. container.removeEventListener('pointerup', this._onPointerUp, false)
  75. application.removeEventListener('selection', this._onSelection, false)
  76. application.pointSelector.deactivate()
  77. application.pointSelector.clearAxisGuides()
  78. this.clearPoints()
  79. }
  80. onSelection(event) {
  81. let object = event.objects.length > 0 ? event.objects[0] : null
  82. this.setObject(object)
  83. }
  84. onPointerUp(event) {
  85. const pointSelector = this.application.pointSelector
  86. if (!pointSelector.isPointSelectionEvent(event)) return
  87. const selection = this.application.selection
  88. let snap = pointSelector.snap
  89. if (snap) {
  90. let positionWorld = snap.positionWorld
  91. let position = new THREE.Vector3()
  92. this.transformVertex(positionWorld, position)
  93. let nextIndex = this.findVertex(position)
  94. if (this.mode === 0) {
  95. // add to new object
  96. if (nextIndex === -1) {
  97. // add new vertex
  98. this.vertices.push(position)
  99. this.index = this.vertices.length - 1
  100. if (this.vertices.length === 1) {
  101. selection.clear()
  102. this.position1World = null
  103. } else {
  104. this.position1World = this.position2World
  105. }
  106. this.position2World = positionWorld
  107. } else if (nextIndex === 0 && this.vertices.length >= 3) {
  108. // close cord
  109. this.makeProfile()
  110. this.position1World = null
  111. this.position2World = null
  112. } // delete vertices
  113. else {
  114. let deleted = this.vertices.length - nextIndex - 1
  115. this.vertices.splice(nextIndex + 1, deleted)
  116. this.index = nextIndex
  117. this.position1World = null
  118. this.position2World = positionWorld
  119. }
  120. } // edit (mode === 1)
  121. else {
  122. this.position1World = null
  123. this.position2World = null
  124. if (this.index !== -1) {
  125. // vertex previously selected
  126. if (nextIndex === -1) {
  127. // move vertex
  128. this.vertices[this.index] = position
  129. } else if (
  130. this.object instanceof Cord &&
  131. this.vertices.length >= 3 &&
  132. ((this.index === 0 && nextIndex === this.vertices.length - 1) || (this.index === this.vertices.length - 1 && nextIndex === 0))
  133. ) {
  134. this.makeProfile()
  135. } else if (this.object instanceof Profile && this.index === 0 && nextIndex === this.vertices.length - 1) {
  136. this.vertices.splice(0, 1)
  137. } else if (this.object instanceof Profile && this.index === this.vertices.length - 1 && nextIndex === 0) {
  138. this.vertices.splice(this.vertices.length - 1, 1)
  139. } else if (this.index === nextIndex + 1) {
  140. this.vertices.splice(this.index, 1)
  141. } else if (this.index === nextIndex - 1) {
  142. this.vertices.splice(nextIndex - 1, 1)
  143. }
  144. this.index = -1 // unselect vertex
  145. this.object.builder = null
  146. } // no vertex previously selected
  147. else {
  148. if (nextIndex !== -1) {
  149. // on object vertex
  150. this.index = nextIndex
  151. } else {
  152. nextIndex = this.findVertexOnEdge(position)
  153. if (nextIndex !== -1) {
  154. this.index = nextIndex
  155. this.vertices.splice(nextIndex, 0, position)
  156. } else {
  157. this.mode = 0
  158. this.index = 0
  159. this.object = null
  160. this.vertices = [positionWorld]
  161. this.position2World = positionWorld
  162. selection.clear()
  163. }
  164. }
  165. }
  166. }
  167. this.updateObject()
  168. this.updatePoints()
  169. this.updateAxis(snap.object)
  170. this.updatePanel()
  171. } else {
  172. this.finish()
  173. }
  174. }
  175. setObject(object) {
  176. if (object && !ObjectUtils.isObjectDescendantOf(object, this.application.scene)) {
  177. // object removed
  178. object = null
  179. }
  180. if (this.object !== object) {
  181. this.object = null
  182. this.vertices = []
  183. this.index = -1
  184. this.position1World = null
  185. this.position2World = null
  186. if (object instanceof Cord) {
  187. this.object = object
  188. this.vertices = [...this.object.geometry.points]
  189. this.mode = 1
  190. } else if (object instanceof Profile) {
  191. this.object = object
  192. let points2 = this.object.geometry.path.getPoints()
  193. this.vertices = []
  194. for (let i = 0; i < points2.length - 1; i++) {
  195. let point2 = points2[i]
  196. this.vertices.push(new THREE.Vector3(point2.x, point2.y, 0))
  197. }
  198. this.mode = 1
  199. }
  200. }
  201. this.updatePoints()
  202. this.updateAxis(object)
  203. this.updatePanel()
  204. }
  205. finish() {
  206. const application = this.application
  207. const pointSelector = application.pointSelector
  208. this.mode = 0 // add
  209. this.vertices = []
  210. this.index = -1
  211. this.object = null
  212. this.position1World = null
  213. this.position2World = null
  214. this.updatePoints()
  215. this.updatePanel()
  216. pointSelector.clearAxisGuides()
  217. this.clearPoints()
  218. application.selection.clear()
  219. }
  220. makeProfile() {
  221. if (this.object instanceof Cord && this.vertices.length >= 3) {
  222. let parent = this.object.parent
  223. let index = parent.children.indexOf(this.object)
  224. let object = new Profile()
  225. object.matrix.copy(this.object.matrix)
  226. this.flattenVertices(object)
  227. parent.children[index] = object
  228. object.parent = parent
  229. this.mode = 1
  230. this.index = -1
  231. this.object = object
  232. this.updateObject()
  233. const application = this.application
  234. application.notifyObjectsChanged(parent, this, 'structureChanged')
  235. application.selection.set(object)
  236. }
  237. }
  238. flattenVertices(profile) {
  239. const vertices = this.vertices
  240. let zAxis = GeometryUtils.calculateNormal(vertices)
  241. let yAxis = GeometryUtils.orthogonalVector(zAxis)
  242. let xAxis = new THREE.Vector3()
  243. xAxis.crossVectors(yAxis, zAxis)
  244. let position = GeometryUtils.centroid(vertices)
  245. let matrix = new THREE.Matrix4()
  246. matrix.makeBasis(xAxis, yAxis, zAxis)
  247. matrix.setPosition(position)
  248. let inverseMatrix = new THREE.Matrix4()
  249. inverseMatrix.copy(matrix).invert()
  250. for (let vertex of vertices) {
  251. vertex.applyMatrix4(inverseMatrix)
  252. vertex.z = 0
  253. }
  254. matrix.premultiply(profile.matrix)
  255. matrix.decompose(profile.position, profile.rotation, profile.scale)
  256. profile.updateMatrix()
  257. }
  258. updateObject() {
  259. const application = this.application
  260. const overlays = application.overlays
  261. if ((this.vertices.length < 2 && this.object instanceof Cord) || (this.vertices.length < 3 && this.object instanceof Profile)) {
  262. application.removeObject(this.object)
  263. this.mode = 0
  264. this.object = null
  265. this.index = -1
  266. this.vertices = []
  267. } else {
  268. let geometry
  269. if (this.object) {
  270. if (this.object instanceof Cord) {
  271. geometry = new CordGeometry(this.vertices)
  272. } else {
  273. let shape = new THREE.Shape()
  274. for (let i = 0; i < this.vertices.length; i++) {
  275. let vertex = this.vertices[i]
  276. vertex.z = 0
  277. if (i === 0) {
  278. shape.moveTo(vertex.x, vertex.y)
  279. } else {
  280. shape.lineTo(vertex.x, vertex.y)
  281. }
  282. }
  283. shape.closePath()
  284. geometry = new ProfileGeometry(shape)
  285. }
  286. this.object.updateGeometry(geometry)
  287. application.notifyObjectsChanged(this.object)
  288. } else if (this.vertices.length > 1) {
  289. // create Cord by default
  290. geometry = new CordGeometry(this.vertices)
  291. this.object = new Cord(geometry, this.lineMaterial)
  292. application.addObject(this.object, null, false, true)
  293. }
  294. }
  295. }
  296. updatePoints() {
  297. const application = this.application
  298. const overlays = application.overlays
  299. this.clearPoints()
  300. if (this.vertices.length > 0) {
  301. let geometry = new THREE.BufferGeometry()
  302. geometry.setFromPoints(this.vertices)
  303. let material = this.mode === 0 ? this.addPointsMaterial : this.editPointsMaterial
  304. this.points = new THREE.Points(geometry, material)
  305. const points = this.points
  306. points.raycast = function() {}
  307. if (this.object) {
  308. this.object.matrixWorld.decompose(points.position, points.rotation, points.scale)
  309. points.updateMatrix()
  310. }
  311. overlays.add(points)
  312. application.repaint()
  313. }
  314. }
  315. updateAxis(object) {
  316. const pointSelector = this.application.pointSelector
  317. if (this.index === -1 || this.vertices.length === 0) {
  318. pointSelector.clearAxisGuides()
  319. } else {
  320. let positionWorld = this.vertices[this.index].clone()
  321. if (this.object) {
  322. // positionWorld is local, convert to world
  323. positionWorld.applyMatrix4(this.object.matrixWorld)
  324. }
  325. let axisMatrixWorld = object ? object.matrixWorld.clone() : new THREE.Matrix4()
  326. axisMatrixWorld.setPosition(positionWorld)
  327. pointSelector.setAxisGuides(axisMatrixWorld, true)
  328. }
  329. }
  330. updatePanel() {
  331. // if (this.mode === 0) {
  332. // if (this.vertices.length === 0) {
  333. // I18N.set(this.helpElem, 'innerHTML', 'tool.draw.first_vertex')
  334. // } else {
  335. // I18N.set(this.helpElem, 'innerHTML', 'tool.draw.add_vertex')
  336. // }
  337. // } else if (this.mode === 1) {
  338. // if (this.index === -1) {
  339. // I18N.set(this.helpElem, 'innerHTML', 'tool.draw.select_vertex')
  340. // } else {
  341. // I18N.set(this.helpElem, 'innerHTML', 'tool.draw.vertex_destination')
  342. // }
  343. // }
  344. // this.application.i18n.update(this.helpElem)
  345. if (this.position1World && this.position2World) {
  346. let offset = this.position1World.distanceTo(this.position2World)
  347. this.offsetInputElem.value = offset
  348. this.offsetInputElem.parentElement.style.display = ''
  349. } else {
  350. this.offsetInputElem.parentElement.style.display = 'none'
  351. }
  352. if (this.object instanceof Cord && this.vertices.length >= 3) {
  353. this.profileButton.style.display = ''
  354. } else {
  355. this.profileButton.style.display = 'none'
  356. }
  357. }
  358. applyOffset() {
  359. let offset = parseFloat(this.offsetInputElem.value)
  360. let positionWorld = new THREE.Vector3()
  361. positionWorld.subVectors(this.position2World, this.position1World)
  362. positionWorld.normalize()
  363. positionWorld.multiplyScalar(offset)
  364. positionWorld.add(this.position1World)
  365. this.position2World = positionWorld
  366. let position = new THREE.Vector3()
  367. this.transformVertex(positionWorld, position)
  368. this.vertices[this.index] = position
  369. this.updateObject()
  370. this.updatePoints()
  371. this.updateAxis(this.object)
  372. }
  373. clearPoints() {
  374. if (this.points !== null) {
  375. this.application.removeObject(this.points)
  376. this.points = null
  377. }
  378. }
  379. findVertex(position) {
  380. for (let i = 0; i < this.vertices.length; i++) {
  381. if (this.vertices[i].distanceToSquared(position) < 0.0001) return i
  382. }
  383. return -1
  384. }
  385. findVertexOnEdge(position) {
  386. if (this.object instanceof Profile) {
  387. for (let i = 0; i < this.vertices.length; i++) {
  388. let p1 = this.vertices[i]
  389. let p2 = this.vertices[(i + 1) % this.vertices.length]
  390. if (GeometryUtils.isPointOnSegment(position, p1, p2)) return i + 1
  391. }
  392. } else {
  393. for (let i = 0; i < this.vertices.length - 1; i++) {
  394. let p1 = this.vertices[i]
  395. let p2 = this.vertices[i + 1]
  396. if (GeometryUtils.isPointOnSegment(position, p1, p2)) return i + 1
  397. }
  398. }
  399. return -1
  400. }
  401. transformVertex(pointWorld, point) {
  402. if (this.object) {
  403. // express pointWorld in object CS
  404. this.inverseMatrixWorld.copy(this.object.matrixWorld).invert()
  405. point.copy(pointWorld)
  406. point.applyMatrix4(this.inverseMatrixWorld)
  407. if (this.object instanceof Profile) {
  408. point.z = 0
  409. }
  410. } else {
  411. point.copy(pointWorld)
  412. }
  413. }
  414. }
  415. export { DrawTool }