-
-
Notifications
You must be signed in to change notification settings - Fork 35.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
OrbitControls: Add zoom to cursor #26165
Merged
Merged
Changes from 13 commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
3b8fb5f
Add initial support for zoom to cursor
gkjohnson 76085e2
simplify implementation, remove ortho special case
gkjohnson cda2535
Handle mouse dolly
gkjohnson 2b35825
Support planar movement, add gui option
gkjohnson 66cadf8
Use relative positioning
gkjohnson c61bbc6
Fix ortho camera zooming
gkjohnson 2daffb3
auto disable zoom to cursor
gkjohnson 002782f
Handle incorrect camera case differently
gkjohnson 24c1c4f
Use cached objects
gkjohnson 30a1a65
Fix corner case where camera starts at target position
gkjohnson 41c1860
Use a common clamp function
gkjohnson 1bbd423
Fix copy paste error
gkjohnson 7606f6d
Add a flag for when to perform the zoom to cursor behavior to avoid u…
gkjohnson 3440b8b
Limit target movement at steep angles
gkjohnson a1585b2
Add constant for tilt amount
gkjohnson 0ad5724
Update comment
gkjohnson d681911
Fix relative cursor position
gkjohnson File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,9 @@ import { | |
Spherical, | ||
TOUCH, | ||
Vector2, | ||
Vector3 | ||
Vector3, | ||
Plane, | ||
Ray | ||
} from 'three'; | ||
|
||
// OrbitControls performs orbiting, dollying (zooming), and panning. | ||
|
@@ -18,6 +20,8 @@ import { | |
const _changeEvent = { type: 'change' }; | ||
const _startEvent = { type: 'start' }; | ||
const _endEvent = { type: 'end' }; | ||
const _ray = new Ray(); | ||
const _plane = new Plane(); | ||
|
||
class OrbitControls extends EventDispatcher { | ||
|
||
|
@@ -72,6 +76,7 @@ class OrbitControls extends EventDispatcher { | |
this.panSpeed = 1.0; | ||
this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up | ||
this.keyPanSpeed = 7.0; // pixels moved per arrow key push | ||
this.zoomToCursor = false; | ||
|
||
// Set to true to automatically rotate around the target | ||
// If auto-rotate is enabled, you must call controls.update() in your animation loop | ||
|
@@ -230,11 +235,6 @@ class OrbitControls extends EventDispatcher { | |
spherical.makeSafe(); | ||
|
||
|
||
spherical.radius *= scale; | ||
|
||
// restrict radius to be between desired limits | ||
spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) ); | ||
|
||
// move target to panned location | ||
|
||
if ( scope.enableDamping === true ) { | ||
|
@@ -247,6 +247,19 @@ class OrbitControls extends EventDispatcher { | |
|
||
} | ||
|
||
// adjust the camera position based on zoom only if we're not zooming to the cursor or if it's an ortho camera | ||
// we adjust zoom later in these cases | ||
if ( scope.zoomToCursor && performCursorZoom || scope.object.isOrthographicCamera ) { | ||
|
||
spherical.radius = clampDistance( spherical.radius ); | ||
|
||
} else { | ||
|
||
spherical.radius = clampDistance( spherical.radius * scale ); | ||
|
||
} | ||
|
||
|
||
offset.setFromSpherical( spherical ); | ||
|
||
// rotate offset back to "camera-up-vector-is-up" space | ||
|
@@ -271,7 +284,82 @@ class OrbitControls extends EventDispatcher { | |
|
||
} | ||
|
||
// adjust camera position | ||
let zoomChanged = false; | ||
if ( scope.zoomToCursor && performCursorZoom ) { | ||
|
||
let newRadius = null; | ||
if ( scope.object.isPerspectiveCamera ) { | ||
|
||
// move the camera down the pointer ray | ||
// this method avoids floating point error | ||
const prevRadius = offset.length(); | ||
newRadius = clampDistance( prevRadius * scale ); | ||
|
||
const radiusDelta = prevRadius - newRadius; | ||
scope.object.position.addScaledVector( dollyDirection, radiusDelta ); | ||
scope.object.updateMatrixWorld(); | ||
|
||
} else if ( scope.object.isOrthographicCamera ) { | ||
|
||
// adjust the ortho camera position based on zoom changes | ||
const mouseBefore = new Vector3( mouse.x, mouse.y, 0 ); | ||
mouseBefore.unproject( scope.object ); | ||
|
||
scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / scale ) ); | ||
scope.object.updateProjectionMatrix(); | ||
zoomChanged = true; | ||
|
||
const mouseAfter = new Vector3( mouse.x, mouse.y, 0 ); | ||
mouseAfter.unproject( scope.object ); | ||
|
||
scope.object.position.sub( mouseAfter ).add( mouseBefore ); | ||
scope.object.updateMatrixWorld(); | ||
|
||
newRadius = offset.length(); | ||
|
||
} else { | ||
|
||
console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled.' ); | ||
scope.zoomToCursor = false; | ||
|
||
} | ||
|
||
// handle the placement of the target | ||
if ( newRadius !== null ) { | ||
|
||
if ( this.screenSpacePanning ) { | ||
|
||
// position the orbit target in front of the new camera position | ||
scope.target.set( 0, 0, - 1 ) | ||
.transformDirection( scope.object.matrix ) | ||
.multiplyScalar( newRadius ) | ||
.add( scope.object.position ); | ||
|
||
} else { | ||
|
||
// get the ray and translation plane to compute target | ||
_ray.origin.copy( scope.object.position ); | ||
_ray.direction.set( 0, 0, - 1 ).transformDirection( scope.object.matrix ); | ||
|
||
_plane.setFromNormalAndCoplanarPoint( scope.object.up, scope.target ); | ||
|
||
_ray.intersectPlane( _plane, scope.target ); | ||
|
||
} | ||
|
||
} | ||
|
||
} else if ( scope.object.isOrthographicCamera ) { | ||
|
||
scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / scale ) ); | ||
scope.object.updateProjectionMatrix(); | ||
zoomChanged = true; | ||
|
||
} | ||
|
||
scale = 1; | ||
performCursorZoom = false; | ||
|
||
// update condition is: | ||
// min(camera displacement, camera rotation in radians)^2 > EPS | ||
|
@@ -350,7 +438,6 @@ class OrbitControls extends EventDispatcher { | |
|
||
let scale = 1; | ||
const panOffset = new Vector3(); | ||
let zoomChanged = false; | ||
|
||
const rotateStart = new Vector2(); | ||
const rotateEnd = new Vector2(); | ||
|
@@ -364,6 +451,10 @@ class OrbitControls extends EventDispatcher { | |
const dollyEnd = new Vector2(); | ||
const dollyDelta = new Vector2(); | ||
|
||
const dollyDirection = new Vector3(); | ||
const mouse = new Vector2(); | ||
let performCursorZoom = false; | ||
|
||
const pointers = []; | ||
const pointerPositions = {}; | ||
|
||
|
@@ -474,16 +565,10 @@ class OrbitControls extends EventDispatcher { | |
|
||
function dollyOut( dollyScale ) { | ||
|
||
if ( scope.object.isPerspectiveCamera ) { | ||
if ( scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera ) { | ||
|
||
scale /= dollyScale; | ||
|
||
} else if ( scope.object.isOrthographicCamera ) { | ||
|
||
scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) ); | ||
scope.object.updateProjectionMatrix(); | ||
zoomChanged = true; | ||
|
||
} else { | ||
|
||
console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); | ||
|
@@ -495,16 +580,10 @@ class OrbitControls extends EventDispatcher { | |
|
||
function dollyIn( dollyScale ) { | ||
|
||
if ( scope.object.isPerspectiveCamera ) { | ||
if ( scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera ) { | ||
|
||
scale *= dollyScale; | ||
|
||
} else if ( scope.object.isOrthographicCamera ) { | ||
|
||
scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) ); | ||
scope.object.updateProjectionMatrix(); | ||
zoomChanged = true; | ||
|
||
} else { | ||
|
||
console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); | ||
|
@@ -514,6 +593,34 @@ class OrbitControls extends EventDispatcher { | |
|
||
} | ||
|
||
function updateMouseParameters( event ) { | ||
|
||
if ( ! scope.zoomToCursor ) { | ||
|
||
return; | ||
|
||
} | ||
|
||
performCursorZoom = true; | ||
|
||
const x = event.clientX - scope.domElement.clientLeft; | ||
const y = event.clientY - scope.domElement.clientTop; | ||
const w = scope.domElement.clientWidth; | ||
const h = scope.domElement.clientHeight; | ||
gkjohnson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
mouse.x = ( x / w ) * 2 - 1; | ||
mouse.y = - ( y / h ) * 2 + 1; | ||
|
||
dollyDirection.set( mouse.x, mouse.y, 1 ).unproject( object ).sub( object.position ).normalize(); | ||
|
||
} | ||
|
||
function clampDistance( dist ) { | ||
|
||
return Math.max( scope.minDistance, Math.min( scope.maxDistance, dist ) ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you could reuse the clamp function available in MathUtils |
||
|
||
} | ||
|
||
// | ||
// event callbacks - update the object state | ||
// | ||
|
@@ -526,6 +633,7 @@ class OrbitControls extends EventDispatcher { | |
|
||
function handleMouseDownDolly( event ) { | ||
|
||
updateMouseParameters( event ); | ||
dollyStart.set( event.clientX, event.clientY ); | ||
|
||
} | ||
|
@@ -592,6 +700,8 @@ class OrbitControls extends EventDispatcher { | |
|
||
function handleMouseWheel( event ) { | ||
|
||
updateMouseParameters( event ); | ||
|
||
if ( event.deltaY < 0 ) { | ||
|
||
dollyIn( getZoomScale() ); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gkjohnson I think
mouseBefore
should have been instantiated once inside the closure.Likewise,
mouseAfter
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops! You're right. That would be a good change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gkjohnson Something like this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a good change but this PR is already merged and I have other priorities at the moment. I'm happy to review a PR from anyone else to get it in, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not understand the code block, so it will not be me.