OrbitTool.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. /*
  2. * OrbitTool.js
  3. *
  4. * @author realor
  5. */
  6. import { Tool } from './Tool.js'
  7. import { I18N } from '../i18n/I18N.js'
  8. import { Solid } from '../core/Solid.js'
  9. import { GestureHandler } from '../ui/GestureHandler.js'
  10. import * as THREE from '../lib/three.module.js'
  11. class OrbitTool extends Tool {
  12. constructor(application, options) {
  13. super(application)
  14. this.name = 'orbit'
  15. this.label = 'tool.orbit.label'
  16. this.help = 'tool.orbit.help'
  17. this.className = 'orbit'
  18. this.setOptions(options)
  19. this.theta = Math.PI
  20. this.phi = Math.PI / 2.1
  21. this.userZoomSpeed = 1.0
  22. this.userRotateSpeed = 1.0
  23. this.minPolarAngle = 0 // radians
  24. this.maxPolarAngle = Math.PI // radians
  25. this.rotateButton = 0
  26. this.zoomButton = 1
  27. this.panButton = 2
  28. // Perspective camera
  29. this.radius = 10
  30. this.minRadius = 0.1
  31. this.maxRadius = Infinity
  32. // Orthographic camera
  33. this.orthoZoom = 1
  34. this.minOrthoZoom = 0.01
  35. this.maxOrthoZoom = Infinity
  36. this.updateCamera = true
  37. // internals
  38. this.vector = new THREE.Vector3()
  39. this.center = new THREE.Vector3() // camera parent CS
  40. this.EPS = 0.00001
  41. this.PIXELS_PER_ROUND = 1800
  42. this.rotateStart = new THREE.Vector2()
  43. this.rotateEnd = new THREE.Vector2()
  44. this.rotateVector = new THREE.Vector2()
  45. this.zoomStart = new THREE.Vector2()
  46. this.zoomEnd = new THREE.Vector2()
  47. this.zoomVector = new THREE.Vector2()
  48. this.panStart = new THREE.Vector2()
  49. this.panEnd = new THREE.Vector2()
  50. this.panVector = new THREE.Vector2()
  51. this.phiDelta = 0
  52. this.thetaDelta = 0
  53. this.zoomDelta = 0
  54. this.xDelta = 0
  55. this.yDelta = 0
  56. this._onWheel = this.onWheel.bind(this)
  57. this._animate = this.animate.bind(this)
  58. this._onScene = this.onScene.bind(this)
  59. const geometry = new THREE.SphereGeometry(1, 8, 8)
  60. const material = new THREE.MeshLambertMaterial({ color: 0xff0000 })
  61. const sphere = new THREE.Mesh(geometry, material)
  62. sphere.name = THREE.Object3D.HIDDEN_PREFIX + 'orbit_center'
  63. this.sphere = sphere
  64. this.createPanel()
  65. this.gestureHandler = new GestureHandler(this)
  66. }
  67. createPanel() {
  68. this.panel = this.application.createPanel(this.label, 'left')
  69. this.panel.preferredHeight = 120
  70. I18N.set(this.panel.bodyElem, 'innerHTML', this.help)
  71. }
  72. activate() {
  73. //this.panel.visible = true
  74. this.resetParameters()
  75. const application = this.application
  76. const container = application.container
  77. container.addEventListener('wheel', this._onWheel, false)
  78. application.addEventListener('animation', this._animate)
  79. application.addEventListener('scene', this._onScene)
  80. this.gestureHandler.enable()
  81. }
  82. deactivate() {
  83. //this.panel.visible = false
  84. const application = this.application
  85. const container = application.container
  86. container.removeEventListener('wheel', this._onWheel, false)
  87. application.removeEventListener('animation', this._animate)
  88. application.removeEventListener('scene', this._onScene)
  89. this.gestureHandler.disable()
  90. }
  91. animate(event) {
  92. const application = this.application
  93. if (!this.updateCamera) return
  94. const camera = application.camera
  95. this.theta += this.thetaDelta
  96. this.phi += this.phiDelta
  97. if (camera instanceof THREE.OrthographicCamera) {
  98. let factor = 1 + this.zoomDelta
  99. if (factor < 0.1) factor = 0.1
  100. else if (factor > 1.5) factor = 1.5
  101. this.orthoZoom *= factor
  102. if (this.orthoZoom < this.minOrthoZoom) {
  103. this.orthoZoom = this.minOrthoZoom
  104. } else if (this.orthoZoom > this.maxOrthoZoom) {
  105. this.orthoZoom = this.maxOrthoZoom
  106. }
  107. camera.zoom = this.orthoZoom
  108. camera.updateProjectionMatrix()
  109. } else if (camera instanceof THREE.PerspectiveCamera) {
  110. this.radius -= this.radius * this.zoomDelta
  111. // restrict radius
  112. if (this.radius < this.minRadius) {
  113. this.radius = this.minRadius
  114. } else if (this.radius > this.maxRadius) {
  115. this.radius = this.maxRadius
  116. }
  117. }
  118. // restrict phi to be between desired limits
  119. this.phi = Math.max(this.minPolarAngle, Math.min(this.maxPolarAngle, this.phi))
  120. // restrict phi to be between EPS and PI-EPS
  121. this.phi = Math.max(this.EPS, Math.min(Math.PI - this.EPS, this.phi))
  122. if (this.xDelta !== 0 || this.yDelta !== 0) {
  123. const matrix = camera.matrix
  124. this.center.set(this.xDelta, this.yDelta, -this.radius)
  125. this.center.applyMatrix4(matrix)
  126. }
  127. const vector = this.vector
  128. const center = this.center
  129. vector.x = Math.sin(this.phi) * Math.sin(this.theta)
  130. vector.y = Math.sin(this.phi) * Math.cos(this.theta)
  131. vector.z = Math.cos(this.phi)
  132. camera.position.copy(center).addScaledVector(vector, this.radius)
  133. camera.updateMatrix()
  134. camera.lookAt(center)
  135. camera.updateMatrix()
  136. this.thetaDelta = 0
  137. this.phiDelta = 0
  138. this.zoomDelta = 0
  139. this.xDelta = 0
  140. this.yDelta = 0
  141. application.notifyObjectsChanged(camera, this)
  142. this.updateCamera = false
  143. }
  144. onScene(event) {
  145. const application = this.application
  146. if (event.source !== this) {
  147. const camera = application.camera
  148. if (event.type === 'nodeChanged' && event.objects.includes(camera)) {
  149. this.resetParameters()
  150. } else if (event.type === 'cameraActivated') {
  151. this.resetParameters()
  152. }
  153. }
  154. }
  155. resetParameters() {
  156. const camera = this.application.camera
  157. camera.updateMatrix()
  158. const matrix = camera.matrix
  159. const me = matrix.elements
  160. const vz = new THREE.Vector3()
  161. vz.x = me[8]
  162. vz.y = me[9]
  163. vz.z = me[10]
  164. vz.normalize()
  165. this.phi = Math.acos(vz.z)
  166. if (Math.abs(vz.x) > 0.01 && Math.abs(vz.y) > 0.01) {
  167. this.theta = Math.atan2(vz.x, vz.y)
  168. } else {
  169. var vx = new THREE.Vector3()
  170. vx.x = me[0]
  171. vx.y = me[1]
  172. vx.z = me[2]
  173. vx.normalize()
  174. this.theta = Math.atan2(vx.y, -vx.x)
  175. }
  176. if (camera instanceof THREE.PerspectiveCamera) {
  177. this.radius = 10
  178. } else if (camera instanceof THREE.OrthographicCamera) {
  179. this.orthoZoom = camera.zoom
  180. }
  181. this.center.x = camera.position.x - this.radius * vz.x
  182. this.center.y = camera.position.y - this.radius * vz.y
  183. this.center.z = camera.position.z - this.radius * vz.z
  184. this.updateCamera = true
  185. }
  186. rotateLeft(angle) {
  187. this.thetaDelta += angle
  188. this.updateCamera = true
  189. }
  190. rotateRight(angle) {
  191. this.thetaDelta -= angle
  192. this.updateCamera = true
  193. }
  194. rotateUp(angle) {
  195. this.phiDelta -= angle
  196. this.updateCamera = true
  197. }
  198. rotateDownn(angle) {
  199. this.phiDelta += angle
  200. this.updateCamera = true
  201. }
  202. panLeft(delta) {
  203. this.xDelta -= delta
  204. this.updateCamera = true
  205. }
  206. panRight(delta) {
  207. this.xDelta += delta
  208. this.updateCamera = true
  209. }
  210. panUp(delta) {
  211. this.yDelta -= delta
  212. this.updateCamera = true
  213. }
  214. panDown(delta) {
  215. this.yDelta += delta
  216. this.updateCamera = true
  217. }
  218. zoomIn(dist) {
  219. this.zoomDelta -= dist
  220. this.updateCamera = true
  221. }
  222. zoomOut(dist) {
  223. this.zoomDelta += dist
  224. this.updateCamera = true
  225. }
  226. setView(theta, phi) {
  227. this.theta = theta
  228. this.phi = phi
  229. this.updateCamera = true
  230. }
  231. updateCenter() {
  232. const application = this.application
  233. const container = application.container
  234. const camera = application.camera
  235. const scene = application.scene
  236. const centerPosition = new THREE.Vector2()
  237. centerPosition.x = container.clientWidth / 2
  238. centerPosition.y = container.clientHeight / 2
  239. const intersect = this.intersect(centerPosition, scene, true)
  240. if (intersect) {
  241. this.center.copy(intersect.point)
  242. } else {
  243. const centerDistance = this.findCenterDistance()
  244. const viewVector = new THREE.Vector3()
  245. viewVector.setFromMatrixColumn(camera.matrix, 2)
  246. this.center.copy(camera.position)
  247. this.center.addScaledVector(viewVector, -centerDistance)
  248. }
  249. // convert center in WCS to camera parent CS
  250. camera.parent.worldToLocal(this.center)
  251. this.radius = this.center.distanceTo(camera.position)
  252. }
  253. onStartGesture() {
  254. this.updateCenter()
  255. // add & update sphere
  256. const application = this.application
  257. application.overlays.add(this.sphere)
  258. const spherePosition = this.center.clone()
  259. const camera = application.camera
  260. const parentMatrix = camera.parent.matrixWorld
  261. spherePosition.applyMatrix4(parentMatrix)
  262. this.sphere.position.copy(spherePosition)
  263. let scale
  264. if (camera instanceof THREE.PerspectiveCamera) {
  265. scale = this.radius * 0.01
  266. } else {
  267. scale = (0.005 * (camera.right - camera.left)) / camera.zoom
  268. }
  269. this.sphere.scale.set(scale, scale, scale)
  270. this.sphere.updateMatrix()
  271. application.notifyObjectsChanged(this.sphere, this)
  272. }
  273. onDrag(position, direction, pointerCount, button) {
  274. if (button === this.panButton || pointerCount === 2) {
  275. const camera = this.application.camera
  276. const container = this.application.container
  277. const vectorcc = new THREE.Vector3()
  278. vectorcc.x = direction.x / container.clientWidth
  279. vectorcc.y = direction.y / container.clientHeight
  280. vectorcc.z = 0
  281. const matrix = new THREE.Matrix4()
  282. matrix.copy(camera.projectionMatrix).invert()
  283. vectorcc.applyMatrix4(matrix)
  284. let lambda
  285. if (camera instanceof THREE.PerspectiveCamera) {
  286. lambda = this.radius / camera.near
  287. } else {
  288. lambda = 2
  289. }
  290. vectorcc.x *= lambda
  291. vectorcc.y *= lambda
  292. this.panLeft(vectorcc.x)
  293. this.panDown(vectorcc.y)
  294. } else if (button === this.rotateButton) {
  295. this.rotateLeft(((2 * Math.PI * direction.x) / this.PIXELS_PER_ROUND) * this.userRotateSpeed)
  296. this.rotateUp(((2 * Math.PI * direction.y) / this.PIXELS_PER_ROUND) * this.userRotateSpeed)
  297. } else if (button === this.zoomButton) {
  298. if (direction.y !== 0) {
  299. let absDir = Math.abs(direction.y)
  300. this.zoomIn(0.002 * Math.sign(direction.y) * Math.pow(absDir, 1.5))
  301. }
  302. }
  303. }
  304. onZoom(position, delta) {
  305. this.zoomIn(0.005 * delta)
  306. }
  307. onEndGesture() {
  308. // remove sphere
  309. this.application.overlays.remove(this.sphere)
  310. this.application.notifyObjectsChanged(this.sphere, this)
  311. }
  312. onWheel(event) {
  313. if (!this.isCanvasEvent(event)) return
  314. event.preventDefault()
  315. var delta = 0
  316. if (event.wheelDelta) {
  317. // WebKit / Opera / Explorer 9
  318. delta = -event.wheelDelta * 0.0005
  319. } else if (event.detail) {
  320. // Firefox
  321. delta = -0.02 * event.detail
  322. }
  323. if (delta !== 0) {
  324. this.zoomIn(delta)
  325. }
  326. }
  327. findCenterDistance() {
  328. const application = this.application
  329. const camera = application.camera
  330. const objectPosition = new THREE.Vector3()
  331. const cameraPosition = new THREE.Vector3()
  332. const viewVector = new THREE.Vector3()
  333. const cameraObjectVector = new THREE.Vector3()
  334. camera.getWorldPosition(cameraPosition)
  335. camera.getWorldDirection(viewVector)
  336. let minSideDistance = Infinity
  337. let minCenterDistance = 0
  338. const traverse = object => {
  339. if (object instanceof Solid || object instanceof THREE.Mesh || object instanceof THREE.Line) {
  340. object.getWorldPosition(objectPosition)
  341. cameraObjectVector.subVectors(objectPosition, cameraPosition)
  342. const objectDistance = cameraObjectVector.length()
  343. const centerDistance = cameraObjectVector.dot(viewVector)
  344. if (centerDistance > 0) {
  345. const sideDistance = Math.sqrt(objectDistance * objectDistance - centerDistance * centerDistance)
  346. if (sideDistance < minSideDistance) {
  347. minSideDistance = sideDistance
  348. minCenterDistance = centerDistance
  349. }
  350. }
  351. } else {
  352. for (let child of object.children) {
  353. traverse(child)
  354. }
  355. }
  356. }
  357. traverse(application.baseObject)
  358. return minCenterDistance === Infinity ? 10 : minCenterDistance
  359. }
  360. }
  361. export { OrbitTool }