diff --git a/.eslintrc.json b/.eslintrc.json index c8740de119e38e394988609bade687307a8c8269..34232f64f5623a56bb067ec6d1f047450ecc109d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -54,7 +54,8 @@ "no-multi-spaces": "error", "block-spacing": "error", "keyword-spacing": "off", - "space-before-blocks": "error" + "space-before-blocks": "error", + "semi-spacing": "error" }, "overrides": [ { diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1a52bf384c81e38a291f36a0ad13191c99357ccb..6ddb14b18f8c8eccb57e7eb12cbcb8321b0a2882 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,10 +8,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - - name: install node v12 + - name: install node v14 uses: actions/setup-node@v1 with: - node-version: 12 + node-version: 14 - name: yarn install run: yarn install - name: eslint diff --git a/CHANGELOG.md b/CHANGELOG.md index 72472eadf3d1774e01741927b0dff994bdd20795..e2cf63915e19266bc929dede3585e37368c4af4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,53 @@ All notable changes to this project will be documented in this file, following t Note that since we don't clearly distinguish between a public and private interfaces there will be changes in non-major versions that are potentially breaking. If we make breaking changes to less used interfaces we will highlight it in here. +## [Unreleased] + +- Add additional measurement controls: orientation (box, axes, ellipsoid) & plane (best fit) +- Improve aromatic bond visuals (add ``aromaticScale``, ``aromaticSpacing``, ``aromaticDashCount`` params) +- [Breaking] Change ``adjustCylinderLength`` default to ``false`` (set to true for focus representation) +- Fix marker highlight color overriding select color + +## [v2.3.5] - 2021-10-19 + +- Fix sequence viewer for PDB files with COMPND record and multichain entities. +- Fix index pair bonds order assignment + +## [v2.3.4] - 2021-10-12 + +- Fix pickScale not taken into account in line/point shader +- Add pixel-scale, pick-scale & pick-padding 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 +- Add points & crosses visuals to Line representation +- Add ``pickPadding`` config option (look around in case target pixel is empty) +- Add ``multipleBonds`` param to bond visuals with options: off, symmetric, offset +- Fix ``argparse`` config in servers. + +## [v2.3.3] - 2021-10-01 + +- Fix direct volume shader + +## [v2.3.2] - 2021-10-01 + +- Prefer WebGL1 on iOS devices until WebGL2 support has stabilized. + +## [v2.3.1] - 2021-09-28 + +- Add Charmm saccharide names +- Treat missing occupancy column as occupancy of 1 +- Fix line shader not accounting for aspect ratio +- [Breaking] Fix point repr & shader + - Was unusable with ``wboit`` + - Replaced ``pointFilledCircle`` & ``pointEdgeBleach`` params by ``pointStyle`` (square, circle, fuzzy) + - Set ``pointSizeAttenuation`` to false by default + - Set ``sizeTheme`` to ``uniform`` by default +- Add ``markerPriority`` option to Renderer (useful in combination with edges of marking pass) +- Add support support for ``chem_comp_bond`` and ``struct_conn`` categories (fixes ModelServer behavior where these categories should have been present) +- Model and VolumeServer: fix argparse config + ## [v2.3.0] - 2021-09-06 - Take include/exclude flags into account when displaying aromatic bonds @@ -111,29 +158,22 @@ Note that since we don't clearly distinguish between a public and private interf - Fixed Measurements UI labels (#166) ## [v2.0.3] - 2021-04-09 -### Added -- Support for ``ColorTheme.palette`` designed for providing gradient-like coloring. -### Changed +- Add support for ``ColorTheme.palette`` designed for providing gradient-like coloring. - [Breaking] The ``zip`` function is now asynchronous and expects a ``RuntimeContext``. Also added ``Zip()`` returning a ``Task``. - [Breaking] Add ``CubeGridFormat`` in ``alpha-orbitals`` extension. ## [v2.0.2] - 2021-03-29 -### Added -- ``Canvas3D.getRenderObjects``. -- [WIP] Animate state interpolating, including model trajectories -### Changed +- Add ``Canvas3D.getRenderObjects``. +- [WIP] Animate state interpolating, including model trajectories - Recognise MSE, SEP, TPO, PTR and PCA as non-standard amino-acids. - -### Fixed -- VolumeFromDensityServerCif transform label - +- Fix VolumeFromDensityServerCif transform label ## [v2.0.1] - 2021-03-23 -### Fixed -- Exclude tsconfig.commonjs.tsbuildinfo from npm bundle +- Exclude tsconfig.commonjs.tsbuildinfo from npm bundle ## [v2.0.0] - 2021-03-23 + Too many changes to list as this is the start of the changelog... Notably, default exports are now forbidden. diff --git a/package-lock.json b/package-lock.json index a182e4106aefca0f6398e10d764ad3072ddcab85..c5ced1fd9ca1bdff83888b8b2206d48fee639edc 100644 Binary files a/package-lock.json and b/package-lock.json differ diff --git a/package.json b/package.json index c0338722ff1886873e60d13c82fad01e9fc1da23..4d3ffe04fce9d1a53438ac80f304562b6c81e7b9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "molstar", - "version": "2.3.0", + "version": "2.3.5", "description": "A comprehensive macromolecular library.", "homepage": "https://github.com/molstar/molstar#readme", "repository": { @@ -94,33 +94,33 @@ "@graphql-codegen/typescript": "^2.2.2", "@graphql-codegen/typescript-graphql-files-modules": "^2.1.0", "@graphql-codegen/typescript-graphql-request": "^4.1.4", - "@graphql-codegen/typescript-operations": "^2.1.4", + "@graphql-codegen/typescript-operations": "^2.1.6", "@types/cors": "^2.8.12", - "@typescript-eslint/eslint-plugin": "^4.31.0", - "@typescript-eslint/parser": "^4.31.0", + "@typescript-eslint/eslint-plugin": "^4.32.0", + "@typescript-eslint/parser": "^4.32.0", "benchmark": "^2.1.4", - "concurrently": "^6.2.1", - "cpx2": "^3.0.2", + "concurrently": "^6.3.0", + "cpx2": "^4.0.0", "crypto-browserify": "^3.12.0", - "css-loader": "^6.2.0", + "css-loader": "^6.3.0", "eslint": "^7.32.0", "extra-watch-webpack-plugin": "^1.0.3", "file-loader": "^6.2.0", "fs-extra": "^10.0.0", - "graphql": "^15.5.3", - "http-server": "^13.0.1", - "jest": "^27.1.1", + "graphql": "^15.6.0", + "http-server": "^13.0.2", + "jest": "^27.2.4", "mini-css-extract-plugin": "^2.3.0", "node-sass": "^6.0.1", "path-browserify": "^1.0.1", "raw-loader": "^4.0.2", "sass-loader": "^12.1.0", - "simple-git": "^2.45.1", + "simple-git": "^2.46.0", "stream-browserify": "^3.0.0", - "style-loader": "^3.2.1", + "style-loader": "^3.3.0", "ts-jest": "^27.0.5", "typescript": "^4.4.3", - "webpack": "^5.52.1", + "webpack": "^5.56.0", "webpack-cli": "^4.8.0", "webpack-version-file-plugin": "^0.4.0" }, @@ -129,10 +129,10 @@ "@types/benchmark": "^2.1.1", "@types/compression": "1.7.2", "@types/express": "^4.17.13", - "@types/jest": "^27.0.1", - "@types/node": "^16.9.1", + "@types/jest": "^27.0.2", + "@types/node": "^16.10.2", "@types/node-fetch": "^2.5.12", - "@types/react": "^17.0.20", + "@types/react": "^17.0.27", "@types/react-dom": "^17.0.9", "@types/swagger-ui-dist": "3.30.1", "argparse": "^2.0.1", @@ -146,8 +146,8 @@ "node-fetch": "^2.6.2", "react": "^17.0.2", "react-dom": "^17.0.2", - "rxjs": "^7.3.0", - "swagger-ui-dist": "^3.52.1", + "rxjs": "^7.3.1", + "swagger-ui-dist": "^3.52.3", "tslib": "^2.3.1", "util.promisify": "^1.1.1", "xhr2": "^0.2.1" diff --git a/src/apps/viewer/index.html b/src/apps/viewer/index.html index 1d9b6ec5ac33d20aebbd2e63c82d77214e5dd12a..40af8122b84c07e39c0c4392580fa905b75b0165 100644 --- a/src/apps/viewer/index.html +++ b/src/apps/viewer/index.html @@ -52,12 +52,22 @@ var collapseLeftPanel = getParam('collapse-left-panel', '[^&]+').trim() === '1'; var pdbProvider = getParam('pdb-provider', '[^&]+').trim().toLowerCase(); var emdbProvider = getParam('emdb-provider', '[^&]+').trim().toLowerCase(); + var mapProvider = getParam('map-provider', '[^&]+').trim().toLowerCase(); + var pixelScale = getParam('pixel-scale', '[^&]+').trim(); + var pickScale = getParam('pick-scale', '[^&]+').trim(); + var pickPadding = getParam('pick-padding', '[^&]+').trim(); var viewer = new molstar.Viewer('app', { layoutShowControls: !hideControls, viewportShowExpand: false, collapseLeftPanel: collapseLeftPanel, pdbProvider: pdbProvider || 'pdbe', emdbProvider: emdbProvider || 'pdbe', + volumeStreamingServer: (mapProvider || 'pdbe') === 'rcsb' + ? 'https://maps.rcsb.org' + : 'https://www.ebi.ac.uk/pdbe/densities', + pixelScale: parseFloat(pixelScale) || 1, + pickScale: parseFloat(pickScale) || 0.25, + pickPadding: isNaN(parseFloat(pickPadding)) ? 1 : parseFloat(pickPadding), }); var snapshotId = getParam('snapshot-id', '[^&]+').trim(); diff --git a/src/apps/viewer/index.ts b/src/apps/viewer/index.ts index 2ffa3624d8e73538b25e234343753eeb491aacd2..84fb6dae83f94e5026e7ee65d860fe2c74ead9ed 100644 --- a/src/apps/viewer/index.ts +++ b/src/apps/viewer/index.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> @@ -71,9 +71,11 @@ const DefaultViewerOptions = { layoutShowLog: true, layoutShowLeftPanel: true, collapseLeftPanel: false, - disableAntialiasing: false, - pixelScale: 1, - enableWboit: true, + disableAntialiasing: PluginConfig.General.DisableAntialiasing.defaultValue, + pixelScale: PluginConfig.General.PixelScale.defaultValue, + pickScale: PluginConfig.General.PickScale.defaultValue, + pickPadding: PluginConfig.General.PickPadding.defaultValue, + enableWboit: PluginConfig.General.EnableWboit.defaultValue, viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue, viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue, @@ -130,6 +132,8 @@ export class Viewer { config: [ [PluginConfig.General.DisableAntialiasing, o.disableAntialiasing], [PluginConfig.General.PixelScale, o.pixelScale], + [PluginConfig.General.PickScale, o.pickScale], + [PluginConfig.General.PickPadding, o.pickPadding], [PluginConfig.General.EnableWboit, o.enableWboit], [PluginConfig.Viewport.ShowExpand, o.viewportShowExpand], [PluginConfig.Viewport.ShowControls, o.viewportShowControls], diff --git a/src/mol-canvas3d/camera.ts b/src/mol-canvas3d/camera.ts index 5d1f7a584c02a832aa21cc3008f45bad241dc54a..95074d76a1d1418d90c61ab2c28c8dea20f6df34 100644 --- a/src/mol-canvas3d/camera.ts +++ b/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, diff --git a/src/mol-canvas3d/camera/util.ts b/src/mol-canvas3d/camera/util.ts index 6e787cbb0595ee654fed8b8ff6f943e700f27663..573d123133eee967a8cbfa0012825f957edcacfb 100644 --- a/src/mol-canvas3d/camera/util.ts +++ b/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); } \ No newline at end of file diff --git a/src/mol-canvas3d/canvas3d.ts b/src/mol-canvas3d/canvas3d.ts index d28751fe5a906c44bcb96e932e14922d43561919..a8a525d6f2e35508dc1c8f85c7e3565e64f24cb4 100644 --- a/src/mol-canvas3d/canvas3d.ts +++ b/src/mol-canvas3d/canvas3d.ts @@ -23,7 +23,7 @@ import { Camera } from './camera'; import { ParamDefinition as PD } from '../mol-util/param-definition'; import { DebugHelperParams } from './helper/bounding-sphere-helper'; import { SetUtils } from '../mol-util/set'; -import { Canvas3dInteractionHelper } from './helper/interaction-events'; +import { Canvas3dInteractionHelper, Canvas3dInteractionHelperParams } from './helper/interaction-events'; import { PostprocessingParams } from './passes/postprocessing'; import { MultiSampleHelper, MultiSampleParams, MultiSamplePass } from './passes/multi-sample'; import { PickData } from './passes/pick'; @@ -84,6 +84,7 @@ export const Canvas3DParams = { marking: PD.Group(MarkingParams), renderer: PD.Group(RendererParams), trackball: PD.Group(TrackballControlsParams), + interaction: PD.Group(Canvas3dInteractionHelperParams), debug: PD.Group(DebugHelperParams), handle: PD.Group(HandleHelperParams), }; @@ -115,19 +116,23 @@ namespace Canvas3DContext { preserveDrawingBuffer: true, pixelScale: 1, pickScale: 0.25, - enableWboit: true + /** extra pixels to around target to check in case target is empty */ + pickPadding: 1, + enableWboit: true, + preferWebGl1: false }; export type Attribs = typeof DefaultAttribs export function fromCanvas(canvas: HTMLCanvasElement, attribs: Partial<Attribs> = {}): Canvas3DContext { const a = { ...DefaultAttribs, ...attribs }; - const { antialias, preserveDrawingBuffer, pixelScale } = a; + const { antialias, preserveDrawingBuffer, pixelScale, preferWebGl1 } = a; const gl = getGLContext(canvas, { antialias, preserveDrawingBuffer, alpha: true, // the renderer requires an alpha channel depth: true, // the renderer requires a depth buffer premultipliedAlpha: true, // the renderer outputs PMA + preferWebGl1 }); if (gl === null) throw new Error('Could not create a WebGL rendering context'); @@ -305,8 +310,8 @@ namespace Canvas3D { const renderer = Renderer.create(webgl, p.renderer); const helper = new Helper(webgl, scene, p); - const pickHelper = new PickHelper(webgl, renderer, scene, helper, passes.pick, { x, y, width, height }); - const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera); + const pickHelper = new PickHelper(webgl, renderer, scene, helper, passes.pick, { x, y, width, height }, attribs.pickPadding); + const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera, p.interaction); const multiSampleHelper = new MultiSampleHelper(passes.multiSample); let cameraResetRequested = false; @@ -642,6 +647,7 @@ namespace Canvas3D { multiSample: { ...p.multiSample }, renderer: { ...renderer.props }, trackball: { ...controls.props }, + interaction: { ...interactionHelper.props }, debug: { ...helper.debug.props }, handle: { ...helper.handle.props }, }; @@ -778,6 +784,7 @@ namespace Canvas3D { if (props.multiSample) Object.assign(p.multiSample, props.multiSample); if (props.renderer) renderer.setProps(props.renderer); if (props.trackball) controls.setProps(props.trackball); + if (props.interaction) interactionHelper.setProps(props.interaction); if (props.debug) helper.debug.setProps(props.debug); if (props.handle) helper.handle.setProps(props.handle); diff --git a/src/mol-canvas3d/helper/interaction-events.ts b/src/mol-canvas3d/helper/interaction-events.ts index 9748199ed503f16e890ab915b0e51dde8e5ed6ae..b35b79f697d27fe211eb5eebc5ede7027d43265b 100644 --- a/src/mol-canvas3d/helper/interaction-events.ts +++ b/src/mol-canvas3d/helper/interaction-events.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> @@ -11,6 +11,8 @@ import { InputObserver, ModifiersKeys, ButtonsType } from '../../mol-util/input/ 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 } from '../../mol-model/structure'; type Canvas3D = import('../canvas3d').Canvas3D type HoverEvent = import('../canvas3d').Canvas3D.HoverEvent @@ -19,6 +21,17 @@ type ClickEvent = import('../canvas3d').Canvas3D.ClickEvent const enum InputEvent { Move, Click, Drag } +const tmpPosA = Vec3(); +const tmpPos = Vec3(); +const tmpNorm = Vec3(); + +export const Canvas3dInteractionHelperParams = { + maxFps: PD.Numeric(30, { min: 10, max: 60, step: 10 }), + preferAtomPixelPadding: PD.Numeric(3, { 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> + export class Canvas3dInteractionHelper { private ev = RxEventHelper.create(); @@ -48,6 +61,12 @@ export class Canvas3dInteractionHelper { private button: ButtonsType.Flag = ButtonsType.create(0); private modifiers: ModifiersKeys = ModifiersKeys.None; + readonly props: Canvas3dInteractionHelperProps; + + setProps(props: Partial<Canvas3dInteractionHelperProps>) { + Object.assign(this.props, props); + } + private identify(e: InputEvent, t: number) { const xyChanged = this.startX !== this.endX || this.startY !== this.endY; @@ -70,7 +89,7 @@ export class Canvas3dInteractionHelper { } if (e === InputEvent.Click) { - const loci = this.getLoci(this.id); + const loci = this.getLoci(this.id, this.position); this.events.click.next({ current: loci, buttons: this.buttons, button: this.button, modifiers: this.modifiers, page: Vec2.create(this.endX, this.endY), position: this.position }); this.prevLoci = loci; return; @@ -78,13 +97,13 @@ export class Canvas3dInteractionHelper { if (!this.inside || this.currentIdentifyT !== t || !xyChanged || this.outsideViewport(this.endX, this.endY)) return; - const loci = this.getLoci(this.id); + const loci = this.getLoci(this.id, this.position); this.events.hover.next({ current: loci, buttons: this.buttons, button: this.button, modifiers: this.modifiers, page: Vec2.create(this.endX, this.endY), position: this.position }); this.prevLoci = loci; } tick(t: number) { - if (this.inside && t - this.prevT > 1000 / this.maxFps) { + if (this.inside && t - this.prevT > 1000 / this.props.maxFps) { this.prevT = t; this.currentIdentifyT = t; this.identify(this.isInteracting ? InputEvent.Drag : InputEvent.Move, t); @@ -144,11 +163,34 @@ export class Canvas3dInteractionHelper { ); } + private getLoci(pickingId: PickingId | undefined, position: Vec3 | undefined) { + 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], 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) { + // divide by two to get radius + radius *= pixelSize / 2; + } + radius += this.props.preferAtomPixelPadding * pixelSize; + if (Vec3.distance(tmpPos, tmpPosA) < radius) { + return { repr, loci: Bond.toFirstStructureElementLoci(loci) }; + } + } + return { repr, loci }; + } + dispose() { this.ev.dispose(); } - constructor(private canvasIdentify: Canvas3D['identify'], private getLoci: Canvas3D['getLoci'], private input: InputObserver, private camera: Camera, private maxFps: number = 30) { + constructor(private canvasIdentify: Canvas3D['identify'], private lociGetter: Canvas3D['getLoci'], private input: InputObserver, private camera: Camera, props: Partial<Canvas3dInteractionHelperProps> = {}) { + this.props = { ...PD.getDefaultValues(Canvas3dInteractionHelperParams), ...props }; + input.drag.subscribe(({ x, y, buttons, button, modifiers }) => { this.isInteracting = true; // console.log('drag'); diff --git a/src/mol-canvas3d/passes/draw.ts b/src/mol-canvas3d/passes/draw.ts index 63db5db451b33d91c3d9c299763a0f424acc7743..c3ef689e3d8c061f8a2c69e91e46938ac65e951b 100644 --- a/src/mol-canvas3d/passes/draw.ts +++ b/src/mol-canvas3d/passes/draw.ts @@ -362,6 +362,7 @@ export class DrawPass { render(renderer: Renderer, camera: Camera | StereoCamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean, postprocessingProps: PostprocessingProps, markingProps: MarkingProps) { renderer.setTransparentBackground(transparentBackground); renderer.setDrawingBufferSize(this.colorTarget.getWidth(), this.colorTarget.getHeight()); + renderer.setPixelRatio(this.webgl.pixelRatio); if (StereoCamera.is(camera)) { this._render(renderer, camera.left, scene, helper, toDrawingBuffer, transparentBackground, postprocessingProps, markingProps); diff --git a/src/mol-canvas3d/passes/pick.ts b/src/mol-canvas3d/passes/pick.ts index e89ca205aa57f181676650d4d85e962f9f09b42e..130fe9d80affe760eed23f612d399ed1372b503b 100644 --- a/src/mol-canvas3d/passes/pick.ts +++ b/src/mol-canvas3d/passes/pick.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ @@ -11,6 +11,7 @@ import { WebGLContext } from '../../mol-gl/webgl/context'; import { GraphicsRenderVariant } from '../../mol-gl/webgl/render-item'; import { RenderTarget } from '../../mol-gl/webgl/render-target'; import { Vec3 } from '../../mol-math/linear-algebra'; +import { spiral2d } from '../../mol-math/misc'; import { decodeFloatRGB, unpackRGBAToDepth } from '../../mol-util/float-packing'; import { Camera, ICamera } from '../camera'; import { StereoCamera } from '../camera/stereo'; @@ -88,6 +89,7 @@ export class PickPass { this.groupPickTarget.bind(); this.renderVariant(renderer, camera, scene, helper, 'pickGroup'); + // printTexture(this.webgl, this.groupPickTarget.texture, { id: 'group' }) this.depthPickTarget.bind(); this.renderVariant(renderer, camera, scene, helper, 'depth'); @@ -111,6 +113,8 @@ export class PickHelper { private pickHeight: number private halfPickWidth: number + private spiral: [number, number][] + private setupBuffers() { const bufferSize = this.pickWidth * this.pickHeight * 4; if (!this.objectBuffer || this.objectBuffer.length !== bufferSize) { @@ -138,6 +142,8 @@ export class PickHelper { this.setupBuffers(); } + + this.spiral = spiral2d(Math.round(this.pickScale * this.pickPadding)); } private syncBuffers() { @@ -177,6 +183,7 @@ export class PickHelper { renderer.setTransparentBackground(false); renderer.setDrawingBufferSize(this.pickPass.objectPickTarget.getWidth(), this.pickPass.objectPickTarget.getHeight()); + renderer.setPixelRatio(this.pickScale); if (StereoCamera.is(camera)) { renderer.setViewport(pickX, pickY, halfPickWidth, pickHeight); @@ -192,7 +199,7 @@ export class PickHelper { this.dirty = false; } - identify(x: number, y: number, camera: Camera | StereoCamera): PickData | undefined { + private identifyInternal(x: number, y: number, camera: Camera | StereoCamera): PickData | undefined { const { webgl, pickScale } = this; if (webgl.isContextLost) return; @@ -251,7 +258,14 @@ export class PickHelper { return { id: { objectId, instanceId, groupId }, position }; } - constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private helper: Helper, private pickPass: PickPass, viewport: Viewport) { + identify(x: number, y: number, camera: Camera | StereoCamera): PickData | undefined { + for (const d of this.spiral) { + const pickData = this.identifyInternal(x + d[0], y + d[1], camera); + if (pickData) return pickData; + } + } + + constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private helper: Helper, private pickPass: PickPass, viewport: Viewport, readonly pickPadding = 1) { this.setViewport(viewport.x, viewport.y, viewport.width, viewport.height); } } \ No newline at end of file diff --git a/src/mol-geo/geometry/direct-volume/direct-volume.ts b/src/mol-geo/geometry/direct-volume/direct-volume.ts index 87bbe53cd7df2a39a10a95febd85056cca5c94f2..c46e32c5d0363e482cefb64d993c30d227848ce7 100644 --- a/src/mol-geo/geometry/direct-volume/direct-volume.ts +++ b/src/mol-geo/geometry/direct-volume/direct-volume.ts @@ -9,7 +9,7 @@ import { LocationIterator, PositionLocation } from '../../../mol-geo/util/locati import { RenderableState } from '../../../mol-gl/renderable'; import { DirectVolumeValues } from '../../../mol-gl/renderable/direct-volume'; import { calculateTransformBoundingSphere } from '../../../mol-gl/renderable/util'; -import { Texture } from '../../../mol-gl/webgl/texture'; +import { createNullTexture, Texture } from '../../../mol-gl/webgl/texture'; import { Box3D, Sphere3D } from '../../../mol-math/geometry'; import { Mat4, Vec2, Vec3, Vec4 } from '../../../mol-math/linear-algebra'; import { Theme } from '../../../mol-theme/theme'; @@ -129,7 +129,15 @@ export namespace DirectVolume { } export function createEmpty(directVolume?: DirectVolume): DirectVolume { - return {} as DirectVolume; // TODO + const bbox = Box3D(); + const gridDimension = Vec3(); + const transform = Mat4.identity(); + const unitToCartn = Mat4.identity(); + const cellDim = Vec3(); + const texture = createNullTexture(); + const stats = Grid.One.stats; + const packedGroup = false; + return create(bbox, gridDimension, transform, unitToCartn, cellDim, texture, stats, packedGroup, directVolume); } export function createRenderModeParam(stats?: Grid['stats']) { diff --git a/src/mol-geo/geometry/image/image.ts b/src/mol-geo/geometry/image/image.ts index 0f34aeff787b93d7e900291fecd77e1de00592df..61447717432b120fb7d10f417ea5e6f08a95ad5e 100644 --- a/src/mol-geo/geometry/image/image.ts +++ b/src/mol-geo/geometry/image/image.ts @@ -7,7 +7,7 @@ import { hashFnv32a } from '../../../mol-data/util'; import { LocationIterator } from '../../../mol-geo/util/location-iterator'; import { RenderableState } from '../../../mol-gl/renderable'; -import { calculateTransformBoundingSphere, TextureImage } from '../../../mol-gl/renderable/util'; +import { calculateTransformBoundingSphere, createTextureImage, TextureImage } from '../../../mol-gl/renderable/util'; import { Sphere3D } from '../../../mol-math/geometry'; import { Vec2, Vec4, Vec3 } from '../../../mol-math/linear-algebra'; import { Theme } from '../../../mol-theme/theme'; @@ -113,7 +113,10 @@ namespace Image { } export function createEmpty(image?: Image): Image { - return {} as Image; // TODO + const imageTexture = createTextureImage(0, 4, Uint8Array); + const corners = image ? image.cornerBuffer.ref.value : new Float32Array(8 * 3); + const groupTexture = createTextureImage(0, 4, Uint8Array); + return create(imageTexture, corners, groupTexture, image); } export const Params = { diff --git a/src/mol-geo/geometry/lines/lines.ts b/src/mol-geo/geometry/lines/lines.ts index 4d61a848458e75611ff15a82010f948377a371c2..9809085b67158cd46b91aee50e5ac8b2b84a9de3 100644 --- a/src/mol-geo/geometry/lines/lines.ts +++ b/src/mol-geo/geometry/lines/lines.ts @@ -164,7 +164,7 @@ export namespace Lines { export const Params = { ...BaseGeometry.Params, - sizeFactor: PD.Numeric(1.5, { min: 0, max: 10, step: 0.1 }), + sizeFactor: PD.Numeric(3, { min: 0, max: 10, step: 0.1 }), lineSizeAttenuation: PD.Boolean(false), }; export type Params = typeof Params diff --git a/src/mol-geo/geometry/points/points.ts b/src/mol-geo/geometry/points/points.ts index a3e999d37306db3ad1e92b5e5270a913750153c3..f75a38d84959f69809947c379ba6901444200644 100644 --- a/src/mol-geo/geometry/points/points.ts +++ b/src/mol-geo/geometry/points/points.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 Alexander Rose <alexander.rose@weirdbyte.de> */ @@ -117,12 +117,19 @@ export namespace Points { // + export const StyleTypes = { + 'square': 'Square', + 'circle': 'Circle', + 'fuzzy': 'Fuzzy', + }; + export type StyleTypes = keyof typeof StyleTypes; + export const StyleTypeNames = Object.keys(StyleTypes) as StyleTypes[]; + export const Params = { ...BaseGeometry.Params, - sizeFactor: PD.Numeric(1.5, { min: 0, max: 10, step: 0.1 }), + sizeFactor: PD.Numeric(3, { min: 0, max: 10, step: 0.1 }), pointSizeAttenuation: PD.Boolean(false), - pointFilledCircle: PD.Boolean(false), - pointEdgeBleach: PD.Numeric(0.2, { min: 0, max: 1, step: 0.05 }), + pointStyle: PD.Select('square', PD.objectToOptions(StyleTypes)), }; export type Params = typeof Params @@ -189,8 +196,7 @@ export namespace Points { ...BaseGeometry.createValues(props, counts), uSizeFactor: ValueCell.create(props.sizeFactor), dPointSizeAttenuation: ValueCell.create(props.pointSizeAttenuation), - dPointFilledCircle: ValueCell.create(props.pointFilledCircle), - uPointEdgeBleach: ValueCell.create(props.pointEdgeBleach), + dPointStyle: ValueCell.create(props.pointStyle), }; } @@ -204,8 +210,7 @@ export namespace Points { BaseGeometry.updateValues(values, props); ValueCell.updateIfChanged(values.uSizeFactor, props.sizeFactor); ValueCell.updateIfChanged(values.dPointSizeAttenuation, props.pointSizeAttenuation); - ValueCell.updateIfChanged(values.dPointFilledCircle, props.pointFilledCircle); - ValueCell.updateIfChanged(values.uPointEdgeBleach, props.pointEdgeBleach); + ValueCell.updateIfChanged(values.dPointStyle, props.pointStyle); } function updateBoundingSphere(values: PointsValues, points: Points) { @@ -229,10 +234,7 @@ export namespace Points { function updateRenderableState(state: RenderableState, props: PD.Values<Params>) { BaseGeometry.updateRenderableState(state, props); - state.opaque = state.opaque && ( - !props.pointFilledCircle || - (props.pointFilledCircle && props.pointEdgeBleach === 0) - ); + state.opaque = state.opaque && props.pointStyle !== 'fuzzy'; state.writeDepth = state.opaque; } } \ No newline at end of file diff --git a/src/mol-geo/geometry/texture-mesh/texture-mesh.ts b/src/mol-geo/geometry/texture-mesh/texture-mesh.ts index 47bbecb2829076c3a7d24f15963ce5b338b7ef9b..f8df9d171ce3ad2daeffaac5588200a895944ed0 100644 --- a/src/mol-geo/geometry/texture-mesh/texture-mesh.ts +++ b/src/mol-geo/geometry/texture-mesh/texture-mesh.ts @@ -19,7 +19,7 @@ import { createEmptyOverpaint } from '../overpaint-data'; import { createEmptyTransparency } from '../transparency-data'; import { TextureMeshValues } from '../../../mol-gl/renderable/texture-mesh'; import { calculateTransformBoundingSphere } from '../../../mol-gl/renderable/util'; -import { Texture } from '../../../mol-gl/webgl/texture'; +import { createNullTexture, Texture } from '../../../mol-gl/webgl/texture'; import { Vec2, Vec4 } from '../../../mol-math/linear-algebra'; import { createEmptyClipping } from '../clipping-data'; import { NullLocation } from '../../../mol-model/location'; @@ -97,7 +97,11 @@ export namespace TextureMesh { } export function createEmpty(textureMesh?: TextureMesh): TextureMesh { - return {} as TextureMesh; // TODO + const vt = textureMesh ? textureMesh.vertexTexture.ref.value : createNullTexture(); + const gt = textureMesh ? textureMesh.groupTexture.ref.value : createNullTexture(); + const nt = textureMesh ? textureMesh.normalTexture.ref.value : createNullTexture(); + const bs = textureMesh ? textureMesh.boundingSphere : Sphere3D(); + return create(0, 0, vt, gt, nt, bs, textureMesh); } export const Params = { diff --git a/src/mol-gl/_spec/renderer.spec.ts b/src/mol-gl/_spec/renderer.spec.ts index 0c2a9ea4fbb1190c0f49664505017011de0190b6..c07e61bc5a9d490ba34cae3e20a6f04188af0ee1 100644 --- a/src/mol-gl/_spec/renderer.spec.ts +++ b/src/mol-gl/_spec/renderer.spec.ts @@ -85,8 +85,7 @@ function createPoints() { uSizeFactor: ValueCell.create(1), dPointSizeAttenuation: ValueCell.create(true), - dPointFilledCircle: ValueCell.create(false), - uPointEdgeBleach: ValueCell.create(0.5), + dPointStyle: ValueCell.create('square'), }; const state: RenderableState = { disposed: false, diff --git a/src/mol-gl/renderable/points.ts b/src/mol-gl/renderable/points.ts index 315d6de4b0c7e8bad628b1855060e3d65d52f2b9..ec815261b18e2c668cb8049365dba004e0be70dc 100644 --- a/src/mol-gl/renderable/points.ts +++ b/src/mol-gl/renderable/points.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018 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> */ @@ -7,9 +7,10 @@ import { Renderable, RenderableState, createRenderable } from '../renderable'; import { WebGLContext } from '../webgl/context'; import { createGraphicsRenderItem } from '../webgl/render-item'; -import { GlobalUniformSchema, BaseSchema, AttributeSpec, UniformSpec, DefineSpec, Values, InternalSchema, SizeSchema, InternalValues, GlobalTextureSchema } from './schema'; +import { GlobalUniformSchema, BaseSchema, AttributeSpec, DefineSpec, Values, InternalSchema, SizeSchema, InternalValues, GlobalTextureSchema } from './schema'; import { PointsShaderCode } from '../shader-code'; import { ValueCell } from '../../mol-util'; +import { Points } from '../../mol-geo/geometry/points/points'; export const PointsSchema = { ...BaseSchema, @@ -17,8 +18,7 @@ export const PointsSchema = { aGroup: AttributeSpec('float32', 1, 0), aPosition: AttributeSpec('float32', 3, 0), dPointSizeAttenuation: DefineSpec('boolean'), - dPointFilledCircle: DefineSpec('boolean'), - uPointEdgeBleach: UniformSpec('f'), + dPointStyle: DefineSpec('string', Points.StyleTypeNames), }; export type PointsSchema = typeof PointsSchema export type PointsValues = Values<PointsSchema> diff --git a/src/mol-gl/renderable/schema.ts b/src/mol-gl/renderable/schema.ts index e9b486a936b8c272dab33c352f867f0cd0443b71..2b2bf6a5e851d851af275a76bf60f3aceaba76b1 100644 --- a/src/mol-gl/renderable/schema.ts +++ b/src/mol-gl/renderable/schema.ts @@ -121,7 +121,6 @@ export const GlobalUniformSchema = { uIsOrtho: UniformSpec('f'), uPixelRatio: UniformSpec('f'), - uViewportHeight: UniformSpec('f'), uViewport: UniformSpec('v4'), uViewOffset: UniformSpec('v2'), uDrawingBufferSize: UniformSpec('v2'), @@ -162,6 +161,7 @@ export const GlobalUniformSchema = { uSelectColor: UniformSpec('v3'), uHighlightStrength: UniformSpec('f'), uSelectStrength: UniformSpec('f'), + uMarkerPriority: UniformSpec('i'), uXrayEdgeFalloff: UniformSpec('f'), diff --git a/src/mol-gl/renderer.ts b/src/mol-gl/renderer.ts index 82991710f04d66d8519a509f990074ef374a939e..0cdb2112fa1e4a7b8e4a63cb127e333d97fd73e5 100644 --- a/src/mol-gl/renderer.ts +++ b/src/mol-gl/renderer.ts @@ -62,6 +62,7 @@ interface Renderer { setViewport: (x: number, y: number, width: number, height: number) => void setTransparentBackground: (value: boolean) => void setDrawingBufferSize: (width: number, height: number) => void + setPixelRatio: (value: number) => void dispose: () => void } @@ -80,6 +81,7 @@ export const RendererParams = { selectColor: PD.Color(Color.fromNormalizedRgb(0.2, 1.0, 0.1)), highlightStrength: PD.Numeric(0.7, { min: 0.0, max: 1.0, step: 0.1 }), selectStrength: PD.Numeric(0.7, { min: 0.0, max: 1.0, step: 0.1 }), + markerPriority: PD.Select(1, [[1, 'Highlight'], [2, 'Select']]), xrayEdgeFalloff: PD.Numeric(1, { min: 0.0, max: 3.0, step: 0.1 }), @@ -233,7 +235,6 @@ namespace Renderer { uViewOffset: ValueCell.create(viewOffset), uPixelRatio: ValueCell.create(ctx.pixelRatio), - uViewportHeight: ValueCell.create(viewport.height), uViewport: ValueCell.create(Viewport.toVec4(Vec4(), viewport)), uDrawingBufferSize: ValueCell.create(drawingBufferSize), @@ -274,6 +275,7 @@ namespace Renderer { uSelectColor: ValueCell.create(Color.toVec3Normalized(Vec3(), p.selectColor)), uHighlightStrength: ValueCell.create(p.highlightStrength), uSelectStrength: ValueCell.create(p.selectStrength), + uMarkerPriority: ValueCell.create(p.markerPriority), uXrayEdgeFalloff: ValueCell.create(p.xrayEdgeFalloff), }; @@ -571,7 +573,7 @@ namespace Renderer { // TODO: simplify, handle in renderable.state??? // uAlpha is updated in "render" so we need to recompute it here const alpha = clamp(r.values.alpha.ref.value * r.state.alphaFactor, 0, 1); - if (alpha === 1 && r.values.transparencyAverage.ref.value !== 1 && r.values.dRenderMode?.ref.value !== 'volume' && !r.values.dPointFilledCircle?.ref.value && !r.values.dXrayShaded?.ref.value) { + if (alpha === 1 && r.values.transparencyAverage.ref.value !== 1 && r.values.dRenderMode?.ref.value !== 'volume' && r.values.dPointStyle?.ref.value !== 'fuzzy' && !r.values.dXrayShaded?.ref.value) { renderObject(r, 'colorWboit'); } } @@ -587,7 +589,7 @@ namespace Renderer { // TODO: simplify, handle in renderable.state??? // uAlpha is updated in "render" so we need to recompute it here const alpha = clamp(r.values.alpha.ref.value * r.state.alphaFactor, 0, 1); - if (alpha < 1 || r.values.transparencyAverage.ref.value > 0 || r.values.dRenderMode?.ref.value === 'volume' || r.values.dPointFilledCircle?.ref.value || !!r.values.uBackgroundColor || r.values.dXrayShaded?.ref.value) { + if (alpha < 1 || r.values.transparencyAverage.ref.value > 0 || r.values.dRenderMode?.ref.value === 'volume' || r.values.dPointStyle?.ref.value === 'fuzzy' || !!r.values.uBackgroundColor || r.values.dXrayShaded?.ref.value) { renderObject(r, 'colorWboit'); } } @@ -670,6 +672,10 @@ namespace Renderer { p.selectStrength = props.selectStrength; ValueCell.update(globalUniforms.uSelectStrength, p.selectStrength); } + if (props.markerPriority !== undefined && props.markerPriority !== p.markerPriority) { + p.markerPriority = props.markerPriority; + ValueCell.update(globalUniforms.uMarkerPriority, p.markerPriority); + } if (props.xrayEdgeFalloff !== undefined && props.xrayEdgeFalloff !== p.xrayEdgeFalloff) { p.xrayEdgeFalloff = props.xrayEdgeFalloff; @@ -700,7 +706,6 @@ namespace Renderer { gl.scissor(x, y, width, height); if (x !== viewport.x || y !== viewport.y || width !== viewport.width || height !== viewport.height) { Viewport.set(viewport, x, y, width, height); - ValueCell.update(globalUniforms.uViewportHeight, height); ValueCell.update(globalUniforms.uViewport, Vec4.set(globalUniforms.uViewport.ref.value, x, y, width, height)); } }, @@ -712,6 +717,9 @@ namespace Renderer { ValueCell.update(globalUniforms.uDrawingBufferSize, Vec2.set(drawingBufferSize, width, height)); } }, + setPixelRatio: (value: number) => { + ValueCell.update(globalUniforms.uPixelRatio, value); + }, props: p, get stats(): RendererStats { diff --git a/src/mol-gl/shader/chunks/apply-marker-color.glsl.ts b/src/mol-gl/shader/chunks/apply-marker-color.glsl.ts index 9b888c7493cab936e2fddc26a84258b3efe49482..6f09848c28ae456bf73cf2d39b8895e63fa46c43 100644 --- a/src/mol-gl/shader/chunks/apply-marker-color.glsl.ts +++ b/src/mol-gl/shader/chunks/apply-marker-color.glsl.ts @@ -1,6 +1,6 @@ export const apply_marker_color = ` -if (marker > 0.1) { - if (intMod(marker, 2.0) > 0.1) { +if (marker > 0.0) { + if ((uMarkerPriority == 1 && marker != 2.0) || (uMarkerPriority != 1 && marker == 1.0)) { gl_FragColor.rgb = mix(gl_FragColor.rgb, uHighlightColor, uHighlightStrength); gl_FragColor.a = max(gl_FragColor.a, uHighlightStrength * 0.002); // for direct-volume rendering } else { diff --git a/src/mol-gl/shader/chunks/assign-material-color.glsl.ts b/src/mol-gl/shader/chunks/assign-material-color.glsl.ts index c147051ec483c6b642b3d0a3af4c4ad224b9634d..9d550b8f14277d2fd128601971edcc78cf9d9136 100644 --- a/src/mol-gl/shader/chunks/assign-material-color.glsl.ts +++ b/src/mol-gl/shader/chunks/assign-material-color.glsl.ts @@ -3,9 +3,8 @@ export const assign_material_color = ` #if defined(dMarkerType_uniform) float marker = uMarker; #elif defined(dMarkerType_groupInstance) - float marker = vMarker; + float marker = floor(vMarker * 255.0 + 0.5); // rounding required to work on some cards on win #endif - marker = floor(marker * 255.0 + 0.5); // rounding required to work on some cards on win #endif #if defined(dRenderVariant_color) diff --git a/src/mol-gl/shader/chunks/common-frag-params.glsl.ts b/src/mol-gl/shader/chunks/common-frag-params.glsl.ts index e3aac5f0e61d0a1740385f21a90a9beaabc95a2e..e062b9e02b4af74910b4af29c06123eb51fed4c1 100644 --- a/src/mol-gl/shader/chunks/common-frag-params.glsl.ts +++ b/src/mol-gl/shader/chunks/common-frag-params.glsl.ts @@ -23,6 +23,7 @@ uniform vec3 uHighlightColor; uniform vec3 uSelectColor; uniform float uHighlightStrength; uniform float uSelectStrength; +uniform int uMarkerPriority; #if defined(dMarkerType_uniform) uniform float uMarker; diff --git a/src/mol-gl/shader/direct-volume.frag.ts b/src/mol-gl/shader/direct-volume.frag.ts index 6611631063258c21475ccd630f82e8304c33f409..e6fc9d32471a93a0a8dcc115f17dd373edf4093d 100644 --- a/src/mol-gl/shader/direct-volume.frag.ts +++ b/src/mol-gl/shader/direct-volume.frag.ts @@ -55,6 +55,7 @@ uniform vec3 uHighlightColor; uniform vec3 uSelectColor; uniform float uHighlightStrength; uniform float uSelectStrength; +uniform int uMarkerPriority; #if defined(dMarkerType_uniform) uniform float uMarker; diff --git a/src/mol-gl/shader/lines.vert.ts b/src/mol-gl/shader/lines.vert.ts index b39198d888020593ba1c9b79b80c3541088532ae..6a8c6c5e2e1c04278adeeef486702299a048c9a7 100644 --- a/src/mol-gl/shader/lines.vert.ts +++ b/src/mol-gl/shader/lines.vert.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 Alexander Rose <alexander.rose@weirdbyte.de> * @@ -18,7 +18,7 @@ precision highp int; #include common_clip uniform float uPixelRatio; -uniform float uViewportHeight; +uniform vec4 uViewport; attribute mat4 aTransform; attribute float aInstance; @@ -39,6 +39,8 @@ void trimSegment(const in vec4 start, inout vec4 end) { } void main(){ + float aspect = uViewport.z / uViewport.w; + #include assign_group #include assign_color_varying #include assign_marker_varying @@ -83,15 +85,15 @@ void main(){ vec2 dir = ndcEnd - ndcStart; // account for clip-space aspect ratio - dir.x *= uPixelRatio; + dir.x *= aspect; dir = normalize(dir); // perpendicular to dir vec2 offset = vec2(dir.y, - dir.x); // undo aspect ratio adjustment - dir.x /= uPixelRatio; - offset.x /= uPixelRatio; + dir.x /= aspect; + offset.x /= aspect; // sign flip if (aMapping.x < 0.0) offset *= -1.0; @@ -99,16 +101,17 @@ void main(){ // calculate linewidth float linewidth; #ifdef dLineSizeAttenuation - linewidth = size * uPixelRatio * ((uViewportHeight / 2.0) / -start.z) * 5.0; + linewidth = size * uPixelRatio * ((uViewport.w / 2.0) / -start.z) * 5.0; #else linewidth = size * uPixelRatio; #endif + linewidth = max(1.0, linewidth); // adjust for linewidth offset *= linewidth; // adjust for clip-space to screen-space conversion - offset /= uViewportHeight; + offset /= uViewport.w; // select end vec4 clip = (aMapping.y < 0.5) ? clipStart : clipEnd; diff --git a/src/mol-gl/shader/points.frag.ts b/src/mol-gl/shader/points.frag.ts index c18506882ebd89581df63f96a0474c9fa28718f8..0d52802e2b016c3dcdec83863ab2ce6180eb0448 100644 --- a/src/mol-gl/shader/points.frag.ts +++ b/src/mol-gl/shader/points.frag.ts @@ -13,10 +13,6 @@ precision highp int; #include color_frag_params #include common_clip -#ifdef dPointFilledCircle - uniform float uPointEdgeBleach; -#endif - const vec2 center = vec2(0.5); const float radius = 0.5; @@ -27,6 +23,15 @@ void main(){ bool interior = false; #include assign_material_color + #if defined(dPointStyle_circle) + float dist = distance(gl_PointCoord, center); + if (dist > radius) discard; + #elif defined(dPointStyle_fuzzy) + float dist = distance(gl_PointCoord, center); + float fuzzyAlpha = 1.0 - smoothstep(0.0, radius, dist); + if (fuzzyAlpha < 0.0001) discard; + #endif + #if defined(dRenderVariant_pick) #include check_picking_alpha gl_FragColor = material; @@ -37,11 +42,8 @@ void main(){ #elif defined(dRenderVariant_color) gl_FragColor = material; - #ifdef dPointFilledCircle - float dist = distance(gl_PointCoord, center); - float alpha = 1.0 - smoothstep(radius - uPointEdgeBleach, radius, dist); - if (alpha < 0.0001) discard; - gl_FragColor.a *= alpha; + #if defined(dPointStyle_fuzzy) + gl_FragColor.a *= fuzzyAlpha; #endif #include apply_marker_color diff --git a/src/mol-gl/shader/points.vert.ts b/src/mol-gl/shader/points.vert.ts index db206d1c9aa0aa9416385734707f199f962658b1..0efca334f6532e87ec7fcdcc128aafbc8651eebe 100644 --- a/src/mol-gl/shader/points.vert.ts +++ b/src/mol-gl/shader/points.vert.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 Alexander Rose <alexander.rose@weirdbyte.de> */ @@ -16,7 +16,7 @@ precision highp int; #include common_clip uniform float uPixelRatio; -uniform float uViewportHeight; +uniform vec4 uViewport; attribute vec3 aPosition; attribute mat4 aTransform; @@ -32,10 +32,11 @@ void main(){ #include assign_size #ifdef dPointSizeAttenuation - gl_PointSize = size * uPixelRatio * ((uViewportHeight / 2.0) / -mvPosition.z) * 5.0; + gl_PointSize = size * uPixelRatio * ((uViewport.w / 2.0) / -mvPosition.z) * 5.0; #else gl_PointSize = size * uPixelRatio; #endif + gl_PointSize = max(1.0, gl_PointSize); gl_Position = uProjection * mvPosition; diff --git a/src/mol-gl/shader/text.vert.ts b/src/mol-gl/shader/text.vert.ts index 18b2c256b9a38948caf8082f12f8f2cbae48e94b..38779f2d02ca3c4d2f96932f74bf0406614ab4d0 100644 --- a/src/mol-gl/shader/text.vert.ts +++ b/src/mol-gl/shader/text.vert.ts @@ -31,7 +31,7 @@ uniform float uOffsetZ; // uniform bool ortho; uniform float uPixelRatio; -uniform float uViewportHeight; +uniform vec4 uViewport; varying vec2 vTexCoord; @@ -60,9 +60,9 @@ void main(void){ // TODO // #ifdef FIXED_SIZE // if (ortho) { - // scale /= pixelRatio * ((uViewportHeight / 2.0) / -uCameraPosition.z) * 0.1; + // scale /= pixelRatio * ((uViewport.w / 2.0) / -uCameraPosition.z) * 0.1; // } else { - // scale /= pixelRatio * ((uViewportHeight / 2.0) / -mvPosition.z) * 0.1; + // scale /= pixelRatio * ((uViewport.w / 2.0) / -mvPosition.z) * 0.1; // } // #endif diff --git a/src/mol-gl/webgl/context.ts b/src/mol-gl/webgl/context.ts index 7f255febaeee485d10adbcb2dfad07ddcec33b3a..5ce242b2aa25b81dfebe4beb48376d0a3ac582c5 100644 --- a/src/mol-gl/webgl/context.ts +++ b/src/mol-gl/webgl/context.ts @@ -18,7 +18,7 @@ import { now } from '../../mol-util/now'; import { Texture, TextureFilter } from './texture'; import { ComputeRenderable } from '../renderable'; -export function getGLContext(canvas: HTMLCanvasElement, attribs?: WebGLContextAttributes): GLRenderingContext | null { +export function getGLContext(canvas: HTMLCanvasElement, attribs?: WebGLContextAttributes & { preferWebGl1?: boolean }): GLRenderingContext | null { function get(id: 'webgl' | 'experimental-webgl' | 'webgl2') { try { return canvas.getContext(id, attribs) as GLRenderingContext | null; @@ -26,7 +26,7 @@ export function getGLContext(canvas: HTMLCanvasElement, attribs?: WebGLContextAt return null; } } - const gl = get('webgl2') || get('webgl') || get('experimental-webgl'); + const gl = (attribs?.preferWebGl1 ? null : get('webgl2')) || get('webgl') || get('experimental-webgl'); if (isDebugMode) console.log(`isWebgl2: ${isWebGL2(gl)}`); return gl; } diff --git a/src/mol-io/reader/common/text/column/token.ts b/src/mol-io/reader/common/text/column/token.ts index b07d20a881152e2566a3e3c7469cb0f50b5dd660..8560c80ecb6968d00d9679ebeebde7384c17fa31 100644 --- a/src/mol-io/reader/common/text/column/token.ts +++ b/src/mol-io/reader/common/text/column/token.ts @@ -1,7 +1,8 @@ /** - * Copyright (c) 2017 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> */ import { Column, ColumnHelpers } from '../../../../../mol-data/db'; @@ -50,4 +51,12 @@ export function areValuesEqualProvider(tokens: Tokens) { } return true; }; +} + +export function areTokensEmpty(tokens: Tokens) { + const { count, indices } = tokens; + for (let i = 0; i < count; ++i) { + if (indices[2 * i] !== indices[2 * i + 1]) return false; + } + return true; } \ No newline at end of file diff --git a/src/mol-math/geometry/primitives/axes3d.ts b/src/mol-math/geometry/primitives/axes3d.ts index bb70f70ef48dcb5af3a4883bf6b62e9762790c04..5db8ae4f5cd692322ebc7dcf7b58572334549f38 100644 --- a/src/mol-math/geometry/primitives/axes3d.ts +++ b/src/mol-math/geometry/primitives/axes3d.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ @@ -48,7 +48,7 @@ namespace Axes3D { return out; } - const tmpTransformMat3 = Mat3.zero(); + const tmpTransformMat3 = Mat3(); /** Transform axes with a Mat4 */ export function transform(out: Axes3D, a: Axes3D, m: Mat4): Axes3D { Vec3.transformMat4(out.origin, a.origin, m); @@ -58,6 +58,13 @@ namespace Axes3D { Vec3.transformMat3(out.dirC, a.dirC, n); return out; } + + export function scale(out: Axes3D, a: Axes3D, scale: number): Axes3D { + Vec3.scale(out.dirA, a.dirA, scale); + Vec3.scale(out.dirB, a.dirB, scale); + Vec3.scale(out.dirC, a.dirC, scale); + return out; + } } export { Axes3D }; \ No newline at end of file diff --git a/src/mol-math/linear-algebra/3d/vec3.ts b/src/mol-math/linear-algebra/3d/vec3.ts index c65a310db89b8fb7c98f5d19906df9f2a5589ea7..c0bf8b04f94f8fbcac6d791041609aae78211347 100644 --- a/src/mol-math/linear-algebra/3d/vec3.ts +++ b/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) { diff --git a/src/mol-math/linear-algebra/tensor.ts b/src/mol-math/linear-algebra/tensor.ts index 94a6df386d7d1fb1ae28582efb1f240e942e29a2..f28143270888831464486009c7a7ba5ade3b892e 100644 --- a/src/mol-math/linear-algebra/tensor.ts +++ b/src/mol-math/linear-algebra/tensor.ts @@ -109,7 +109,10 @@ export namespace Tensor { set: (t, d, x) => t[d] = x, add: (t, d, x) => t[d] += x, dataOffset: (d) => d, - getCoords: (o, c) => { c[0] = o; return c as number[]; } + getCoords: (o, c) => { + c[0] = o; + return c as number[]; + } }; case 2: { // column major @@ -120,7 +123,11 @@ export namespace Tensor { set: (t, i, j, x) => t[j * rows + i] = x, add: (t, i, j, x) => t[j * rows + i] += x, dataOffset: (i, j) => j * rows + i, - getCoords: (o, c) => { c[0] = o % rows; c[1] = Math.floor(o / rows) ; return c as number[]; } + getCoords: (o, c) => { + c[0] = o % rows; + c[1] = Math.floor(o / rows); + return c as number[]; + } }; } if (ao[0] === 1 && ao[1] === 0) { @@ -130,7 +137,11 @@ export namespace Tensor { set: (t, i, j, x) => t[i * cols + j] = x, add: (t, i, j, x) => t[i * cols + j] += x, dataOffset: (i, j) => i * cols + j, - getCoords: (o, c) => { c[0] = Math.floor(o / cols); c[1] = o % cols; return c as number[]; } + getCoords: (o, c) => { + c[0] = Math.floor(o / cols); + c[1] = o % cols; + return c as number[]; + } }; } throw new Error('bad axis order'); diff --git a/src/mol-math/misc.ts b/src/mol-math/misc.ts index 235baa8a53d672a2136e76df813193000b64c1e0..05afcf7f225d9a53ec90906813f7259ddbb388ff 100644 --- a/src/mol-math/misc.ts +++ b/src/mol-math/misc.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> */ @@ -37,4 +37,28 @@ export function absMax(...values: number[]) { /** Length of an arc with angle in radians */ export function arcLength(angle: number, radius: number) { return angle * radius; +} + +/** Create an outward spiral of given `radius` on a 2d grid */ +export function spiral2d(radius: number) { + let x = 0; + let y = 0; + const delta = [0, -1]; + const size = radius * 2 + 1; + const halfSize = size / 2; + const out: [number, number][] = []; + + for (let i = Math.pow(size, 2); i > 0; --i) { + if ((-halfSize < x && x <= halfSize) && (-halfSize < y && y <= halfSize)) { + out.push([x, y]); + } + + if (x === y || (x < 0 && x === -y) || (x > 0 && x === 1 - y)) { + [delta[0], delta[1]] = [-delta[1], delta[0]]; // change direction + } + + x += delta[0]; + y += delta[1]; + } + return out; } \ No newline at end of file diff --git a/src/mol-model-formats/structure/basic/atomic.ts b/src/mol-model-formats/structure/basic/atomic.ts index 250bca12f4559df4fdb0bb485d898aa03e05d24f..10ad709b99e408c000add90f88bac8f6e612ed56 100644 --- a/src/mol-model-formats/structure/basic/atomic.ts +++ b/src/mol-model-formats/structure/basic/atomic.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> @@ -100,7 +100,7 @@ function getConformation(atom_site: AtomSite): AtomicConformation { return { id: UUID.create22(), atomId: atom_site.id, - occupancy: atom_site.occupancy, + occupancy: atom_site.occupancy.isDefined ? atom_site.occupancy : Column.ofConst(1, atom_site._rowCount, Column.Schema.float), B_iso_or_equiv: atom_site.B_iso_or_equiv, xyzDefined: atom_site.Cartn_x.isDefined && atom_site.Cartn_y.isDefined && atom_site.Cartn_z.isDefined, x: atom_site.Cartn_x.toArray({ array: Float32Array }), diff --git a/src/mol-model-formats/structure/pdb/atom-site.ts b/src/mol-model-formats/structure/pdb/atom-site.ts index 94aa82e7110f4b448cce2be92801c8b6ba86e52d..7fcc6389101767f11bcf71164ecfaff54a2ba2b1 100644 --- a/src/mol-model-formats/structure/pdb/atom-site.ts +++ b/src/mol-model-formats/structure/pdb/atom-site.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-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> @@ -10,6 +10,7 @@ import { mmCIF_Schema } from '../../../mol-io/reader/cif/schema/mmcif'; import { TokenBuilder, Tokenizer } from '../../../mol-io/reader/common/text/tokenizer'; import { guessElementSymbolTokens } from '../util'; import { Column } from '../../../mol-data/db'; +import { areTokensEmpty } from '../../../mol-io/reader/common/text/column/token'; type AtomSiteTemplate = typeof getAtomSiteTemplate extends (...args: any) => infer T ? T : never export function getAtomSiteTemplate(data: string, count: number) { @@ -63,7 +64,7 @@ export function getAtomSite(sites: AtomSiteTemplate): { [K in keyof mmCIF_Schema label_seq_id: CifField.ofUndefined(sites.index, Column.Schema.int), label_entity_id: CifField.ofStrings(sites.label_entity_id), - occupancy: CifField.ofTokens(sites.occupancy), + occupancy: areTokensEmpty(sites.occupancy) ? CifField.ofUndefined(sites.index, Column.Schema.float) : CifField.ofTokens(sites.occupancy), type_symbol: CifField.ofTokens(sites.type_symbol), pdbx_PDB_ins_code: CifField.ofTokens(sites.pdbx_PDB_ins_code), diff --git a/src/mol-model-formats/structure/pdb/entity.ts b/src/mol-model-formats/structure/pdb/entity.ts index b40f9b2e2cd6808d34d9993ee0afb6d00461596b..9276a0c851c8a830df298f9c6642e211365295eb 100644 --- a/src/mol-model-formats/structure/pdb/entity.ts +++ b/src/mol-model-formats/structure/pdb/entity.ts @@ -25,7 +25,7 @@ export function parseCmpnd(lines: Tokens, lineStart: number, lineEnd: number) { let currentSpec: Spec | undefined; let currentCompound: EntityCompound = { chains: [], description: '' }; - const Compounds: EntityCompound[] = []; + const compounds: EntityCompound[] = []; for (let i = lineStart; i < lineEnd; i++) { const line = getLine(i); @@ -55,7 +55,7 @@ export function parseCmpnd(lines: Tokens, lineStart: number, lineEnd: number) { chains: [], description: '' }; - Compounds.push(currentCompound); + compounds.push(currentCompound); } else if (currentSpec === 'MOLECULE') { if (currentCompound.description) currentCompound.description += ' '; currentCompound.description += value; @@ -64,7 +64,31 @@ export function parseCmpnd(lines: Tokens, lineStart: number, lineEnd: number) { } } - return Compounds; + // Define a seprate entity for each chain + // -------------------------------------- + // + // This is a workaround for how sequences are currently determined for PDB files. + // + // The current approach infers the "observed sequence" from the atomic hierarchy. + // However, for example for PDB ID 3HHR, this approach fails, since chains B and C + // belong to the same entity but contain different observed sequence, which causes display + // errors in the sequence viewer (since the sequences are determined "per entity"). + // + // A better approach could be to parse SEQRES categories and use it to construct + // entity_poly_seq category. However, this would require constructing label_seq_id (with gaps) + // from RES ID pdb column (auth_seq_id), which isn't a trivial exercise. + // + // (properly formatted) mmCIF structures do not exhibit this issue. + const singletons: EntityCompound[] = []; + for (const comp of compounds) { + for (const chain of comp.chains) { + singletons.push({ + description: comp.description, + chains: [chain] + }); + } + } + return singletons; } export function parseHetnam(lines: Tokens, lineStart: number, lineEnd: number) { diff --git a/src/mol-model-formats/structure/pdb/secondary-structure.ts b/src/mol-model-formats/structure/pdb/secondary-structure.ts index d6c6e60e11caf9f8d2a2985e563ee76d3c467aa9..626a201bd917ee58939eedd26e49efce69fc9fa9 100644 --- a/src/mol-model-formats/structure/pdb/secondary-structure.ts +++ b/src/mol-model-formats/structure/pdb/secondary-structure.ts @@ -102,7 +102,7 @@ export function parseHelix(lines: Tokens, lineStart: number, lineEnd: number): C const beg_auth_comp_id = CifField.ofStrings(helices.map(h => h.initResName)); const end_auth_asym_id = CifField.ofStrings(helices.map(h => h.endChainID)); - const end_auth_comp_id = CifField.ofStrings(helices.map(h => h.endResName));; + const end_auth_comp_id = CifField.ofStrings(helices.map(h => h.endResName)); const struct_conf: CifCategory.Fields<mmCIF_Schema['struct_conf']> = { beg_label_asym_id: beg_auth_asym_id, diff --git a/src/mol-model-props/computed/helix-orientation/helix-orientation.ts b/src/mol-model-props/computed/helix-orientation/helix-orientation.ts index 5f83a2ce64f7244a7495596b329458270a68946e..512531204bc7ff861993f0bc10f6e6dcafa31a0a 100644 --- a/src/mol-model-props/computed/helix-orientation/helix-orientation.ts +++ b/src/mol-model-props/computed/helix-orientation/helix-orientation.ts @@ -127,7 +127,8 @@ export function calcHelixOrientation(model: Model): HelixOrientation { Vec3.fromArray(v2, centers, e3 - 6); Vec3.normalize(axis, Vec3.sub(axis, v1, v2)); const eI = traceElementIndex[e]; - Vec3.set(a1, x[eI], y[eI], z[eI]);Vec3.copy(vt, a1); + Vec3.set(a1, x[eI], y[eI], z[eI]); + Vec3.copy(vt, a1); Vec3.projectPointOnVector(vt, vt, axis, v1); Vec3.toArray(vt, centers, e3); } diff --git a/src/mol-model/loci.ts b/src/mol-model/loci.ts index 64d264efdb40ca2370b4e1f30554f0a8db7ee245..a85152bc8fcfa6dab7b9a062adb8c25ad29736fa 100644 --- a/src/mol-model/loci.ts +++ b/src/mol-model/loci.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 Alexander Rose <alexander.rose@weirdbyte.de> */ @@ -283,8 +283,8 @@ namespace Loci { * Converts structure related loci to StructureElement.Loci and applies * granularity if given */ - export function normalize(loci: Loci, granularity?: Granularity) { - if (granularity !== 'element' && Bond.isLoci(loci)) { + export function normalize(loci: Loci, granularity?: Granularity, alwaysConvertBonds = false) { + if ((granularity !== 'element' || alwaysConvertBonds) && Bond.isLoci(loci)) { // convert Bond.Loci to a StructureElement.Loci so granularity can be applied loci = Bond.toStructureElementLoci(loci); } diff --git a/src/mol-model/structure/export/categories/misc.ts b/src/mol-model/structure/export/categories/misc.ts index 31887e0d6864f09315fa4fcce6aa6edcfd50a077..a74772ede2aa31ffc63b6a4107124663052528ce 100644 --- a/src/mol-model/structure/export/categories/misc.ts +++ b/src/mol-model/structure/export/categories/misc.ts @@ -22,6 +22,18 @@ export const _chem_comp: CifCategory<CifExportContext> = { } }; +export const _chem_comp_bond: CifCategory<CifExportContext> = { + name: 'chem_comp_bond', + instance({ firstModel, structures, cache }) { + const chem_comp_bond = getModelMmCifCategory(structures[0].model, 'chem_comp_bond'); + if (!chem_comp_bond) return CifCategory.Empty; + const { comp_id } = chem_comp_bond; + const names = cache.uniqueResidueNames || (cache.uniqueResidueNames = getUniqueResidueNamesFromStructures(structures)); + const indices = Column.indicesOf(comp_id, id => names.has(id)); + return CifCategory.ofTable(chem_comp_bond, indices); + } +}; + export const _pdbx_chem_comp_identifier: CifCategory<CifExportContext> = { name: 'pdbx_chem_comp_identifier', instance({ firstModel, structures, cache }) { diff --git a/src/mol-model/structure/export/mmcif.ts b/src/mol-model/structure/export/mmcif.ts index 6615a4f87199336ab5497743a33ce49afd7c62e4..eb806f24670562536e68c523d15052a1043a9eb8 100644 --- a/src/mol-model/structure/export/mmcif.ts +++ b/src/mol-model/structure/export/mmcif.ts @@ -11,7 +11,7 @@ import { Structure } from '../structure'; import { _atom_site } from './categories/atom_site'; import CifCategory = CifWriter.Category import { _struct_conf, _struct_sheet_range } from './categories/secondary-structure'; -import { _chem_comp, _pdbx_chem_comp_identifier, _pdbx_nonpoly_scheme } from './categories/misc'; +import { _chem_comp, _chem_comp_bond, _pdbx_chem_comp_identifier, _pdbx_nonpoly_scheme } from './categories/misc'; import { Model } from '../model'; import { getUniqueEntityIndicesFromStructures, copy_mmCif_category, copy_source_mmCifCategory } from './categories/utils'; import { _struct_asym, _entity_poly, _entity_poly_seq } from './categories/sequence'; @@ -81,9 +81,12 @@ const Categories = [ copy_mmCif_category('pdbx_entity_branch_link'), copy_mmCif_category('pdbx_branch_scheme'), + // Struct conn + copy_mmCif_category('struct_conn'), + // Misc - // TODO: filter for actual present residues? _chem_comp, + _chem_comp_bond, _pdbx_chem_comp_identifier, copy_mmCif_category('atom_sites'), diff --git a/src/mol-model/structure/structure/carbohydrates/constants.ts b/src/mol-model/structure/structure/carbohydrates/constants.ts index 6517d7c7ab756f6355292c957b31bf2b850f40c4..dc8a1c449c8ea5aa14e7fe1742d20d553c6cf5e9 100644 --- a/src/mol-model/structure/structure/carbohydrates/constants.ts +++ b/src/mol-model/structure/structure/carbohydrates/constants.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> * @author David Sehnal <david.sehnal@gmail.com> @@ -309,14 +309,48 @@ const UnknownSaccharideNames = [ 'PUF', 'GDA', '9WJ', // via updated CCD ]; +/** + * From http://glycam.org/docs/othertoolsservice/2016/06/09/3d-snfg-list-of-residue-names/#CHARMM + */ +const CharmmSaccharideNames: { [k: string]: string[] } = { + Glc: ['AGLC', 'BGLC'], + GlcNAc: ['AGLCNA', 'BGLCNA', 'BGLCN0'], + GlcA: ['AGLCA', 'BGLCA', 'BGLCA0'], + Man: ['AMAN', 'BMAN'], + Rha: ['ARHM', 'BRHM'], + Ara: ['AARB', 'BARB'], + Gal: ['AGAL', 'BGAL'], + GalNAc: ['AGALNA', 'BGALNA'], + Gul: ['AGUL', 'BGUL'], + Alt: ['AALT', 'BALT'], + All: ['AALL', 'BALL'], + Tal: ['ATAL', 'BTAL'], + Ido: ['AIDO', 'BIDO'], + IdoA: ['AIDOA', 'BIDOA'], + Fuc: ['AFUC', 'BFUC'], + Lyx: ['ALYF', 'BLYF'], + Xyl: ['AXYL', 'BXYL', 'AXYF', 'BXYF'], + Rib: ['ARIB', 'BRIB'], + Fru: ['AFRU', 'BFRU'], + Neu5Ac: ['ANE5AC', 'BNE5AC'], +}; + export const SaccharideCompIdMap = (function () { const map = new Map<string, SaccharideComponent>(); for (let i = 0, il = Monosaccharides.length; i < il; ++i) { const saccharide = Monosaccharides[i]; - const names = CommonSaccharideNames[saccharide.abbr]; - if (names) { - for (let j = 0, jl = names.length; j < jl; ++j) { - map.set(names[j], saccharide); + + const common = CommonSaccharideNames[saccharide.abbr]; + if (common) { + for (let j = 0, jl = common.length; j < jl; ++j) { + map.set(common[j], saccharide); + } + } + + const charmm = CharmmSaccharideNames[saccharide.abbr]; + if (charmm) { + for (let j = 0, jl = charmm.length; j < jl; ++j) { + map.set(charmm[j], saccharide); } } } diff --git a/src/mol-model/structure/structure/element/loci.ts b/src/mol-model/structure/structure/element/loci.ts index 617f869a57916cf5402bb8bd18409376ee0111a2..53c2780c60f4191ab4300bb880b507b9ee490370 100644 --- a/src/mol-model/structure/structure/element/loci.ts +++ b/src/mol-model/structure/structure/element/loci.ts @@ -582,6 +582,20 @@ export namespace Loci { return PrincipalAxes.ofPositions(positions); } + export function getPrincipalAxesMany(locis: Loci[]): PrincipalAxes { + let elementCount = 0; + locis.forEach(l => { + elementCount += size(l); + }); + const positions = new Float32Array(3 * elementCount); + let offset = 0; + locis.forEach(l => { + toPositionsArray(l, positions, offset); + offset += size(l) * 3; + }); + return PrincipalAxes.ofPositions(positions); + } + function sourceIndex(unit: Unit, element: ElementIndex) { return Unit.isAtomic(unit) ? unit.model.atomicHierarchy.atomSourceIndex.value(element) diff --git a/src/mol-model/structure/structure/unit.ts b/src/mol-model/structure/structure/unit.ts index c68322ec210bc4674c196888f0596ebdff4780f1..62a2ec7cf35d25612abaf47062cb22832d68bca5 100644 --- a/src/mol-model/structure/structure/unit.ts +++ b/src/mol-model/structure/structure/unit.ts @@ -369,7 +369,7 @@ namespace Unit { readonly props: CoarseProperties; getChild(elements: StructureElement.Set): Unit { - if (elements.length === this.elements.length) return this as any as Unit /** lets call this an ugly temporary hack */; + if (elements.length === this.elements.length) return this as any as Unit; // lets call this an ugly temporary hack return createCoarse(this.id, this.invariantId, this.chainGroupId, this.traits, this.model, this.kind, elements, this.conformation, CoarseProperties()); } @@ -465,7 +465,7 @@ namespace Unit { export class Gaussians extends Coarse<Kind.Gaussians, CoarseGaussianConformation> { } function createCoarse<K extends Kind.Gaussians | Kind.Spheres>(id: number, invariantId: number, chainGroupId: number, traits: Traits, model: Model, kind: K, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping<ElementIndex>, props: CoarseProperties): K extends Kind.Spheres ? Spheres : Gaussians { - return new Coarse(id, invariantId, chainGroupId, traits, model, kind, elements, conformation, props) as any /** lets call this an ugly temporary hack */; + return new Coarse(id, invariantId, chainGroupId, traits, model, kind, elements, conformation, props) as any; // lets call this an ugly temporary hack } export function areSameChainOperatorGroup(a: Unit, b: Unit) { diff --git a/src/mol-model/structure/structure/unit/bonds.ts b/src/mol-model/structure/structure/unit/bonds.ts index e00ee03148b64183cff7dc8d94f6c9af68a967ff..20f562397f43989f92f78ea1347a77ae26027c1e 100644 --- a/src/mol-model/structure/structure/unit/bonds.ts +++ b/src/mol-model/structure/structure/unit/bonds.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> @@ -8,7 +8,7 @@ import { Unit, StructureElement } from '../../structure'; import { Structure } from '../structure'; import { BondType } from '../../model/types'; -import { SortedArray, Iterator } from '../../../../mol-data/int'; +import { SortedArray, Iterator, OrderedSet } from '../../../../mol-data/int'; import { CentroidHelper } from '../../../../mol-math/geometry/centroid-helper'; import { Sphere3D } from '../../../../mol-math/geometry'; @@ -132,6 +132,11 @@ namespace Bond { return StructureElement.Loci(loci.structure, elements); } + export function toFirstStructureElementLoci(loci: Loci): StructureElement.Loci { + const { aUnit, aIndex } = loci.bonds[0]; + return StructureElement.Loci(loci.structure, [{ unit: aUnit, indices: OrderedSet.ofSingleton(aIndex) }]); + } + export function getType(structure: Structure, location: Location<Unit.Atomic>): BondType { if (location.aUnit === location.bUnit) { const bonds = location.aUnit.bonds; diff --git a/src/mol-model/structure/structure/unit/bonds/intra-compute.ts b/src/mol-model/structure/structure/unit/bonds/intra-compute.ts index 427bf7378901e3e50d3b2d9aa1b05cb271263959..9a77f179656b166a9dfdfc4a7465714ffc2522a3 100644 --- a/src/mol-model/structure/structure/unit/bonds/intra-compute.ts +++ b/src/mol-model/structure/structure/unit/bonds/intra-compute.ts @@ -79,7 +79,7 @@ function findIndexPairBonds(unit: Unit.Atomic) { if ((d !== -1 && equalEps(dist, d, 0.5)) || dist < maxDistance) { atomA[atomA.length] = _aI; atomB[atomB.length] = _bI; - orders[order.length] = order[i]; + orders[orders.length] = order[i]; flags[flags.length] = flag[i]; } } @@ -133,7 +133,7 @@ function findBonds(unit: Unit.Atomic, props: BondComputationProps): IntraUnitBon const p = partnerA.atomIndex === aI ? partnerB : partnerA; const _bI = SortedArray.indexOf(unit.elements, p.atomIndex) as StructureElement.UnitIndex; - if (_bI < 0) continue; + if (_bI < 0 || atoms[_bI] < aI) continue; atomA[atomA.length] = _aI; atomB[atomB.length] = _bI; diff --git a/src/mol-plugin-state/manager/interactivity.ts b/src/mol-plugin-state/manager/interactivity.ts index 947014640573c2f8f97bec9775256822592167c3..8805432230c49f0046ac8d8f24c5c98d08646775 100644 --- a/src/mol-plugin-state/manager/interactivity.ts +++ b/src/mol-plugin-state/manager/interactivity.ts @@ -101,10 +101,10 @@ namespace InteractivityManager { // TODO clear, then re-apply remaining providers } - protected normalizedLoci(reprLoci: Representation.Loci, applyGranularity = true) { + protected normalizedLoci(reprLoci: Representation.Loci, applyGranularity: boolean, alwaysConvertBonds = false) { const { loci, repr } = reprLoci; const granularity = applyGranularity ? this.props.granularity : undefined; - return { loci: Loci.normalize(loci, granularity), repr }; + return { loci: Loci.normalize(loci, granularity, alwaysConvertBonds), repr }; } protected mark(current: Representation.Loci, action: MarkerAction, noRender = false) { @@ -187,7 +187,8 @@ namespace InteractivityManager { toggle(current: Representation.Loci, applyGranularity = true) { if (Loci.isEmpty(current.loci)) return; - const normalized = this.normalizedLoci(current, applyGranularity); + const normalized = this.normalizedLoci(current, applyGranularity, true); + if (StructureElement.Loci.is(normalized.loci)) { this.toggleSel(normalized); } else { @@ -198,7 +199,7 @@ namespace InteractivityManager { toggleExtend(current: Representation.Loci, applyGranularity = true) { if (Loci.isEmpty(current.loci)) return; - const normalized = this.normalizedLoci(current, applyGranularity); + const normalized = this.normalizedLoci(current, applyGranularity, true); if (StructureElement.Loci.is(normalized.loci)) { const loci = this.sel.tryGetRange(normalized.loci) || normalized.loci; this.toggleSel({ loci, repr: normalized.repr }); @@ -206,7 +207,7 @@ namespace InteractivityManager { } select(current: Representation.Loci, applyGranularity = true) { - const normalized = this.normalizedLoci(current, applyGranularity); + const normalized = this.normalizedLoci(current, applyGranularity, true); if (StructureElement.Loci.is(normalized.loci)) { this.sel.modify('add', normalized.loci); } @@ -214,7 +215,7 @@ namespace InteractivityManager { } selectJoin(current: Representation.Loci, applyGranularity = true) { - const normalized = this.normalizedLoci(current, applyGranularity); + const normalized = this.normalizedLoci(current, applyGranularity, true); if (StructureElement.Loci.is(normalized.loci)) { this.sel.modify('intersect', normalized.loci); } @@ -222,7 +223,7 @@ namespace InteractivityManager { } selectOnly(current: Representation.Loci, applyGranularity = true) { - const normalized = this.normalizedLoci(current, applyGranularity); + const normalized = this.normalizedLoci(current, applyGranularity, true); if (StructureElement.Loci.is(normalized.loci)) { // only deselect for the structure of the given loci this.deselect({ loci: Structure.toStructureElementLoci(normalized.loci.structure), repr: normalized.repr }, false); @@ -232,7 +233,7 @@ namespace InteractivityManager { } deselect(current: Representation.Loci, applyGranularity = true) { - const normalized = this.normalizedLoci(current, applyGranularity); + const normalized = this.normalizedLoci(current, applyGranularity, true); if (StructureElement.Loci.is(normalized.loci)) { this.sel.modify('remove', normalized.loci); } @@ -255,8 +256,9 @@ namespace InteractivityManager { // do a full deselect/select for the current structure so visuals that are // marked with granularity unequal to 'element' and join/intersect operations // are handled properly - super.mark({ loci: Structure.Loci(loci.structure) }, MarkerAction.Deselect, true); - super.mark({ loci: this.sel.getLoci(loci.structure) }, MarkerAction.Select); + const selLoci = this.sel.getLoci(loci.structure); + super.mark({ loci: Structure.Loci(loci.structure) }, MarkerAction.Deselect, !Loci.isEmpty(selLoci)); + super.mark({ loci: selLoci }, MarkerAction.Select); } else { super.mark(current, action); } diff --git a/src/mol-plugin-state/manager/structure/measurement.ts b/src/mol-plugin-state/manager/structure/measurement.ts index 4907eb3e7edf84d536604126b5811b722e8265ec..bbd3f14226a471b5c3bfb4c1c8cc8bd8ddffce0a 100644 --- a/src/mol-plugin-state/manager/structure/measurement.ts +++ b/src/mol-plugin-state/manager/structure/measurement.ts @@ -1,7 +1,8 @@ /** - * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-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> */ import { StructureElement } from '../../../mol-model/structure'; @@ -15,6 +16,7 @@ import { StatefulPluginComponent } from '../../component'; import { ParamDefinition as PD } from '../../../mol-util/param-definition'; import { MeasurementRepresentationCommonTextParams, LociLabelTextParams } from '../../../mol-repr/shape/loci/common'; import { LineParams } from '../../../mol-repr/structure/representation/line'; +import { Expression } from '../../../mol-script/language/expression'; export { StructureMeasurementManager }; @@ -35,6 +37,7 @@ export interface StructureMeasurementManagerState { angles: StructureMeasurementCell[], dihedrals: StructureMeasurementCell[], orientations: StructureMeasurementCell[], + planes: StructureMeasurementCell[], options: StructureMeasurementOptions } @@ -222,19 +225,25 @@ class StructureMeasurementManager extends StatefulPluginComponent<StructureMeasu await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } }); } - async addOrientation(a: StructureElement.Loci) { - const cellA = this.plugin.helpers.substructureParent.get(a.structure); + async addOrientation(locis: StructureElement.Loci[]) { + const selections: { key: string, ref: string, groupId?: string, expression: Expression }[] = []; + const dependsOn: string[] = []; - if (!cellA) return; + for (let i = 0, il = locis.length; i < il; ++i) { + const l = locis[i]; + const cell = this.plugin.helpers.substructureParent.get(l.structure); + if (!cell) continue; - const dependsOn = [cellA.transform.ref]; + arraySetAdd(dependsOn, cell.transform.ref); + selections.push({ key: `l${i}`, ref: cell.transform.ref, expression: StructureElement.Loci.toExpression(l) }); + } + + if (selections.length === 0) return; const update = this.getGroup(); update .apply(StateTransforms.Model.MultiStructureSelectionFromExpression, { - selections: [ - { key: 'a', ref: cellA.transform.ref, expression: StructureElement.Loci.toExpression(a) }, - ], + selections, isTransitive: true, label: 'Orientation' }, { dependsOn }) @@ -244,6 +253,34 @@ class StructureMeasurementManager extends StatefulPluginComponent<StructureMeasu await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } }); } + async addPlane(locis: StructureElement.Loci[]) { + const selections: { key: string, ref: string, groupId?: string, expression: Expression }[] = []; + const dependsOn: string[] = []; + + for (let i = 0, il = locis.length; i < il; ++i) { + const l = locis[i]; + const cell = this.plugin.helpers.substructureParent.get(l.structure); + if (!cell) continue; + + arraySetAdd(dependsOn, cell.transform.ref); + selections.push({ key: `l${i}`, ref: cell.transform.ref, expression: StructureElement.Loci.toExpression(l) }); + } + + if (selections.length === 0) return; + + const update = this.getGroup(); + update + .apply(StateTransforms.Model.MultiStructureSelectionFromExpression, { + selections, + isTransitive: true, + label: 'Plane' + }, { dependsOn }) + .apply(StateTransforms.Representation.StructureSelectionsPlane3D); + + const state = this.plugin.state.data; + await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } }); + } + private _empty: any[] = []; private getTransforms<T extends StateTransformer<A, B, any>, A extends PluginStateObject.Molecule.Structure.Selections, B extends StateObject>(transformer: T) { const state = this.plugin.state.data; @@ -259,13 +296,14 @@ class StructureMeasurementManager extends StatefulPluginComponent<StructureMeasu distances: this.getTransforms(StateTransforms.Representation.StructureSelectionsDistance3D), angles: this.getTransforms(StateTransforms.Representation.StructureSelectionsAngle3D), dihedrals: this.getTransforms(StateTransforms.Representation.StructureSelectionsDihedral3D), - orientations: this.getTransforms(StateTransforms.Representation.StructureSelectionsOrientation3D) + orientations: this.getTransforms(StateTransforms.Representation.StructureSelectionsOrientation3D), + planes: this.getTransforms(StateTransforms.Representation.StructureSelectionsPlane3D), }); if (updated) this.stateUpdated(); } constructor(private plugin: PluginContext) { - super({ labels: [], distances: [], angles: [], dihedrals: [], orientations: [], options: DefaultStructureMeasurementOptions }); + super({ labels: [], distances: [], angles: [], dihedrals: [], orientations: [], planes: [], options: DefaultStructureMeasurementOptions }); plugin.state.data.events.changed.subscribe(e => { if (e.inTransaction || plugin.behaviors.state.isAnimating.value) return; diff --git a/src/mol-plugin-state/manager/structure/selection.ts b/src/mol-plugin-state/manager/structure/selection.ts index a8e99b6d0f709cda0a278e8871ab4c72d5e7d531..c043a9e614cde87c2312401549f90828ff847b96 100644 --- a/src/mol-plugin-state/manager/structure/selection.ts +++ b/src/mol-plugin-state/manager/structure/selection.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-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> @@ -22,6 +22,7 @@ import { PluginStateObject as PSO } from '../../objects'; import { UUID } from '../../../mol-util'; import { StructureRef } from './hierarchy-state'; import { Boundary } from '../../../mol-math/geometry/boundary'; +import { iterableToArray } from '../../../mol-data/util'; interface StructureSelectionManagerState { entries: Map<string, SelectionEntry>, @@ -405,14 +406,8 @@ export class StructureSelectionManager extends StatefulPluginComponent<Structure } getPrincipalAxes(): PrincipalAxes { - const elementCount = this.elementCount(); - const positions = new Float32Array(3 * elementCount); - let offset = 0; - this.entries.forEach(v => { - StructureElement.Loci.toPositionsArray(v.selection, positions, offset); - offset += StructureElement.Loci.size(v.selection) * 3; - }); - return PrincipalAxes.ofPositions(positions); + const values = iterableToArray(this.entries.values()); + return StructureElement.Loci.getPrincipalAxesMany(values.map(v => v.selection)); } modify(modifier: StructureSelectionModifier, loci: Loci) { diff --git a/src/mol-plugin-state/transforms/helpers.ts b/src/mol-plugin-state/transforms/helpers.ts index 55ca620ae5581c1021a46947a0f2b66a63d44f60..9bdd857bd67f5149b7b4f7cb029a676fbc5437d6 100644 --- a/src/mol-plugin-state/transforms/helpers.ts +++ b/src/mol-plugin-state/transforms/helpers.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ @@ -10,6 +10,7 @@ import { LabelData } from '../../mol-repr/shape/loci/label'; import { OrientationData } from '../../mol-repr/shape/loci/orientation'; import { AngleData } from '../../mol-repr/shape/loci/angle'; import { DihedralData } from '../../mol-repr/shape/loci/dihedral'; +import { PlaneData } from '../../mol-repr/shape/loci/plane'; export function getDistanceDataFromStructureSelections(s: ReadonlyArray<PluginStateObject.Molecule.Structure.SelectionEntry>): DistanceData { const lociA = s[0].loci; @@ -38,6 +39,9 @@ export function getLabelDataFromStructureSelections(s: ReadonlyArray<PluginState } export function getOrientationDataFromStructureSelections(s: ReadonlyArray<PluginStateObject.Molecule.Structure.SelectionEntry>): OrientationData { - const loci = s[0].loci; - return { locis: [loci] }; + return { locis: s.map(v => v.loci) }; +} + +export function getPlaneDataFromStructureSelections(s: ReadonlyArray<PluginStateObject.Molecule.Structure.SelectionEntry>): PlaneData { + return { locis: s.map(v => v.loci) }; } \ No newline at end of file diff --git a/src/mol-plugin-state/transforms/model.ts b/src/mol-plugin-state/transforms/model.ts index 204f783e5ddafc3a2af4a14f613d02c37e029101..ea75c491b015fd1f6967dd0b3ac20f1d473d5ee1 100644 --- a/src/mol-plugin-state/transforms/model.ts +++ b/src/mol-plugin-state/transforms/model.ts @@ -645,7 +645,8 @@ const MultiStructureSelectionFromExpression = PluginStateTransform.BuiltIn({ totalSize += StructureElement.Loci.size(loci.loci); continue; - } if (entry.expression !== sel.expression) { + } + if (entry.expression !== sel.expression) { recreate = true; } else { // TODO: properly support "transitive" queries. For that Structure.areUnitAndIndicesEqual needs to be fixed; diff --git a/src/mol-plugin-state/transforms/representation.ts b/src/mol-plugin-state/transforms/representation.ts index 6c1d37b0fd74453bf47878dca70639d0ddc91756..1b257422f4ad930792ac663b4daf07dd5b4d7197 100644 --- a/src/mol-plugin-state/transforms/representation.ts +++ b/src/mol-plugin-state/transforms/representation.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> @@ -28,7 +28,7 @@ import { BaseGeometry } from '../../mol-geo/geometry/base'; import { Script } from '../../mol-script/script'; import { UnitcellParams, UnitcellRepresentation, getUnitcellData } from '../../mol-repr/shape/model/unitcell'; import { DistanceParams, DistanceRepresentation } from '../../mol-repr/shape/loci/distance'; -import { getDistanceDataFromStructureSelections, getLabelDataFromStructureSelections, getOrientationDataFromStructureSelections, getAngleDataFromStructureSelections, getDihedralDataFromStructureSelections } from './helpers'; +import { getDistanceDataFromStructureSelections, getLabelDataFromStructureSelections, getOrientationDataFromStructureSelections, getAngleDataFromStructureSelections, getDihedralDataFromStructureSelections, getPlaneDataFromStructureSelections } from './helpers'; import { LabelParams, LabelRepresentation } from '../../mol-repr/shape/loci/label'; import { OrientationRepresentation, OrientationParams } from '../../mol-repr/shape/loci/orientation'; import { AngleParams, AngleRepresentation } from '../../mol-repr/shape/loci/angle'; @@ -40,6 +40,7 @@ import { Mesh } from '../../mol-geo/geometry/mesh/mesh'; import { getBoxMesh } from './shape'; import { Shape } from '../../mol-model/shape'; import { Box3D } from '../../mol-math/geometry'; +import { PlaneParams, PlaneRepresentation } from '../../mol-repr/shape/loci/plane'; export { StructureRepresentation3D }; export { ExplodeStructureRepresentation3D }; @@ -986,4 +987,37 @@ const StructureSelectionsOrientation3D = PluginStateTransform.BuiltIn({ return StateTransformer.UpdateResult.Updated; }); }, +}); + +export { StructureSelectionsPlane3D }; +type StructureSelectionsPlane3D = typeof StructureSelectionsPlane3D +const StructureSelectionsPlane3D = PluginStateTransform.BuiltIn({ + name: 'structure-selections-plane-3d', + display: '3D Plane', + from: SO.Molecule.Structure.Selections, + to: SO.Shape.Representation3D, + params: () => ({ + ...PlaneParams, + }) +})({ + canAutoUpdate({ oldParams, newParams }) { + return true; + }, + apply({ a, params }, plugin: PluginContext) { + return Task.create('Structure Plane', async ctx => { + const data = getPlaneDataFromStructureSelections(a.data); + const repr = PlaneRepresentation({ webgl: plugin.canvas3d?.webgl, ...plugin.representation.structure.themes }, () => PlaneParams); + await repr.createOrUpdate(params, data).runInContext(ctx); + return new SO.Shape.Representation3D({ repr, sourceData: data }, { label: `Plane` }); + }); + }, + update({ a, b, oldParams, newParams }, plugin: PluginContext) { + return Task.create('Structure Plane', async ctx => { + const props = { ...b.data.repr.props, ...newParams }; + const data = getPlaneDataFromStructureSelections(a.data); + await b.data.repr.createOrUpdate(props, data).runInContext(ctx); + b.data.sourceData = data; + return StateTransformer.UpdateResult.Updated; + }); + }, }); \ No newline at end of file diff --git a/src/mol-plugin-ui/controls/line-graph/line-graph-component.tsx b/src/mol-plugin-ui/controls/line-graph/line-graph-component.tsx index 46e7e8788322fb17a7903ad29893ed3c3014bfea..968068d2b19af01f43c727df22a60a8c180729b6 100644 --- a/src/mol-plugin-ui/controls/line-graph/line-graph-component.tsx +++ b/src/mol-plugin-ui/controls/line-graph/line-graph-component.tsx @@ -207,7 +207,7 @@ export class LineGraphComponent extends React.Component<any, LineGraphComponentS const updatedPoint = this.unNormalizePoint(Vec2.create(this.updatedX, this.updatedY)); const points = this.state.points.filter((_, i) => i !== selected[0]); - points.push(updatedPoint);; + points.push(updatedPoint); points.sort((a, b) => { if (a[0] === b[0]) { if (a[0] === 0) { @@ -372,7 +372,7 @@ export class LineGraphComponent extends React.Component<any, LineGraphComponentS const data = points; const size = data.length; - for (let i = 0; i < size - 1;i++) { + for (let i = 0; i < size - 1; i++) { const x1 = data[i][0]; const y1 = data[i][1]; const x2 = data[i + 1][0]; diff --git a/src/mol-plugin-ui/sequence/sequence.tsx b/src/mol-plugin-ui/sequence/sequence.tsx index c7bda2a661b723364c886d4c331bb4213055f9c2..bea9c49754924dc736f196a689dc6834f5fe8767 100644 --- a/src/mol-plugin-ui/sequence/sequence.tsx +++ b/src/mol-plugin-ui/sequence/sequence.tsx @@ -153,7 +153,11 @@ export class Sequence<P extends SequenceProps> extends PluginUIComponent<P> { private getBackgroundColor(marker: number) { // TODO: make marker color configurable if (typeof marker === 'undefined') console.error('unexpected marker value'); - return marker === 0 ? '' : marker % 2 === 0 ? 'rgb(51, 255, 25)' /* selected */ : 'rgb(255, 102, 153)' /* highlighted */; + return marker === 0 + ? '' + : marker % 2 === 0 + ? 'rgb(51, 255, 25)' // selected + : 'rgb(255, 102, 153)'; // highlighted } private getResidueClass(seqIdx: number, label: string) { diff --git a/src/mol-plugin-ui/structure/measurements.tsx b/src/mol-plugin-ui/structure/measurements.tsx index b1cdfb2a33ae1bfa5b289a29e7f79959bd4b36cd..29763a3cfe262d53a12b4a2ecbfbf8b833caf3f1 100644 --- a/src/mol-plugin-ui/structure/measurements.tsx +++ b/src/mol-plugin-ui/structure/measurements.tsx @@ -1,5 +1,5 @@ /** - * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> * @author David Sehnal <david.sehnal@gmail.com> @@ -15,7 +15,8 @@ import { AngleData } from '../../mol-repr/shape/loci/angle'; import { DihedralData } from '../../mol-repr/shape/loci/dihedral'; import { DistanceData } from '../../mol-repr/shape/loci/distance'; import { LabelData } from '../../mol-repr/shape/loci/label'; -import { angleLabel, dihedralLabel, distanceLabel, lociLabel } from '../../mol-theme/label'; +import { OrientationData } from '../../mol-repr/shape/loci/orientation'; +import { angleLabel, dihedralLabel, distanceLabel, lociLabel, structureElementLociLabelMany } from '../../mol-theme/label'; import { FiniteArray } from '../../mol-util/type-helpers'; import { CollapsableControls, PurePluginUIComponent } from '../base'; import { ActionMenu } from '../controls/action-menu'; @@ -67,6 +68,8 @@ export class MeasurementList extends PurePluginUIComponent { {this.renderGroup(measurements.distances, 'Distances')} {this.renderGroup(measurements.angles, 'Angles')} {this.renderGroup(measurements.dihedrals, 'Dihedrals')} + {this.renderGroup(measurements.orientations, 'Orientations')} + {this.renderGroup(measurements.planes, 'Planes')} </div>; } } @@ -108,13 +111,31 @@ export class MeasurementControls extends PurePluginUIComponent<{}, { isBusy: boo this.plugin.managers.structure.measurement.addLabel(loci[0].loci); } + addOrientation = () => { + const locis: StructureElement.Loci[] = []; + this.plugin.managers.structure.selection.entries.forEach(v => { + locis.push(v.selection); + }); + this.plugin.managers.structure.measurement.addOrientation(locis); + } + + addPlane = () => { + const locis: StructureElement.Loci[] = []; + this.plugin.managers.structure.selection.entries.forEach(v => { + locis.push(v.selection); + }); + this.plugin.managers.structure.measurement.addPlane(locis); + } + get actions(): ActionMenu.Items { const history = this.selection.additionsHistory; const ret: ActionMenu.Item[] = [ - { kind: 'item', label: `Label ${history.length === 0 ? ' (1 selection required)' : ' (1st selection)'}`, value: this.addLabel, disabled: history.length === 0 }, - { kind: 'item', label: `Distance ${history.length < 2 ? ' (2 selections required)' : ' (top 2 selections)'}`, value: this.measureDistance, disabled: history.length < 2 }, - { kind: 'item', label: `Angle ${history.length < 3 ? ' (3 selections required)' : ' (top 3 selections)'}`, value: this.measureAngle, disabled: history.length < 3 }, - { kind: 'item', label: `Dihedral ${history.length < 4 ? ' (4 selections required)' : ' (top 4 selections)'}`, value: this.measureDihedral, disabled: history.length < 4 }, + { kind: 'item', label: `Label ${history.length === 0 ? ' (1 selection item required)' : ' (1st selection item)'}`, value: this.addLabel, disabled: history.length === 0 }, + { kind: 'item', label: `Distance ${history.length < 2 ? ' (2 selection items required)' : ' (top 2 selection items)'}`, value: this.measureDistance, disabled: history.length < 2 }, + { kind: 'item', label: `Angle ${history.length < 3 ? ' (3 selection items required)' : ' (top 3 items)'}`, value: this.measureAngle, disabled: history.length < 3 }, + { kind: 'item', label: `Dihedral ${history.length < 4 ? ' (4 selection items required)' : ' (top 4 selection items)'}`, value: this.measureDihedral, disabled: history.length < 4 }, + { kind: 'item', label: `Orientation ${history.length === 0 ? ' (selection required)' : ' (current selection)'}`, value: this.addOrientation, disabled: history.length === 0 }, + { kind: 'item', label: `Plane ${history.length === 0 ? ' (selection required)' : ' (current selection)'}`, value: this.addPlane, disabled: history.length === 0 }, ]; return ret; } @@ -219,7 +240,7 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen } get selections() { - return this.props.cell.obj?.data.sourceData as Partial<DistanceData & AngleData & DihedralData & LabelData> | undefined; + return this.props.cell.obj?.data.sourceData as Partial<DistanceData & AngleData & DihedralData & LabelData & OrientationData> | undefined; } delete = () => { @@ -266,6 +287,7 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen if (selections.pairs) return selections.pairs[0].loci; if (selections.triples) return selections.triples[0].loci; if (selections.quads) return selections.quads[0].loci; + if (selections.locis) return selections.locis; return []; } @@ -277,6 +299,7 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen if (selections.pairs) return distanceLabel(selections.pairs[0], { condensed: true, unitLabel: this.plugin.managers.structure.measurement.state.options.distanceUnitLabel }); if (selections.triples) return angleLabel(selections.triples[0], { condensed: true }); if (selections.quads) return dihedralLabel(selections.quads[0], { condensed: true }); + if (selections.locis) return structureElementLociLabelMany(selections.locis, { countsOnly: true }); return '<empty>'; } diff --git a/src/mol-plugin/behavior/dynamic/representation.ts b/src/mol-plugin/behavior/dynamic/representation.ts index 45928cbe516f9a34ea5d0fb950053c131850f3cf..a561ae846bf9be8815987038e3ce131086c6ae28 100644 --- a/src/mol-plugin/behavior/dynamic/representation.ts +++ b/src/mol-plugin/behavior/dynamic/representation.ts @@ -16,7 +16,7 @@ import { ButtonsType, ModifiersKeys } from '../../../mol-util/input/input-observ import { Binding } from '../../../mol-util/binding'; import { ParamDefinition as PD } from '../../../mol-util/param-definition'; import { EmptyLoci, Loci } from '../../../mol-model/loci'; -import { Structure, StructureElement, StructureProperties } from '../../../mol-model/structure'; +import { Bond, Structure, StructureElement, StructureProperties } from '../../../mol-model/structure'; import { arrayMax } from '../../../mol-util/array'; import { Representation } from '../../../mol-repr/representation'; import { LociLabel } from '../../../mol-plugin-state/manager/loci-label'; @@ -34,6 +34,7 @@ const DefaultHighlightLociBindings = { const HighlightLociParams = { bindings: PD.Value(DefaultHighlightLociBindings, { isHidden: true }), ignore: PD.Value<Loci['kind'][]>([], { isHidden: true }), + preferAtoms: PD.Boolean(false, { description: 'Always prefer atoms over bonds' }), mark: PD.Boolean(true) }; type HighlightLociProps = PD.Values<typeof HighlightLociParams> @@ -46,10 +47,17 @@ export const HighlightLoci = PluginBehavior.create({ if (!this.ctx.canvas3d || !this.params.mark) return; this.ctx.canvas3d.mark(interactionLoci, action, noRender); } + private getLoci(loci: Loci) { + return this.params.preferAtoms && Bond.isLoci(loci) && loci.bonds.length === 2 + ? Bond.toFirstStructureElementLoci(loci) + : loci; + } register() { this.subscribeObservable(this.ctx.behaviors.interaction.hover, ({ current, buttons, modifiers }) => { if (!this.ctx.canvas3d || this.ctx.isBusy) return; - if (this.params.ignore?.indexOf(current.loci.kind) >= 0) { + + const loci = this.getLoci(current.loci); + if (this.params.ignore?.indexOf(loci.kind) >= 0) { this.ctx.managers.interactivity.lociHighlights.highlightOnly({ repr: current.repr, loci: EmptyLoci }); return; } @@ -58,13 +66,13 @@ export const HighlightLoci = PluginBehavior.create({ if (Binding.match(this.params.bindings.hoverHighlightOnly, buttons, modifiers)) { // remove repr to highlight loci everywhere on hover - this.ctx.managers.interactivity.lociHighlights.highlightOnly({ loci: current.loci }); + this.ctx.managers.interactivity.lociHighlights.highlightOnly({ loci }); matched = true; } if (Binding.match(this.params.bindings.hoverHighlightOnlyExtend, buttons, modifiers)) { // remove repr to highlight loci everywhere on hover - this.ctx.managers.interactivity.lociHighlights.highlightOnlyExtend({ loci: current.loci }); + this.ctx.managers.interactivity.lociHighlights.highlightOnlyExtend({ loci }); matched = true; } @@ -95,6 +103,7 @@ const DefaultSelectLociBindings = { const SelectLociParams = { bindings: PD.Value(DefaultSelectLociBindings, { isHidden: true }), ignore: PD.Value<Loci['kind'][]>([], { isHidden: true }), + preferAtoms: PD.Boolean(false, { description: 'Always prefer atoms over bonds' }), mark: PD.Boolean(true) }; type SelectLociProps = PD.Values<typeof SelectLociParams> @@ -108,6 +117,11 @@ export const SelectLoci = PluginBehavior.create({ if (!this.ctx.canvas3d || !this.params.mark) return; this.ctx.canvas3d.mark({ loci: reprLoci.loci }, action, noRender); } + private getLoci(loci: Loci) { + return this.params.preferAtoms && Bond.isLoci(loci) && loci.bonds.length === 2 + ? Bond.toFirstStructureElementLoci(loci) + : loci; + } private applySelectMark(ref: string, clear?: boolean) { const cell = this.ctx.state.data.cells.get(ref); if (cell && SO.isRepresentation3D(cell.obj)) { @@ -123,10 +137,10 @@ export const SelectLoci = PluginBehavior.create({ } } register() { - const lociIsEmpty = (current: Representation.Loci) => Loci.isEmpty(current.loci); - const lociIsNotEmpty = (current: Representation.Loci) => !Loci.isEmpty(current.loci); + const lociIsEmpty = (loci: Loci) => Loci.isEmpty(loci); + const lociIsNotEmpty = (loci: Loci) => !Loci.isEmpty(loci); - const actions: [keyof typeof DefaultSelectLociBindings, (current: Representation.Loci) => void, ((current: Representation.Loci) => boolean) | undefined][] = [ + const actions: [keyof typeof DefaultSelectLociBindings, (current: Representation.Loci) => void, ((current: Loci) => boolean) | undefined][] = [ ['clickSelect', current => this.ctx.managers.interactivity.lociSelects.select(current), lociIsNotEmpty], ['clickToggle', current => this.ctx.managers.interactivity.lociSelects.toggle(current), lociIsNotEmpty], ['clickToggleExtend', current => this.ctx.managers.interactivity.lociSelects.toggleExtend(current), lociIsNotEmpty], @@ -145,12 +159,14 @@ export const SelectLoci = PluginBehavior.create({ this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current, button, modifiers }) => { if (!this.ctx.canvas3d || this.ctx.isBusy || !this.ctx.selectionMode) return; - if (this.params.ignore?.indexOf(current.loci.kind) >= 0) return; + + const loci = this.getLoci(current.loci); + if (this.params.ignore?.indexOf(loci.kind) >= 0) return; // only trigger the 1st action that matches for (const [binding, action, condition] of actions) { - if (Binding.match(this.params.bindings[binding], button, modifiers) && (!condition || condition(current))) { - action(current); + if (Binding.match(this.params.bindings[binding], button, modifiers) && (!condition || condition(loci))) { + action({ repr: current.repr, loci }); break; } } diff --git a/src/mol-plugin/behavior/dynamic/selection/structure-focus-representation.ts b/src/mol-plugin/behavior/dynamic/selection/structure-focus-representation.ts index 43ad81abd070d9b3d51ec70d004b6bc6ce6ff664..fbd8fa87c570f069641eacf95c3e36b46badd536 100644 --- a/src/mol-plugin/behavior/dynamic/selection/structure-focus-representation.ts +++ b/src/mol-plugin/behavior/dynamic/selection/structure-focus-representation.ts @@ -25,7 +25,7 @@ const StructureFocusRepresentationParams = (plugin: PluginContext) => { expandRadius: PD.Numeric(5, { min: 1, max: 10, step: 1 }), targetParams: PD.Group(reprParams, { label: 'Target', - customDefault: createStructureRepresentationParams(plugin, void 0, { type: 'ball-and-stick', size: 'physical', typeParams: { sizeFactor: 0.26, alpha: 0.51 } }) + customDefault: createStructureRepresentationParams(plugin, void 0, { type: 'ball-and-stick', size: 'physical', typeParams: { sizeFactor: 0.26, alpha: 0.51, adjustCylinderLength: true } }) }), surroundingsParams: PD.Group(reprParams, { label: 'Surroundings', diff --git a/src/mol-plugin/config.ts b/src/mol-plugin/config.ts index 2144de4e0b7b4c78e6765fbab56807e48f0b6745..bfd3b5aa3d996ca7844abc339c29c6d90a293402 100644 --- a/src/mol-plugin/config.ts +++ b/src/mol-plugin/config.ts @@ -19,6 +19,16 @@ export class PluginConfigItem<T = any> { function item<T>(key: string, defaultValue?: T) { return new PluginConfigItem(key, defaultValue); } + +// adapted from https://stackoverflow.com/questions/9038625/detect-if-device-is-ios +function is_iOS() { + if (typeof navigator === 'undefined' || typeof window === 'undefined') return false; + const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent); + const isAppleDevice = navigator.userAgent.includes('Macintosh'); + const isTouchScreen = navigator.maxTouchPoints >= 4; // true for iOS 13 (and hopefully beyond) + return !(window as any).MSStream && (isIOS || (isAppleDevice && isTouchScreen)); +} + export const PluginConfig = { item, General: { @@ -27,7 +37,11 @@ export const PluginConfig = { DisablePreserveDrawingBuffer: item('plugin-config.disable-preserve-drawing-buffer', false), PixelScale: item('plugin-config.pixel-scale', 1), PickScale: item('plugin-config.pick-scale', 0.25), + PickPadding: item('plugin-config.pick-padding', 3), EnableWboit: item('plugin-config.enable-wboit', true), + // as of Oct 1 2021, WebGL 2 doesn't work on iOS 15. + // TODO: check back in a few weeks to see if it was fixed + PreferWebGl1: item('plugin-config.prefer-webgl1', is_iOS()), }, State: { DefaultServer: item('plugin-state.server', 'https://webchem.ncbr.muni.cz/molstar-state'), diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index f08217e64b4caa412eb3af40e6bdb3b4c4089a11..76542ab04fb46674bdbb0404c2ccd55a0368ffad 100644 --- a/src/mol-plugin/context.ts +++ b/src/mol-plugin/context.ts @@ -196,8 +196,11 @@ export class PluginContext { const preserveDrawingBuffer = !(this.config.get(PluginConfig.General.DisablePreserveDrawingBuffer) ?? false); const pixelScale = this.config.get(PluginConfig.General.PixelScale) || 1; const pickScale = this.config.get(PluginConfig.General.PickScale) || 0.25; + const pickPadding = this.config.get(PluginConfig.General.PickPadding) ?? 1; const enableWboit = this.config.get(PluginConfig.General.EnableWboit) || false; - (this.canvas3dContext as Canvas3DContext) = Canvas3DContext.fromCanvas(canvas, { antialias, preserveDrawingBuffer, pixelScale, pickScale, enableWboit }); + const preferWebGl1 = this.config.get(PluginConfig.General.PreferWebGl1) || false; + (this.canvas3dContext as Canvas3DContext) = Canvas3DContext.fromCanvas(canvas, { antialias, preserveDrawingBuffer, pixelScale, pickScale, enableWboit, preferWebGl1 }); + (this.canvas3dContext as Canvas3DContext) = Canvas3DContext.fromCanvas(canvas, { antialias, preserveDrawingBuffer, pixelScale, pickScale, pickPadding, enableWboit }); } (this.canvas3d as Canvas3D) = Canvas3D.create(this.canvas3dContext!); this.canvas3dInit.next(true); diff --git a/src/mol-repr/representation.ts b/src/mol-repr/representation.ts index bad2eb42e7e851310c44f6df16845529f58f688e..9be1c63371d7150c4294aa2b4a32acb56e2514dd 100644 --- a/src/mol-repr/representation.ts +++ b/src/mol-repr/representation.ts @@ -65,12 +65,16 @@ export namespace RepresentationProvider { export type AnyRepresentationProvider = RepresentationProvider<any, {}, Representation.State> -const EmptyRepresentationProvider = { +export const EmptyRepresentationProvider: RepresentationProvider = { + name: '', label: '', description: '', factory: () => Representation.Empty, getParams: () => ({}), - defaultValues: {} + defaultValues: {}, + defaultColorTheme: ColorTheme.EmptyProvider, + defaultSizeTheme: SizeTheme.EmptyProvider, + isApplicable: () => true }; function getTypes(list: { name: string, provider: RepresentationProvider<any, any, any> }[]) { @@ -114,7 +118,7 @@ export class RepresentationRegistry<D, S extends Representation.State> { } get<P extends PD.Params>(name: string): RepresentationProvider<D, P, S> { - return this._map.get(name) || EmptyRepresentationProvider as unknown as RepresentationProvider<D, P, S>; + return this._map.get(name) || EmptyRepresentationProvider; } get list() { @@ -226,7 +230,7 @@ namespace Representation { let version = 0; const updated = new Subject<number>(); const currentState = stateBuilder.create(); - const currentTheme = Theme.createEmpty(); + let currentTheme = Theme.createEmpty(); let currentParams: P; let currentProps: PD.Values<P>; @@ -314,6 +318,7 @@ namespace Representation { } }, setTheme: (theme: Theme) => { + currentTheme = theme; for (let i = 0, il = reprList.length; i < il; ++i) { reprList[i].setTheme(theme); } diff --git a/src/mol-repr/shape/loci/orientation.ts b/src/mol-repr/shape/loci/orientation.ts index 4a5b120ec8acf1b73722a4c90d56db084290672a..451aff38efdfbee738e3dd54ec5db5f38ef8946c 100644 --- a/src/mol-repr/shape/loci/orientation.ts +++ b/src/mol-repr/shape/loci/orientation.ts @@ -1,10 +1,9 @@ /** - * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ -import { Loci } from '../../../mol-model/loci'; import { RuntimeContext } from '../../../mol-task'; import { ParamDefinition as PD } from '../../../mol-util/param-definition'; import { ColorNames } from '../../../mol-util/color/names'; @@ -13,21 +12,23 @@ import { Representation, RepresentationParamsGetter, RepresentationContext } fro import { Shape } from '../../../mol-model/shape'; import { Mesh } from '../../../mol-geo/geometry/mesh/mesh'; import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder'; -import { lociLabel } from '../../../mol-theme/label'; +import { structureElementLociLabelMany } from '../../../mol-theme/label'; import { addAxes } from '../../../mol-geo/geometry/mesh/builder/axes'; import { addOrientedBox } from '../../../mol-geo/geometry/mesh/builder/box'; import { addEllipsoid } from '../../../mol-geo/geometry/mesh/builder/ellipsoid'; import { Axes3D } from '../../../mol-math/geometry'; import { Vec3 } from '../../../mol-math/linear-algebra'; import { MarkerActions } from '../../../mol-util/marker-action'; +import { StructureElement } from '../../../mol-model/structure'; export interface OrientationData { - locis: Loci[] + locis: StructureElement.Loci[] } const SharedParams = { color: PD.Color(ColorNames.orange), - scale: PD.Numeric(2, { min: 0.1, max: 10, step: 0.1 }) + scaleFactor: PD.Numeric(1, { min: 0.1, max: 10, step: 0.1 }), + radiusScale: PD.Numeric(2, { min: 0.1, max: 10, step: 0.1 }) }; const AxesParams = { @@ -57,97 +58,84 @@ const OrientationVisuals = { export const OrientationParams = { ...AxesParams, ...BoxParams, + ...EllipsoidParams, visuals: PD.MultiSelect(['box'], PD.objectToOptions(OrientationVisuals)), - color: PD.Color(ColorNames.orange), - scale: PD.Numeric(2, { min: 0.1, max: 5, step: 0.1 }) }; export type OrientationParams = typeof OrientationParams export type OrientationProps = PD.Values<OrientationParams> // -function orientationLabel(loci: Loci) { - const label = lociLabel(loci, { countsOnly: true }); +function getAxesName(locis: StructureElement.Loci[]) { + const label = structureElementLociLabelMany(locis, { countsOnly: true }); return `Principal Axes of ${label}`; } -function getOrientationName(data: OrientationData) { - return data.locis.length === 1 ? orientationLabel(data.locis[0]) : `${data.locis.length} Orientations`; -} - -// - function buildAxesMesh(data: OrientationData, props: OrientationProps, mesh?: Mesh): Mesh { const state = MeshBuilder.createState(256, 128, mesh); - for (let i = 0, il = data.locis.length; i < il; ++i) { - const principalAxes = Loci.getPrincipalAxes(data.locis[i]); - if (principalAxes) { - state.currentGroup = i; - addAxes(state, principalAxes.momentsAxes, props.scale, 2, 20); - } - } + const principalAxes = StructureElement.Loci.getPrincipalAxesMany(data.locis); + Axes3D.scale(principalAxes.momentsAxes, principalAxes.momentsAxes, props.scaleFactor); + + state.currentGroup = 0; + addAxes(state, principalAxes.momentsAxes, props.radiusScale, 2, 20); return MeshBuilder.getMesh(state); } function getAxesShape(ctx: RuntimeContext, data: OrientationData, props: OrientationProps, shape?: Shape<Mesh>) { const mesh = buildAxesMesh(data, props, shape && shape.geometry); - const name = getOrientationName(data); - const getLabel = function (groupId: number) { - return orientationLabel(data.locis[groupId]); - }; - return Shape.create(name, data, mesh, () => props.color, () => 1, getLabel); + const name = getAxesName(data.locis); + return Shape.create(name, data, mesh, () => props.color, () => 1, () => name); } // +function getBoxName(locis: StructureElement.Loci[]) { + const label = structureElementLociLabelMany(locis, { countsOnly: true }); + return `Oriented Box of ${label}`; +} + function buildBoxMesh(data: OrientationData, props: OrientationProps, mesh?: Mesh): Mesh { const state = MeshBuilder.createState(256, 128, mesh); - for (let i = 0, il = data.locis.length; i < il; ++i) { - const principalAxes = Loci.getPrincipalAxes(data.locis[i]); - if (principalAxes) { - state.currentGroup = i; - addOrientedBox(state, principalAxes.boxAxes, props.scale, 2, 20); - } - } + const principalAxes = StructureElement.Loci.getPrincipalAxesMany(data.locis); + Axes3D.scale(principalAxes.boxAxes, principalAxes.boxAxes, props.scaleFactor); + + state.currentGroup = 0; + addOrientedBox(state, principalAxes.boxAxes, props.radiusScale, 2, 20); return MeshBuilder.getMesh(state); } function getBoxShape(ctx: RuntimeContext, data: OrientationData, props: OrientationProps, shape?: Shape<Mesh>) { const mesh = buildBoxMesh(data, props, shape && shape.geometry); - const name = getOrientationName(data); - const getLabel = function (groupId: number) { - return orientationLabel(data.locis[groupId]); - }; - return Shape.create(name, data, mesh, () => props.color, () => 1, getLabel); + const name = getBoxName(data.locis); + return Shape.create(name, data, mesh, () => props.color, () => 1, () => name); } // +function getEllipsoidName(locis: StructureElement.Loci[]) { + const label = structureElementLociLabelMany(locis, { countsOnly: true }); + return `Oriented Ellipsoid of ${label}`; +} + function buildEllipsoidMesh(data: OrientationData, props: OrientationProps, mesh?: Mesh): Mesh { const state = MeshBuilder.createState(256, 128, mesh); - for (let i = 0, il = data.locis.length; i < il; ++i) { - const principalAxes = Loci.getPrincipalAxes(data.locis[i]); - if (principalAxes) { - const axes = principalAxes.boxAxes; - const { origin, dirA, dirB } = axes; - const size = Axes3D.size(Vec3(), axes); - Vec3.scale(size, size, 0.5); - const radiusScale = Vec3.create(size[2], size[1], size[0]); - - state.currentGroup = i; - addEllipsoid(state, origin, dirA, dirB, radiusScale, 2); - } - } + const principalAxes = StructureElement.Loci.getPrincipalAxesMany(data.locis); + + const axes = principalAxes.boxAxes; + const { origin, dirA, dirB } = axes; + const size = Axes3D.size(Vec3(), axes); + Vec3.scale(size, size, 0.5 * props.scaleFactor); + const radiusScale = Vec3.create(size[2], size[1], size[0]); + + state.currentGroup = 0; + addEllipsoid(state, origin, dirA, dirB, radiusScale, 2); return MeshBuilder.getMesh(state); } function getEllipsoidShape(ctx: RuntimeContext, data: OrientationData, props: OrientationProps, shape?: Shape<Mesh>) { const mesh = buildEllipsoidMesh(data, props, shape && shape.geometry); - const name = getOrientationName(data); - const getLabel = function (groupId: number) { - return orientationLabel(data.locis[groupId]); - }; - return Shape.create(name, data, mesh, () => props.color, () => 1, getLabel); + const name = getEllipsoidName(data.locis); + return Shape.create(name, data, mesh, () => props.color, () => 1, () => name); } // diff --git a/src/mol-repr/shape/loci/plane.ts b/src/mol-repr/shape/loci/plane.ts new file mode 100644 index 0000000000000000000000000000000000000000..719f3ae19611accf74fde85089eb347c7ff14d2d --- /dev/null +++ b/src/mol-repr/shape/loci/plane.ts @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { RuntimeContext } from '../../../mol-task'; +import { ParamDefinition as PD } from '../../../mol-util/param-definition'; +import { ColorNames } from '../../../mol-util/color/names'; +import { ShapeRepresentation } from '../representation'; +import { Representation, RepresentationParamsGetter, RepresentationContext } from '../../representation'; +import { Shape } from '../../../mol-model/shape'; +import { Mesh } from '../../../mol-geo/geometry/mesh/mesh'; +import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder'; +import { structureElementLociLabelMany } from '../../../mol-theme/label'; +import { Mat4, Vec3 } from '../../../mol-math/linear-algebra'; +import { MarkerActions } from '../../../mol-util/marker-action'; +import { Plane } from '../../../mol-geo/primitive/plane'; +import { StructureElement } from '../../../mol-model/structure'; +import { Axes3D } from '../../../mol-math/geometry'; + +export interface PlaneData { + locis: StructureElement.Loci[] +} + +const _PlaneParams = { + ...Mesh.Params, + color: PD.Color(ColorNames.orange), + scaleFactor: PD.Numeric(1, { min: 0.1, max: 10, step: 0.1 }), +}; +type _PlaneParams = typeof _PlaneParams + +const PlaneVisuals = { + 'plane': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<PlaneData, _PlaneParams>) => ShapeRepresentation(getPlaneShape, Mesh.Utils), +}; + +export const PlaneParams = { + ..._PlaneParams, + visuals: PD.MultiSelect(['plane'], PD.objectToOptions(PlaneVisuals)), +}; +export type PlaneParams = typeof PlaneParams +export type PlaneProps = PD.Values<PlaneParams> + +// + +function getPlaneName(locis: StructureElement.Loci[]) { + const label = structureElementLociLabelMany(locis, { countsOnly: true }); + return `Best Fit Plane of ${label}`; +} + +const tmpMat = Mat4(); +const tmpV = Vec3(); +function buildPlaneMesh(data: PlaneData, props: PlaneProps, mesh?: Mesh): Mesh { + const state = MeshBuilder.createState(256, 128, mesh); + const principalAxes = StructureElement.Loci.getPrincipalAxesMany(data.locis); + const axes = principalAxes.boxAxes; + const plane = Plane(); + + Vec3.add(tmpV, axes.origin, axes.dirC); + Mat4.targetTo(tmpMat, tmpV, axes.origin, axes.dirB); + Mat4.scale(tmpMat, tmpMat, Axes3D.size(tmpV, axes)); + Mat4.scaleUniformly(tmpMat, tmpMat, props.scaleFactor); + Mat4.setTranslation(tmpMat, axes.origin); + + state.currentGroup = 0; + MeshBuilder.addPrimitive(state, tmpMat, plane); + MeshBuilder.addPrimitiveFlipped(state, tmpMat, plane); + return MeshBuilder.getMesh(state); +} + +function getPlaneShape(ctx: RuntimeContext, data: PlaneData, props: PlaneProps, shape?: Shape<Mesh>) { + const mesh = buildPlaneMesh(data, props, shape && shape.geometry); + const name = getPlaneName(data.locis); + return Shape.create(name, data, mesh, () => props.color, () => 1, () => name); +} + +// + +export type PlaneRepresentation = Representation<PlaneData, PlaneParams> +export function PlaneRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<PlaneData, PlaneParams>): PlaneRepresentation { + const repr = Representation.createMulti('Plane', ctx, getParams, Representation.StateBuilder, PlaneVisuals as unknown as Representation.Def<PlaneData, PlaneParams>); + repr.setState({ markerActions: MarkerActions.Highlighting }); + return repr; +} \ No newline at end of file diff --git a/src/mol-repr/structure/representation/line.ts b/src/mol-repr/structure/representation/line.ts index 2f9c5c5d6e03c692f5069e94f82a2db80f4cd797..c0936561151e29bc9222e7ac2c755c058d9344c7 100644 --- a/src/mol-repr/structure/representation/line.ts +++ b/src/mol-repr/structure/representation/line.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ @@ -14,23 +14,32 @@ import { Representation, RepresentationParamsGetter, RepresentationContext } fro import { ThemeRegistryContext } from '../../../mol-theme/theme'; import { Structure } from '../../../mol-model/structure'; import { getUnitKindsParam } from '../params'; +import { ElementPointParams, ElementPointVisual } from '../visual/element-point'; +import { ElementCrossParams, ElementCrossVisual } from '../visual/element-cross'; const LineVisuals = { 'intra-bond': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, IntraUnitBondLineParams>) => UnitsRepresentation('Intra-unit bond line', ctx, getParams, IntraUnitBondLineVisual), 'inter-bond': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, InterUnitBondLineParams>) => ComplexRepresentation('Inter-unit bond line', ctx, getParams, InterUnitBondLineVisual), + 'element-point': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, ElementPointParams>) => UnitsRepresentation('Points', ctx, getParams, ElementPointVisual), + 'element-cross': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, ElementCrossParams>) => UnitsRepresentation('Crosses', ctx, getParams, ElementCrossVisual), }; export const LineParams = { ...IntraUnitBondLineParams, ...InterUnitBondLineParams, + ...ElementPointParams, + ...ElementCrossParams, + multipleBonds: PD.Select('offset', PD.arrayToOptions(['off', 'symmetric', 'offset'] as const)), includeParent: PD.Boolean(false), - sizeFactor: PD.Numeric(1.5, { min: 0.01, max: 10, step: 0.01 }), + sizeFactor: PD.Numeric(3, { min: 0.01, max: 10, step: 0.01 }), unitKinds: getUnitKindsParam(['atomic']), - visuals: PD.MultiSelect(['intra-bond', 'inter-bond'], PD.objectToOptions(LineVisuals)) + visuals: PD.MultiSelect(['intra-bond', 'inter-bond', 'element-point', 'element-cross'], PD.objectToOptions(LineVisuals)) }; export type LineParams = typeof LineParams export function getLineParams(ctx: ThemeRegistryContext, structure: Structure) { - return PD.clone(LineParams); + const params = PD.clone(LineParams); + params.pointStyle.defaultValue = 'circle'; + return params; } export type LineRepresentation = StructureRepresentation<LineParams> @@ -41,7 +50,7 @@ export function LineRepresentation(ctx: RepresentationContext, getParams: Repres export const LineRepresentationProvider = StructureRepresentationProvider({ name: 'line', label: 'Line', - description: 'Displays bonds as lines.', + description: 'Displays bonds as lines and atoms as points or croses.', factory: LineRepresentation, getParams: getLineParams, defaultValues: PD.getDefaultValues(LineParams), diff --git a/src/mol-repr/structure/representation/point.ts b/src/mol-repr/structure/representation/point.ts index 019d61ca955a7ca1934dc58bebd06cfa797621bd..0bcddfe18913d220af265f931f988ca057117d70 100644 --- a/src/mol-repr/structure/representation/point.ts +++ b/src/mol-repr/structure/representation/point.ts @@ -32,11 +32,11 @@ export function PointRepresentation(ctx: RepresentationContext, getParams: Repre export const PointRepresentationProvider = StructureRepresentationProvider({ name: 'point', label: 'Point', - description: 'Displays elements (atoms, coarse spheres) as spheres.', + description: 'Displays elements (atoms, coarse spheres) as points.', factory: PointRepresentation, getParams: getPointParams, defaultValues: PD.getDefaultValues(PointParams), defaultColorTheme: { name: 'element-symbol' }, - defaultSizeTheme: { name: 'physical' }, + defaultSizeTheme: { name: 'uniform' }, isApplicable: (structure: Structure) => structure.elementCount > 0 }); \ No newline at end of file diff --git a/src/mol-repr/structure/visual/bond-inter-unit-cylinder.ts b/src/mol-repr/structure/visual/bond-inter-unit-cylinder.ts index d9bf85548561a1fb6fcef6264f84f6d843c7fbf6..5385bac813dbc3f0b461d149c8d9d3a116de447f 100644 --- a/src/mol-repr/structure/visual/bond-inter-unit-cylinder.ts +++ b/src/mol-repr/structure/visual/bond-inter-unit-cylinder.ts @@ -40,7 +40,10 @@ function getInterUnitBondCylinderBuilderProps(structure: Structure, theme: Theme const bonds = structure.interUnitBonds; const { edgeCount, edges } = bonds; - const { sizeFactor, sizeAspectRatio, adjustCylinderLength, aromaticBonds } = props; + const { sizeFactor, sizeAspectRatio, adjustCylinderLength, aromaticBonds, multipleBonds } = props; + + const mbOff = multipleBonds === 'off'; + const mbSymmetric = multipleBonds === 'symmetric'; const delta = Vec3(); @@ -136,14 +139,16 @@ function getInterUnitBondCylinderBuilderProps(structure: Structure, theme: Theme // show metallic coordinations and hydrogen bonds with dashed cylinders return LinkStyle.Dashed; } else if (o === 3) { - return LinkStyle.Triple; + return mbOff ? LinkStyle.Solid : + mbSymmetric ? LinkStyle.Triple : + LinkStyle.OffsetTriple; } else if (aromaticBonds && BondType.is(f, BondType.Flag.Aromatic)) { return LinkStyle.Aromatic; - } else if (o === 2) { - return LinkStyle.Double; } - return LinkStyle.Solid; + return (o !== 2 || mbOff) ? LinkStyle.Solid : + mbSymmetric ? LinkStyle.Double : + LinkStyle.OffsetDouble; }, radius: (edgeIndex: number) => { return radius(edgeIndex) * sizeAspectRatio; @@ -210,13 +215,17 @@ export function InterUnitBondCylinderImpostorVisual(materialId: number): Complex newProps.linkSpacing !== currentProps.linkSpacing || newProps.ignoreHydrogens !== currentProps.ignoreHydrogens || newProps.linkCap !== currentProps.linkCap || + newProps.aromaticScale !== currentProps.aromaticScale || + newProps.aromaticSpacing !== currentProps.aromaticSpacing || + newProps.aromaticDashCount !== currentProps.aromaticDashCount || newProps.dashCount !== currentProps.dashCount || newProps.dashScale !== currentProps.dashScale || newProps.dashCap !== currentProps.dashCap || newProps.stubCap !== currentProps.stubCap || !arrayEqual(newProps.includeTypes, currentProps.includeTypes) || !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes) || - newProps.adjustCylinderLength !== currentProps.adjustCylinderLength + newProps.adjustCylinderLength !== currentProps.adjustCylinderLength || + newProps.multipleBonds !== currentProps.multipleBonds ); if (newStructure.interUnitBonds !== currentStructure.interUnitBonds) { @@ -248,13 +257,17 @@ export function InterUnitBondCylinderMeshVisual(materialId: number): ComplexVisu newProps.linkSpacing !== currentProps.linkSpacing || newProps.ignoreHydrogens !== currentProps.ignoreHydrogens || newProps.linkCap !== currentProps.linkCap || + newProps.aromaticScale !== currentProps.aromaticScale || + newProps.aromaticSpacing !== currentProps.aromaticSpacing || + newProps.aromaticDashCount !== currentProps.aromaticDashCount || newProps.dashCount !== currentProps.dashCount || newProps.dashScale !== currentProps.dashScale || newProps.dashCap !== currentProps.dashCap || newProps.stubCap !== currentProps.stubCap || !arrayEqual(newProps.includeTypes, currentProps.includeTypes) || !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes) || - newProps.adjustCylinderLength !== currentProps.adjustCylinderLength + newProps.adjustCylinderLength !== currentProps.adjustCylinderLength || + newProps.multipleBonds !== currentProps.multipleBonds ); if (newStructure.interUnitBonds !== currentStructure.interUnitBonds) { diff --git a/src/mol-repr/structure/visual/bond-inter-unit-line.ts b/src/mol-repr/structure/visual/bond-inter-unit-line.ts index 5578b8011c44b6f38eeaab7e443296a8ba4aa5c2..1fb4b9784d8fe7cfbb7a56fa27a9243d96ab69f0 100644 --- a/src/mol-repr/structure/visual/bond-inter-unit-line.ts +++ b/src/mol-repr/structure/visual/bond-inter-unit-line.ts @@ -32,10 +32,14 @@ function setRefPosition(pos: Vec3, structure: Structure, unit: Unit.Atomic, inde function createInterUnitBondLines(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<InterUnitBondLineParams>, lines?: Lines) { const bonds = structure.interUnitBonds; const { edgeCount, edges } = bonds; - const { sizeFactor, aromaticBonds } = props; if (!edgeCount) return Lines.createEmpty(lines); + const { sizeFactor, aromaticBonds, multipleBonds } = props; + + const mbOff = multipleBonds === 'off'; + const mbSymmetric = multipleBonds === 'symmetric'; + const ref = Vec3(); const loc = StructureElement.Location.create(); @@ -74,14 +78,16 @@ function createInterUnitBondLines(ctx: VisualContext, structure: Structure, them // show metallic coordinations and hydrogen bonds with dashed cylinders return LinkStyle.Dashed; } else if (o === 3) { - return LinkStyle.Triple; + return mbOff ? LinkStyle.Solid : + mbSymmetric ? LinkStyle.Triple : + LinkStyle.OffsetTriple; } else if (aromaticBonds && BondType.is(f, BondType.Flag.Aromatic)) { return LinkStyle.Aromatic; - } else if (o === 2) { - return LinkStyle.Double; } - return LinkStyle.Solid; + return (o !== 2 || mbOff) ? LinkStyle.Solid : + mbSymmetric ? LinkStyle.Double : + LinkStyle.OffsetDouble; }, radius: (edgeIndex: number) => { const b = edges[edgeIndex]; @@ -125,10 +131,12 @@ export function InterUnitBondLineVisual(materialId: number): ComplexVisual<Inter newProps.sizeFactor !== currentProps.sizeFactor || newProps.linkScale !== currentProps.linkScale || newProps.linkSpacing !== currentProps.linkSpacing || + newProps.aromaticDashCount !== currentProps.aromaticDashCount || newProps.dashCount !== currentProps.dashCount || newProps.ignoreHydrogens !== currentProps.ignoreHydrogens || !arrayEqual(newProps.includeTypes, currentProps.includeTypes) || - !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes) + !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes) || + newProps.multipleBonds !== currentProps.multipleBonds ); if (newStructure.interUnitBonds !== currentStructure.interUnitBonds) { diff --git a/src/mol-repr/structure/visual/bond-intra-unit-cylinder.ts b/src/mol-repr/structure/visual/bond-intra-unit-cylinder.ts index a53098c30270d139fd18420af02261c418162b76..abd5d82a7282c0f403faec6fbb92597030c134f5 100644 --- a/src/mol-repr/structure/visual/bond-intra-unit-cylinder.ts +++ b/src/mol-repr/structure/visual/bond-intra-unit-cylinder.ts @@ -33,7 +33,10 @@ function getIntraUnitBondCylinderBuilderProps(unit: Unit.Atomic, structure: Stru const bonds = unit.bonds; const { edgeCount, a, b, edgeProps, offset } = bonds; const { order: _order, flags: _flags } = edgeProps; - const { sizeFactor, sizeAspectRatio, adjustCylinderLength, aromaticBonds, includeTypes, excludeTypes } = props; + const { sizeFactor, sizeAspectRatio, adjustCylinderLength, aromaticBonds, includeTypes, excludeTypes, multipleBonds } = props; + + const mbOff = multipleBonds === 'off'; + const mbSymmetric = multipleBonds === 'symmetric'; const include = BondType.fromNames(includeTypes); const exclude = BondType.fromNames(excludeTypes); @@ -130,7 +133,9 @@ function getIntraUnitBondCylinderBuilderProps(unit: Unit.Atomic, structure: Stru // show metallic coordinations and hydrogen bonds with dashed cylinders return LinkStyle.Dashed; } else if (o === 3) { - return LinkStyle.Triple; + return mbOff ? LinkStyle.Solid : + mbSymmetric ? LinkStyle.Triple : + LinkStyle.OffsetTriple; } else if (aromaticBonds) { const aI = a[edgeIndex], bI = b[edgeIndex]; const aR = elementAromaticRingIndices.get(aI); @@ -146,7 +151,9 @@ function getIntraUnitBondCylinderBuilderProps(unit: Unit.Atomic, structure: Stru } } - return o === 2 ? LinkStyle.Double : LinkStyle.Solid; + return (o !== 2 || mbOff) ? LinkStyle.Solid : + mbSymmetric ? LinkStyle.Double : + LinkStyle.OffsetDouble; }, radius: (edgeIndex: number) => { return radius(edgeIndex) * sizeAspectRatio; @@ -221,6 +228,9 @@ export function IntraUnitBondCylinderImpostorVisual(materialId: number): UnitsVi newProps.linkSpacing !== currentProps.linkSpacing || newProps.ignoreHydrogens !== currentProps.ignoreHydrogens || newProps.linkCap !== currentProps.linkCap || + newProps.aromaticScale !== currentProps.aromaticScale || + newProps.aromaticSpacing !== currentProps.aromaticSpacing || + newProps.aromaticDashCount !== currentProps.aromaticDashCount || newProps.dashCount !== currentProps.dashCount || newProps.dashScale !== currentProps.dashScale || newProps.dashCap !== currentProps.dashCap || @@ -228,7 +238,8 @@ export function IntraUnitBondCylinderImpostorVisual(materialId: number): UnitsVi !arrayEqual(newProps.includeTypes, currentProps.includeTypes) || !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes) || newProps.adjustCylinderLength !== currentProps.adjustCylinderLength || - newProps.aromaticBonds !== currentProps.aromaticBonds + newProps.aromaticBonds !== currentProps.aromaticBonds || + newProps.multipleBonds !== currentProps.multipleBonds ); const newUnit = newStructureGroup.group.units[0]; @@ -264,6 +275,9 @@ export function IntraUnitBondCylinderMeshVisual(materialId: number): UnitsVisual newProps.linkSpacing !== currentProps.linkSpacing || newProps.ignoreHydrogens !== currentProps.ignoreHydrogens || newProps.linkCap !== currentProps.linkCap || + newProps.aromaticScale !== currentProps.aromaticScale || + newProps.aromaticSpacing !== currentProps.aromaticSpacing || + newProps.aromaticDashCount !== currentProps.aromaticDashCount || newProps.dashCount !== currentProps.dashCount || newProps.dashScale !== currentProps.dashScale || newProps.dashCap !== currentProps.dashCap || @@ -271,7 +285,8 @@ export function IntraUnitBondCylinderMeshVisual(materialId: number): UnitsVisual !arrayEqual(newProps.includeTypes, currentProps.includeTypes) || !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes) || newProps.adjustCylinderLength !== currentProps.adjustCylinderLength || - newProps.aromaticBonds !== currentProps.aromaticBonds + newProps.aromaticBonds !== currentProps.aromaticBonds || + newProps.multipleBonds !== currentProps.multipleBonds ); const newUnit = newStructureGroup.group.units[0]; diff --git a/src/mol-repr/structure/visual/bond-intra-unit-line.ts b/src/mol-repr/structure/visual/bond-intra-unit-line.ts index 49e9c1bb2f72988f3846401463b5626615de13ec..1ad1fa45f7bbbad9eda44e63e311bd0e995925a4 100644 --- a/src/mol-repr/structure/visual/bond-intra-unit-line.ts +++ b/src/mol-repr/structure/visual/bond-intra-unit-line.ts @@ -39,7 +39,10 @@ function createIntraUnitBondLines(ctx: VisualContext, unit: Unit, structure: Str if (!edgeCount) return Lines.createEmpty(lines); const { order: _order, flags: _flags } = edgeProps; - const { sizeFactor, aromaticBonds, includeTypes, excludeTypes } = props; + const { sizeFactor, aromaticBonds, includeTypes, excludeTypes, multipleBonds } = props; + + const mbOff = multipleBonds === 'off'; + const mbSymmetric = multipleBonds === 'symmetric'; const include = BondType.fromNames(includeTypes); const exclude = BondType.fromNames(excludeTypes); @@ -91,7 +94,9 @@ function createIntraUnitBondLines(ctx: VisualContext, unit: Unit, structure: Str // show metallic coordinations and hydrogen bonds with dashed cylinders return LinkStyle.Dashed; } else if (o === 3) { - return LinkStyle.Triple; + return mbOff ? LinkStyle.Solid : + mbSymmetric ? LinkStyle.Triple : + LinkStyle.OffsetTriple; } else if (aromaticBonds) { const aI = a[edgeIndex], bI = b[edgeIndex]; const aR = elementAromaticRingIndices.get(aI); @@ -107,7 +112,9 @@ function createIntraUnitBondLines(ctx: VisualContext, unit: Unit, structure: Str } } - return o === 2 ? LinkStyle.Double : LinkStyle.Solid; + return (o !== 2 || mbOff) ? LinkStyle.Solid : + mbSymmetric ? LinkStyle.Double : + LinkStyle.OffsetDouble; }, radius: (edgeIndex: number) => { location.element = elements[a[edgeIndex]]; @@ -146,11 +153,13 @@ export function IntraUnitBondLineVisual(materialId: number): UnitsVisual<IntraUn newProps.sizeFactor !== currentProps.sizeFactor || newProps.linkScale !== currentProps.linkScale || newProps.linkSpacing !== currentProps.linkSpacing || + newProps.aromaticDashCount !== currentProps.aromaticDashCount || newProps.dashCount !== currentProps.dashCount || newProps.ignoreHydrogens !== currentProps.ignoreHydrogens || !arrayEqual(newProps.includeTypes, currentProps.includeTypes) || !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes) || - newProps.aromaticBonds !== currentProps.aromaticBonds + newProps.aromaticBonds !== currentProps.aromaticBonds || + newProps.multipleBonds !== currentProps.multipleBonds ); const newUnit = newStructureGroup.group.units[0]; diff --git a/src/mol-repr/structure/visual/element-cross.ts b/src/mol-repr/structure/visual/element-cross.ts new file mode 100644 index 0000000000000000000000000000000000000000..557a970feedadcd0090fc661e273dc0d7647df62 --- /dev/null +++ b/src/mol-repr/structure/visual/element-cross.ts @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { ParamDefinition as PD } from '../../../mol-util/param-definition'; +import { UnitsVisual, UnitsLinesParams, UnitsLinesVisual } from '../units-visual'; +import { VisualContext } from '../../visual'; +import { Unit, Structure, StructureElement } from '../../../mol-model/structure'; +import { Theme } from '../../../mol-theme/theme'; +import { Vec3 } from '../../../mol-math/linear-algebra'; +import { ElementIterator, getElementLoci, eachElement, makeElementIgnoreTest } from './util/element'; +import { VisualUpdateState } from '../../util'; +import { Sphere3D } from '../../../mol-math/geometry'; +import { Lines } from '../../../mol-geo/geometry/lines/lines'; +import { LinesBuilder } from '../../../mol-geo/geometry/lines/lines-builder'; +import { bondCount } from '../../../mol-model-props/computed/chemistry/util'; + +// avoiding namespace lookup improved performance in Chrome (Aug 2020) +const v3scaleAndAdd = Vec3.scaleAndAdd; +const v3unitX = Vec3.unitX; +const v3unitY = Vec3.unitY; +const v3unitZ = Vec3.unitZ; + +export const ElementCrossParams = { + ...UnitsLinesParams, + lineSizeAttenuation: PD.Boolean(false), + ignoreHydrogens: PD.Boolean(false), + traceOnly: PD.Boolean(false), + crosses: PD.Select('lone', PD.arrayToOptions(['lone', 'all'] as const)), + crossSize: PD.Numeric(0.35, { min: 0, max: 2, step: 0.01 }), +}; +export type ElementCrossParams = typeof ElementCrossParams + +export function createElementCross(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<ElementCrossParams>, lines: Lines) { + const { child } = structure; + if (child && !child.unitMap.get(unit.id)) return Lines.createEmpty(lines); + + const elements = unit.elements; + const n = elements.length; + const builder = LinesBuilder.create(n, n / 10, lines); + + const p = Vec3(); + const s = Vec3(); + const e = Vec3(); + + const pos = unit.conformation.invariantPosition; + const ignore = makeElementIgnoreTest(structure, unit, props); + + const r = props.crossSize / 2; + const lone = props.crosses === 'lone'; + + for (let i = 0 as StructureElement.UnitIndex; i < n; ++i) { + if (ignore && ignore(elements[i])) continue; + if (lone && Unit.isAtomic(unit) && bondCount(structure, unit, i) !== 0) continue; + + pos(elements[i], p); + v3scaleAndAdd(s, p, v3unitX, r); + v3scaleAndAdd(e, p, v3unitX, -r); + builder.add(s[0], s[1], s[2], e[0], e[1], e[2], i); + v3scaleAndAdd(s, p, v3unitY, r); + v3scaleAndAdd(e, p, v3unitY, -r); + builder.add(s[0], s[1], s[2], e[0], e[1], e[2], i); + v3scaleAndAdd(s, p, v3unitZ, r); + v3scaleAndAdd(e, p, v3unitZ, -r); + builder.add(s[0], s[1], s[2], e[0], e[1], e[2], i); + } + + const l = builder.getLines(); + + const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1 * props.sizeFactor); + l.setBoundingSphere(sphere); + + return l; +} + +export function ElementCrossVisual(materialId: number): UnitsVisual<ElementCrossParams> { + return UnitsLinesVisual<ElementCrossParams>({ + defaultProps: PD.getDefaultValues(ElementCrossParams), + createGeometry: createElementCross, + createLocationIterator: ElementIterator.fromGroup, + getLoci: getElementLoci, + eachLocation: eachElement, + setUpdateState: (state: VisualUpdateState, newProps: PD.Values<ElementCrossParams>, currentProps: PD.Values<ElementCrossParams>) => { + state.createGeometry = ( + newProps.ignoreHydrogens !== currentProps.ignoreHydrogens || + newProps.traceOnly !== currentProps.traceOnly || + newProps.crosses !== currentProps.crosses || + newProps.crossSize !== currentProps.crossSize + ); + } + }, materialId); +} \ No newline at end of file diff --git a/src/mol-repr/structure/visual/element-point.ts b/src/mol-repr/structure/visual/element-point.ts index 5ccea9224332e198f413b13140f957f60d8e5fbc..3f41e74ff0f114c0a6efee414248c67b32e31021 100644 --- a/src/mol-repr/structure/visual/element-point.ts +++ b/src/mol-repr/structure/visual/element-point.ts @@ -18,7 +18,7 @@ import { Sphere3D } from '../../../mol-math/geometry'; export const ElementPointParams = { ...UnitsPointsParams, - pointSizeAttenuation: PD.Boolean(true), + pointSizeAttenuation: PD.Boolean(false), ignoreHydrogens: PD.Boolean(false), traceOnly: PD.Boolean(false), }; diff --git a/src/mol-repr/structure/visual/util/bond.ts b/src/mol-repr/structure/visual/util/bond.ts index 5e2ddafd6c006f47817c606fddc93b0c0d35646b..d2fb94a7769d27880135d6008e0c46c724ae1ef4 100644 --- a/src/mol-repr/structure/visual/util/bond.ts +++ b/src/mol-repr/structure/visual/util/bond.ts @@ -20,6 +20,7 @@ export const BondParams = { excludeTypes: PD.MultiSelect([] as BondType.Names[], PD.objectToOptions(BondType.Names)), ignoreHydrogens: PD.Boolean(false), aromaticBonds: PD.Boolean(false, { description: 'Display aromatic bonds with dashes' }), + multipleBonds: PD.Select('symmetric', PD.arrayToOptions(['off', 'symmetric', 'offset'] as const)), }; export const DefaultBondProps = PD.getDefaultValues(BondParams); export type BondProps = typeof DefaultBondProps @@ -27,7 +28,7 @@ export type BondProps = typeof DefaultBondProps export const BondCylinderParams = { ...LinkCylinderParams, ...BondParams, - adjustCylinderLength: PD.Boolean(true, { description: 'Shorten cylinders to reduce overlap with spheres.' }) + adjustCylinderLength: PD.Boolean(false, { description: 'Shorten cylinders to reduce overlap with spheres. Useful for for transparent bonds. Not working well with aromatic bonds.' }) }; export const DefaultBondCylinderProps = PD.getDefaultValues(BondCylinderParams); export type BondCylinderProps = typeof DefaultBondCylinderProps diff --git a/src/mol-repr/structure/visual/util/link.ts b/src/mol-repr/structure/visual/util/link.ts index 34752801b2bd219933552315cf5b03fedcf16933..c62aea5d0a496badcf2924404c35ce6153cf9c3f 100644 --- a/src/mol-repr/structure/visual/util/link.ts +++ b/src/mol-repr/structure/visual/util/link.ts @@ -21,6 +21,9 @@ export const LinkCylinderParams = { linkScale: PD.Numeric(0.45, { min: 0, max: 1, step: 0.01 }), linkSpacing: PD.Numeric(1, { min: 0, max: 2, step: 0.01 }), linkCap: PD.Boolean(false), + aromaticScale: PD.Numeric(0.3, { min: 0, max: 1, step: 0.01 }), + aromaticSpacing: PD.Numeric(1.5, { min: 0, max: 3, step: 0.01 }), + aromaticDashCount: PD.Numeric(2, { min: 2, max: 6, step: 2 }), dashCount: PD.Numeric(4, { min: 2, max: 10, step: 2 }), dashScale: PD.Numeric(0.8, { min: 0, max: 2, step: 0.1 }), dashCap: PD.Boolean(true), @@ -33,6 +36,7 @@ export type LinkCylinderProps = typeof DefaultLinkCylinderProps export const LinkLineParams = { linkScale: PD.Numeric(0.5, { min: 0, max: 1, step: 0.1 }), linkSpacing: PD.Numeric(0.1, { min: 0, max: 2, step: 0.01 }), + aromaticDashCount: PD.Numeric(2, { min: 2, max: 6, step: 2 }), dashCount: PD.Numeric(4, { min: 2, max: 10, step: 2 }), }; export const DefaultLinkLineProps = PD.getDefaultValues(LinkLineParams); @@ -83,10 +87,12 @@ export const enum LinkStyle { Solid = 0, Dashed = 1, Double = 2, - Triple = 3, - Disk = 4, - Aromatic = 5, - MirroredAromatic = 6, + OffsetDouble = 3, + Triple = 4, + OffsetTriple = 5, + Disk = 6, + Aromatic = 7, + MirroredAromatic = 8, } // avoiding namespace lookup improved performance in Chrome (Aug 2020) @@ -105,7 +111,7 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuil if (!linkCount) return Mesh.createEmpty(mesh); - const { linkScale, linkSpacing, radialSegments, linkCap, dashCount, dashScale, dashCap, stubCap } = props; + const { linkScale, linkSpacing, radialSegments, linkCap, aromaticScale, aromaticSpacing, aromaticDashCount, dashCount, dashScale, dashCap, stubCap } = props; const vertexCountEstimate = radialSegments * 2 * linkCount * 2; const builderState = MeshBuilder.createState(vertexCountEstimate, vertexCountEstimate / 4, mesh); @@ -128,14 +134,15 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuil position(va, vb, edgeIndex); v3sub(tmpV12, vb, va); + const dirFlag = v3dot(tmpV12, up) > 0; const linkRadius = radius(edgeIndex); const linkStyle = style ? style(edgeIndex) : LinkStyle.Solid; const linkStub = stubCap && (stub ? stub(edgeIndex) : false); - const [topCap, bottomCap] = (v3dot(tmpV12, up) > 0) ? [linkStub, linkCap] : [linkCap, linkStub]; + const [topCap, bottomCap] = dirFlag ? [linkStub, linkCap] : [linkCap, linkStub]; builderState.currentGroup = edgeIndex; - const aromaticOffsetFactor = 4.5; + const aromaticSegmentCount = aromaticDashCount + 1; if (linkStyle === LinkStyle.Solid) { cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius; @@ -148,9 +155,9 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuil cylinderProps.topCap = cylinderProps.bottomCap = dashCap; addFixedCountDashedCylinder(builderState, va, vb, 0.5, segmentCount, cylinderProps); - } else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.Triple || linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) { - const order = linkStyle === LinkStyle.Double ? 2 : - linkStyle === LinkStyle.Triple ? 3 : 1.5; + } else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.OffsetDouble || linkStyle === LinkStyle.Triple || linkStyle === LinkStyle.OffsetTriple || linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) { + const order = (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.OffsetDouble) ? 2 : + (linkStyle === LinkStyle.Triple || linkStyle === LinkStyle.OffsetTriple) ? 3 : 1.5; const multiRadius = linkRadius * (linkScale / (0.5 * order)); const absOffset = (linkRadius - multiRadius) * linkSpacing; @@ -163,18 +170,49 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuil cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius; addCylinder(builderState, va, vb, 0.5, cylinderProps); - cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius * linkScale; + const aromaticOffset = linkRadius + aromaticScale * linkRadius + aromaticScale * linkRadius * aromaticSpacing; + + v3setMagnitude(tmpV12, v3sub(tmpV12, vb, va), linkRadius * 0.5); + v3add(va, va, tmpV12); + v3sub(vb, vb, tmpV12); + + cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius * aromaticScale; cylinderProps.topCap = cylinderProps.bottomCap = dashCap; - v3setMagnitude(vShift, vShift, absOffset * aromaticOffsetFactor); + v3setMagnitude(vShift, vShift, aromaticOffset); v3sub(va, va, vShift); v3sub(vb, vb, vShift); - addFixedCountDashedCylinder(builderState, va, vb, 0.5, 3, cylinderProps); + addFixedCountDashedCylinder(builderState, va, vb, 0.5, aromaticSegmentCount, cylinderProps); if (linkStyle === LinkStyle.MirroredAromatic) { - v3setMagnitude(vShift, vShift, absOffset * aromaticOffsetFactor * 2); + v3setMagnitude(vShift, vShift, aromaticOffset * 2); + v3add(va, va, vShift); + v3add(vb, vb, vShift); + addFixedCountDashedCylinder(builderState, va, vb, 0.5, aromaticSegmentCount, cylinderProps); + } + } else if (linkStyle === LinkStyle.OffsetDouble || linkStyle === LinkStyle.OffsetTriple) { + const multipleOffset = linkRadius + multiRadius + linkScale * linkRadius * linkSpacing; + v3setMagnitude(vShift, vShift, multipleOffset); + + cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius; + addCylinder(builderState, va, vb, 0.5, cylinderProps); + + v3scale(tmpV12, tmpV12, linkSpacing * linkScale * 0.2); + v3add(va, va, tmpV12); + v3sub(vb, vb, tmpV12); + + cylinderProps.radiusTop = cylinderProps.radiusBottom = multiRadius; + cylinderProps.topCap = dirFlag ? linkStub : dashCap; + cylinderProps.bottomCap = dirFlag ? dashCap : linkStub; + v3setMagnitude(vShift, vShift, multipleOffset); + v3sub(va, va, vShift); + v3sub(vb, vb, vShift); + addCylinder(builderState, va, vb, 0.5, cylinderProps); + + if (order === 3) { + v3setMagnitude(vShift, vShift, multipleOffset * 2); v3add(va, va, vShift); v3add(vb, vb, vShift); - addFixedCountDashedCylinder(builderState, va, vb, 0.5, 3, cylinderProps); + addCylinder(builderState, va, vb, 0.5, cylinderProps); } } else { v3setMagnitude(vShift, vShift, absOffset); @@ -208,7 +246,7 @@ export function createLinkCylinderImpostors(ctx: VisualContext, linkBuilder: Lin if (!linkCount) return Cylinders.createEmpty(cylinders); - const { linkScale, linkSpacing, linkCap, dashCount, dashScale, dashCap, stubCap } = props; + const { linkScale, linkSpacing, linkCap, aromaticScale, aromaticSpacing, aromaticDashCount, dashCount, dashScale, dashCap, stubCap } = props; const cylindersCountEstimate = linkCount * 2; const builder = CylindersBuilder.create(cylindersCountEstimate, cylindersCountEstimate / 4, cylinders); @@ -222,9 +260,8 @@ export function createLinkCylinderImpostors(ctx: VisualContext, linkBuilder: Lin const segmentCount = dashCount % 2 === 1 ? dashCount : dashCount + 1; const lengthScale = 0.5 - (0.5 / 2 / segmentCount); - const aromaticSegmentCount = 3; + const aromaticSegmentCount = aromaticDashCount + 1; const aromaticLengthScale = 0.5 - (0.5 / 2 / aromaticSegmentCount); - const aromaticOffsetFactor = 4.5; for (let edgeIndex = 0, _eI = linkCount; edgeIndex < _eI; ++edgeIndex) { if (ignore && ignore(edgeIndex)) continue; @@ -242,9 +279,9 @@ export function createLinkCylinderImpostors(ctx: VisualContext, linkBuilder: Lin v3scale(tmpV12, v3sub(tmpV12, vb, va), lengthScale); v3sub(vb, vb, tmpV12); builder.addFixedCountDashes(va, vb, segmentCount, dashScale, dashCap, dashCap, edgeIndex); - } else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.Triple || linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) { - const order = linkStyle === LinkStyle.Double ? 2 : - linkStyle === LinkStyle.Triple ? 3 : 1.5; + } else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.OffsetDouble || linkStyle === LinkStyle.Triple || linkStyle === LinkStyle.OffsetTriple || linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) { + const order = (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.OffsetDouble) ? 2 : + (linkStyle === LinkStyle.Triple || linkStyle === LinkStyle.OffsetTriple) ? 3 : 1.5; const multiScale = linkScale / (0.5 * order); const absOffset = (linkRadius - multiScale * linkRadius) * linkSpacing; @@ -254,24 +291,40 @@ export function createLinkCylinderImpostors(ctx: VisualContext, linkBuilder: Lin if (linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) { builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], 1, linkCap, linkStub, edgeIndex); + const aromaticOffset = linkRadius + aromaticScale * linkRadius + aromaticScale * linkRadius * aromaticSpacing; + v3scale(tmpV12, v3sub(tmpV12, vb, va), aromaticLengthScale); v3sub(vb, vb, tmpV12); - v3setMagnitude(vShift, vShift, absOffset * aromaticOffsetFactor); + v3setMagnitude(tmpV12, v3sub(tmpV12, vb, va), linkRadius * 0.5); + v3add(va, va, tmpV12); + + v3setMagnitude(vShift, vShift, aromaticOffset); v3sub(va, va, vShift); v3sub(vb, vb, vShift); - builder.addFixedCountDashes(va, vb, aromaticSegmentCount, linkScale, dashCap, dashCap, edgeIndex); + builder.addFixedCountDashes(va, vb, aromaticSegmentCount, aromaticScale, dashCap, dashCap, edgeIndex); if (linkStyle === LinkStyle.MirroredAromatic) { - v3setMagnitude(vShift, vShift, absOffset * aromaticOffsetFactor * 2); + v3setMagnitude(vShift, vShift, aromaticOffset * 2); v3add(va, va, vShift); v3add(vb, vb, vShift); - builder.addFixedCountDashes(va, vb, aromaticSegmentCount, linkScale, dashCap, dashCap, edgeIndex); + builder.addFixedCountDashes(va, vb, aromaticSegmentCount, aromaticScale, dashCap, dashCap, edgeIndex); } + } else if (linkStyle === LinkStyle.OffsetDouble || linkStyle === LinkStyle.OffsetTriple) { + const multipleOffset = linkRadius + multiScale * linkRadius + linkScale * linkRadius * linkSpacing; + v3setMagnitude(vShift, vShift, multipleOffset); + + builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], 1, linkCap, linkStub, edgeIndex); + + v3setMagnitude(tmpV12, v3sub(tmpV12, va, vb), linkRadius / 1.5); + v3sub(va, va, tmpV12); + + if (order === 3) builder.add(va[0] + vShift[0], va[1] + vShift[1], va[2] + vShift[2], vm[0] + vShift[0], vm[1] + vShift[1], vm[2] + vShift[2], multiScale, linkCap, linkStub, edgeIndex); + builder.add(va[0] - vShift[0], va[1] - vShift[1], va[2] - vShift[2], vm[0] - vShift[0], vm[1] - vShift[1], vm[2] - vShift[2], multiScale, dashCap, linkStub, edgeIndex); } else { v3setMagnitude(vShift, vShift, absOffset); - if (order === 3) builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], multiScale, linkCap, false, edgeIndex); + if (order === 3) builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], multiScale, linkCap, linkStub, edgeIndex); builder.add(va[0] + vShift[0], va[1] + vShift[1], va[2] + vShift[2], vm[0] + vShift[0], vm[1] + vShift[1], vm[2] + vShift[2], multiScale, linkCap, linkStub, edgeIndex); builder.add(va[0] - vShift[0], va[1] - vShift[1], va[2] - vShift[2], vm[0] - vShift[0], vm[1] - vShift[1], vm[2] - vShift[2], multiScale, linkCap, linkStub, edgeIndex); } @@ -296,7 +349,7 @@ export function createLinkLines(ctx: VisualContext, linkBuilder: LinkBuilderProp if (!linkCount) return Lines.createEmpty(lines); - const { linkScale, linkSpacing, dashCount } = props; + const { linkScale, linkSpacing, aromaticDashCount, dashCount } = props; const linesCountEstimate = linkCount * 2; const builder = LinesBuilder.create(linesCountEstimate, linesCountEstimate / 4, lines); @@ -310,9 +363,10 @@ export function createLinkLines(ctx: VisualContext, linkBuilder: LinkBuilderProp const segmentCount = dashCount % 2 === 1 ? dashCount : dashCount + 1; const lengthScale = 0.5 - (0.5 / 2 / segmentCount); - const aromaticSegmentCount = 3; + const aromaticSegmentCount = aromaticDashCount + 1; const aromaticLengthScale = 0.5 - (0.5 / 2 / aromaticSegmentCount); const aromaticOffsetFactor = 4.5; + const multipleOffsetFactor = 3; for (let edgeIndex = 0, _eI = linkCount; edgeIndex < _eI; ++edgeIndex) { if (ignore && ignore(edgeIndex)) continue; @@ -328,9 +382,9 @@ export function createLinkLines(ctx: VisualContext, linkBuilder: LinkBuilderProp v3scale(tmpV12, v3sub(tmpV12, vb, va), lengthScale); v3sub(vb, vb, tmpV12); builder.addFixedCountDashes(va, vb, segmentCount, edgeIndex); - } else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.Triple || linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) { - const order = linkStyle === LinkStyle.Double ? 2 : - linkStyle === LinkStyle.Triple ? 3 : 1.5; + } else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.OffsetDouble || linkStyle === LinkStyle.Triple || linkStyle === LinkStyle.OffsetTriple || linkStyle === LinkStyle.Aromatic || linkStyle === LinkStyle.MirroredAromatic) { + const order = linkStyle === LinkStyle.Double || linkStyle === LinkStyle.OffsetDouble ? 2 : + linkStyle === LinkStyle.Triple || linkStyle === LinkStyle.OffsetTriple ? 3 : 1.5; const multiRadius = 1 * (linkScale / (0.5 * order)); const absOffset = (1 - multiRadius) * linkSpacing; @@ -354,8 +408,18 @@ export function createLinkLines(ctx: VisualContext, linkBuilder: LinkBuilderProp v3add(vb, vb, vShift); builder.addFixedCountDashes(va, vb, aromaticSegmentCount, edgeIndex); } + } else if (linkStyle === LinkStyle.OffsetDouble || linkStyle === LinkStyle.OffsetTriple) { + v3setMagnitude(vShift, vShift, absOffset * multipleOffsetFactor); + + builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], edgeIndex); + + v3scale(tmpV12, v3sub(tmpV12, va, vb), linkSpacing * linkScale); + v3sub(va, va, tmpV12); + + if (order === 3) builder.add(va[0] + vShift[0], va[1] + vShift[1], va[2] + vShift[2], vm[0] + vShift[0], vm[1] + vShift[1], vm[2] + vShift[2], edgeIndex); + builder.add(va[0] - vShift[0], va[1] - vShift[1], va[2] - vShift[2], vm[0] - vShift[0], vm[1] - vShift[1], vm[2] - vShift[2], edgeIndex); } else { - v3setMagnitude(vShift, vShift, absOffset); + v3setMagnitude(vShift, vShift, absOffset * 1.5); if (order === 3) builder.add(va[0], va[1], va[2], vm[0], vm[1], vm[2], edgeIndex); builder.add(va[0] + vShift[0], va[1] + vShift[1], va[2] + vShift[2], vm[0] + vShift[0], vm[1] + vShift[1], vm[2] + vShift[2], edgeIndex); diff --git a/src/mol-theme/label.ts b/src/mol-theme/label.ts index 73c123a859f98f572151db59980b1841fd6500cf..d43df4dd7e87a0d2904d8afb271e9f0303849d2f 100644 --- a/src/mol-theme/label.ts +++ b/src/mol-theme/label.ts @@ -92,6 +92,14 @@ export function structureElementStatsLabel(stats: StructureElement.Stats, option return o.htmlStyling ? label : stripTags(label); } +export function structureElementLociLabelMany(locis: StructureElement.Loci[], options: Partial<LabelOptions> = {}): string { + const stats = StructureElement.Stats.create(); + for (const l of locis) { + StructureElement.Stats.add(stats, stats, StructureElement.Stats.ofLoci(l)); + } + return structureElementStatsLabel(stats, options); +} + function _structureElementStatsLabel(stats: StructureElement.Stats, countsOnly = false, hidePrefix = false, condensed = false, reverse = false): string { const { structureCount, chainCount, residueCount, conformationCount, elementCount } = stats; diff --git a/src/mol-util/marker-action.ts b/src/mol-util/marker-action.ts index 53bcd5e4fadca982ce45040684d6c0847e0d680f..324b625b6a313fa368026c68150e3f68a3b31a54 100644 --- a/src/mol-util/marker-action.ts +++ b/src/mol-util/marker-action.ts @@ -68,11 +68,19 @@ export function applyMarkerAction(array: Uint8Array, set: OrderedSet, action: Ma if (Interval.is(set)) { const start = Interval.start(set); const end = Interval.end(set); - const view = new Uint32Array(array.buffer, 0, array.buffer.byteLength >> 2); - const viewStart = (start + 3) >> 2; const viewEnd = viewStart + ((end - 4 * viewStart) >> 2); + if (viewEnd <= viewStart) { + // avoid edge cases with overlapping front/end intervals + for (let i = start; i < end; ++i) { + applyMarkerActionAtPosition(array, i, action); + } + return true; + } + + const view = new Uint32Array(array.buffer, 0, array.buffer.byteLength >> 2); + const frontStart = start; const frontEnd = Math.min(4 * viewStart, end); const backStart = Math.max(start, 4 * viewEnd); diff --git a/src/mol-util/zip/bin.ts b/src/mol-util/zip/bin.ts index 1b3d2a1a4feffe2be4fa128fc7342ff184691f06..fae13877ad1b1226aea461d673e90d2ae4f335fe 100644 --- a/src/mol-util/zip/bin.ts +++ b/src/mol-util/zip/bin.ts @@ -92,7 +92,7 @@ export function sizeUTF8(str: string) { for (let ci = 0; ci < strl; ci++) { const code = str.charCodeAt(ci); if ((code & (0xffffffff - (1 << 7) + 1)) === 0) { - i++ ; + i++; } else if ((code & (0xffffffff - (1 << 11) + 1)) === 0) { i += 2; } else if ((code & (0xffffffff - (1 << 16) + 1)) === 0) { diff --git a/src/servers/model/CHANGELOG.md b/src/servers/model/CHANGELOG.md index db7e0e2f24be611e661b46e0825a1be4da63c134..3835d9dbc948cf2f6dca77c6fdb1a6640c0c82ea 100644 --- a/src/servers/model/CHANGELOG.md +++ b/src/servers/model/CHANGELOG.md @@ -1,3 +1,6 @@ +# 0.9.8 +* fix support for chem_comp_bond and struct_conn categories + # 0.9.7 * add Surrounding Ligands query diff --git a/src/servers/model/config.ts b/src/servers/model/config.ts index 0cfd9d6b42edfd409a408707d0aef441728715e5..5fa1823a046593b2cbeaa08b125b6412fdd05685 100644 --- a/src/servers/model/config.ts +++ b/src/servers/model/config.ts @@ -229,10 +229,10 @@ function addJsonConfigArgs(parser: argparse.ArgumentParser) { 'JSON config file path', 'If a property is not specified, cmd line param/OS variable/default value are used.' ].join('\n'), - required: false + required: false, }); - parser.add_argument('--printCfg', { help: 'Print current config for validation and exit.', required: false, nargs: 0 }); - parser.add_argument('--cfgTemplate', { help: 'Prints default JSON config template to be modified and exits.', required: false, nargs: 0 }); + parser.add_argument('--printCfg', { help: 'Print current config for validation and exit.', required: false, action: 'store_true' }); + parser.add_argument('--cfgTemplate', { help: 'Prints default JSON config template to be modified and exits.', required: false, action: 'store_true' }); } function setConfig(config: ModelServerConfig) { @@ -271,7 +271,7 @@ function parseConfigArguments() { export function configureServer() { const config = parseConfigArguments(); - if (config.cfgTemplate !== null) { + if (!!config.cfgTemplate) { console.log(JSON.stringify(ModelServerConfigTemplate, null, 2)); process.exit(0); } @@ -284,7 +284,7 @@ export function configureServer() { setConfig(cfg); } - if (config.printCfg !== null) { + if (!!config.printCfg) { console.log(JSON.stringify(ModelServerConfig, null, 2)); process.exit(0); } diff --git a/src/servers/model/version.ts b/src/servers/model/version.ts index 48ad2176e31bbd3cf37859acef4438aafe90dee4..84aac74f66131719349de17f4ccbafbfd9b1459b 100644 --- a/src/servers/model/version.ts +++ b/src/servers/model/version.ts @@ -1,7 +1,7 @@ /** - * 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 David Sehnal <david.sehnal@gmail.com> */ -export const VERSION = '0.9.7'; \ No newline at end of file +export const VERSION = '0.9.8'; \ No newline at end of file diff --git a/src/servers/volume/config.ts b/src/servers/volume/config.ts index 53d247d7205284c441f20ba1eef93fa728c6f69c..cc51934c1acc0275d88c3bb37b1e846b8e91e7d1 100644 --- a/src/servers/volume/config.ts +++ b/src/servers/volume/config.ts @@ -89,10 +89,10 @@ function addJsonConfigArgs(parser: argparse.ArgumentParser) { 'JSON config file path', 'If a property is not specified, cmd line param/OS variable/default value are used.' ].join('\n'), - required: false + required: false, }); - parser.add_argument('--printCfg', { help: 'Print current config for validation and exit.', required: false, nargs: 0 }); - parser.add_argument('--cfgTemplate', { help: 'Prints default JSON config template to be modified and exits.', required: false, nargs: 0 }); + parser.add_argument('--printCfg', { help: 'Print current config for validation and exit.', required: false, action: 'store_true' }); + parser.add_argument('--cfgTemplate', { help: 'Prints default JSON config template to be modified and exits.', required: false, action: 'store_true' }); } export interface ServerJsonConfig { @@ -165,7 +165,7 @@ export function configureServer() { addLimitsArgs(parser); const config = parser.parse_args() as FullServerConfig & ServerJsonConfig; - if (config.cfgTemplate !== null) { + if (!!config.cfgTemplate) { console.log(JSON.stringify(ServerConfigTemplate, null, 2)); process.exit(0); } @@ -178,7 +178,7 @@ export function configureServer() { setConfig(cfg); } - if (config.printCfg !== null) { + if (config.printCfg) { console.log(JSON.stringify({ ...ServerConfig, ...LimitsConfig }, null, 2)); process.exit(0); } @@ -197,7 +197,7 @@ export function configureLocal() { description: VOLUME_SERVER_HEADER }); parser.add_argument('--jobs', { help: `Path to a JSON file with job specification.`, required: false }); - parser.add_argument('--jobsTemplate', { help: 'Print example template for jobs.json and exit.', required: false, nargs: 0 });; + parser.add_argument('--jobsTemplate', { help: 'Print example template for jobs.json and exit.', required: false, nargs: 0 }); addJsonConfigArgs(parser); addLimitsArgs(parser);