Skip to content
This repository was archived by the owner on Nov 3, 2025. It is now read-only.

Commit 789ba55

Browse files
Updated OrbitControls.js
- Add `ZoomToCursor` functionality as per `https://github.com/mrdoob/three.js/pull/24983`
1 parent b06618b commit 789ba55

File tree

1 file changed

+145
-3
lines changed

1 file changed

+145
-3
lines changed

viewers/static/js/OrbitControls.js

Lines changed: 145 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,64 @@
9797

9898
this._domElementKeyEvents = null; //
9999
// public methods
100+
101+
// ZOOM-TO-CURSOR
102+
this.cursorScreen = new THREE.Vector3();
103+
this.cursorWorld = new THREE.Vector3();
104+
this.enableZoomToCursor = false;
105+
this.adjustmentAfterZoomNeeded = false;
106+
this.maxTargetDistanceFromOrigin = Infinity;
107+
100108
//
101109

110+
// ZOOM-TO-CURSOR
111+
this.adjustAfterZoom = function () {
112+
113+
const lastTarget = scope.target.clone();
114+
const newCursorWorld = new THREE.Vector3( scope.cursorScreen.x, scope.cursorScreen.y, scope.target.clone().project( scope.object ).z ).clone().unproject( scope.object );
115+
const delta = new THREE.Vector3().subVectors( scope.cursorWorld, newCursorWorld );
116+
117+
let target = null;
118+
119+
if ( !scope.screenSpacePanning ) {
120+
121+
const plane = new THREE.Plane( scope.object.up.clone() );
122+
const ray = new THREE.Ray( scope.object.position.clone(), new THREE.Vector3().subVectors( scope.target.clone().add( delta ), scope.object.position ).normalize());
123+
target = ray.intersectPlane( plane, new THREE.Vector3() );
124+
125+
if ( target === null || new THREE.Vector3().subVectors( scope.object.position, scope.target ).normalize().multiply( scope.object.up.clone().normalize() ).length() < 0.00001 ) {
126+
127+
scope.target.add( delta );
128+
if ( scope.target.length() > this.maxTargetDistanceFromOrigin ) scope.target.setLength( this.maxTargetDistanceFromOrigin );
129+
scope.object.position.add( new THREE.Vector3().subVectors( scope.target, lastTarget ) );
130+
131+
const mulVector = new THREE.Vector3( 1 - scope.object.up.x, 1 - scope.object.up.y, 1 - scope.object.up.z );
132+
scope.target.multiply( mulVector );
133+
scope.object.position.multiply( mulVector );
134+
135+
} else {
136+
137+
if ( target.length() > this.maxTargetDistanceFromOrigin ) target.setLength( this.maxTargetDistanceFromOrigin );
138+
scope.target.copy( target );
139+
scope.object.position.add( new THREE.Vector3().subVectors( scope.target, lastTarget ) );
140+
141+
}
142+
143+
} else {
144+
145+
scope.target.add( delta );
146+
scope.object.position.add( delta );
147+
148+
}
149+
150+
}
151+
152+
this.setCursorWorld = function () {
153+
154+
scope.cursorWorld.copy( new THREE.Vector3( scope.cursorScreen.x, scope.cursorScreen.y, scope.target.clone().project( scope.object ).z ).unproject( scope.object ) );
155+
156+
}
157+
102158
this.getPolarAngle = function () {
103159

104160
return spherical.phi;
@@ -236,9 +292,27 @@
236292
// min(camera displacement, camera rotation in radians)^2 > EPS
237293
// using small-angle approximation cos(x/2) = 1 - x^2 / 8
238294

295+
// ZOOM-TO-CURSOR
296+
if ( scope.target.length() > this.maxTargetDistanceFromOrigin ) {
297+
298+
const lastTarget = scope.target.clone();
299+
scope.target.setLength( this.maxTargetDistanceFromOrigin );
300+
scope.object.position.add( new THREE.Vector3().subVectors( scope.target, lastTarget ) );
301+
302+
}
303+
239304
if ( zoomChanged || lastPosition.distanceToSquared( scope.object.position ) > EPS || 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) {
240305

241306
scope.dispatchEvent( _changeEvent );
307+
308+
// ZOOM-TO-CURSOR
309+
if ( scope.enableZoomToCursor && scope.adjustmentAfterZoomNeeded ) {
310+
311+
scope.adjustmentAfterZoomNeeded = false;
312+
this.adjustAfterZoom();
313+
314+
}
315+
242316
lastPosition.copy( scope.object.position );
243317
lastQuaternion.copy( scope.object.quaternion );
244318
zoomChanged = false;
@@ -408,6 +482,9 @@
408482

409483
function dollyOut( dollyScale ) {
410484

485+
// ZOOM-TO-CURSOR
486+
if ( scope.enableZoomToCursor ) scope.setCursorWorld();
487+
411488
if ( scope.object.isPerspectiveCamera ) {
412489

413490
scale /= dollyScale;
@@ -425,10 +502,21 @@
425502

426503
}
427504

505+
// ZOOM-TO-CURSOR
506+
if ( scope.enableZoomToCursor ) {
507+
508+
if ( scope.object.isOrthographicCamera ) scope.adjustAfterZoom();
509+
else if ( scope.object.isPerspectiveCamera ) scope.adjustmentAfterZoomNeeded = true;
510+
511+
}
512+
428513
}
429514

430515
function dollyIn( dollyScale ) {
431516

517+
// ZOOM-TO-CURSOR
518+
if ( scope.enableZoomToCursor ) scope.setCursorWorld();
519+
432520
if ( scope.object.isPerspectiveCamera ) {
433521

434522
scale *= dollyScale;
@@ -446,6 +534,14 @@
446534

447535
}
448536

537+
// ZOOM-TO-CURSOR
538+
if ( scope.enableZoomToCursor ) {
539+
540+
if ( scope.object.isOrthographicCamera ) scope.adjustAfterZoom();
541+
else if ( scope.object.isPerspectiveCamera ) scope.adjustmentAfterZoomNeeded = true;
542+
543+
}
544+
449545
} //
450546
// event callbacks - update the object state
451547
//
@@ -1065,10 +1161,56 @@
10651161
scope.domElement.addEventListener( 'contextmenu', onContextMenu );
10661162
scope.domElement.addEventListener( 'pointerdown', onPointerDown );
10671163
scope.domElement.addEventListener( 'pointercancel', onPointerCancel );
1068-
scope.domElement.addEventListener( 'wheel', onMouseWheel, {
1069-
passive: false
1070-
} ); // force an update at start
1164+
scope.domElement.addEventListener( 'wheel', onMouseWheel, { passive: false } );
1165+
1166+
// ZOOM-TO-CURSOR
1167+
scope.domElement.addEventListener( 'mousemove', event => {
1168+
1169+
if ( !scope.enableZoomToCursor ) return;
1170+
1171+
scope.cursorScreen.copy(
1172+
new THREE.Vector3(
1173+
( ( event.clientX - scope.domElement.getBoundingClientRect().left ) / scope.domElement.clientWidth) * 2 - 1,
1174+
- ( (event.clientY - scope.domElement.getBoundingClientRect().top ) / scope.domElement.clientHeight) * 2 + 1,
1175+
scope.target.clone().project( scope.object ).z
1176+
)
1177+
);
1178+
1179+
});
1180+
1181+
const handleTouch = event => {
1182+
1183+
const touches = event.touches;
1184+
let touch;
1185+
1186+
if ( touches.length === 1 ) {
1187+
1188+
touch = new THREE.Vector2( touches[ 0 ].clientX, touches[ 0 ].clientY );
1189+
1190+
} else if ( touches.length === 2 ) {
1191+
1192+
touch = new THREE.Vector2( ( touches[ 0 ].clientX + touches[ 1 ].clientX ) / 2, ( touches[ 0 ].clientY + touches[ 1 ].clientY ) / 2 );
1193+
1194+
}
1195+
1196+
if ( touch !== undefined ) {
1197+
1198+
scope.cursorScreen.copy(
1199+
new THREE.Vector3(
1200+
( (touch.x - scope.domElement.getBoundingClientRect().left ) / scope.domElement.clientWidth ) * 2 - 1,
1201+
- ( ( touch.y - scope.domElement.getBoundingClientRect().top ) / scope.domElement.clientHeight ) * 2 + 1,
1202+
scope.target.clone().project( scope.object ).z
1203+
)
1204+
);
1205+
1206+
}
1207+
1208+
};
1209+
1210+
scope.domElement.addEventListener( 'touchstart', handleTouch);
1211+
scope.domElement.addEventListener( 'touchmove', handleTouch);
10711212

1213+
// force an update at start
10721214
this.update();
10731215

10741216
}

0 commit comments

Comments
 (0)