Skip to content
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 17 commits into from
Jul 10, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 131 additions & 21 deletions examples/jsm/controls/OrbitControls.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {
Spherical,
TOUCH,
Vector2,
Vector3
Vector3,
Plane,
Ray
} from 'three';

// OrbitControls performs orbiting, dollying (zooming), and panning.
Expand All @@ -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 {

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 ) {
Expand All @@ -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
Expand All @@ -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 );
Copy link
Collaborator

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.

Copy link
Collaborator Author

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.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gkjohnson Something like this.

mouseBefore.set( mouse.x, mouse.y, 0 ).unproject( scope.object );
...
mouseAfter.set( mouse.x, mouse.y, 0 ).unproject( scope.object );
...
scope.object.updateWorldMatrix( false, false ); // if you say it is needed

Copy link
Collaborator Author

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.

Copy link
Collaborator

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.

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
Expand Down Expand Up @@ -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();
Expand All @@ -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 = {};

Expand Down Expand Up @@ -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.' );
Expand All @@ -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.' );
Expand All @@ -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 ) );
Copy link
Contributor

Choose a reason for hiding this comment

The 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
//
Expand All @@ -526,6 +633,7 @@ class OrbitControls extends EventDispatcher {

function handleMouseDownDolly( event ) {

updateMouseParameters( event );
dollyStart.set( event.clientX, event.clientY );

}
Expand Down Expand Up @@ -592,6 +700,8 @@ class OrbitControls extends EventDispatcher {

function handleMouseWheel( event ) {

updateMouseParameters( event );

if ( event.deltaY < 0 ) {

dollyIn( getZoomScale() );
Expand Down
1 change: 1 addition & 0 deletions examples/misc_controls_map.html
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@


const gui = new GUI();
gui.add( controls, 'zoomToCursor' );
gui.add( controls, 'screenSpacePanning' );

}
Expand Down