Просмотр исходного кода

improve/fix implicit atom picking

Alexander Rose 3 лет назад
Родитель
Сommit
254c9efbf5

+ 2 - 0
CHANGELOG.md

@@ -18,6 +18,8 @@ Note that since we don't clearly distinguish between a public and private interf
 - Add pixel-scale & pick-scale GET params to Viewer app
 - Fix selecting bonds not adding their atoms in selection manager
 - Add ``preferAtoms`` option to SelectLoci/HighlightLoci behaviors
+- Make the implicit atoms of bond visuals pickable
+    - Add ``preferAtomPixelPadding`` to Canvas3dInteractionHelper
 
 ## [v2.3.0] - 2021-09-06
 

+ 25 - 3
src/mol-canvas3d/camera.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -27,6 +27,10 @@ interface ICamera {
     readonly fogNear: number,
 }
 
+const tmpPos1 = Vec3();
+const tmpPos2 = Vec3();
+const tmpClip = Vec4();
+
 class Camera implements ICamera {
     readonly view: Mat4 = Mat4.identity();
     readonly projection: Mat4 = Mat4.identity();
@@ -155,14 +159,32 @@ class Camera implements ICamera {
         }
     }
 
+    /** Transform point into 2D window coordinates. */
     project(out: Vec4, point: Vec3) {
         return cameraProject(out, point, this.viewport, this.projectionView);
     }
 
