diff --git a/CHANGELOG.md b/CHANGELOG.md index 419e49b61..39b93620f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Note that since we don't clearly distinguish between a public and private interf ## [Unreleased] - Fix empty transforms default in `ShapeFromPly` +- Add `Camera.changed` event and rotation/translation setter/getter - Add `instanceGranularity: 'auto'` as a memory guard - Honor `instanceGranularity` in `Visual.getLoci` - Add mesoscale representation preset diff --git a/src/mol-canvas3d/camera.ts b/src/mol-canvas3d/camera.ts index fd1569d2d..8a1ff7ecd 100644 --- a/src/mol-canvas3d/camera.ts +++ b/src/mol-canvas3d/camera.ts @@ -7,7 +7,7 @@ import { Viewport, cameraProject, cameraUnproject } from './camera/util'; import { CameraTransitionManager } from './camera/transition'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, Subject } from 'rxjs'; import { Scene } from '../mol-gl/scene'; import { assertUnreachable } from '../mol-util/type-helpers'; import { Ray3D } from '../mol-math/geometry/primitives/ray3d'; @@ -15,6 +15,7 @@ import { Mat4 } from '../mol-math/linear-algebra/3d/mat4'; import { Vec4 } from '../mol-math/linear-algebra/3d/vec4'; import { Vec3 } from '../mol-math/linear-algebra/3d/vec3'; import { EPSILON } from '../mol-math/linear-algebra/3d/common'; +import { Euler } from '../mol-math/linear-algebra/3d/euler'; export type { ICamera }; @@ -42,6 +43,12 @@ interface ICamera { } const tmpClip = Vec4(); +const tmpForward = Vec3(); +const tmpRight = Vec3(); +const tmpUp = Vec3(); +const tmpBack = Vec3(); +const tmpDelta = Vec3(); +const tmpRotMat = Mat4.identity(); export class Camera implements ICamera { readonly view: Mat4 = Mat4.identity(); @@ -70,6 +77,8 @@ export class Camera implements ICamera { readonly transition: CameraTransitionManager = new CameraTransitionManager(this); readonly stateChanged = new BehaviorSubject>(this.state); + /** Fires whenever update() produces a changed view/projection (covers all mutations, including direct ones from controls). */ + readonly changed = new Subject(); get position() { return this.state.position; } set position(v: Vec3) { Vec3.copy(this.state.position, v); } @@ -123,6 +132,7 @@ export class Camera implements ICamera { Mat4.copy(this.prevView, this.view); Mat4.copy(this.prevProjection, this.projection); + this.changed.next(); } return changed; @@ -237,6 +247,57 @@ export class Camera implements ICamera { return out; } + /** How much the camera is rotated around its target. Uses 'ZYX' order. */ + getRotation(out: Euler) { + const { position, target, up } = this.state; + Vec3.normalize(tmpForward, Vec3.sub(tmpForward, target, position)); + Vec3.normalize(tmpRight, Vec3.cross(tmpRight, tmpForward, up)); + Vec3.cross(tmpUp, tmpRight, tmpForward); + + Mat4.setIdentity(tmpRotMat); + tmpRotMat[0] = tmpRight[0]; tmpRotMat[1] = tmpRight[1]; tmpRotMat[2] = tmpRight[2]; + tmpRotMat[4] = tmpUp[0]; tmpRotMat[5] = tmpUp[1]; tmpRotMat[6] = tmpUp[2]; + tmpRotMat[8] = -tmpForward[0]; tmpRotMat[9] = -tmpForward[1]; tmpRotMat[10] = -tmpForward[2]; + + return Euler.fromMat4(out, tmpRotMat, 'ZYX'); + } + + /** Set the camera rotation around its target. Expects 'ZYX' order. */ + setRotation(rotation: Euler, durationMs?: number) { + const snapshot = this.state as Camera.Snapshot; + const distance = Vec3.distance(snapshot.position, snapshot.target); + + Mat4.fromEuler(tmpRotMat, rotation, 'ZYX'); + + // back = R * (0,0,1) → column 2 of R + Vec3.set(tmpBack, tmpRotMat[8], tmpRotMat[9], tmpRotMat[10]); + // up = R * (0,1,0) → column 1 of R + Vec3.set(tmpUp, tmpRotMat[4], tmpRotMat[5], tmpRotMat[6]); + + const state = Camera.copySnapshot(Camera.createDefaultSnapshot(), snapshot); + Vec3.scaleAndAdd(state.position, snapshot.target, tmpBack, distance); + Vec3.copy(state.up, tmpUp); + + this.setState(state, durationMs); + } + + /** Translation of the camera target relative to world origin (0, 0, 0) */ + getTranslation(out: Vec3) { + return Vec3.copy(out, this.state.target); + } + + /** Set the camera target to the given translation, moving position by the same delta so orientation/distance are preserved */ + setTranslation(translation: Vec3, durationMs?: number) { + const snapshot = this.state as Camera.Snapshot; + Vec3.sub(tmpDelta, translation, snapshot.target); + + const state = Camera.copySnapshot(Camera.createDefaultSnapshot(), snapshot); + Vec3.add(state.position, snapshot.position, tmpDelta); + Vec3.copy(state.target, translation); + + this.setState(state, durationMs); + } + constructor(state?: Partial, viewport = Viewport.create(0, 0, 128, 128)) { this.viewport = viewport; Camera.copySnapshot(this.state, state);