Skip to content

Commit a8e2150

Browse files
authored
Tune pan (#3319)
* dynamic pan sensitivity * fixed touch recentering * fixed iOS recenter
1 parent 0790b0e commit a8e2150

File tree

1 file changed

+90
-63
lines changed

1 file changed

+90
-63
lines changed

packages/model-viewer/src/three-components/SmoothControls.ts

Lines changed: 90 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ export class SmoothControls extends EventDispatcher {
135135
// Pan state
136136
public enablePan = true;
137137
private panProjection = new Matrix3();
138-
private panMetersPerPixel = 0;
138+
private panPerPixel = 0;
139139

140140
// Internal orbital position state
141141
private spherical = new Spherical();
@@ -525,7 +525,7 @@ export class SmoothControls extends EventDispatcher {
525525
}
526526

527527
private onMouseMove = (event: MouseEvent) => {
528-
if (this.panMetersPerPixel > 0) {
528+
if (this.panPerPixel > 0) {
529529
this.movePan(event.clientX, event.clientY);
530530
} else {
531531
this.handleSinglePointerMove(event);
@@ -560,7 +560,7 @@ export class SmoothControls extends EventDispatcher {
560560
this.userAdjustOrbit(0, 0, deltaZoom);
561561
}
562562

563-
if (this.panMetersPerPixel > 0) {
563+
if (this.panPerPixel > 0) {
564564
const thisX =
565565
0.5 * (targetTouches[0].clientX + targetTouches[1].clientX);
566566
const thisY =
@@ -612,10 +612,10 @@ export class SmoothControls extends EventDispatcher {
612612

613613
private initializePan() {
614614
(this.scene.element as any)[$panElement].style.opacity = 1;
615-
const {theta, phi, radius} = this.spherical;
615+
const {theta, phi} = this.spherical;
616616
const psi = theta - this.scene.yaw;
617-
this.panMetersPerPixel = (PAN_SENSITIVITY * radius) /
618-
this.element.getBoundingClientRect().height;
617+
this.panPerPixel =
618+
PAN_SENSITIVITY / this.element.getBoundingClientRect().height;
619619
this.panProjection.set(
620620
-Math.cos(psi),
621621
-Math.cos(phi) * Math.sin(psi),
@@ -634,7 +634,8 @@ export class SmoothControls extends EventDispatcher {
634634
thisX - lastPointerPosition.clientX,
635635
thisY - lastPointerPosition.clientY,
636636
0);
637-
dxy.multiplyScalar(this.panMetersPerPixel);
637+
const metersPerPixel = this.spherical.radius * this.panPerPixel;
638+
dxy.multiplyScalar(metersPerPixel);
638639

639640
lastPointerPosition.clientX = thisX;
640641
lastPointerPosition.clientY = thisY;
@@ -646,57 +647,65 @@ export class SmoothControls extends EventDispatcher {
646647
}
647648

648649
private recenter(pointer: Pointer) {
649-
if (!this.enablePan) {
650+
if (!this.enablePan ||
651+
Math.abs(pointer.clientX - this.startPointerPosition.clientX) >
652+
TAP_DISTANCE ||
653+
Math.abs(pointer.clientY - this.startPointerPosition.clientY) >
654+
TAP_DISTANCE) {
650655
return;
651656
}
652657
const {scene} = this;
653658
(scene.element as any)[$panElement].style.opacity = 0;
654-
if (Math.abs(pointer.clientX - this.startPointerPosition.clientX) <
655-
TAP_DISTANCE &&
656-
Math.abs(pointer.clientY - this.startPointerPosition.clientY) <
657-
TAP_DISTANCE) {
658-
const hit = scene.positionAndNormalFromPoint(
659-
scene.getNDC(pointer.clientX, pointer.clientY));
660-
661-
if (hit == null) {
662-
const {cameraTarget} = scene.element;
663-
scene.element.cameraTarget = '';
664-
scene.element.cameraTarget = cameraTarget;
665-
// Zoom all the way out.
666-
this.userAdjustOrbit(0, 0, 1);
667-
} else {
668-
scene.target.worldToLocal(hit.position);
669-
scene.setTarget(hit.position.x, hit.position.y, hit.position.z);
670-
// Zoom in on the tapped point.
671-
this.userAdjustOrbit(0, 0, -5 * ZOOM_SENSITIVITY);
672-
}
673-
} else if (this.panMetersPerPixel > 0) {
674-
const hit = scene.positionAndNormalFromPoint(vector2.set(0, 0));
675-
if (hit == null)
676-
return;
677659

660+
const hit = scene.positionAndNormalFromPoint(
661+
scene.getNDC(pointer.clientX, pointer.clientY));
662+
663+
if (hit == null) {
664+
const {cameraTarget} = scene.element;
665+
scene.element.cameraTarget = '';
666+
scene.element.cameraTarget = cameraTarget;
667+
// Zoom all the way out.
668+
this.userAdjustOrbit(0, 0, 1);
669+
} else {
678670
scene.target.worldToLocal(hit.position);
679-
const goalTarget = scene.getTarget();
680-
const {theta, phi} = this.spherical;
681-
682-
// Set target to surface hit point, except the target is still settling,
683-
// so offset the goal accordingly so the transition is smooth even though
684-
// this will drift the target slightly away from the hit point.
685-
const psi = theta - scene.yaw;
686-
const n = vector3.set(
687-
Math.sin(phi) * Math.sin(psi),
688-
Math.cos(phi),
689-
Math.sin(phi) * Math.cos(psi));
690-
const dr = n.dot(hit.position.sub(goalTarget));
691-
goalTarget.add(n.multiplyScalar(dr));
692-
693-
scene.setTarget(goalTarget.x, goalTarget.y, goalTarget.z);
694-
// Change the camera radius to match the change in target so that the
695-
// camera itself does not move, unless it hits a radius bound.
696-
this.setOrbit(undefined, undefined, this.goalSpherical.radius - dr);
671+
scene.setTarget(hit.position.x, hit.position.y, hit.position.z);
672+
// Zoom in on the tapped point.
673+
this.userAdjustOrbit(0, 0, -5 * ZOOM_SENSITIVITY);
697674
}
698675
}
699676

677+
private resetRadius() {
678+
if (!this.enablePan || this.panPerPixel === 0) {
679+
return;
680+
}
681+
const {scene} = this;
682+
(scene.element as any)[$panElement].style.opacity = 0;
683+
684+
const hit = scene.positionAndNormalFromPoint(vector2.set(0, 0));
685+
if (hit == null)
686+
return;
687+
688+
scene.target.worldToLocal(hit.position);
689+
const goalTarget = scene.getTarget();
690+
const {theta, phi} = this.spherical;
691+
692+
// Set target to surface hit point, except the target is still settling,
693+
// so offset the goal accordingly so the transition is smooth even though
694+
// this will drift the target slightly away from the hit point.
695+
const psi = theta - scene.yaw;
696+
const n = vector3.set(
697+
Math.sin(phi) * Math.sin(psi),
698+
Math.cos(phi),
699+
Math.sin(phi) * Math.cos(psi));
700+
const dr = n.dot(hit.position.sub(goalTarget));
701+
goalTarget.add(n.multiplyScalar(dr));
702+
703+
scene.setTarget(goalTarget.x, goalTarget.y, goalTarget.z);
704+
// Change the camera radius to match the change in target so that the
705+
// camera itself does not move, unless it hits a radius bound.
706+
this.setOrbit(undefined, undefined, this.goalSpherical.radius - dr);
707+
}
708+
700709
private onPointerDown(fn: () => void) {
701710
if (!this.canInteract) {
702711
return;
@@ -716,7 +725,11 @@ export class SmoothControls extends EventDispatcher {
716725
event.shiftKey)) {
717726
this.initializePan();
718727
}
719-
this.handleSinglePointerDown(event);
728+
this.lastPointerPosition.clientX = event.clientX;
729+
this.lastPointerPosition.clientY = event.clientY;
730+
this.startPointerPosition.clientX = event.clientX;
731+
this.startPointerPosition.clientY = event.clientY;
732+
this.element.style.cursor = 'grabbing';
720733
});
721734
};
722735

@@ -735,13 +748,21 @@ export class SmoothControls extends EventDispatcher {
735748
};
736749

737750
private onTouchChange(event: TouchEvent) {
738-
const {targetTouches} = event;
751+
const {targetTouches, changedTouches} = event;
739752

740753
switch (targetTouches.length) {
741754
default:
742755
case 1:
743756
this.touchMode = this.touchModeRotate;
744-
this.handleSinglePointerDown(targetTouches[0]);
757+
this.lastPointerPosition.clientX = targetTouches[0].clientX;
758+
this.lastPointerPosition.clientY = targetTouches[0].clientY;
759+
if (targetTouches[0].identifier ===
760+
changedTouches[0].identifier) { // finger down
761+
this.startPointerPosition.clientX = targetTouches[0].clientX;
762+
this.startPointerPosition.clientY = targetTouches[0].clientY;
763+
} else { // finger up
764+
this.resetRadius();
765+
}
745766
break;
746767
case 2:
747768
this.touchMode = (this.touchDecided && this.touchMode === null) ?
@@ -750,24 +771,22 @@ export class SmoothControls extends EventDispatcher {
750771
this.touchDecided = true;
751772
if (this.enablePan) {
752773
this.initializePan();
774+
const x = 0.5 * (targetTouches[0].clientX + targetTouches[1].clientX);
775+
const y = 0.5 * (targetTouches[0].clientY + targetTouches[1].clientY);
776+
this.lastPointerPosition.clientX = x;
777+
this.lastPointerPosition.clientY = y;
778+
this.startPointerPosition.clientX = x;
779+
this.startPointerPosition.clientY = y;
753780
}
754781
break;
755782
}
756783

757784
this.lastTouches = targetTouches;
758785
}
759786

760-
private handleSinglePointerDown(pointer: Pointer) {
761-
this.lastPointerPosition.clientX = pointer.clientX;
762-
this.lastPointerPosition.clientY = pointer.clientY;
763-
this.startPointerPosition.clientX = pointer.clientX;
764-
this.startPointerPosition.clientY = pointer.clientY;
765-
this.element.style.cursor = 'grabbing';
766-
}
767-
768787
private onPointerUp() {
769788
this.element.style.cursor = 'grab';
770-
this.panMetersPerPixel = 0;
789+
this.panPerPixel = 0;
771790

772791
if (this.isUserPointing) {
773792
this.dispatchEvent(
@@ -777,7 +796,11 @@ export class SmoothControls extends EventDispatcher {
777796

778797
private onMouseUp = (event: MouseEvent) => {
779798
self.removeEventListener('mousemove', this.onMouseMove);
780-
this.recenter(event);
799+
if (this.panPerPixel > 0) {
800+
this.resetRadius();
801+
} else {
802+
this.recenter(event);
803+
}
781804

782805
this.onPointerUp();
783806
};
@@ -787,7 +810,11 @@ export class SmoothControls extends EventDispatcher {
787810
this.onTouchChange(event);
788811
}
789812
if (event.targetTouches.length === 0) {
790-
this.recenter(event.changedTouches[0]);
813+
if (this.panPerPixel > 0) {
814+
this.resetRadius();
815+
} else {
816+
this.recenter(event.changedTouches[0]);
817+
}
791818
}
792819

793820
this.onPointerUp();

0 commit comments

Comments
 (0)