-    unproject(out: Vec3, point: Vec3) {
+    /**
+     * Transform point from screen space to 3D coordinates.
+     * The point must have `x` and `y` set to 2D window coordinates
+     * and `z` between 0 (near) and 1 (far); the optional `w` is not used.
+     */
+    unproject(out: Vec3, point: Vec3 | Vec4) {
         return cameraUnproject(out, point, this.viewport, this.inverseProjectionView);
     }
 
+    /** World space pixel size at given `point` */
+    getPixelSize(point: Vec3) {
+        // project -> unproject of `point` does not exactly return the same
+        // to get a sufficiently accurate measure we unproject the original
+        // clip position in addition to the one shifted bey one pixel
+        this.project(tmpClip, point);
+        this.unproject(tmpPos1, tmpClip);
+        tmpClip[0] += 1;
+        this.unproject(tmpPos2, tmpClip);
+        return Vec3.distance(tmpPos1, tmpPos2);
+    }
+
     constructor(state?: Partial<Camera.Snapshot>, viewport = Viewport.create(0, 0, 128, 128), props: Partial<{ pixelScale: number }> = {}) {
         this.viewport = viewport;
         this.pixelScale = props.pixelScale || 1;
@@ -178,7 +200,7 @@ namespace Camera {
     /**
      * Sets an offseted view in a larger frustum. This is useful for
      * - multi-window or multi-monitor/multi-machine setups
-     * - jittering the camera position for
+     * - jittering the camera position for sampling
      */
     export interface ViewOffset {
         enabled: boolean,

+ 16 - 18
src/mol-canvas3d/camera/util.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -55,14 +55,11 @@ namespace Viewport {
 
 //
 
-const NEAR_RANGE = 0;
-const FAR_RANGE = 1;
-
 const tmpVec4 = Vec4();
 
 /** Transform point into 2D window coordinates. */
 export function cameraProject(out: Vec4, point: Vec3, viewport: Viewport, projectionView: Mat4) {
-    const { x: vX, y: vY, width: vWidth, height: vHeight } = viewport;
+    const { x, y, width, height } = viewport;
 
     // clip space -> NDC -> window coordinates, implicit 1.0 for w component
     Vec4.set(tmpVec4, point[0], point[1], point[2], 1.0);
@@ -78,27 +75,28 @@ export function cameraProject(out: Vec4, point: Vec3, viewport: Viewport, projec
         tmpVec4[2] /= w;
     }
 
-    // transform into window coordinates, set fourth component is (1/clip.w) as in gl_FragCoord.w
-    out[0] = vX + vWidth / 2 * tmpVec4[0] + (0 + vWidth / 2);
-    out[1] = vY + vHeight / 2 * tmpVec4[1] + (0 + vHeight / 2);
-    out[2] = (FAR_RANGE - NEAR_RANGE) / 2 * tmpVec4[2] + (FAR_RANGE + NEAR_RANGE) / 2;
+    // transform into window coordinates, set fourth component to 1 / clip.w as in gl_FragCoord.w
+    out[0] = (tmpVec4[0] + 1) * width * 0.5 + x;
+    out[1] = (1 - tmpVec4[1]) * height * 0.5 + y; // flip Y
+    out[2] = (tmpVec4[2] + 1) * 0.5;
     out[3] = w === 0 ? 0 : 1 / w;
     return out;
 }
 
 /**
  * Transform point from screen space to 3D coordinates.
- * The point must have x and y set to 2D window coordinates and z between 0 (near) and 1 (far).
+ * The point must have `x` and `y` set to 2D window coordinates
+ * and `z` between 0 (near) and 1 (far); the optional `w` is not used.
  */
-export function cameraUnproject(out: Vec3, point: Vec3, viewport: Viewport, inverseProjectionView: Mat4) {
-    const { x: vX, y: vY, width: vWidth, height: vHeight } = viewport;
+export function cameraUnproject(out: Vec3, point: Vec3 | Vec4, viewport: Viewport, inverseProjectionView: Mat4) {
+    const { x, y, width, height } = viewport;
 
-    const x = point[0] - vX;
-    const y = (vHeight - point[1] - 1) - vY;
-    const z = point[2];
+    const px = point[0] - x;
+    const py = (height - point[1] - 1) - y;
+    const pz = point[2];
 
-    out[0] = (2 * x) / vWidth - 1;
-    out[1] = (2 * y) / vHeight - 1;
-    out[2] = 2 * z - 1;
+    out[0] = (2 * px) / width - 1;
+    out[1] = (2 * py) / height - 1;
+    out[2] = 2 * pz - 1;
     return Vec3.transformMat4(out, out, inverseProjectionView);
 }

+ 15 - 11
src/mol-canvas3d/helper/interaction-events.ts

@@ -12,8 +12,7 @@ import { RxEventHelper } from '../../mol-util/rx-event-helper';
 import { Vec2, Vec3 } from '../../mol-math/linear-algebra';
 import { Camera } from '../camera';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
-import { Bond, StructureElement } from '../../mol-model/structure';
-import { OrderedSet } from '../../mol-data/int';
+import { Bond } from '../../mol-model/structure';
 
 type Canvas3D = import('../canvas3d').Canvas3D
 type HoverEvent = import('../canvas3d').Canvas3D.HoverEvent
@@ -22,11 +21,13 @@ type ClickEvent = import('../canvas3d').Canvas3D.ClickEvent
 
 const enum InputEvent { Move, Click, Drag }
 
-const pA = Vec3();
+const tmpPosA = Vec3();
+const tmpPos = Vec3();
+const tmpNorm = Vec3();
 
 export const Canvas3dInteractionHelperParams = {
     maxFps: PD.Numeric(30, { min: 10, max: 60, step: 10 }),
-    preferAtomDistanceFactor: PD.Numeric(1.5, { min: 0, max: 3, step: 0.1 }),
+    preferAtomPixelPadding: PD.Numeric(5, { min: 0, max: 20, step: 1 }, { description: 'Number of extra pixels at which to prefer atoms over bonds.' }),
 };
 export type Canvas3dInteractionHelperParams = typeof Canvas3dInteractionHelperParams
 export type Canvas3dInteractionHelperProps = PD.Values<Canvas3dInteractionHelperParams>
@@ -166,15 +167,18 @@ export class Canvas3dInteractionHelper {
         const { repr, loci } = this.lociGetter(pickingId);
         if (position && repr && Bond.isLoci(loci) && loci.bonds.length === 2) {
             const { aUnit, aIndex } = loci.bonds[0];
-            aUnit.conformation.position(aUnit.elements[aIndex], pA);
-            // TODO: project position onto camera plane at pA
-            let size = repr.theme.size.size(loci.bonds[0]) * (repr.props.sizeFactor ?? 1);
+            aUnit.conformation.position(aUnit.elements[aIndex], tmpPosA);
+            Vec3.sub(tmpNorm, this.camera.state.position, this.camera.state.target);
+            Vec3.projectPointOnPlane(tmpPos, position, tmpNorm, tmpPosA);
+            const pixelSize = this.camera.getPixelSize(tmpPos);
+            let radius = repr.theme.size.size(loci.bonds[0]) * (repr.props.sizeFactor ?? 1);
             if (repr.props.lineSizeAttenuation === false) {
-                // TODO: properly scale?
-                size /= this.camera.pixelRatio * ((this.camera.viewport.height / 2.0) / -position[2]) * 5.0;
+                // divide by two to get radius
+                radius *= pixelSize / 2;
             }
-            if (Vec3.distance(position, pA) < size * this.props.preferAtomDistanceFactor) {
-                return { repr, loci: StructureElement.Loci(loci.structure, [{ unit: aUnit, indices: OrderedSet.ofSingleton(aIndex) }]) };
+            radius += this.props.preferAtomPixelPadding * pixelSize;
+            if (Vec3.distance(tmpPos, tmpPosA) < radius) {
+                return { repr, loci: Bond.toFirstStructureElementLoci(loci) };
             }
         }
         return { repr, loci };

+ 11 - 3
src/mol-math/linear-algebra/3d/vec3.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2017-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -540,9 +540,17 @@ namespace Vec3 {
 
     /** Project `point` onto `vector` starting from `origin` */
     export function projectPointOnVector(out: Vec3, point: Vec3, vector: Vec3, origin: Vec3) {
-        sub(out, copy(out, point), origin);
+        sub(out, point, origin);
         const scalar = dot(vector, out) / squaredMagnitude(vector);
-        return add(out, scale(out, copy(out, vector), scalar), origin);
+        return add(out, scale(out, vector, scalar), origin);
+    }
+
+    const tmpProjectPlane = zero();
+    /** Project `point` onto `plane` defined by `normal` starting from `origin` */
+    export function projectPointOnPlane(out: Vec3, point: Vec3, normal: Vec3, origin: Vec3) {
+        normalize(tmpProjectPlane, normal);
+        sub(out, point, origin);
+        return sub(out, point, scale(tmpProjectPlane, tmpProjectPlane, dot(out, tmpProjectPlane)));
     }
 
     export function projectOnVector(out: Vec3, p: Vec3, vector: Vec3) {