Ver código fonte

mol-plugin: focus camera on loci select behavior

David Sehnal 6 anos atrás
pai
commit
fae55845f0

+ 7 - 0
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);
     // }

+ 39 - 0
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() {
+
+    }
+}

+ 63 - 1
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
+
+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));
+    }
+}

+ 14 - 14
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>) {

+ 3 - 1
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,
 }

+ 10 - 5
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) {
         }
     }
 }

+ 28 - 0
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' }
+});

+ 11 - 2
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);
             });
         }
     },

+ 2 - 1
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 })
     ]
 }