@@ -9,8 +9,15 @@ import {
99 TOUCH ,
1010 Vector2 ,
1111 Vector3 ,
12+ Ray ,
13+ Plane ,
14+ MathUtils ,
1215} from 'three'
1316
17+ const _ray = /* @__PURE__ */ new Ray ( )
18+ const _plane = /* @__PURE__ */ new Plane ( )
19+ const TILT_LIMIT = Math . cos ( 70 * MathUtils . DEG2RAD )
20+
1421// This set of controls performs orbiting, dollying (zooming), and panning.
1522// Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
1623//
@@ -57,6 +64,7 @@ class OrbitControls extends EventDispatcher {
5764 panSpeed = 1.0
5865 screenSpacePanning = true // if false, pan orthogonal to world-space direction camera.up
5966 keyPanSpeed = 7.0 // pixels moved per arrow key push
67+ zoomToCursor = false
6068 // Set to true to automatically rotate around the target
6169 // If auto-rotate is enabled, you must call controls.update() in your animation loop
6270 autoRotate = false
@@ -255,10 +263,6 @@ class OrbitControls extends EventDispatcher {
255263 // restrict phi to be between desired limits
256264 spherical . phi = Math . max ( scope . minPolarAngle , Math . min ( scope . maxPolarAngle , spherical . phi ) )
257265 spherical . makeSafe ( )
258- spherical . radius *= scale
259-
260- // restrict radius to be between desired limits
261- spherical . radius = Math . max ( scope . minDistance , Math . min ( scope . maxDistance , spherical . radius ) )
262266
263267 // move target to panned location
264268
@@ -268,6 +272,17 @@ class OrbitControls extends EventDispatcher {
268272 scope . target . add ( panOffset )
269273 }
270274
275+ // adjust the camera position based on zoom only if we're not zooming to the cursor or if it's an ortho camera
276+ // we adjust zoom later in these cases
277+ if (
278+ ( scope . zoomToCursor && performCursorZoom ) ||
279+ ( scope . object as THREE . OrthographicCamera ) . isOrthographicCamera
280+ ) {
281+ spherical . radius = clampDistance ( spherical . radius )
282+ } else {
283+ spherical . radius = clampDistance ( spherical . radius * scale )
284+ }
285+
271286 offset . setFromSpherical ( spherical )
272287
273288 // rotate offset back to "camera-up-vector-is-up" space
@@ -288,7 +303,72 @@ class OrbitControls extends EventDispatcher {
288303 panOffset . set ( 0 , 0 , 0 )
289304 }
290305
306+ // adjust camera position
307+ let zoomChanged = false
308+ if ( scope . zoomToCursor && performCursorZoom ) {
309+ let newRadius = null
310+ if ( scope . object instanceof PerspectiveCamera && scope . object . isPerspectiveCamera ) {
311+ // move the camera down the pointer ray
312+ // this method avoids floating point error
313+ const prevRadius = offset . length ( )
314+ newRadius = clampDistance ( prevRadius * scale )
315+
316+ const radiusDelta = prevRadius - newRadius
317+ scope . object . position . addScaledVector ( dollyDirection , radiusDelta )
318+ scope . object . updateMatrixWorld ( )
319+ } else if ( ( scope . object as THREE . OrthographicCamera ) . isOrthographicCamera ) {
320+ // adjust the ortho camera position based on zoom changes
321+ const mouseBefore = new Vector3 ( mouse . x , mouse . y , 0 )
322+ mouseBefore . unproject ( scope . object )
323+
324+ scope . object . zoom = Math . max ( scope . minZoom , Math . min ( scope . maxZoom , scope . object . zoom / scale ) )
325+ scope . object . updateProjectionMatrix ( )
326+ zoomChanged = true
327+
328+ const mouseAfter = new Vector3 ( mouse . x , mouse . y , 0 )
329+ mouseAfter . unproject ( scope . object )
330+
331+ scope . object . position . sub ( mouseAfter ) . add ( mouseBefore )
332+ scope . object . updateMatrixWorld ( )
333+
334+ newRadius = offset . length ( )
335+ } else {
336+ console . warn ( 'WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled.' )
337+ scope . zoomToCursor = false
338+ }
339+
340+ // handle the placement of the target
341+ if ( newRadius !== null ) {
342+ if ( scope . screenSpacePanning ) {
343+ // position the orbit target in front of the new camera position
344+ scope . target
345+ . set ( 0 , 0 , - 1 )
346+ . transformDirection ( scope . object . matrix )
347+ . multiplyScalar ( newRadius )
348+ . add ( scope . object . position )
349+ } else {
350+ // get the ray and translation plane to compute target
351+ _ray . origin . copy ( scope . object . position )
352+ _ray . direction . set ( 0 , 0 , - 1 ) . transformDirection ( scope . object . matrix )
353+
354+ // if the camera is 20 degrees above the horizon then don't adjust the focus target to avoid
355+ // extremely large values
356+ if ( Math . abs ( scope . object . up . dot ( _ray . direction ) ) < TILT_LIMIT ) {
357+ object . lookAt ( scope . target )
358+ } else {
359+ _plane . setFromNormalAndCoplanarPoint ( scope . object . up , scope . target )
360+ _ray . intersectPlane ( _plane , scope . target )
361+ }
362+ }
363+ }
364+ } else if ( scope . object instanceof OrthographicCamera && scope . object . isOrthographicCamera ) {
365+ scope . object . zoom = Math . max ( scope . minZoom , Math . min ( scope . maxZoom , scope . object . zoom / scale ) )
366+ scope . object . updateProjectionMatrix ( )
367+ zoomChanged = true
368+ }
369+
291370 scale = 1
371+ performCursorZoom = false
292372
293373 // update condition is:
294374 // min(camera displacement, camera rotation in radians)^2 > EPS
@@ -374,7 +454,6 @@ class OrbitControls extends EventDispatcher {
374454
375455 let scale = 1
376456 const panOffset = new Vector3 ( )
377- let zoomChanged = false
378457
379458 const rotateStart = new Vector2 ( )
380459 const rotateEnd = new Vector2 ( )
@@ -388,6 +467,10 @@ class OrbitControls extends EventDispatcher {
388467 const dollyEnd = new Vector2 ( )
389468 const dollyDelta = new Vector2 ( )
390469
470+ const dollyDirection = new Vector3 ( )
471+ const mouse = new Vector2 ( )
472+ let performCursorZoom = false
473+
391474 const pointers : PointerEvent [ ] = [ ]
392475 const pointerPositions : { [ key : string ] : Vector2 } = { }
393476
@@ -481,31 +564,52 @@ class OrbitControls extends EventDispatcher {
481564 } ) ( )
482565
483566 function dollyOut ( dollyScale : number ) {
484- if ( scope . object instanceof PerspectiveCamera && scope . object . isPerspectiveCamera ) {
567+ if (
568+ ( scope . object instanceof PerspectiveCamera && scope . object . isPerspectiveCamera ) ||
569+ ( scope . object instanceof OrthographicCamera && scope . object . isOrthographicCamera )
570+ ) {
485571 scale /= dollyScale
486- } else if ( scope . object instanceof OrthographicCamera && scope . object . isOrthographicCamera ) {
487- scope . object . zoom = Math . max ( scope . minZoom , Math . min ( scope . maxZoom , scope . object . zoom * dollyScale ) )
488- scope . object . updateProjectionMatrix ( )
489- zoomChanged = true
490572 } else {
491573 console . warn ( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' )
492574 scope . enableZoom = false
493575 }
494576 }
495577
496578 function dollyIn ( dollyScale : number ) {
497- if ( scope . object instanceof PerspectiveCamera && scope . object . isPerspectiveCamera ) {
579+ if (
580+ ( scope . object instanceof PerspectiveCamera && scope . object . isPerspectiveCamera ) ||
581+ ( scope . object instanceof OrthographicCamera && scope . object . isOrthographicCamera )
582+ ) {
498583 scale *= dollyScale
499- } else if ( scope . object instanceof OrthographicCamera && scope . object . isOrthographicCamera ) {
500- scope . object . zoom = Math . max ( scope . minZoom , Math . min ( scope . maxZoom , scope . object . zoom / dollyScale ) )
501- scope . object . updateProjectionMatrix ( )
502- zoomChanged = true
503584 } else {
504585 console . warn ( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' )
505586 scope . enableZoom = false
506587 }
507588 }
508589
590+ function updateMouseParameters ( event : MouseEvent ) : void {
591+ if ( ! scope . zoomToCursor || ! scope . domElement ) {
592+ return
593+ }
594+
595+ performCursorZoom = true
596+
597+ const rect = scope . domElement . getBoundingClientRect ( )
598+ const x = event . clientX - rect . left
599+ const y = event . clientY - rect . top
600+ const w = rect . width
601+ const h = rect . height
602+
603+ mouse . x = ( x / w ) * 2 - 1
604+ mouse . y = - ( y / h ) * 2 + 1
605+
606+ dollyDirection . set ( mouse . x , mouse . y , 1 ) . unproject ( scope . object ) . sub ( scope . object . position ) . normalize ( )
607+ }
608+
609+ function clampDistance ( dist : number ) : number {
610+ return Math . max ( scope . minDistance , Math . min ( scope . maxDistance , dist ) )
611+ }
612+
509613 //
510614 // event callbacks - update the object state
511615 //
@@ -515,6 +619,7 @@ class OrbitControls extends EventDispatcher {
515619 }
516620
517621 function handleMouseDownDolly ( event : MouseEvent ) {
622+ updateMouseParameters ( event )
518623 dollyStart . set ( event . clientX , event . clientY )
519624 }
520625
@@ -559,6 +664,8 @@ class OrbitControls extends EventDispatcher {
559664 }
560665
561666 function handleMouseWheel ( event : WheelEvent ) {
667+ updateMouseParameters ( event )
668+
562669 if ( event . deltaY < 0 ) {
563670 dollyIn ( getZoomScale ( ) )
564671 } else if ( event . deltaY > 0 ) {
0 commit comments