|
@@ -10,9 +10,11 @@
|
|
|
|
|
|
import { Quat, Vec2, Vec3, EPSILON } from '../../mol-math/linear-algebra';
|
|
|
import { cameraLookAt, Viewport } from '../camera/util';
|
|
|
-import InputObserver, { DragInput, WheelInput, ButtonsType, PinchInput } from '../../mol-util/input/input-observer';
|
|
|
-import { Object3D } from '../../mol-gl/object3d';
|
|
|
+import InputObserver, { DragInput, WheelInput, PinchInput } from '../../mol-util/input/input-observer';
|
|
|
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
|
|
+import { Camera } from '../camera';
|
|
|
+import { Bindings } from './bindings';
|
|
|
+import { absMax } from '../../mol-math/misc';
|
|
|
|
|
|
export const TrackballControlsParams = {
|
|
|
noScroll: PD.Boolean(true, { isHidden: true }),
|
|
@@ -28,7 +30,9 @@ export const TrackballControlsParams = {
|
|
|
dynamicDampingFactor: PD.Numeric(0.2, {}, { isHidden: true }),
|
|
|
|
|
|
minDistance: PD.Numeric(0.01, {}, { isHidden: true }),
|
|
|
- maxDistance: PD.Numeric(1e150, {}, { isHidden: true })
|
|
|
+ maxDistance: PD.Numeric(1e150, {}, { isHidden: true }),
|
|
|
+
|
|
|
+ bindings: PD.Value(Bindings.Default, { isHidden: true })
|
|
|
}
|
|
|
export type TrackballControlsProps = PD.Values<typeof TrackballControlsParams>
|
|
|
|
|
@@ -44,11 +48,10 @@ interface TrackballControls {
|
|
|
dispose: () => void
|
|
|
}
|
|
|
namespace TrackballControls {
|
|
|
- export function create(input: InputObserver, object: Object3D & { target: Vec3 }, props: Partial<TrackballControlsProps> = {}): TrackballControls {
|
|
|
+ export function create(input: InputObserver, camera: Camera, props: Partial<TrackballControlsProps> = {}): TrackballControls {
|
|
|
const p = { ...PD.getDefaultValues(TrackballControlsParams), ...props }
|
|
|
|
|
|
- const viewport: Viewport = { x: 0, y: 0, width: 0, height: 0 }
|
|
|
- const target: Vec3 = object.target
|
|
|
+ const viewport = Viewport()
|
|
|
|
|
|
let disposed = false
|
|
|
|
|
@@ -60,91 +63,113 @@ namespace TrackballControls {
|
|
|
let _isInteracting = false;
|
|
|
|
|
|
// For internal use
|
|
|
- const lastPosition = Vec3.zero()
|
|
|
+ const lastPosition = Vec3()
|
|
|
|
|
|
- const _eye = Vec3.zero()
|
|
|
+ const _eye = Vec3()
|
|
|
|
|
|
- const _movePrev = Vec2.zero()
|
|
|
- const _moveCurr = Vec2.zero()
|
|
|
+ const _rotPrev = Vec2()
|
|
|
+ const _rotCurr = Vec2()
|
|
|
+ const _rotLastAxis = Vec3()
|
|
|
+ let _rotLastAngle = 0
|
|
|
|
|
|
- const _lastAxis = Vec3.zero()
|
|
|
- let _lastAngle = 0
|
|
|
+ const _zRotPrev = Vec2()
|
|
|
+ const _zRotCurr = Vec2()
|
|
|
+ let _zRotLastAngle = 0
|
|
|
|
|
|
- const _zoomStart = Vec2.zero()
|
|
|
- const _zoomEnd = Vec2.zero()
|
|
|
+ const _zoomStart = Vec2()
|
|
|
+ const _zoomEnd = Vec2()
|
|
|
|
|
|
- const _panStart = Vec2.zero()
|
|
|
- const _panEnd = Vec2.zero()
|
|
|
+ const _panStart = Vec2()
|
|
|
+ const _panEnd = Vec2()
|
|
|
|
|
|
// Initial values for reseting
|
|
|
- const target0 = Vec3.clone(target)
|
|
|
- const position0 = Vec3.clone(object.position)
|
|
|
- const up0 = Vec3.clone(object.up)
|
|
|
+ const target0 = Vec3.clone(camera.target)
|
|
|
+ const position0 = Vec3.clone(camera.position)
|
|
|
+ const up0 = Vec3.clone(camera.up)
|
|
|
|
|
|
- const mouseOnScreenVec2 = Vec2.zero()
|
|
|
+ const mouseOnScreenVec2 = Vec2()
|
|
|
function getMouseOnScreen(pageX: number, pageY: number) {
|
|
|
- Vec2.set(
|
|
|
+ return Vec2.set(
|
|
|
mouseOnScreenVec2,
|
|
|
(pageX - viewport.x) / viewport.width,
|
|
|
(pageY - viewport.y) / viewport.height
|
|
|
);
|
|
|
- return mouseOnScreenVec2;
|
|
|
}
|
|
|
|
|
|
- const mouseOnCircleVec2 = Vec2.zero()
|
|
|
+ const mouseOnCircleVec2 = Vec2()
|
|
|
function getMouseOnCircle(pageX: number, pageY: number) {
|
|
|
- Vec2.set(
|
|
|
+ return Vec2.set(
|
|
|
mouseOnCircleVec2,
|
|
|
- ((pageX - viewport.width * 0.5 - viewport.x) / (viewport.width * 0.5)),
|
|
|
- ((viewport.height + 2 * (viewport.y - pageY)) / viewport.width) // screen.width intentional
|
|
|
+ (pageX - viewport.width * 0.5 - viewport.x) / (viewport.width * 0.5),
|
|
|
+ (viewport.height + 2 * (viewport.y - pageY)) / viewport.width // screen.width intentional
|
|
|
);
|
|
|
- return mouseOnCircleVec2;
|
|
|
}
|
|
|
|
|
|
- const rotAxis = Vec3.zero()
|
|
|
- const rotQuat = Quat.zero()
|
|
|
- const rotEyeDir = Vec3.zero()
|
|
|
- const rotObjUpDir = Vec3.zero()
|
|
|
- const rotObjSideDir = Vec3.zero()
|
|
|
- const rotMoveDir = Vec3.zero()
|
|
|
+ const rotAxis = Vec3()
|
|
|
+ const rotQuat = Quat()
|
|
|
+ const rotEyeDir = Vec3()
|
|
|
+ const rotObjUpDir = Vec3()
|
|
|
+ const rotObjSideDir = Vec3()
|
|
|
+ const rotMoveDir = Vec3()
|
|
|
|
|
|
function rotateCamera() {
|
|
|
- Vec3.set(rotMoveDir, _moveCurr[0] - _movePrev[0], _moveCurr[1] - _movePrev[1], 0);
|
|
|
- let angle = Vec3.magnitude(rotMoveDir);
|
|
|
+ const dx = _rotCurr[0] - _rotPrev[0]
|
|
|
+ const dy = _rotCurr[1] - _rotPrev[1]
|
|
|
+ Vec3.set(rotMoveDir, dx, dy, 0);
|
|
|
+
|
|
|
+ const angle = Vec3.magnitude(rotMoveDir) * p.rotateSpeed;
|
|
|
|
|
|
if (angle) {
|
|
|
- Vec3.copy(_eye, object.position)
|
|
|
- Vec3.sub(_eye, _eye, target)
|
|
|
+ Vec3.sub(_eye, camera.position, camera.target)
|
|
|
|
|
|
- Vec3.normalize(rotEyeDir, Vec3.copy(rotEyeDir, _eye))
|
|
|
- Vec3.normalize(rotObjUpDir, Vec3.copy(rotObjUpDir, object.up))
|
|
|
+ Vec3.normalize(rotEyeDir, _eye)
|
|
|
+ Vec3.normalize(rotObjUpDir, camera.up)
|
|
|
Vec3.normalize(rotObjSideDir, Vec3.cross(rotObjSideDir, rotObjUpDir, rotEyeDir))
|
|
|
|
|
|
- Vec3.setMagnitude(rotObjUpDir, rotObjUpDir, _moveCurr[1] - _movePrev[1])
|
|
|
- Vec3.setMagnitude(rotObjSideDir, rotObjSideDir, _moveCurr[0] - _movePrev[0])
|
|
|
-
|
|
|
- Vec3.add(rotMoveDir, Vec3.copy(rotMoveDir, rotObjUpDir), rotObjSideDir)
|
|
|
+ Vec3.setMagnitude(rotObjUpDir, rotObjUpDir, dy)
|
|
|
+ Vec3.setMagnitude(rotObjSideDir, rotObjSideDir, dx)
|
|
|
|
|
|
+ Vec3.add(rotMoveDir, rotObjUpDir, rotObjSideDir)
|
|
|
Vec3.normalize(rotAxis, Vec3.cross(rotAxis, rotMoveDir, _eye))
|
|
|
-
|
|
|
- angle *= p.rotateSpeed;
|
|
|
Quat.setAxisAngle(rotQuat, rotAxis, angle)
|
|
|
|
|
|
Vec3.transformQuat(_eye, _eye, rotQuat)
|
|
|
- Vec3.transformQuat(object.up, object.up, rotQuat)
|
|
|
+ Vec3.transformQuat(camera.up, camera.up, rotQuat)
|
|
|
|
|
|
- Vec3.copy(_lastAxis, rotAxis)
|
|
|
- _lastAngle = angle;
|
|
|
- } else if (!p.staticMoving && _lastAngle) {
|
|
|
- _lastAngle *= Math.sqrt(1.0 - p.dynamicDampingFactor);
|
|
|
- Vec3.sub(_eye, Vec3.copy(_eye, object.position), target)
|
|
|
- Quat.setAxisAngle(rotQuat, _lastAxis, _lastAngle)
|
|
|
+ Vec3.copy(_rotLastAxis, rotAxis)
|
|
|
+ _rotLastAngle = angle;
|
|
|
+ } else if (!p.staticMoving && _rotLastAngle) {
|
|
|
+ _rotLastAngle *= Math.sqrt(1.0 - p.dynamicDampingFactor);
|
|
|
+ Vec3.sub(_eye, camera.position, camera.target)
|
|
|
+ Quat.setAxisAngle(rotQuat, _rotLastAxis, _rotLastAngle)
|
|
|
|
|
|
Vec3.transformQuat(_eye, _eye, rotQuat)
|
|
|
- Vec3.transformQuat(object.up, object.up, rotQuat)
|
|
|
+ Vec3.transformQuat(camera.up, camera.up, rotQuat)
|
|
|
+ }
|
|
|
+
|
|
|
+ Vec2.copy(_rotPrev, _rotCurr)
|
|
|
+ }
|
|
|
+
|
|
|
+ const zRotQuat = Quat()
|
|
|
+
|
|
|
+ function zRotateCamera() {
|
|
|
+ const dx = _zRotCurr[0] - _zRotPrev[0]
|
|
|
+ const dy = _zRotCurr[1] - _zRotPrev[1]
|
|
|
+ const angle = p.rotateSpeed * (-dx + dy) * -0.05
|
|
|
+
|
|
|
+ if (angle) {
|
|
|
+ Vec3.sub(_eye, camera.position, camera.target)
|
|
|
+ Quat.setAxisAngle(zRotQuat, _eye, angle)
|
|
|
+ Vec3.transformQuat(camera.up, camera.up, zRotQuat)
|
|
|
+ _zRotLastAngle = angle;
|
|
|
+ } else if (!p.staticMoving && _zRotLastAngle) {
|
|
|
+ _zRotLastAngle *= Math.sqrt(1.0 - p.dynamicDampingFactor);
|
|
|
+ Vec3.sub(_eye, camera.position, camera.target)
|
|
|
+ Quat.setAxisAngle(zRotQuat, _eye, _zRotLastAngle)
|
|
|
+ Vec3.transformQuat(camera.up, camera.up, zRotQuat)
|
|
|
}
|
|
|
|
|
|
- Vec2.copy(_movePrev, _moveCurr)
|
|
|
+ Vec2.copy(_zRotPrev, _zRotCurr)
|
|
|
}
|
|
|
|
|
|
function zoomCamera() {
|
|
@@ -160,9 +185,9 @@ namespace TrackballControls {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- const panMouseChange = Vec2.zero()
|
|
|
- const panObjUp = Vec3.zero()
|
|
|
- const panOffset = Vec3.zero()
|
|
|
+ const panMouseChange = Vec2()
|
|
|
+ const panObjUp = Vec3()
|
|
|
+ const panOffset = Vec3()
|
|
|
|
|
|
function panCamera() {
|
|
|
Vec2.sub(panMouseChange, Vec2.copy(panMouseChange, _panEnd), _panStart)
|
|
@@ -170,14 +195,14 @@ namespace TrackballControls {
|
|
|
if (Vec2.squaredMagnitude(panMouseChange)) {
|
|
|
Vec2.scale(panMouseChange, panMouseChange, Vec3.magnitude(_eye) * p.panSpeed)
|
|
|
|
|
|
- Vec3.cross(panOffset, Vec3.copy(panOffset, _eye), object.up)
|
|
|
+ Vec3.cross(panOffset, Vec3.copy(panOffset, _eye), camera.up)
|
|
|
Vec3.setMagnitude(panOffset, panOffset, panMouseChange[0])
|
|
|
|
|
|
- Vec3.setMagnitude(panObjUp, object.up, panMouseChange[1])
|
|
|
+ Vec3.setMagnitude(panObjUp, camera.up, panMouseChange[1])
|
|
|
Vec3.add(panOffset, panOffset, panObjUp)
|
|
|
|
|
|
- Vec3.add(object.position, object.position, panOffset)
|
|
|
- Vec3.add(target, target, panOffset)
|
|
|
+ Vec3.add(camera.position, camera.position, panOffset)
|
|
|
+ Vec3.add(camera.target, camera.target, panOffset)
|
|
|
|
|
|
if (p.staticMoving) {
|
|
|
Vec2.copy(_panStart, _panEnd)
|
|
@@ -193,13 +218,13 @@ namespace TrackballControls {
|
|
|
function checkDistances() {
|
|
|
if (Vec3.squaredMagnitude(_eye) > p.maxDistance * p.maxDistance) {
|
|
|
Vec3.setMagnitude(_eye, _eye, p.maxDistance)
|
|
|
- Vec3.add(object.position, target, _eye)
|
|
|
+ Vec3.add(camera.position, camera.target, _eye)
|
|
|
Vec2.copy(_zoomStart, _zoomEnd)
|
|
|
}
|
|
|
|
|
|
if (Vec3.squaredMagnitude(_eye) < p.minDistance * p.minDistance) {
|
|
|
Vec3.setMagnitude(_eye, _eye, p.minDistance)
|
|
|
- Vec3.add(object.position, target, _eye)
|
|
|
+ Vec3.add(camera.position, camera.target, _eye)
|
|
|
Vec2.copy(_zoomStart, _zoomEnd)
|
|
|
}
|
|
|
}
|
|
@@ -210,18 +235,19 @@ namespace TrackballControls {
|
|
|
if (lastUpdated === t) return;
|
|
|
if (p.spin) spin(t - lastUpdated);
|
|
|
|
|
|
- Vec3.sub(_eye, object.position, target)
|
|
|
+ Vec3.sub(_eye, camera.position, camera.target)
|
|
|
|
|
|
rotateCamera()
|
|
|
+ zRotateCamera()
|
|
|
zoomCamera()
|
|
|
panCamera()
|
|
|
|
|
|
- Vec3.add(object.position, target, _eye)
|
|
|
+ Vec3.add(camera.position, camera.target, _eye)
|
|
|
checkDistances()
|
|
|
- cameraLookAt(object.position, object.up, object.direction, target)
|
|
|
+ cameraLookAt(camera.position, camera.up, camera.direction, camera.target)
|
|
|
|
|
|
- if (Vec3.squaredDistance(lastPosition, object.position) > EPSILON) {
|
|
|
- Vec3.copy(lastPosition, object.position)
|
|
|
+ if (Vec3.squaredDistance(lastPosition, camera.position) > EPSILON) {
|
|
|
+ Vec3.copy(lastPosition, camera.position)
|
|
|
}
|
|
|
|
|
|
lastUpdated = t;
|
|
@@ -229,53 +255,72 @@ namespace TrackballControls {
|
|
|
|
|
|
/** Reset object's vectors and the target vector to their initial values */
|
|
|
function reset() {
|
|
|
- Vec3.copy(target, target0)
|
|
|
- Vec3.copy(object.position, position0)
|
|
|
- Vec3.copy(object.up, up0)
|
|
|
+ Vec3.copy(camera.target, target0)
|
|
|
+ Vec3.copy(camera.position, position0)
|
|
|
+ Vec3.copy(camera.up, up0)
|
|
|
|
|
|
- Vec3.sub(_eye, object.position, target)
|
|
|
- cameraLookAt(object.position, object.up, object.direction, target)
|
|
|
- Vec3.copy(lastPosition, object.position)
|
|
|
+ Vec3.sub(_eye, camera.position, camera.target)
|
|
|
+ cameraLookAt(camera.position, camera.up, camera.direction, camera.target)
|
|
|
+ Vec3.copy(lastPosition, camera.position)
|
|
|
}
|
|
|
|
|
|
// listeners
|
|
|
|
|
|
- function onDrag({ pageX, pageY, buttons, isStart }: DragInput) {
|
|
|
+ function onDrag({ pageX, pageY, buttons, modifiers, isStart }: DragInput) {
|
|
|
_isInteracting = true;
|
|
|
|
|
|
+ const dragRotate = Bindings.match(p.bindings.drag.rotate, buttons, modifiers)
|
|
|
+ const dragRotateZ = Bindings.match(p.bindings.drag.rotateZ, buttons, modifiers)
|
|
|
+ const dragPan = Bindings.match(p.bindings.drag.pan, buttons, modifiers)
|
|
|
+ const dragZoom = Bindings.match(p.bindings.drag.zoom, buttons, modifiers)
|
|
|
+
|
|
|
+ getMouseOnCircle(pageX, pageY)
|
|
|
+ getMouseOnScreen(pageX, pageY)
|
|
|
+
|
|
|
if (isStart) {
|
|
|
- if (buttons === ButtonsType.Flag.Primary) {
|
|
|
- Vec2.copy(_moveCurr, getMouseOnCircle(pageX, pageY))
|
|
|
- Vec2.copy(_movePrev, _moveCurr)
|
|
|
- } else if (buttons === ButtonsType.Flag.Auxilary) {
|
|
|
- Vec2.copy(_zoomStart, getMouseOnScreen(pageX, pageY))
|
|
|
+ if (dragRotate) {
|
|
|
+ Vec2.copy(_rotCurr, mouseOnCircleVec2)
|
|
|
+ Vec2.copy(_rotPrev, _rotCurr)
|
|
|
+ }
|
|
|
+ if (dragRotateZ) {
|
|
|
+ Vec2.copy(_zRotCurr, mouseOnCircleVec2)
|
|
|
+ Vec2.copy(_zRotPrev, _zRotCurr)
|
|
|
+ }
|
|
|
+ if (dragZoom) {
|
|
|
+ Vec2.copy(_zoomStart, mouseOnScreenVec2)
|
|
|
Vec2.copy(_zoomEnd, _zoomStart)
|
|
|
- } else if (buttons === ButtonsType.Flag.Secondary) {
|
|
|
- Vec2.copy(_panStart, getMouseOnScreen(pageX, pageY))
|
|
|
+ }
|
|
|
+ if (dragPan) {
|
|
|
+ Vec2.copy(_panStart, mouseOnScreenVec2)
|
|
|
Vec2.copy(_panEnd, _panStart)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- if (buttons === ButtonsType.Flag.Primary) {
|
|
|
- Vec2.copy(_moveCurr, getMouseOnCircle(pageX, pageY))
|
|
|
- } else if (buttons === ButtonsType.Flag.Auxilary) {
|
|
|
- Vec2.copy(_zoomEnd, getMouseOnScreen(pageX, pageY))
|
|
|
- } else if (buttons === ButtonsType.Flag.Secondary) {
|
|
|
- Vec2.copy(_panEnd, getMouseOnScreen(pageX, pageY))
|
|
|
- }
|
|
|
+ if (dragRotate) Vec2.copy(_rotCurr, mouseOnCircleVec2)
|
|
|
+ if (dragRotateZ) Vec2.copy(_zRotCurr, mouseOnCircleVec2)
|
|
|
+ if (dragZoom) Vec2.copy(_zoomEnd, mouseOnScreenVec2)
|
|
|
+ if (dragPan) Vec2.copy(_panEnd, mouseOnScreenVec2)
|
|
|
}
|
|
|
|
|
|
function onInteractionEnd() {
|
|
|
_isInteracting = false;
|
|
|
}
|
|
|
|
|
|
- function onWheel({ dy }: WheelInput) {
|
|
|
- _zoomEnd[1] += dy * 0.0001
|
|
|
+ function onWheel({ dx, dy, dz, buttons, modifiers }: WheelInput) {
|
|
|
+ if (Bindings.match(p.bindings.scroll.zoom, buttons, modifiers)) {
|
|
|
+ _zoomEnd[1] += absMax(dx, dy, dz) * 0.0001
|
|
|
+ }
|
|
|
+ if (Bindings.match(p.bindings.scroll.clipNear, buttons, modifiers)) {
|
|
|
+ const radius = Math.max(0, camera.state.radius + absMax(dx, dy, dz) * 0.005)
|
|
|
+ camera.setState({ radius })
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- function onPinch({ fraction }: PinchInput) {
|
|
|
- _isInteracting = true;
|
|
|
- _zoomEnd[1] += (fraction - 1) * 0.1
|
|
|
+ function onPinch({ fraction, buttons, modifiers }: PinchInput) {
|
|
|
+ if (Bindings.match(p.bindings.scroll.zoom, buttons, modifiers)) {
|
|
|
+ _isInteracting = true;
|
|
|
+ _zoomEnd[1] += (fraction - 1) * 0.1
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
function dispose() {
|
|
@@ -292,7 +337,7 @@ namespace TrackballControls {
|
|
|
function spin(deltaT: number) {
|
|
|
const frameSpeed = (p.spinSpeed || 0) / 1000;
|
|
|
_spinSpeed[0] = 60 * Math.min(Math.abs(deltaT), 1000 / 8) / 1000 * frameSpeed;
|
|
|
- if (!_isInteracting) Vec2.add(_moveCurr, _movePrev, _spinSpeed);
|
|
|
+ if (!_isInteracting) Vec2.add(_rotCurr, _rotPrev, _spinSpeed);
|
|
|
}
|
|
|
|
|
|
// force an update at start
|