diff --git a/src/mol-canvas3d/camera.ts b/src/mol-canvas3d/camera.ts index 553225dc30bd4f29b7aa05aa79ead926cbe7a389..2f6f5781c2266c748c0cb0985fb30658d8ae1f1a 100644 --- a/src/mol-canvas3d/camera.ts +++ b/src/mol-canvas3d/camera.ts @@ -127,6 +127,23 @@ class Camera implements ICamera { return state; } + getInvariantFocus(target: Vec3, radius: number, up: Vec3, dir: Vec3): Partial<Camera.Snapshot> { + const r = Math.max(radius, 0.01); + const targetDistance = this.getTargetDistance(r); + + Vec3.copy(this.deltaDirection, dir); + Vec3.setMagnitude(this.deltaDirection, this.deltaDirection, targetDistance); + Vec3.sub(this.newPosition, target, this.deltaDirection); + + const state = Camera.copySnapshot(Camera.createDefaultSnapshot(), this.state); + state.target = Vec3.clone(target); + state.radius = r; + state.position = Vec3.clone(this.newPosition); + Vec3.copy(state.up, up); + + return state; + } + focus(target: Vec3, radius: number, durationMs?: number, up?: Vec3, dir?: Vec3) { if (radius > 0) { this.setState(this.getFocus(target, radius, up, dir), durationMs); diff --git a/src/mol-canvas3d/canvas3d.ts b/src/mol-canvas3d/canvas3d.ts index b35321002134afda10e3997870ba5840edc8f5cb..f2f817cdce7cb7c0890dbf089210575d24c3fe12 100644 --- a/src/mol-canvas3d/canvas3d.ts +++ b/src/mol-canvas3d/canvas3d.ts @@ -305,7 +305,11 @@ namespace Canvas3D { let loci: Loci = EmptyLoci; let repr: Representation.Any = Representation.Empty; if (pickingId) { + const cameraHelperLoci = helper.camera.getLoci(pickingId); + if (cameraHelperLoci !== EmptyLoci) return { loci: cameraHelperLoci, repr }; + loci = helper.handle.getLoci(pickingId); + reprRenderObjects.forEach((_, _repr) => { const _loci = _repr.getLoci(pickingId); if (!isEmptyLoci(_loci)) { @@ -467,7 +471,7 @@ namespace Canvas3D { const duration = nextCameraResetDuration === undefined ? p.cameraResetDurationMs : nextCameraResetDuration; const focus = camera.getFocus(center, radius); const next = typeof nextCameraResetSnapshot === 'function' ? nextCameraResetSnapshot(scene, camera) : nextCameraResetSnapshot; - const snapshot = next ? { ...focus, ...nextCameraResetSnapshot } : focus; + const snapshot = next ? { ...focus, ...next } : focus; camera.setState({ ...snapshot, radiusMax: scene.boundingSphere.radius }, duration); } diff --git a/src/mol-canvas3d/helper/camera-helper.ts b/src/mol-canvas3d/helper/camera-helper.ts index 757446edca24303323cf2c5d83c13ccbb2a5e230..2c0d08aecc0804f6ec87780513638483653d1c76 100644 --- a/src/mol-canvas3d/helper/camera-helper.ts +++ b/src/mol-canvas3d/helper/camera-helper.ts @@ -4,27 +4,29 @@ * @author Alexander Rose <alexander.rose@weirdbyte.de> */ -import { WebGLContext } from '../../mol-gl/webgl/context'; -import { Scene } from '../../mol-gl/scene'; -import { Camera, ICamera } from '../camera'; -import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder'; -import { Vec3, Mat4 } from '../../mol-math/linear-algebra'; +import produce from 'immer'; +import { addCylinder } from '../../mol-geo/geometry/mesh/builder/cylinder'; import { addSphere } from '../../mol-geo/geometry/mesh/builder/sphere'; -import { GraphicsRenderObject } from '../../mol-gl/render-object'; import { Mesh } from '../../mol-geo/geometry/mesh/mesh'; -import { ColorNames } from '../../mol-util/color/names'; -import { addCylinder } from '../../mol-geo/geometry/mesh/builder/cylinder'; -import { Viewport } from '../camera/util'; +import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder'; +import { PickingId } from '../../mol-geo/geometry/picking'; +import { GraphicsRenderObject } from '../../mol-gl/render-object'; +import { Scene } from '../../mol-gl/scene'; +import { WebGLContext } from '../../mol-gl/webgl/context'; import { Sphere3D } from '../../mol-math/geometry'; -import { ParamDefinition as PD } from '../../mol-util/param-definition'; -import produce from 'immer'; +import { Mat4, Vec3 } from '../../mol-math/linear-algebra'; +import { DataLoci, EmptyLoci, Loci } from '../../mol-model/loci'; import { Shape } from '../../mol-model/shape'; +import { ColorNames } from '../../mol-util/color/names'; +import { ParamDefinition as PD } from '../../mol-util/param-definition'; +import { Camera, ICamera } from '../camera'; +import { Viewport } from '../camera/util'; // TODO add scale line/grid const AxesParams = { ...Mesh.Params, - alpha: { ...Mesh.Params.alpha, defaultValue: 0.33 }, + alpha: { ...Mesh.Params.alpha, defaultValue: 0.51 }, ignoreLight: { ...Mesh.Params.ignoreLight, defaultValue: true }, colorX: PD.Color(ColorNames.red, { isEssential: true }), colorY: PD.Color(ColorNames.green, { isEssential: true }), @@ -87,6 +89,12 @@ export class CameraHelper { return this.props.axes.name === 'on'; } + getLoci(pickingId: PickingId) { + const { objectId, groupId, instanceId } = pickingId; + if (!this.renderObject || objectId !== this.renderObject.id || groupId === CameraHelperAxis.None) return EmptyLoci; + return CameraAxesLoci(this, groupId, instanceId); + } + update(camera: ICamera) { if (!this.renderObject) return; @@ -102,6 +110,38 @@ export class CameraHelper { } } +export const enum CameraHelperAxis { + None = 0, + X, + Y, + Z, + XY, + XZ, + YZ +} + +function getAxisLabel(axis: number) { + switch (axis) { + case CameraHelperAxis.X: return 'X Axis'; + case CameraHelperAxis.Y: return 'Y Axis'; + case CameraHelperAxis.Z: return 'Z Axis'; + case CameraHelperAxis.XY: return 'XY Plane'; + case CameraHelperAxis.XZ: return 'XZ Plane'; + case CameraHelperAxis.YZ: return 'YZ Plane'; + default: return 'Axes'; + } +} + +function CameraAxesLoci(cameraHelper: CameraHelper, groupId: number, instanceId: number) { + return DataLoci('camera-axes', cameraHelper, [{ groupId, instanceId }], + void 0 /** bounding sphere */, + () => getAxisLabel(groupId)); +} +export type CameraAxesLoci = ReturnType<typeof CameraAxesLoci> +export function isCameraAxesLoci(x: Loci): x is CameraAxesLoci { + return x.kind === 'data-loci' && x.tag === 'camera-axes'; +} + function updateCamera(camera: Camera, viewport: Viewport, viewOffset: Camera.ViewOffset) { const { near, far } = camera; @@ -134,27 +174,52 @@ function updateCamera(camera: Camera, viewport: Viewport, viewOffset: Camera.Vie function createAxesMesh(scale: number, mesh?: Mesh) { const state = MeshBuilder.createState(512, 256, mesh); - const radius = 0.05 * scale; + const radius = 0.075 * scale; const x = Vec3.scale(Vec3(), Vec3.unitX, scale); const y = Vec3.scale(Vec3(), Vec3.unitY, scale); const z = Vec3.scale(Vec3(), Vec3.unitZ, scale); const cylinderProps = { radiusTop: radius, radiusBottom: radius, radialSegments: 32 }; - state.currentGroup = 0; + state.currentGroup = CameraHelperAxis.None; addSphere(state, Vec3.origin, radius, 2); - state.currentGroup = 1; + state.currentGroup = CameraHelperAxis.X; addSphere(state, x, radius, 2); addCylinder(state, Vec3.origin, x, 1, cylinderProps); - state.currentGroup = 2; + state.currentGroup = CameraHelperAxis.Y; addSphere(state, y, radius, 2); addCylinder(state, Vec3.origin, y, 1, cylinderProps); - state.currentGroup = 3; + state.currentGroup = CameraHelperAxis.Z; addSphere(state, z, radius, 2); addCylinder(state, Vec3.origin, z, 1, cylinderProps); + Vec3.scale(x, x, 0.5); + Vec3.scale(y, y, 0.5); + Vec3.scale(z, z, 0.5); + + state.currentGroup = CameraHelperAxis.XY; + MeshBuilder.addTriangle(state, Vec3.origin, x, y); + MeshBuilder.addTriangle(state, Vec3.origin, y, x); + const xy = Vec3.add(Vec3(), x, y); + MeshBuilder.addTriangle(state, xy, x, y); + MeshBuilder.addTriangle(state, xy, y, x); + + state.currentGroup = CameraHelperAxis.XZ; + MeshBuilder.addTriangle(state, Vec3.origin, x, z); + MeshBuilder.addTriangle(state, Vec3.origin, z, x); + const xz = Vec3.add(Vec3(), x, z); + MeshBuilder.addTriangle(state, xz, x, z); + MeshBuilder.addTriangle(state, xz, z, x); + + state.currentGroup = CameraHelperAxis.YZ; + MeshBuilder.addTriangle(state, Vec3.origin, y, z); + MeshBuilder.addTriangle(state, Vec3.origin, z, y); + const yz = Vec3.add(Vec3(), y, z); + MeshBuilder.addTriangle(state, yz, y, z); + MeshBuilder.addTriangle(state, yz, z, y); + return MeshBuilder.getMesh(state); } diff --git a/src/mol-canvas3d/passes/pick.ts b/src/mol-canvas3d/passes/pick.ts index a694af7127cef0925f64cbc21591e34656845722..946f93ef09583279185a4367785fd0001faa3326 100644 --- a/src/mol-canvas3d/passes/pick.ts +++ b/src/mol-canvas3d/passes/pick.ts @@ -66,14 +66,20 @@ export class PickPass { private renderVariant(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper, variant: GraphicsRenderVariant) { const depth = this.drawPass.depthTexturePrimitives; renderer.clear(false); + + renderer.update(camera); renderer.renderPick(scene.primitives, camera, variant, null); renderer.renderPick(scene.volumes, camera, variant, depth); renderer.renderPick(helper.handle.scene, camera, variant, null); + + if (helper.camera.isEnabled) { + helper.camera.update(camera); + renderer.update(helper.camera.camera); + renderer.renderPick(helper.camera.scene, helper.camera.camera, variant, null); + } } render(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper) { - renderer.update(camera); - this.objectPickTarget.bind(); this.renderVariant(renderer, camera, scene, helper, 'pickObject'); diff --git a/src/mol-math/linear-algebra/3d/vec3.ts b/src/mol-math/linear-algebra/3d/vec3.ts index 4e0cd46cf29e8f6ae9bf3d82b14f8c1287451795..f6d4774ff9b316eb7df5359a3845db96a630c10d 100644 --- a/src/mol-math/linear-algebra/3d/vec3.ts +++ b/src/mol-math/linear-algebra/3d/vec3.ts @@ -590,6 +590,9 @@ namespace Vec3 { export const unitX: ReadonlyVec3 = create(1, 0, 0); export const unitY: ReadonlyVec3 = create(0, 1, 0); export const unitZ: ReadonlyVec3 = create(0, 0, 1); + export const negUnitX: ReadonlyVec3 = create(-1, 0, 0); + export const negUnitY: ReadonlyVec3 = create(0, -1, 0); + export const negUnitZ: ReadonlyVec3 = create(0, 0, -1); } export { Vec3 }; \ No newline at end of file diff --git a/src/mol-model/loci.ts b/src/mol-model/loci.ts index 6a808bbe176ec16e07d7444ad8998ddb8d94d4b9..87b7d63608f53483842fd21bb76baf36807b784a 100644 --- a/src/mol-model/loci.ts +++ b/src/mol-model/loci.ts @@ -37,9 +37,10 @@ export interface DataLoci<T = unknown, E = unknown> { readonly kind: 'data-loci', readonly tag: string readonly data: T, - readonly elements: ReadonlyArray<E> + readonly elements: ReadonlyArray<E>, - getBoundingSphere(boundingSphere: Sphere3D): Sphere3D + /** if undefined, won't zoom */ + getBoundingSphere?(boundingSphere: Sphere3D): Sphere3D getLabel(): string } export function isDataLoci(x?: Loci): x is DataLoci { @@ -159,7 +160,7 @@ namespace Loci { } else if (loci.kind === 'group-loci') { return ShapeGroup.getBoundingSphere(loci, boundingSphere); } else if (loci.kind === 'data-loci') { - return loci.getBoundingSphere(boundingSphere); + return loci.getBoundingSphere?.(boundingSphere); } else if (loci.kind === 'volume-loci') { return Volume.getBoundingSphere(loci.volume, boundingSphere); } else if (loci.kind === 'isosurface-loci') { diff --git a/src/mol-plugin/behavior/dynamic/camera.ts b/src/mol-plugin/behavior/dynamic/camera.ts index 2d114aeaf0b3bc82ddeccd94d3137ef5d81a9a85..eb38500d4bb1ea404c794a34513d13b03d0e06f9 100644 --- a/src/mol-plugin/behavior/dynamic/camera.ts +++ b/src/mol-plugin/behavior/dynamic/camera.ts @@ -11,6 +11,8 @@ import { PluginBehavior } from '../behavior'; import { ButtonsType, ModifiersKeys } from '../../../mol-util/input/input-observer'; import { Binding } from '../../../mol-util/binding'; import { PluginCommands } from '../../commands'; +import { CameraHelperAxis, isCameraAxesLoci } from '../../../mol-canvas3d/helper/camera-helper'; +import { Vec3 } from '../../../mol-math/linear-algebra'; const B = ButtonsType; const M = ModifiersKeys; @@ -62,4 +64,58 @@ export const FocusLoci = PluginBehavior.create<FocusLociProps>({ }, params: () => FocusLociParams, display: { name: 'Camera Focus Loci on Canvas' } +}); + +export const CameraAxisHelper = PluginBehavior.create<{}>({ + name: 'camera-axis-helper', + category: 'interaction', + ctor: class extends PluginBehavior.Handler<{}> { + register(): void { + + let lastPlane = CameraHelperAxis.None; + let state = 0; + + this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current }) => { + if (!this.ctx.canvas3d || !isCameraAxesLoci(current.loci)) return; + + const axis = current.loci.elements[0].groupId; + if (axis === CameraHelperAxis.None) { + lastPlane = CameraHelperAxis.None; + state = 0; + return; + } else if (axis >= CameraHelperAxis.X && axis <= CameraHelperAxis.Z) { + lastPlane = CameraHelperAxis.None; + state = 0; + const up = Vec3(); + up[axis - 1] = 1; + this.ctx.canvas3d.requestCameraReset({ snapshot: { up } }); + } else { + if (lastPlane === axis) { + state = (state + 1) % 2; + } else { + lastPlane = axis; + state = 0; + } + + let up: Vec3, dir: Vec3; + if (axis === CameraHelperAxis.XY) { + up = state ? Vec3.unitX : Vec3.unitY; + dir = Vec3.negUnitZ; + } else if (axis === CameraHelperAxis.XZ) { + up = state ? Vec3.unitX : Vec3.unitZ; + dir = Vec3.negUnitY; + } else { + up = state ? Vec3.unitY : Vec3.unitZ; + dir = Vec3.negUnitX; + } + + this.ctx.canvas3d.requestCameraReset({ + snapshot: (scene, camera) => camera.getInvariantFocus(scene.boundingSphereVisible.center, scene.boundingSphereVisible.radius, up, dir) + }); + } + }); + } + }, + params: () => ({}), + display: { name: 'Camera Axis Helper' } }); \ No newline at end of file diff --git a/src/mol-plugin/spec.ts b/src/mol-plugin/spec.ts index 0ea667b5e0289f90ecf776407df52d11147c739e..67c9c5885101769154b7dd4d4c653ccb9e428235 100644 --- a/src/mol-plugin/spec.ts +++ b/src/mol-plugin/spec.ts @@ -111,6 +111,7 @@ export const DefaultPluginSpec = (): PluginSpec => ({ PluginSpec.Behavior(PluginBehaviors.Representation.DefaultLociLabelProvider), PluginSpec.Behavior(PluginBehaviors.Representation.FocusLoci), PluginSpec.Behavior(PluginBehaviors.Camera.FocusLoci), + PluginSpec.Behavior(PluginBehaviors.Camera.CameraAxisHelper), PluginSpec.Behavior(StructureFocusRepresentation), PluginSpec.Behavior(PluginBehaviors.CustomProps.StructureInfo),