uglymol
Version:
Macromolecular Viewer for Crystallographers
291 lines (270 loc) • 9.61 kB
text/typescript
import { Vector3, Quaternion, Matrix4 } from './uthree/main';
import type { OrthographicCamera } from './uthree/main';
// Properties defined with Object.defineProperties() in JS are not understood
// by TypeScript; add them here.
export type OrCameraType = OrthographicCamera & {
position: Vector3;
quaternion: Quaternion;
scale: Vector3;
modelViewMatrix: Matrix4;
};
// map 2d position to sphere with radius 1.
function project_on_ball(x: number, y: number) {
let z = 0;
const length_sq = x * x + y * y;
if (length_sq < 1) { // in ellipse
z = Math.sqrt(1.0 - length_sq);
} else { // in a corner
const length = Math.sqrt(length_sq);
x /= length;
y /= length;
}
return [x, y, z]; // guaranteed to be normalized
}
// object used in computations (to avoid creating and deleting it)
const _m1 = new Matrix4();
export const STATE = { NONE: -1, ROTATE: 0, PAN: 1, ZOOM: 2, PAN_ZOOM: 3,
SLAB: 4, ROLL: 5, AUTO_ROTATE: 6, GO: 7 };
const auto_speed = 1.0;
// based on three.js/examples/js/controls/OrthographicTrackballControls.js
export class Controls {
_camera: OrCameraType;
_target: Vector3;
_state: number;
_rotate_start: Vector3;
_rotate_end: Vector3;
_zoom_start: [number, number];
_zoom_end: [number, number];
_pinch_start: number;
_pinch_end: number;
_pan_start: [number, number];
_pan_end: [number, number];
_panned: boolean;
_rotating: number | boolean;
_auto_stamp: number | null;
_go_func: (() => void) | null;
slab_width: [number, number, number|null];
constructor(camera: OrCameraType, target: Vector3) {
this._camera = camera;
this._target = target;
this._state = STATE.NONE;
this._rotate_start = new Vector3();
this._rotate_end = new Vector3();
this._zoom_start = [0, 0];
this._zoom_end = [0, 0];
this._pinch_start = 0;
this._pinch_end = 0;
this._pan_start = [0, 0];
this._pan_end = [0, 0];
this._panned = true;
this._rotating = 0.0;
this._auto_stamp = null;
this._go_func = null;
// the far plane is more distant from the target than the near plane (3:1)
this.slab_width = [2.5, 7.5, null];
}
_rotate_camera(eye: Vector3) {
const quat = new Quaternion();
quat.setFromUnitVectors(this._rotate_end, this._rotate_start);
eye.applyQuaternion(quat);
this._camera.up.applyQuaternion(quat);
this._rotate_end.applyQuaternion(quat);
this._rotate_start.copy(this._rotate_end);
}
_zoom_camera(eye: Vector3) {
const dx = this._zoom_end[0] - this._zoom_start[0];
const dy = this._zoom_end[1] - this._zoom_start[1];
if (this._state === STATE.ZOOM) {
this._camera.zoom /= (1 - dx + dy);
} else if (this._state === STATE.SLAB) {
this._target.addScaledVector(eye, -5.0 / eye.length() * dy);
} else if (this._state === STATE.ROLL) {
const quat = new Quaternion();
quat.setFromAxisAngle(eye, 0.05 * (dx - dy));
this._camera.up.applyQuaternion(quat);
}
this._zoom_start[0] = this._zoom_end[0];
this._zoom_start[1] = this._zoom_end[1];
return this._state === STATE.SLAB ? 10*dx : null;
}
_pan_camera(eye: Vector3) {
let dx = this._pan_end[0] - this._pan_start[0];
let dy = this._pan_end[1] - this._pan_start[1];
dx *= 0.5 * (this._camera.right - this._camera.left) / this._camera.zoom;
dy *= 0.5 * (this._camera.bottom - this._camera.top) / this._camera.zoom;
const pan = eye.clone().cross(this._camera.up).setLength(dx);
pan.addScaledVector(this._camera.up, dy / this._camera.up.length());
this._camera.position.add(pan);
this._target.add(pan);
this._pan_start[0] = this._pan_end[0];
this._pan_start[1] = this._pan_end[1];
}
_auto_rotate(eye: Vector3) {
this._rotate_start.copy(eye).normalize();
const now = Date.now();
const elapsed = (this._auto_stamp !== null ? now - this._auto_stamp : 16.7);
let speed = 1.8e-5 * elapsed * auto_speed;
this._auto_stamp = now;
if (this._rotating === true) {
speed = -speed;
} else if (this._rotating !== false) {
this._rotating += 0.02;
speed = 4e-5 * auto_speed * Math.cos(this._rotating);
}
this._rotate_end.crossVectors(this._camera.up, eye).multiplyScalar(speed)
.add(this._rotate_start);
}
toggle_auto(param: number|boolean) {
if (this._state === STATE.AUTO_ROTATE &&
typeof param === typeof this._rotating) {
this._state = STATE.NONE;
} else {
this._state = STATE.AUTO_ROTATE;
this._auto_stamp = null;
this._rotating = param;
}
}
is_going() { return this._state === STATE.GO; }
is_moving() { return this._state !== STATE.NONE; }
update() {
let changed = false;
const eye = this._camera.position.clone().sub(this._target);
if (this._state === STATE.AUTO_ROTATE) {
this._auto_rotate(eye);
}
if (!this._rotate_start.equals(this._rotate_end)) {
this._rotate_camera(eye);
changed = true;
}
if (this._pinch_end !== this._pinch_start) {
this._camera.zoom *= this._pinch_end / this._pinch_start;
this._pinch_start = this._pinch_end;
changed = true;
}
if (this._zoom_end[0] !== this._zoom_start[0] ||
this._zoom_end[1] !== this._zoom_start[1]) {
const dslab = this._zoom_camera(eye);
if (dslab) {
this.slab_width[0] = Math.max(this.slab_width[0] + dslab, 0.01);
this.slab_width[1] = Math.max(this.slab_width[1] + dslab, 0.01);
}
changed = true;
}
if (this._pan_end[0] !== this._pan_start[0] ||
this._pan_end[1] !== this._pan_start[1]) {
this._pan_camera(eye);
this._panned = true;
changed = true;
}
this._camera.position.addVectors(this._target, eye);
if (this._state === STATE.GO && this._go_func) {
this._go_func();
changed = true;
}
//this._camera.lookAt(this._target);
_m1.lookAt(this._camera.position, this._target, this._camera.up);
this._camera.quaternion.setFromRotationMatrix(_m1);
return changed;
}
start(new_state: number, x: number, y: number, dist?: number) {
if (this._state === STATE.NONE || this._state === STATE.AUTO_ROTATE) {
this._state = new_state;
}
this.move(x, y, dist);
switch (this._state) {
case STATE.ROTATE:
this._rotate_start.copy(this._rotate_end);
break;
case STATE.ZOOM:
case STATE.SLAB:
case STATE.ROLL:
this._zoom_start[0] = this._zoom_end[0];
this._zoom_start[1] = this._zoom_end[1];
break;
case STATE.PAN:
this._pan_start[0] = this._pan_end[0];
this._pan_start[1] = this._pan_end[1];
this._panned = false;
break;
case STATE.PAN_ZOOM:
this._pinch_start = this._pinch_end;
this._pan_start[0] = this._pan_end[0];
this._pan_start[1] = this._pan_end[1];
break;
}
}
move(x: number, y: number, dist?: number) {
switch (this._state) {
case STATE.ROTATE: {
const xyz = project_on_ball(x, y);
//console.log(this._camera.projectionMatrix);
//console.log(this._camera.matrixWorld);
// TODO maybe use project()/unproject()/applyProjection()
const eye = this._camera.position.clone().sub(this._target);
const up = this._camera.up;
this._rotate_end.crossVectors(up, eye).setLength(xyz[0]);
this._rotate_end.addScaledVector(up, xyz[1] / up.length());
this._rotate_end.addScaledVector(eye, xyz[2] / eye.length());
break;
}
case STATE.ZOOM:
case STATE.SLAB:
case STATE.ROLL:
this._zoom_end = [x, y];
break;
case STATE.PAN:
this._pan_end = [x, y];
break;
case STATE.PAN_ZOOM:
if (dist == null) return; // should not happen
this._pan_end = [x, y];
this._pinch_end = dist;
break;
}
}
// returned coordinates can be used for atom picking
stop() {
let ret = null;
if (this._state === STATE.PAN && !this._panned) ret = this._pan_end;
this._state = STATE.NONE;
this._rotate_start.copy(this._rotate_end);
this._pinch_start = this._pinch_end;
this._pan_start[0] = this._pan_end[0];
this._pan_start[1] = this._pan_end[1];
return ret;
}
// cam_up (if set) must be orthogonal to the view
go_to(targ: Vector3, cam_pos?: Vector3, cam_up?: Vector3, steps?: number) {
if ((!targ || targ.distanceToSquared(this._target) < 0.001) &&
(!cam_pos || cam_pos.distanceToSquared(this._camera.position) < 0.1) &&
(!cam_up || cam_up.distanceToSquared(this._camera.up) < 0.1)) {
return;
}
this._state = STATE.GO;
steps = (steps || 60) / auto_speed;
const alphas: number[] = [];
let prev_pos = 0;
for (let i = 1; i <= steps; ++i) {
let pos = i / steps;
// quadratic easing
pos = pos < 0.5 ? 2 * pos * pos : -2 * pos * (pos-2) - 1;
alphas.push((pos - prev_pos) / (1 - prev_pos));
prev_pos = pos;
}
this._go_func = function () {
const a = alphas.shift();
if (targ) {
// unspecified cam_pos - _camera stays in the same distance to _target
if (!cam_pos) this._camera.position.sub(this._target);
this._target.lerp(targ, a);
if (!cam_pos) this._camera.position.add(this._target);
}
if (cam_pos) this._camera.position.lerp(cam_pos, a);
if (cam_up) this._camera.up.lerp(cam_up, a);
if (alphas.length === 0) {
this._state = STATE.NONE;
this._go_func = null;
}
};
}
}