ScaleTool.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. /*
  2. * ScaleTool.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 ScaleTool extends TransformationTool {
  13. static CHANGED_PROPERTIES = ['position', 'rotation', 'scale']
  14. constructor(application, options) {
  15. super(application)
  16. this.name = 'scale'
  17. this.label = 'tool.scale.label'
  18. this.className = 'scale'
  19. this.minScale = 0.001
  20. this.snapFactor = 0.1
  21. this.snapPercent = 0.3
  22. this.setOptions(options)
  23. // internals
  24. this.firstPointWorld = new THREE.Vector3()
  25. this.anchorPointWorld = new THREE.Vector3()
  26. this.scale = 1
  27. this.keepProportions = true
  28. this.baseMatrixWorld = new THREE.Matrix4()
  29. this.baseMatrixWorldInverse = new THREE.Matrix4()
  30. this._vector1 = new THREE.Vector3()
  31. this._vector2 = new THREE.Vector3()
  32. this.createPanel()
  33. }
  34. createPanel() {
  35. this.panel = this.application.createPanel(this.label, 'left', 'panel_scale')
  36. this.panel.preferredHeight = 180
  37. this.helpElem = document.createElement('div')
  38. this.panel.bodyElem.appendChild(this.helpElem)
  39. this.scaleInputElem = Controls.addNumberField(this.panel.bodyElem, 'scale_factor', 'label.scale_factor', 1)
  40. this.scaleInputElem.step = this.snapFactor
  41. this.scaleInputElem.addEventListener(
  42. 'change',
  43. event => {
  44. this.scale = this.scaleInputElem.value
  45. this.lengthInputElem.value = this.scaleToLength(this.scale)
  46. this.scaleObjects()
  47. },
  48. false
  49. )
  50. this.lengthInputElem = Controls.addNumberField(this.panel.bodyElem, 'scale_length', 'label.length', 0)
  51. this.lengthInputElem.step = this.snapFactor
  52. this.lengthInputElem.addEventListener(
  53. 'change',
  54. event => {
  55. this.scale = this.lengthToScale(this.lengthInputElem.value)
  56. this.scaleInputElem.value = this.scale
  57. this.scaleObjects()
  58. },
  59. false
  60. )
  61. this.keepPropCheckbox = Controls.addCheckBoxField(this.panel.bodyElem, 'scale_keep_prop', 'label.scale_keep_proportions', this.keepProportions)
  62. this.keepPropCheckbox.addEventListener('change', event => {
  63. this.keepProportions = this.keepPropCheckbox.checked
  64. this.scaleObjects()
  65. })
  66. this.buttonsPanel = document.createElement('div')
  67. this.panel.bodyElem.appendChild(this.buttonsPanel)
  68. this.acceptButton = Controls.addButton(this.buttonsPanel, 'cancel_section', 'button.accept', event => {
  69. this.scale = this.scaleInputElem.value
  70. this.scaleObjects()
  71. this.setStage(0)
  72. })
  73. this.cancelButton = Controls.addButton(this.buttonsPanel, 'cancel_section', 'button.cancel', event => {
  74. this.scale = 1
  75. this.scaleObjects()
  76. this.setStage(0)
  77. })
  78. }
  79. onPointerMove(event) {
  80. if (this.stage === 2) {
  81. const application = this.application
  82. const pointSelector = application.pointSelector
  83. if (!pointSelector.isPointSelectionEvent(event)) return
  84. event.preventDefault()
  85. const snap = pointSelector.snap
  86. if (snap) {
  87. let anchorPoint = this._vector1
  88. anchorPoint.copy(this.anchorPointWorld)
  89. anchorPoint.applyMatrix4(this.baseMatrixWorldInverse)
  90. let destinationPoint = this._vector2
  91. destinationPoint.copy(snap.positionWorld)
  92. destinationPoint.applyMatrix4(this.baseMatrixWorldInverse)
  93. let z1 = anchorPoint.z
  94. let z2 = destinationPoint.z
  95. let scale = z2 / z1
  96. if (Math.abs(scale) > this.minScale) {
  97. let divisions = scale / this.snapFactor
  98. let intDivisions = Math.round(divisions)
  99. let decimals = divisions - intDivisions
  100. if (decimals >= -this.snapPercent && decimals <= this.snapPercent) {
  101. const k = 1000000
  102. scale = Math.round(k * intDivisions * this.snapFactor) / k
  103. }
  104. this.scale = scale
  105. this.lengthInputElem.value = this.scaleToLength(scale)
  106. this.scaleObjects()
  107. this.scaleInputElem.value = this.scale
  108. }
  109. }
  110. }
  111. }
  112. onPointerUp(event) {
  113. const application = this.application
  114. const pointSelector = application.pointSelector
  115. if (!pointSelector.isPointSelectionEvent(event)) return
  116. const snap = pointSelector.snap
  117. if (this.stage === 0) {
  118. // origin point
  119. if (snap) {
  120. this.firstPointWorld.copy(snap.positionWorld)
  121. if (snap.object) {
  122. let axisMatrixWorld = this.axisMatrixWorld
  123. if (snap.object) {
  124. axisMatrixWorld.copy(snap.object.matrixWorld)
  125. } else {
  126. axisMatrixWorld.identity()
  127. }
  128. axisMatrixWorld.setPosition(this.firstPointWorld)
  129. if (application.selection.isEmpty()) {
  130. application.selection.set(snap.object)
  131. }
  132. }
  133. this.setStage(1)
  134. } else {
  135. this.setStage(0)
  136. }
  137. } else if (this.stage === 1 || this.stage === 3) {
  138. // anchor point
  139. this.objectMatrices.clear()
  140. if (snap) {
  141. if (!snap.positionWorld.equals(this.firstPointWorld)) {
  142. this.anchorPointWorld.copy(snap.positionWorld)
  143. this.createBaseMatrix()
  144. this.axisMatrixWorld.copy(this.baseMatrixWorld)
  145. this.scaleInputElem.value = 0
  146. this.setStage(2)
  147. }
  148. } else {
  149. this.setStage(0)
  150. }
  151. } else if (this.stage === 2) {
  152. // destination point
  153. this.setStage(3)
  154. }
  155. }
  156. setStage(stage) {
  157. this.stage = stage
  158. const application = this.application
  159. switch (stage) {
  160. case 0: // set origin
  161. this.scale = 1
  162. application.pointSelector.clearAxisGuides()
  163. application.pointSelector.excludeSelection = false
  164. application.pointSelector.auxiliaryPoints = []
  165. application.pointSelector.activate()
  166. this.scaleInputElem.parentElement.style.display = 'none'
  167. this.lengthInputElem.parentElement.style.display = 'none'
  168. this.buttonsPanel.style.display = 'none'
  169. I18N.set(this.helpElem, 'innerHTML', 'tool.scale.select_first_point')
  170. application.i18n.update(this.helpElem)
  171. application.repaint()
  172. break
  173. case 1: // set anchor point
  174. application.pointSelector.setAxisGuides(this.axisMatrixWorld, true)
  175. application.pointSelector.excludeSelection = false
  176. application.pointSelector.auxiliaryPoints = []
  177. application.pointSelector.activate()
  178. this.scaleInputElem.parentElement.style.display = 'none'
  179. this.lengthInputElem.parentElement.style.display = 'none'
  180. this.buttonsPanel.style.display = 'none'
  181. I18N.set(this.helpElem, 'innerHTML', 'tool.scale.select_anchor_point')
  182. application.i18n.update(this.helpElem)
  183. break
  184. case 2: // set destination point
  185. application.pointSelector.setAxisGuides(this.axisMatrixWorld, true)
  186. application.pointSelector.excludeSelection = true
  187. application.pointSelector.auxiliaryPoints = []
  188. application.pointSelector.activate()
  189. this.scaleInputElem.parentElement.style.display = ''
  190. this.lengthInputElem.parentElement.style.display = ''
  191. this.scaleInputElem.disabled = true
  192. this.lengthInputElem.disabled = true
  193. this.scaleInputElem.value = 1
  194. this.lengthInputElem.value = this.scaleToLength(1)
  195. this.buttonsPanel.style.display = 'none'
  196. I18N.set(this.helpElem, 'innerHTML', 'tool.scale.select_destination_point')
  197. application.i18n.update(this.helpElem)
  198. break
  199. case 3: // edit scale factor/distance
  200. application.pointSelector.setAxisGuides(this.axisMatrixWorld, true)
  201. application.pointSelector.excludeSelection = false
  202. application.pointSelector.auxiliaryPoints = []
  203. application.pointSelector.activate()
  204. this.scaleInputElem.parentElement.style.display = ''
  205. this.lengthInputElem.parentElement.style.display = ''
  206. this.scaleInputElem.disabled = false
  207. this.lengthInputElem.disabled = false
  208. this.buttonsPanel.style.display = ''
  209. I18N.set(this.helpElem, 'innerHTML', 'tool.scale.edit_scale')
  210. application.i18n.update(this.helpElem)
  211. break
  212. }
  213. }
  214. createBaseMatrix() {
  215. const scaleAxisWorld = this._vector1
  216. scaleAxisWorld.subVectors(this.anchorPointWorld, this.firstPointWorld)
  217. scaleAxisWorld.normalize()
  218. let yVector = GeometryUtils.orthogonalVector(scaleAxisWorld).normalize()
  219. let xVector = new THREE.Vector3()
  220. xVector.crossVectors(yVector, scaleAxisWorld)
  221. const baseMatrixWorld = this.baseMatrixWorld
  222. baseMatrixWorld.makeBasis(xVector, yVector, scaleAxisWorld)
  223. baseMatrixWorld.setPosition(this.firstPointWorld)
  224. const baseMatrixWorldInverse = this.baseMatrixWorldInverse
  225. baseMatrixWorldInverse.copy(baseMatrixWorld).invert()
  226. }
  227. scaleObjects() {
  228. const application = this.application
  229. const baseMatrixWorld = this.baseMatrixWorld
  230. const baseMatrixWorldInverse = this.baseMatrixWorldInverse
  231. const scaleZ = this.scale
  232. const scaleXY = this.keepProportions ? scaleZ : 1
  233. const scaleMatrixWorld = this._matrix1.makeScale(scaleXY, scaleXY, scaleZ)
  234. scaleMatrixWorld.multiply(baseMatrixWorldInverse)
  235. scaleMatrixWorld.premultiply(baseMatrixWorld)
  236. this.transformObjects(scaleMatrixWorld, ScaleTool.CHANGED_PROPERTIES)
  237. }
  238. resetTool() {
  239. super.resetTool()
  240. this.scale = 1
  241. }
  242. lengthToScale(length) {
  243. let anchorPoint = this._vector1
  244. anchorPoint.copy(this.anchorPointWorld)
  245. anchorPoint.applyMatrix4(this.baseMatrixWorldInverse)
  246. let z1 = anchorPoint.z
  247. return length / z1
  248. }
  249. scaleToLength(scale) {
  250. let anchorPoint = this._vector1
  251. anchorPoint.copy(this.anchorPointWorld)
  252. anchorPoint.applyMatrix4(this.baseMatrixWorldInverse)
  253. let z1 = anchorPoint.z
  254. return scale * z1
  255. }
  256. }
  257. export { ScaleTool }