diff --git a/src/mol-canvas3d/camera.ts b/src/mol-canvas3d/camera.ts index 38d77bef246faf01e48f20b8c1e2cce288cb0aec..616c5bf793bb62b095e8fbfff4bcf6ab22253105 100644 --- a/src/mol-canvas3d/camera.ts +++ b/src/mol-canvas3d/camera.ts @@ -82,6 +82,13 @@ class Camera implements Object3D { return ret; } + focus(target: Vec3, radius: number) { + const position = Vec3.zero(); + Vec3.scale(position, this.state.direction, -radius); + Vec3.add(position, position, target); + this.setState({ target, position }); + } + // lookAt(target: Vec3) { // cameraLookAt(this.position, this.up, this.direction, target); // } diff --git a/src/mol-math/geometry/centroid-helper.ts b/src/mol-math/geometry/centroid-helper.ts new file mode 100644 index 0000000000000000000000000000000000000000..eaa7001dc4d46fa8ff144aba18c9ddc551b891ec --- /dev/null +++ b/src/mol-math/geometry/centroid-helper.ts @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { Vec3 } from 'mol-math/linear-algebra/3d'; + +export class CentroidHelper { + private count = 0; + + center: Vec3 = Vec3.zero(); + radiusSq = 0; + + reset() { + Vec3.set(this.center, 0, 0, 0); + this.radiusSq = 0; + this.count = 0; + } + + includeStep(p: Vec3) { + Vec3.add(this.center, this.center, p); + this.count++; + } + + finishedIncludeStep() { + if (this.count === 0) return; + Vec3.scale(this.center, this.center, 1 / this.count); + } + + radiusStep(p: Vec3) { + const d = Vec3.squaredDistance(p, this.center); + if (d > this.radiusSq) this.radiusSq = d; + } + + constructor() { + + } +} \ No newline at end of file diff --git a/src/mol-model/loci.ts b/src/mol-model/loci.ts index 054e7ea69fce89b7fe2b90abd44034de2e8a2e4d..6bc2d08de6d013bd7946df8d27fdd6505870fe70 100644 --- a/src/mol-model/loci.ts +++ b/src/mol-model/loci.ts @@ -7,6 +7,10 @@ import { StructureElement } from './structure' import { Link } from './structure/structure/unit/links' import { Shape } from './shape'; +import { Sphere3D } from 'mol-math/geometry'; +import { CentroidHelper } from 'mol-math/geometry/centroid-helper'; +import { Vec3 } from 'mol-math/linear-algebra'; +import { OrderedSet } from 'mol-data/int'; /** A Loci that includes every loci */ export const EveryLoci = { kind: 'every-loci' as 'every-loci' } @@ -37,4 +41,62 @@ export function areLociEqual(lociA: Loci, lociB: Loci) { return false } -export type Loci = StructureElement.Loci | Link.Loci | EveryLoci | EmptyLoci | Shape.Loci \ No newline at end of file + +export { Loci } + +type Loci = StructureElement.Loci | Link.Loci | EveryLoci | EmptyLoci | Shape.Loci + +namespace Loci { + + const sphereHelper = new CentroidHelper(), tempPos = Vec3.zero(); + + export function getBoundingSphere(loci: Loci): Sphere3D | undefined { + if (loci.kind === 'every-loci' || loci.kind === 'empty-loci') return void 0; + + sphereHelper.reset(); + if (loci.kind === 'element-loci') { + for (const e of loci.elements) { + const { indices } = e; + const pos = e.unit.conformation.position; + const { elements } = e.unit; + for (let i = 0, _i = OrderedSet.size(indices); i < _i; i++) { + pos(elements[OrderedSet.getAt(indices, i)], tempPos); + sphereHelper.includeStep(tempPos); + } + } + sphereHelper.finishedIncludeStep(); + for (const e of loci.elements) { + const { indices } = e; + const pos = e.unit.conformation.position; + const { elements } = e.unit; + for (let i = 0, _i = OrderedSet.size(indices); i < _i; i++) { + pos(elements[OrderedSet.getAt(indices, i)], tempPos); + sphereHelper.radiusStep(tempPos); + } + } + } else if (loci.kind === 'link-loci') { + for (const e of loci.links) { + let pos = e.aUnit.conformation.position; + pos(e.aUnit.elements[e.aIndex], tempPos); + sphereHelper.includeStep(tempPos); + pos = e.bUnit.conformation.position; + pos(e.bUnit.elements[e.bIndex], tempPos); + sphereHelper.includeStep(tempPos); + } + sphereHelper.finishedIncludeStep(); + for (const e of loci.links) { + let pos = e.aUnit.conformation.position; + pos(e.aUnit.elements[e.aIndex], tempPos); + sphereHelper.radiusStep(tempPos); + pos = e.bUnit.conformation.position; + pos(e.bUnit.elements[e.bIndex], tempPos); + sphereHelper.radiusStep(tempPos); + } + } else if (loci.kind === 'group-loci') { + // TODO + return void 0; + } + + return Sphere3D.create(Vec3.clone(sphereHelper.center), Math.sqrt(sphereHelper.radiusSq)); + } +} \ No newline at end of file diff --git a/src/mol-model/structure/util.ts b/src/mol-model/structure/util.ts index 04cdc89b23f622081e21736b9a9effe8ece39ea1..ab709c2b7ced014e8c13886bbd56669cfb94f773 100644 --- a/src/mol-model/structure/util.ts +++ b/src/mol-model/structure/util.ts @@ -62,20 +62,20 @@ export function residueLabel(model: Model, rI: number) { return `${label_asym_id.value(cI)} ${label_comp_id.value(rI)} ${label_seq_id.value(rI)}` } -const centerPos = Vec3.zero() -const centerMin = Vec3.zero() -export function getCenterAndRadius(centroid: Vec3, unit: Unit, indices: ArrayLike<number>) { - const pos = unit.conformation.position - const { elements } = unit - Vec3.set(centroid, 0, 0, 0) - for (let i = 0, il = indices.length; i < il; ++i) { - pos(elements[indices[i]], centerPos) - Vec3.add(centroid, centroid, centerPos) - Vec3.min(centerMin, centerMin, centerPos) - } - Vec3.scale(centroid, centroid, 1/indices.length) - return Vec3.distance(centerMin, centroid) -} +// const centerPos = Vec3.zero() +// const centerMin = Vec3.zero() +// export function getCenterAndRadius(centroid: Vec3, unit: Unit, indices: ArrayLike<number>) { +// const pos = unit.conformation.position +// const { elements } = unit +// Vec3.set(centroid, 0, 0, 0) +// for (let i = 0, il = indices.length; i < il; ++i) { +// pos(elements[indices[i]], centerPos) +// Vec3.add(centroid, centroid, centerPos) +// Vec3.min(centerMin, centerMin, centerPos) +// } +// Vec3.scale(centroid, centroid, 1/indices.length) +// return Vec3.distance(centerMin, centroid) +// } const matrixPos = Vec3.zero() export function getPositionMatrix(unit: Unit, indices: ArrayLike<number>) { diff --git a/src/mol-plugin/behavior.ts b/src/mol-plugin/behavior.ts index 307b3f134810838f99ba3490fc31c37ddea1b6d0..0f5238d0f3a8068e45a5755bcc39e9929e6e6968 100644 --- a/src/mol-plugin/behavior.ts +++ b/src/mol-plugin/behavior.ts @@ -11,6 +11,7 @@ import * as StaticRepresentation from './behavior/static/representation' import * as StaticCamera from './behavior/static/camera' import * as DynamicRepresentation from './behavior/dynamic/representation' +import * as DynamicCamera from './behavior/dynamic/camera' export const BuiltInPluginBehaviors = { State: StaticState, @@ -19,5 +20,6 @@ export const BuiltInPluginBehaviors = { } export const PluginBehaviors = { - Representation: DynamicRepresentation + Representation: DynamicRepresentation, + Camera: DynamicCamera, } \ No newline at end of file diff --git a/src/mol-plugin/behavior/behavior.ts b/src/mol-plugin/behavior/behavior.ts index f97fedad1137c055836ffd61ad48c26a4ef760ad..39a00f7baec767151e2c421b255b2635964e5b1c 100644 --- a/src/mol-plugin/behavior/behavior.ts +++ b/src/mol-plugin/behavior/behavior.ts @@ -11,6 +11,7 @@ import { PluginContext } from 'mol-plugin/context'; import { PluginCommand } from '../command'; import { Observable } from 'rxjs'; import { ParamDefinition } from 'mol-util/param-definition'; +import { shallowEqual } from 'mol-util'; export { PluginBehavior } @@ -26,7 +27,7 @@ namespace PluginBehavior { export class Root extends PluginStateObject.Create({ name: 'Root', typeClass: 'Root' }) { } export class Behavior extends PluginStateObject.CreateBehavior<PluginBehavior>({ name: 'Behavior' }) { } - export interface Ctor<P = undefined> { new(ctx: PluginContext, params?: P): PluginBehavior<P> } + export interface Ctor<P = undefined> { new(ctx: PluginContext, params: P): PluginBehavior<P> } export interface CreateParams<P> { name: string, @@ -63,7 +64,7 @@ namespace PluginBehavior { } export function simpleCommandHandler<T>(cmd: PluginCommand<T>, action: (data: T, ctx: PluginContext) => void | Promise<void>) { - return class implements PluginBehavior<undefined> { + return class implements PluginBehavior<{}> { private sub: PluginCommand.Subscription | undefined = void 0; register(): void { this.sub = cmd.subscribe(this.ctx, data => action(data, this.ctx)); @@ -76,7 +77,7 @@ namespace PluginBehavior { } } - export abstract class Handler implements PluginBehavior<undefined> { + export abstract class Handler<P = { }> implements PluginBehavior<P> { private subs: PluginCommand.Subscription[] = []; protected subscribeCommand<T>(cmd: PluginCommand<T>, action: PluginCommand.Action<T>) { this.subs.push(cmd.subscribe(this.ctx, action)); @@ -92,8 +93,12 @@ namespace PluginBehavior { for (const s of this.subs) s.unsubscribe(); this.subs = []; } - constructor(protected ctx: PluginContext) { - + update(params: P): boolean { + if (shallowEqual(params, this.params)) return false; + this.params = params; + return true; + } + constructor(protected ctx: PluginContext, protected params: P) { } } } \ No newline at end of file diff --git a/src/mol-plugin/behavior/dynamic/camera.ts b/src/mol-plugin/behavior/dynamic/camera.ts new file mode 100644 index 0000000000000000000000000000000000000000..4431eec954a3fcc90d73705940c338339fbe2e23 --- /dev/null +++ b/src/mol-plugin/behavior/dynamic/camera.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { Loci } from 'mol-model/loci'; +import { ParamDefinition } from 'mol-util/param-definition'; +import { PluginBehavior } from '../behavior'; + +export const FocusLociOnSelect = PluginBehavior.create<{ minRadius: number, extraRadius: number }>({ + name: 'focus-loci-on-select', + ctor: class extends PluginBehavior.Handler<{ minRadius: number, extraRadius: number }> { + register(): void { + this.subscribeObservable(this.ctx.behaviors.canvas.selectLoci, current => { + if (!this.ctx.canvas3d) return; + const sphere = Loci.getBoundingSphere(current.loci); + if (!sphere) return; + this.ctx.canvas3d.camera.focus(sphere.center, Math.max(sphere.radius + this.params.extraRadius, this.params.minRadius)); + }); + } + }, + params: () => ({ + minRadius: ParamDefinition.Numeric(10, { min: 1, max: 50, step: 1 }), + extraRadius: ParamDefinition.Numeric(4, { min: 1, max: 50, step: 1 }, { description: 'Value added to the boundning sphere radius of the Loci.' }) + }), + display: { name: 'Focus Loci on Select', group: 'Camera' } +}); \ No newline at end of file diff --git a/src/mol-plugin/behavior/dynamic/representation.ts b/src/mol-plugin/behavior/dynamic/representation.ts index 77f7f6d946bbca42695e5b7eea3ee2875d3f9f18..e6b5116048879d7ebc6db458f893ba9addeb4c65 100644 --- a/src/mol-plugin/behavior/dynamic/representation.ts +++ b/src/mol-plugin/behavior/dynamic/representation.ts @@ -34,9 +34,18 @@ export const SelectLoci = PluginBehavior.create({ name: 'representation-select-loci', ctor: class extends PluginBehavior.Handler { register(): void { - this.subscribeObservable(this.ctx.behaviors.canvas.selectLoci, ({ loci }) => { + let prevLoci: Loci = EmptyLoci, prevRepr: any = void 0; + this.subscribeObservable(this.ctx.behaviors.canvas.selectLoci, current => { if (!this.ctx.canvas3d) return; - this.ctx.canvas3d.mark(loci, MarkerAction.Toggle); + if (current.repr !== prevRepr || !areLociEqual(current.loci, prevLoci)) { + this.ctx.canvas3d.mark(prevLoci, MarkerAction.Deselect); + this.ctx.canvas3d.mark(current.loci, MarkerAction.Select); + prevLoci = current.loci; + prevRepr = current.repr; + } else { + this.ctx.canvas3d.mark(current.loci, MarkerAction.Toggle); + } + // this.ctx.canvas3d.mark(loci, MarkerAction.Toggle); }); } }, diff --git a/src/mol-plugin/index.ts b/src/mol-plugin/index.ts index f21c16e9fb9b1992666c738c6e92d8e10e1522cc..29471e0c80abb6bb4ce1da3ee9a22b5c15d48d94 100644 --- a/src/mol-plugin/index.ts +++ b/src/mol-plugin/index.ts @@ -33,7 +33,8 @@ const DefaultSpec: PluginSpec = { behaviors: [ PluginSpec.Behavior(PluginBehaviors.Representation.HighlightLoci), PluginSpec.Behavior(PluginBehaviors.Representation.SelectLoci), - PluginSpec.Behavior(PluginBehaviors.Representation.DefaultLociLabelProvider) + PluginSpec.Behavior(PluginBehaviors.Representation.DefaultLociLabelProvider), + PluginSpec.Behavior(PluginBehaviors.Camera.FocusLociOnSelect, { minRadius: 20, extraRadius: 4 }) ] }