Compare commits

..

91 Commits

Author SHA1 Message Date
Alexander Rose
cbd417ca13 2.3.7 2021-11-15 19:31:07 -08:00
Alexander Rose
1578211157 add helix profile option to cartoon repr 2021-11-13 13:16:11 -08:00
Alexander Rose
15932dc5df handle parent in Structure.remapModel 2021-11-13 09:23:57 -08:00
dsehnal
7db2205956 changelog 2021-11-09 21:18:55 +01:00
dsehnal
d87f0d236a ViewerOptions.collapseRightPanel 2021-11-09 21:17:01 +01:00
dsehnal
16daca6008 undo test code 2021-11-09 21:13:28 +01:00
dsehnal
a0d919c8db Viewer.loadTrajectory 2021-11-09 21:12:24 +01:00
David Sehnal
ffee2bf1c4 Merge pull request #285 from MadCatX/tweak-measurement-order-labels
Tweak measurement order labels
2021-11-09 10:28:13 +01:00
Michal Malý
de77f6ac59 Fix a visual glitch where the label border was initially rendered with
half of the intended size.
2021-11-09 09:59:25 +01:00
Michal Malý
c8c2ebcd65 Reduce Z-offset of measurement order labels to avoid too early clipping 2021-11-09 09:59:25 +01:00
Michal Malý
42796b984f Reduce border width of measurement order labels 2021-11-09 09:58:58 +01:00
David Sehnal
746557bf52 Merge pull request #284 from russellp17/master
Move jest types to devDependencies
2021-11-08 22:48:15 +01:00
Russell Parker
a5020a9e96 Move jest types to devDependencies 2021-11-08 15:58:14 -05:00
dsehnal
2ebb0a35fd 2.3.6 2021-11-08 18:57:06 +01:00
dsehnal
64aaa92d45 changelog 2021-11-08 18:54:52 +01:00
dsehnal
4baf391efe prefer webgl1 in safari 15.1
- WebGL2 is broken there for Mol* shaders
- It works again in Safari 15.4 tech preview
2021-11-08 18:50:16 +01:00
David Sehnal
5afdcff6a5 Merge pull request #273 from molstar/shader-tests
add basic unit tests for graphics shaders
2021-11-08 18:40:51 +01:00
dsehnal
339c397860 package lock 2021-11-08 18:30:52 +01:00
dsehnal
a58cbd31ef Merge branch 'master' of https://github.com/molstar/molstar into shader-tests 2021-11-08 18:30:30 +01:00
David Sehnal
d232b01cf9 Merge pull request #283 from MadCatX/improve-measurements-ux
Improve measurements user experience
2021-11-08 17:12:37 +01:00
Michal Malý
ec95270854 Use the entire element to trigger highlighting of loci from additionsHistory 2021-11-08 15:46:07 +01:00
Michal Malý
78cc0d960f Show the order of locis to be used for measurements in 3D view 2021-11-08 15:46:02 +01:00
Alexander Rose
7f39cf0f37 add missing updateFocusRepr to atomicDetail preset
- fixes #280
2021-11-02 22:10:35 -07:00
Alexander Rose
4592510a95 linting fix 2021-10-30 21:50:15 -07:00
Alexander Rose
46d5442dc5 Merge pull request #252 from corredD/forkdev
binary model loading support and latest mycoplasma model.
2021-10-30 17:11:27 -07:00
Alexander Rose
271cff4aba changelog 2021-10-30 17:09:35 -07:00
Alexander Rose
94fd5a97d6 Merge branch 'master' of https://github.com/molstar/molstar into pr/corredD/252 2021-10-30 16:53:35 -07:00
Alexander Rose
28678e2f80 Merge pull request #270 from molstar/measurements
Additional measurement controls
2021-10-30 16:52:32 -07:00
Alexander Rose
406307a432 add radiusScale param to orientation measurement 2021-10-30 16:48:47 -07:00
Alexander Rose
56345b5096 Merge branch 'master' of https://github.com/molstar/molstar into measurements 2021-10-30 16:31:12 -07:00
Alexander Rose
3fcc42ee0e fix, proper EmptyRepresentationProvider 2021-10-30 16:26:37 -07:00
Alexander Rose
b903677f8a gh action, add npm run build 2021-10-30 16:12:42 -07:00
Alexander Rose
ef4b632a07 update package-lock 2021-10-30 16:05:24 -07:00
Alexander Rose
e9d485ca85 gh action, use npm ci 2021-10-30 16:01:09 -07:00
Alexander Rose
a149fa5929 gh action 2021-10-30 15:53:58 -07:00
Alexander Rose
bb3dde585b gh action 2021-10-30 15:46:23 -07:00
Alexander Rose
cd6bbeaa86 gh action 2021-10-30 15:44:06 -07:00
Alexander Rose
e3d24dae4b gh action 2021-10-30 15:43:05 -07:00
Alexander Rose
687a814a62 gh action
- update eslint
- add jest
2021-10-30 15:02:25 -07:00
Alexander Rose
8f2e99dc51 improve shader tests
- made gl package optional
- skip test if gl package not available
2021-10-30 14:52:11 -07:00
Alexander Rose
568be030c3 Merge branch 'master' of https://github.com/molstar/molstar into shader-tests 2021-10-30 14:45:19 -07:00
Alexander Rose
97c3ab8b5a changelog 2021-10-30 14:44:27 -07:00
Alexander Rose
5db646d139 fix marker highlight color overriding select color 2021-10-30 14:43:57 -07:00
Alexander Rose
340f8f1af3 add additional aromatic bond visual params 2021-10-30 14:42:17 -07:00
dsehnal
4484a4452c 2.3.5 2021-10-19 18:22:50 +02:00
dsehnal
65b654a0a2 fix index pair bonds order assignment 2021-10-19 18:20:54 +02:00
dsehnal
a8e0c13b0e fix sequence viewer for PDB files with COMPND record and multichain entities 2021-10-17 13:21:57 +02:00
dsehnal
41d67eb642 2.3.4 2021-10-12 19:05:48 +02:00
dsehnal
c76c8335d1 changelog 2021-10-12 19:03:37 +02:00
dsehnal
42528b7be5 fix argparse config 2021-10-12 19:01:23 +02:00
Alexander Rose
b371f8c11c Merge branch 'master' of https://github.com/molstar/molstar into shader-tests 2021-10-03 15:24:02 -07:00
Alexander Rose
3d651b40f0 use node 14 in lint action 2021-10-03 15:12:58 -07:00
Alexander Rose
895a13fc0d Merge branch 'master' into shader-tests 2021-10-03 14:36:23 -07:00
Alexander Rose
c94acff82e update packages 2021-10-03 11:12:24 -07:00
Alexander Rose
2f9ac711d1 add basic unit tests for graphics shaders
- compile using `gl` package
2021-10-03 10:52:58 -07:00
Alexander Rose
93b9953f6d add missing createEmpty* geo utils
- direct-volume
- image
- texture-mesh
2021-10-03 10:43:56 -07:00
Alexander Rose
dcaf6f8927 add multipleBonds param to bond visuals 2021-10-02 15:44:17 -07:00
Alexander Rose
d96eb404e1 add elements crosses visual 2021-10-02 15:09:13 -07:00
Alexander Rose
07322819f0 Merge pull request #271 from molstar/pick-atom
Picking improvements
2021-10-02 13:41:25 -07:00
Alexander Rose
9be686686d add pickPadding config option 2021-10-02 09:34:56 -07:00
Alexander Rose
3df539c9e1 Merge branch 'master' of https://github.com/molstar/molstar into pick-atom 2021-10-02 09:30:07 -07:00
Alexander Rose
e840059a38 pick improvements
- ensure lines & points are at least 1 pixel big
- look around center pixel in a spiral for hits
2021-09-26 17:20:16 -07:00
Alexander Rose
1bd0339dec add points visual to line repr 2021-09-26 17:13:49 -07:00
Alexander Rose
d0eaf2f71e Merge branch 'master' of https://github.com/molstar/molstar into pick-atom 2021-09-26 13:41:23 -07:00
Alexander Rose
254c9efbf5 improve/fix implicit atom picking 2021-09-26 13:06:56 -07:00
Alexander Rose
73e9aed98c add preferAtoms param to Select/Highlight behaviors 2021-09-26 12:58:29 -07:00
Alexander Rose
6e60d9713a fix bond atoms not added in selection manager 2021-09-26 12:53:53 -07:00
Alexander Rose
ef0593b1e2 add pixel-scale & pick-scale GET params to Viewer 2021-09-26 12:43:57 -07:00
Alexander Rose
7831fa8b33 fix: pickScale not considered in line/point shader 2021-09-26 12:39:44 -07:00
Alexander Rose
d4bb1a6e93 wip: prefer to pick atoms close to ends over bonds 2021-09-19 16:24:45 -07:00
Alexander Rose
b06c134b61 add additional measurement controls
- orientation (box, axes, ellipsoid)
- plane (best fit)
2021-09-18 22:39:40 -07:00
Alexander Rose
3436d03468 add helpers to work with many locis
- StructureElement.Loci.getPrincipalAxesMany
- structureElementLociLabelMany
2021-09-18 22:13:22 -07:00
Alexander Rose
58df6f3b85 formating 2021-09-18 22:12:08 -07:00
Alexander Rose
ffaf008dce limit max display counts in sequence panel
- MaxSelectOptionsCount
- MaxSequenceWrappersCount
- workaround for cellpack models
2021-09-18 16:30:14 -07:00
ludovic autin
eb196a41b5 change variables names to avoid confusion with other types. Added the xrayshading for the compartment geometry 2021-09-14 11:25:15 -07:00
ludovic autin
35baaaf594 support for compartment PLY file. 2021-09-13 11:46:27 -07:00
Alexander Rose
0fc305aaea fix linting issues 2021-09-12 23:32:55 -07:00
Alexander Rose
bf67546a61 Merge branch 'master' of https://github.com/molstar/molstar into pr/corredD/252 2021-09-12 23:30:26 -07:00
Alexander Rose
80d54afdd0 Merge branch 'master' into forkdev 2021-09-12 20:16:00 -07:00
Alexander Rose
2a74dfcf46 factor out membrane sphere handling 2021-09-12 10:26:10 -07:00
Alexander Rose
b0f447adde fix cellpack results file asset handling 2021-09-12 10:17:37 -07:00
ludovic autin
b8628ccff1 Merge branch 'master' of https://github.com/molstar/molstar into forkdev 2021-09-01 09:55:13 -07:00
ludovic autin
4c30057edf I am trying to get the binary asset to get cached. But still can't save a working session. 2021-08-27 16:14:43 -07:00
ludovic autin
53a4826274 Merge branch 'master' of https://github.com/molstar/molstar into forkdev 2021-08-27 14:55:22 -07:00
ludovic autin
393fc99ed2 moved the model loading in model.ts and get the info from the recipe dictionary ( result file and lipids file ) if available. Fix the dates and some const / let variable defintion 2021-08-16 12:35:51 -07:00
ludovic autin
7c5dff1c8b Merge remote-tracking branch 'upstream/master' into forkdev 2021-08-16 11:55:09 -07:00
ludovic autin
ce3f13431d endianess test with IsNativeEndianLittle 2021-08-13 10:58:25 -07:00
ludovic autin
df54766ab2 lower-case icosahedron 2021-08-12 10:48:27 -07:00
ludovic autin
94233fbcd9 eslint error fix 2021-08-11 11:07:34 -07:00
ludovic autin
d044496eaa fix eslint errors 2021-08-11 11:04:08 -07:00
ludovic autin
ca825d720e binary model loading support, latest mycoplasma model. 2021-08-11 10:44:44 -07:00
91 changed files with 6275 additions and 3840 deletions

View File

@@ -1,18 +0,0 @@
on:
push:
pull_request:
jobs:
eslint:
name: eslint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: install node v12
uses: actions/setup-node@v1
with:
node-version: 12
- name: yarn install
run: yarn install
- name: eslint
uses: icrawl/action-eslint@v1

20
.github/workflows/node.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
on:
push:
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 14
- run: npm ci
- run: sudo apt-get install xvfb
- name: Lint
run: npm run lint
- name: Test
run: xvfb-run --auto-servernum npm run jest
- name: Build
run: npm run build

View File

@@ -6,6 +6,40 @@ Note that since we don't clearly distinguish between a public and private interf
## [Unreleased]
- Added ``ViewerOptions.collapseRightPanel``
- Added ``Viewer.loadTrajectory`` to support loading "composed" trajectories (e.g. from gro + xtc)
- Fix: handle parent in Structure.remapModel
- Add ``rounded`` and ``square`` helix profile options to Cartoon representation (in addition to the default ``elliptical``)
## [v2.3.6] - 2021-11-8
- 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
- CellPack extension update
- add binary model support
- add compartment (including membrane) geometry support
- add latest mycoplasma model example
- Prefer WebGL1 in Safari 15.1.
## [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
@@ -15,11 +49,10 @@ Note that since we don't clearly distinguish between a public and private interf
- 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 occupany of 1
- 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``
@@ -137,29 +170,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``.
- Add ``Canvas3D.getRenderObjects``.
- [WIP] Animate state interpolating, including model trajectories
### Changed
- 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
## [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.

View File

@@ -122,9 +122,9 @@ and navigate to `build/viewer`
**Convert any CIF to BinaryCIF**
node lib/servers/model/preprocess -i file.cif -ob file.bcif
node lib/commonjs/servers/model/preprocess -i file.cif -ob file.bcif
To see all available commands, use ``node lib/servers/model/preprocess -h``.
To see all available commands, use ``node lib/commonjs/servers/model/preprocess -h``.
Or

7490
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "molstar",
"version": "2.3.3",
"version": "2.3.7",
"description": "A comprehensive macromolecular library.",
"homepage": "https://github.com/molstar/molstar#readme",
"repository": {
@@ -94,33 +94,35 @@
"@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",
"@types/gl": "^4.1.0",
"@types/jest": "^27.0.2",
"@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 +131,9 @@
"@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/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,10 +147,13 @@
"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"
},
"optionalDependencies": {
"gl": "^4.9.2"
}
}

View File

@@ -36,7 +36,7 @@
emdbProvider: 'rcsb',
});
viewer.loadPdb('7bv2');
viewer.loadEmdb('EMD-30210', { detail: 6 });
viewer.loadEmdb('EMD-30210', { detail: 6 });
// viewer.loadAllModelsOrAssemblyFromUrl('https://cs.litemol.org/5ire/full', 'mmcif', false, { representationParams: { theme: { globalName: 'operator-name' } } })
</script>

View File

@@ -53,6 +53,9 @@
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,
@@ -61,7 +64,10 @@
emdbProvider: emdbProvider || 'pdbe',
volumeStreamingServer: (mapProvider || 'pdbe') === 'rcsb'
? 'https://maps.rcsb.org'
: 'https://www.ebi.ac.uk/pdbe/densities'
: '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();

View File

@@ -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>
@@ -9,25 +9,28 @@ import { ANVILMembraneOrientation } from '../../extensions/anvil/behavior';
import { CellPack } from '../../extensions/cellpack';
import { DnatcoConfalPyramids } from '../../extensions/dnatco';
import { G3DFormat, G3dProvider } from '../../extensions/g3d/format';
import { Mp4Export } from '../../extensions/mp4-export';
import { GeometryExport } from '../../extensions/geo-export';
import { Mp4Export } from '../../extensions/mp4-export';
import { PDBeStructureQualityReport } from '../../extensions/pdbe';
import { RCSBAssemblySymmetry, RCSBValidationReport } from '../../extensions/rcsb';
import { DownloadStructure, PdbDownloadProvider } from '../../mol-plugin-state/actions/structure';
import { DownloadDensity } from '../../mol-plugin-state/actions/volume';
import { PresetTrajectoryHierarchy } from '../../mol-plugin-state/builder/structure/hierarchy-preset';
import { StructureRepresentationPresetProvider } from '../../mol-plugin-state/builder/structure/representation-preset';
import { DataFormatProvider } from '../../mol-plugin-state/formats/provider';
import { BuildInStructureFormat } from '../../mol-plugin-state/formats/structure';
import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
import { BuildInVolumeFormat } from '../../mol-plugin-state/formats/volume';
import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params';
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { StateTransforms } from '../../mol-plugin-state/transforms';
import { TrajectoryFromModelAndCoordinates } from '../../mol-plugin-state/transforms/model';
import { createPlugin } from '../../mol-plugin-ui';
import { PluginUIContext } from '../../mol-plugin-ui/context';
import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
import { DefaultPluginUISpec, PluginUISpec } from '../../mol-plugin-ui/spec';
import { PluginCommands } from '../../mol-plugin/commands';
import { PluginConfig } from '../../mol-plugin/config';
import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
import { PluginSpec } from '../../mol-plugin/spec';
import { PluginState } from '../../mol-plugin/state';
import { StateObjectSelector } from '../../mol-state';
@@ -71,9 +74,12 @@ const DefaultViewerOptions = {
layoutShowLog: true,
layoutShowLeftPanel: true,
collapseLeftPanel: false,
disableAntialiasing: false,
pixelScale: 1,
enableWboit: true,
collapseRightPanel: false,
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,
@@ -112,7 +118,7 @@ export class Viewer {
regionState: {
bottom: 'full',
left: o.collapseLeftPanel ? 'collapsed' : 'full',
right: 'full',
right: o.collapseRightPanel ? 'hidden' : 'full',
top: 'full',
}
},
@@ -130,6 +136,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],
@@ -324,6 +332,56 @@ export class Viewer {
});
}
/**
* @example
* viewer.loadTrajectory({
* model: { kind: 'model-url', url: 'villin.gro', format: 'gro' },
* coordinates: { kind: 'coordinates-url', url: 'villin.xtc', format: 'xtc', isBinary: true },
* preset: 'all-models' // or 'default'
* });
*/
async loadTrajectory(params: LoadTrajectoryParams) {
const plugin = this.plugin;
let model: StateObjectSelector, coords: StateObjectSelector;
if (params.model.kind === 'model-data' || params.model.kind === 'model-url') {
const data = params.model.kind === 'model-data'
? await plugin.builders.data.rawData({ data: params.model.data, label: params.modelLabel })
: await plugin.builders.data.download({ url: params.model.url, isBinary: params.model.isBinary, label: params.modelLabel });
const trajectory = await plugin.builders.structure.parseTrajectory(data, params.model.format ?? 'mmcif');
model = await plugin.builders.structure.createModel(trajectory);
} else {
const data = params.model.kind === 'topology-data'
? await plugin.builders.data.rawData({ data: params.model.data, label: params.modelLabel })
: await plugin.builders.data.download({ url: params.model.url, isBinary: params.model.isBinary, label: params.modelLabel });
const provider = plugin.dataFormats.get(params.model.format);
model = await provider!.parse(plugin, data);
}
{
const data = params.coordinates.kind === 'coordinates-data'
? await plugin.builders.data.rawData({ data: params.coordinates.data, label: params.coordinatesLabel })
: await plugin.builders.data.download({ url: params.coordinates.url, isBinary: params.coordinates.isBinary, label: params.coordinatesLabel });
const provider = plugin.dataFormats.get(params.coordinates.format);
coords = await provider!.parse(plugin, data);
}
const trajectory = await plugin.build().toRoot()
.apply(TrajectoryFromModelAndCoordinates, {
modelRef: model.ref,
coordinatesRef: coords.ref
}, { dependsOn: [model.ref, coords.ref] })
.commit();
const preset = await plugin.builders.structure.hierarchy.applyPreset(trajectory, params.preset ?? 'default');
return { model, coords, preset };
}
handleResize() {
this.plugin.layout.events.updated.next(void 0);
}
@@ -339,4 +397,16 @@ export interface VolumeIsovalueInfo {
color: Color,
alpha?: number,
volumeIndex?: number
}
export interface LoadTrajectoryParams {
model: { kind: 'model-url', url: string, format?: BuiltInTrajectoryFormat /* mmcif */, isBinary?: boolean }
| { kind: 'model-data', data: string | number[] | ArrayBuffer | Uint8Array, format?: BuiltInTrajectoryFormat /* mmcif */ }
| { kind: 'topology-url', url: string, format: BuildInStructureFormat, isBinary?: boolean }
| { kind: 'topology-data', data: string | number[] | ArrayBuffer | Uint8Array, format: BuildInStructureFormat },
modelLabel?: string,
coordinates: { kind: 'coordinates-url', url: string, format: BuildInStructureFormat, isBinary?: boolean }
| { kind: 'coordinates-data', data: string | number[] | ArrayBuffer | Uint8Array, format: BuildInStructureFormat },
coordinatesLabel?: string,
preset?: keyof PresetTrajectoryHierarchy
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2020-2021 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>
*/

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 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>
*/

View File

@@ -1,7 +1,7 @@
/**
* 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 Ludovic Autin <autin@scripps.edu>
* @author Ludovic Autin <ludovic.autin@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/

View File

@@ -13,16 +13,27 @@ export interface CellPack {
export interface CellPacking {
name: string,
location: 'surface' | 'interior' | 'cytoplasme',
location: 'surface' | 'interior' | 'cytoplasme'
ingredients: Packing['ingredients']
compartment?: CellCompartment
}
//
export interface CellCompartment {
filename?: string
geom_type?: 'raw' | 'file' | 'sphere' | 'mb' | 'None'
compartment_primitives?: CompartmentPrimitives
}
export interface Cell {
recipe: Recipe
options?: RecipeOptions
cytoplasme?: Packing
compartments?: { [key: string]: Compartment }
mapping_ids?: { [key: number]: [number, string] }
}
export interface RecipeOptions {
resultfile?: string
}
export interface Recipe {
@@ -35,8 +46,29 @@ export interface Recipe {
export interface Compartment {
surface?: Packing
interior?: Packing
geom?: unknown
geom_type?: 'raw' | 'file' | 'sphere' | 'mb' | 'None'
mb?: CompartmentPrimitives
}
// Primitives discribing a compartment
export const enum CompartmentPrimitiveType {
MetaBall = 0,
Sphere = 1,
Cube = 2,
Cylinder = 3,
Cone = 4,
Plane = 5,
None = 6
}
export interface CompartmentPrimitives{
positions?: number[];
radii?: number[];
types?: CompartmentPrimitiveType[];
}
export interface Packing {
ingredients: { [key: string]: Ingredient }
}
@@ -64,11 +96,13 @@ export interface Ingredient {
[curveX: string]: unknown;
/** the orientation in the membrane */
principalAxis?: Vec3;
principalVector?: Vec3;
/** offset along membrane */
offset?: Vec3;
ingtype?: string;
color?: Vec3;
confidence?: number;
Type?: string;
}
export interface IngredientSource {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 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>
*/

View File

@@ -2,13 +2,14 @@
* Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Ludovic Autin <ludovic.autin@gmail.com>
*/
import { StateAction, StateBuilder, StateTransformer, State } from '../../mol-state';
import { PluginContext } from '../../mol-plugin/context';
import { PluginStateObject as PSO } from '../../mol-plugin-state/objects';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Ingredient, IngredientSource, CellPacking } from './data';
import { Ingredient, CellPacking, CompartmentPrimitives } from './data';
import { getFromPdb, getFromCellPackDB, IngredientFiles, parseCif, parsePDBfile, getStructureMean, getFromOPM } from './util';
import { Model, Structure, StructureSymmetry, StructureSelection, QueryContext, Unit, Trajectory } from '../../mol-model/structure';
import { trajectoryFromMmCIF, MmcifFormat } from '../../mol-model-formats/structure/mmcif';
@@ -17,7 +18,7 @@ import { Mat4, Vec3, Quat } from '../../mol-math/linear-algebra';
import { SymmetryOperator } from '../../mol-math/geometry';
import { Task, RuntimeContext } from '../../mol-task';
import { StateTransforms } from '../../mol-plugin-state/transforms';
import { ParseCellPack, StructureFromCellpack, DefaultCellPackBaseUrl, StructureFromAssemblies } from './state';
import { ParseCellPack, StructureFromCellpack, DefaultCellPackBaseUrl, StructureFromAssemblies, CreateCompartmentSphere } from './state';
import { MolScriptBuilder as MS } from '../../mol-script/language/builder';
import { getMatFromResamplePoints } from './curve';
import { compile } from '../../mol-script/runtime/query/compiler';
@@ -28,8 +29,9 @@ import { createModels } from '../../mol-model-formats/structure/basic/parser';
import { CellpackPackingPreset, CellpackMembranePreset } from './preset';
import { Asset } from '../../mol-util/assets';
import { Color } from '../../mol-util/color';
import { readFromFile } from '../../mol-util/data-source';
import { objectForEach } from '../../mol-util/object';
import { readFromFile } from '../../mol-util/data-source';
import { ColorNames } from '../../mol-util/color/names';
function getCellPackModelUrl(fileName: string, baseUrl: string) {
return `${baseUrl}/results/${fileName}`;
@@ -41,10 +43,14 @@ class TrajectoryCache {
get(id: string) { return this.map.get(id); }
}
async function getModel(plugin: PluginContext, id: string, ingredient: Ingredient, baseUrl: string, trajCache: TrajectoryCache, file?: Asset.File) {
async function getModel(plugin: PluginContext, id: string, ingredient: Ingredient,
baseUrl: string, trajCache: TrajectoryCache, location: string,
file?: Asset.File
) {
const assetManager = plugin.managers.asset;
const modelIndex = (ingredient.source.model) ? parseInt(ingredient.source.model) : 0;
const surface = (ingredient.ingtype) ? (ingredient.ingtype === 'transmembrane') : false;
let surface = (ingredient.ingtype) ? (ingredient.ingtype === 'transmembrane') : false;
if (location === 'surface') surface = true;
let trajectory = trajCache.get(id);
const assets: Asset.Wrapper[] = [];
if (!trajectory) {
@@ -72,6 +78,7 @@ async function getModel(plugin: PluginContext, id: string, ingredient: Ingredien
try {
const data = await getFromOPM(plugin, id, assetManager);
assets.push(data.asset);
data.pdb.id! = id.toUpperCase();
trajectory = await plugin.runTask(trajectoryFromPDB(data.pdb));
} catch (e) {
// fallback to getFromPdb
@@ -100,7 +107,7 @@ async function getModel(plugin: PluginContext, id: string, ingredient: Ingredien
return { model, assets };
}
async function getStructure(plugin: PluginContext, model: Model, source: IngredientSource, props: { assembly?: string } = {}) {
async function getStructure(plugin: PluginContext, model: Model, source: Ingredient, props: { assembly?: string } = {}) {
let structure = Structure.ofModel(model);
const { assembly } = props;
@@ -108,11 +115,12 @@ async function getStructure(plugin: PluginContext, model: Model, source: Ingredi
structure = await plugin.runTask(StructureSymmetry.buildAssembly(structure, assembly));
}
let query;
if (source.selection) {
const asymIds: string[] = source.selection.replace(' ', '').replace(':', '').split('or');
if (source.source.selection) {
const sel = source.source.selection;
// selection can have the model ID as well. remove it
const asymIds: string[] = sel.replace(/ /g, '').replace(/:/g, '').split('or').slice(1);
query = MS.struct.modifier.union([
MS.struct.generator.atomGroups({
'entity-test': MS.core.rel.eq([MS.ammp('entityType'), 'polymer']),
'chain-test': MS.core.set.has([MS.set(...asymIds), MS.ammp('auth_asym_id')])
})
]);
@@ -123,11 +131,11 @@ async function getStructure(plugin: PluginContext, model: Model, source: Ingredi
})
]);
}
const compiled = compile<StructureSelection>(query);
const result = compiled(new QueryContext(structure));
structure = StructureSelection.unionStructure(result);
// change here if possible the label ?
// structure.label = source.name;
return structure;
}
@@ -141,9 +149,9 @@ function getTransformLegacy(trans: Vec3, rot: Quat) {
}
function getTransform(trans: Vec3, rot: Quat) {
const q: Quat = Quat.create(rot[0], rot[1], rot[2], rot[3]);
const q: Quat = Quat.create(-rot[0], rot[1], rot[2], -rot[3]);
const m: Mat4 = Mat4.fromQuat(Mat4.zero(), q);
const p: Vec3 = Vec3.create(trans[0], trans[1], trans[2]);
const p: Vec3 = Vec3.create(-trans[0], trans[1], trans[2]);
Mat4.setTranslation(m, p);
return m;
}
@@ -168,7 +176,7 @@ function getCurveTransforms(ingredient: Ingredient) {
for (let i = 0; i < n; ++i) {
const cname = `curve${i}`;
if (!(cname in ingredient)) {
// console.warn(`Expected '${cname}' in ingredient`)
console.warn(`Expected '${cname}' in ingredient`);
continue;
}
const _points = ingredient[cname] as Vec3[];
@@ -179,7 +187,7 @@ function getCurveTransforms(ingredient: Ingredient) {
// test for resampling
const distance: number = Vec3.distance(_points[0], _points[1]);
if (distance >= segmentLength + 2.0) {
console.info(distance);
// console.info(distance);
resampling = true;
}
const points = new Float32Array(_points.length * 3);
@@ -190,8 +198,8 @@ function getCurveTransforms(ingredient: Ingredient) {
return instances;
}
function getAssembly(transforms: Mat4[], structure: Structure) {
const builder = Structure.Builder();
function getAssembly(name: string, transforms: Mat4[], structure: Structure) {
const builder = Structure.Builder({ label: name });
const { units } = structure;
for (let i = 0, il = transforms.length; i < il; ++i) {
@@ -307,13 +315,13 @@ async function getCurve(plugin: PluginContext, name: string, ingredient: Ingredi
});
const curveModel = await plugin.runTask(curveModelTask);
return getStructure(plugin, curveModel, ingredient.source);
// ingredient.source.selection = undefined;
return getStructure(plugin, curveModel, ingredient);
}
async function getIngredientStructure(plugin: PluginContext, ingredient: Ingredient, baseUrl: string, ingredientFiles: IngredientFiles, trajCache: TrajectoryCache) {
async function getIngredientStructure(plugin: PluginContext, ingredient: Ingredient, baseUrl: string, ingredientFiles: IngredientFiles, trajCache: TrajectoryCache, location: 'surface' | 'interior' | 'cytoplasme') {
const { name, source, results, nbCurve } = ingredient;
if (source.pdb === 'None') return;
const file = ingredientFiles[source.pdb];
if (!file) {
// TODO can these be added to the library?
@@ -325,13 +333,13 @@ async function getIngredientStructure(plugin: PluginContext, ingredient: Ingredi
}
// model id in case structure is NMR
const { model, assets } = await getModel(plugin, source.pdb || name, ingredient, baseUrl, trajCache, file);
const { model, assets } = await getModel(plugin, source.pdb || name, ingredient, baseUrl, trajCache, location, file);
if (!model) return;
let structure: Structure;
if (nbCurve) {
structure = await getCurve(plugin, name, ingredient, getCurveTransforms(ingredient), model);
} else {
if ((!results || results.length === 0)) return;
let bu: string|undefined = source.bu ? source.bu : undefined;
if (bu) {
if (bu === 'AU') {
@@ -340,10 +348,11 @@ async function getIngredientStructure(plugin: PluginContext, ingredient: Ingredi
bu = bu.slice(2);
}
}
structure = await getStructure(plugin, model, source, { assembly: bu });
structure = await getStructure(plugin, model, ingredient, { assembly: bu });
// transform with offset and pcp
let legacy: boolean = true;
if (ingredient.offset || ingredient.principalAxis) {
const pcp = ingredient.principalVector ? ingredient.principalVector : ingredient.principalAxis;
if (pcp) {
legacy = false;
const structureMean = getStructureMean(structure);
Vec3.negate(structureMean, structureMean);
@@ -351,38 +360,44 @@ async function getIngredientStructure(plugin: PluginContext, ingredient: Ingredi
Mat4.setTranslation(m1, structureMean);
structure = Structure.transform(structure, m1);
if (ingredient.offset) {
if (!Vec3.exactEquals(ingredient.offset, Vec3.zero())) {
const o: Vec3 = Vec3.create(ingredient.offset[0], ingredient.offset[1], ingredient.offset[2]);
if (!Vec3.exactEquals(o, Vec3.zero())) { // -1, 1, 4e-16 ??
if (location !== 'surface') {
Vec3.negate(o, o);
}
const m: Mat4 = Mat4.identity();
Mat4.setTranslation(m, ingredient.offset);
Mat4.setTranslation(m, o);
structure = Structure.transform(structure, m);
}
}
if (ingredient.principalAxis) {
if (!Vec3.exactEquals(ingredient.principalAxis, Vec3.unitZ)) {
if (pcp) {
const p: Vec3 = Vec3.create(pcp[0], pcp[1], pcp[2]);
if (!Vec3.exactEquals(p, Vec3.unitZ)) {
const q: Quat = Quat.identity();
Quat.rotationTo(q, ingredient.principalAxis, Vec3.unitZ);
Quat.rotationTo(q, p, Vec3.unitZ);
const m: Mat4 = Mat4.fromQuat(Mat4.zero(), q);
structure = Structure.transform(structure, m);
}
}
}
structure = getAssembly(getResultTransforms(results, legacy), structure);
structure = getAssembly(name, getResultTransforms(results, legacy), structure);
}
return { structure, assets };
}
export function createStructureFromCellPack(plugin: PluginContext, packing: CellPacking, baseUrl: string, ingredientFiles: IngredientFiles) {
return Task.create('Create Packing Structure', async ctx => {
const { ingredients, name } = packing;
const { ingredients, location, name } = packing;
const assets: Asset.Wrapper[] = [];
const trajCache = new TrajectoryCache();
const structures: Structure[] = [];
const colors: Color[] = [];
let skipColors: boolean = false;
for (const iName in ingredients) {
if (ctx.shouldUpdate) await ctx.update(iName);
const ingredientStructure = await getIngredientStructure(plugin, ingredients[iName], baseUrl, ingredientFiles, trajCache);
const ingredientStructure = await getIngredientStructure(plugin, ingredients[iName], baseUrl, ingredientFiles, trajCache, location);
if (ingredientStructure) {
structures.push(ingredientStructure.structure);
assets.push(...ingredientStructure.assets);
@@ -390,7 +405,7 @@ export function createStructureFromCellPack(plugin: PluginContext, packing: Cell
if (c) {
colors.push(Color.fromNormalizedRgb(c[0], c[1], c[2]));
} else {
skipColors = true;
colors.push(Color.fromNormalizedRgb(1, 0, 0));
}
}
}
@@ -414,21 +429,20 @@ export function createStructureFromCellPack(plugin: PluginContext, packing: Cell
}
if (ctx.shouldUpdate) await ctx.update(`${name} - structure`);
const structure = Structure.create(units);
const structure = Structure.create(units, { label: name + '.' + location });
for (let i = 0, il = structure.models.length; i < il; ++i) {
Model.TrajectoryInfo.set(structure.models[i], { size: il, index: i });
}
return { structure, assets, colors: skipColors ? undefined : colors };
return { structure, assets, colors: colors };
});
}
async function handleHivRna(plugin: PluginContext, packings: CellPacking[], baseUrl: string) {
for (let i = 0, il = packings.length; i < il; ++i) {
if (packings[i].name === 'HIV1_capsid_3j3q_PackInner_0_1_0') {
if (packings[i].name === 'HIV1_capsid_3j3q_PackInner_0_1_0' || packings[i].name === 'HIV_capsid') {
const url = Asset.getUrlAsset(plugin.managers.asset, `${baseUrl}/extras/rna_allpoints.json`);
const json = await plugin.runTask(plugin.managers.asset.resolve(url, 'json', false));
const points = json.data.points as number[];
const curve0: Vec3[] = [];
for (let j = 0, jl = points.length; j < jl; j += 3) {
curve0.push(Vec3.fromArray(Vec3(), points, j));
@@ -465,7 +479,8 @@ async function loadMembrane(plugin: PluginContext, name: string, state: State, p
}
}
}
let legacy_membrane: boolean = false; // temporary variable until all membrane are converted to the new correct cif format
let geometry_membrane: boolean = false; // membrane can be a mesh geometry
let b = state.build().toRoot();
if (file) {
if (file.name.endsWith('.cif')) {
@@ -474,27 +489,82 @@ async function loadMembrane(plugin: PluginContext, name: string, state: State, p
b = b.apply(StateTransforms.Data.ReadFile, { file, isBinary: true, label: file.name }, { state: { isGhost: true } });
}
} else {
const url = Asset.getUrlAsset(plugin.managers.asset, `${params.baseUrl}/membranes/${name}.bcif`);
b = b.apply(StateTransforms.Data.Download, { url, isBinary: true, label: name }, { state: { isGhost: true } });
if (name.toLowerCase().endsWith('.bcif')) {
const url = Asset.getUrlAsset(plugin.managers.asset, `${params.baseUrl}/membranes/${name}`);
b = b.apply(StateTransforms.Data.Download, { url, isBinary: true, label: name }, { state: { isGhost: true } });
} else if (name.toLowerCase().endsWith('.cif')) {
const url = Asset.getUrlAsset(plugin.managers.asset, `${params.baseUrl}/membranes/${name}`);
b = b.apply(StateTransforms.Data.Download, { url, isBinary: false, label: name }, { state: { isGhost: true } });
} else if (name.toLowerCase().endsWith('.ply')) {
const url = Asset.getUrlAsset(plugin.managers.asset, `${params.baseUrl}/geometries/${name}`);
b = b.apply(StateTransforms.Data.Download, { url, isBinary: false, label: name }, { state: { isGhost: true } });
geometry_membrane = true;
} else {
const url = Asset.getUrlAsset(plugin.managers.asset, `${params.baseUrl}/membranes/${name}.bcif`);
b = b.apply(StateTransforms.Data.Download, { url, isBinary: true, label: name }, { state: { isGhost: true } });
legacy_membrane = true;
}
}
const membrane = await b.apply(StateTransforms.Data.ParseCif, undefined, { state: { isGhost: true } })
.apply(StateTransforms.Model.TrajectoryFromMmCif, undefined, { state: { isGhost: true } })
.apply(StateTransforms.Model.ModelFromTrajectory, undefined, { state: { isGhost: true } })
.apply(StructureFromAssemblies, undefined, { state: { isGhost: true } })
.commit({ revertOnError: true });
const membraneParams = {
representation: params.preset.representation,
const props = {
type: {
name: 'assembly' as const,
params: { id: '1' }
}
};
if (legacy_membrane) {
// old membrane
const membrane = await b.apply(StateTransforms.Data.ParseCif, undefined, { state: { isGhost: true } })
.apply(StateTransforms.Model.TrajectoryFromMmCif, undefined, { state: { isGhost: true } })
.apply(StateTransforms.Model.ModelFromTrajectory, undefined, { state: { isGhost: true } })
.apply(StructureFromAssemblies, undefined, { state: { isGhost: true } })
.commit({ revertOnError: true });
const membraneParams = {
representation: params.preset.representation,
};
await CellpackMembranePreset.apply(membrane, membraneParams, plugin);
} else if (geometry_membrane) {
await b.apply(StateTransforms.Data.ParsePly, undefined, { state: { isGhost: true } })
.apply(StateTransforms.Model.ShapeFromPly)
.apply(StateTransforms.Representation.ShapeRepresentation3D, { xrayShaded: true,
doubleSided: true, coloring: { name: 'uniform', params: { color: ColorNames.orange } } })
.commit({ revertOnError: true });
} else {
const membrane = await b.apply(StateTransforms.Data.ParseCif, undefined, { state: { isGhost: true } })
.apply(StateTransforms.Model.TrajectoryFromMmCif, undefined, { state: { isGhost: true } })
.apply(StateTransforms.Model.ModelFromTrajectory, undefined, { state: { isGhost: true } })
.apply(StateTransforms.Model.StructureFromModel, props, { state: { isGhost: true } })
.commit({ revertOnError: true });
const membraneParams = {
representation: params.preset.representation,
};
await CellpackMembranePreset.apply(membrane, membraneParams, plugin);
}
}
await CellpackMembranePreset.apply(membrane, membraneParams, plugin);
async function handleMembraneSpheres(state: State, primitives: CompartmentPrimitives) {
const nSpheres = primitives.positions!.length / 3;
// console.log('ok mb ', nSpheres);
// TODO : take in account the type of the primitives.
for (let j = 0; j < nSpheres; j++) {
await state.build()
.toRoot()
.apply(CreateCompartmentSphere, {
center: Vec3.create(
primitives.positions![j * 3 + 0],
primitives.positions![j * 3 + 1],
primitives.positions![j * 3 + 2]
),
radius: primitives!.radii![j]
})
.commit();
}
}
async function loadPackings(plugin: PluginContext, runtime: RuntimeContext, state: State, params: LoadCellPackModelParams) {
const ingredientFiles = params.ingredients || [];
let cellPackJson: StateBuilder.To<PSO.Format.Json, StateTransformer<PSO.Data.String, PSO.Format.Json>>;
let resultsFile: Asset.File | null = params.results;
if (params.source.name === 'id') {
const url = Asset.getUrlAsset(plugin.managers.asset, getCellPackModelUrl(params.source.params, params.baseUrl));
cellPackJson = state.build().toRoot()
@@ -506,29 +576,36 @@ async function loadPackings(plugin: PluginContext, runtime: RuntimeContext, stat
return;
}
let jsonFile: Asset.File;
let modelFile: Asset.File;
if (file.name.toLowerCase().endsWith('.zip')) {
const data = await readFromFile(file.file, 'zip').runInContext(runtime);
jsonFile = Asset.File(new File([data['model.json']], 'model.json'));
if (data['model.json']) {
modelFile = Asset.File(new File([data['model.json']], 'model.json'));
} else {
throw new Error('model.json missing from zip file');
}
if (data['results.bin']) {
resultsFile = Asset.File(new File([data['results.bin']], 'results.bin'));
}
objectForEach(data, (v, k) => {
if (k === 'model.json') return;
if (k === 'results.bin') return;
ingredientFiles.push(Asset.File(new File([v], k)));
});
} else {
jsonFile = file;
modelFile = file;
}
cellPackJson = state.build().toRoot()
.apply(StateTransforms.Data.ReadFile, { file: jsonFile, isBinary: false, label: jsonFile.name }, { state: { isGhost: true } });
.apply(StateTransforms.Data.ReadFile, { file: modelFile, isBinary: false, label: modelFile.name }, { state: { isGhost: true } });
}
const cellPackBuilder = cellPackJson
.apply(StateTransforms.Data.ParseJson, undefined, { state: { isGhost: true } })
.apply(ParseCellPack);
.apply(ParseCellPack, { resultsFile, baseUrl: params.baseUrl });
const cellPackObject = await state.updateTree(cellPackBuilder).runInContext(runtime);
const { packings } = cellPackObject.obj!.data;
const { packings } = cellPackObject.obj!.data;
await handleHivRna(plugin, packings, params.baseUrl);
for (let i = 0, il = packings.length; i < il; ++i) {
@@ -544,8 +621,30 @@ async function loadPackings(plugin: PluginContext, runtime: RuntimeContext, stat
representation: params.preset.representation,
};
await CellpackPackingPreset.apply(packing, packingParams, plugin);
if (packings[i].location === 'surface' && params.membrane) {
await loadMembrane(plugin, packings[i].name, state, params);
if (packings[i].compartment) {
if (params.membrane === 'lipids') {
if (packings[i].compartment!.geom_type) {
if (packings[i].compartment!.geom_type === 'file') {
// TODO: load mesh files or vertex,faces data
await loadMembrane(plugin, packings[i].compartment!.filename!, state, params);
} else if (packings[i].compartment!.compartment_primitives) {
await handleMembraneSpheres(state, packings[i].compartment!.compartment_primitives!);
}
} else {
// try loading membrane from repo as a bcif file or from the given list of files.
if (params.membrane === 'lipids') {
await loadMembrane(plugin, packings[i].name, state, params);
}
}
} else if (params.membrane === 'geometry') {
if (packings[i].compartment!.compartment_primitives) {
await handleMembraneSpheres(state, packings[i].compartment!.compartment_primitives!);
} else if (packings[i].compartment!.geom_type === 'file') {
if (packings[i].compartment!.filename!.toLowerCase().endsWith('.ply')) {
await loadMembrane(plugin, packings[i].compartment!.filename!, state, params);
}
}
}
}
}
}
@@ -555,19 +654,19 @@ const LoadCellPackModelParams = {
'id': PD.Select('InfluenzaModel2.json', [
['blood_hiv_immature_inside.json', 'Blood HIV immature'],
['HIV_immature_model.json', 'HIV immature'],
['BloodHIV1.0_mixed_fixed_nc1.cpr', 'Blood HIV'],
['HIV-1_0.1.6-8_mixed_radii_pdb.cpr', 'HIV'],
['Blood_HIV.json', 'Blood HIV'],
['HIV-1_0.1.6-8_mixed_radii_pdb.json', 'HIV'],
['influenza_model1.json', 'Influenza envelope'],
['InfluenzaModel2.json', 'Influenza Complete'],
['InfluenzaModel2.json', 'Influenza complete'],
['ExosomeModel.json', 'Exosome Model'],
['Mycoplasma1.5_mixed_pdb_fixed.cpr', 'Mycoplasma simple'],
['MycoplasmaModel.json', 'Mycoplasma WholeCell model'],
['MycoplasmaGenitalium.json', 'Mycoplasma Genitalium curated model'],
] as const, { description: 'Download the model definition with `id` from the server at `baseUrl.`' }),
'file': PD.File({ accept: '.json,.cpr,.zip', description: 'Open model definition from .json/.cpr file or open .zip file containing model definition plus ingredients.' }),
'file': PD.File({ accept: '.json,.cpr,.zip', description: 'Open model definition from .json/.cpr file or open .zip file containing model definition plus ingredients.', label: 'Recipe file' }),
}, { options: [['id', 'Id'], ['file', 'File']] }),
baseUrl: PD.Text(DefaultCellPackBaseUrl),
membrane: PD.Boolean(true),
ingredients: PD.FileList({ accept: '.cif,.bcif,.pdb', label: 'Ingredients' }),
results: PD.File({ accept: '.bin', description: 'open results file in binary format from cellpackgpu for the specified recipe', label: 'Results file' }),
membrane: PD.Select('lipids', PD.arrayToOptions(['lipids', 'geometry', 'none'])),
ingredients: PD.FileList({ accept: '.cif,.bcif,.pdb', label: 'Ingredient files' }),
preset: PD.Group({
traceOnly: PD.Boolean(false),
representation: PD.Select('gaussian-surface', PD.arrayToOptions(['spacefill', 'gaussian-surface', 'point', 'orientation']))
@@ -581,4 +680,4 @@ export const LoadCellPackModel = StateAction.build({
from: PSO.Root
})(({ state, params }, ctx: PluginContext) => Task.create('CellPack Loader', async taskCtx => {
await loadPackings(ctx, taskCtx, state, params);
}));
}));

View File

@@ -1,7 +1,8 @@
/**
* Copyright (c) 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>
* @author Ludovic Autin <ludovic.autin@gmail.com>
*/
import { StateObjectRef } from '../../mol-state';
@@ -9,8 +10,6 @@ import { StructureRepresentationPresetProvider, presetStaticComponent } from '..
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { ColorNames } from '../../mol-util/color/names';
import { CellPackGenerateColorThemeProvider } from './color/generate';
import { CellPackInfoProvider } from './property';
import { CellPackProvidedColorThemeProvider } from './color/provided';
export const CellpackPackingPresetParams = {
traceOnly: PD.Boolean(true),
@@ -42,8 +41,8 @@ export const CellpackPackingPreset = StructureRepresentationPresetProvider({
Object.assign(reprProps, { sizeFactor: 2 });
}
const info = structureCell.obj?.data && CellPackInfoProvider.get(structureCell.obj?.data).value;
const color = info?.colors ? CellPackProvidedColorThemeProvider.name : CellPackGenerateColorThemeProvider.name;
// default is generated
const color = CellPackGenerateColorThemeProvider.name;
const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, {});
const representations = {
@@ -92,4 +91,4 @@ export const CellpackMembranePreset = StructureRepresentationPresetProvider({
return { components, representations };
}
});
});

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 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>
*/
@@ -34,4 +34,4 @@ export const CellPackInfoProvider: CustomStructureProperty.Provider<typeof CellP
value: { ...CellPackInfoParams.info.defaultValue, ...props.info }
};
}
});
});

View File

@@ -0,0 +1,70 @@
/**
* Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Ludovic Autin <ludovic.autin@gmail.com>
*/
import { ShapeRepresentation } from '../../mol-repr/shape/representation';
import { Shape } from '../../mol-model/shape';
import { ColorNames } from '../../mol-util/color/names';
import { RuntimeContext } from '../../mol-task';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
// import { Polyhedron, DefaultPolyhedronProps } from '../../mol-geo/primitive/polyhedron';
// import { Icosahedron } from '../../mol-geo/primitive/icosahedron';
import { Sphere } from '../../mol-geo/primitive/sphere';
import { Mat4, Vec3 } from '../../mol-math/linear-algebra';
import { RepresentationParamsGetter, Representation, RepresentationContext } from '../../mol-repr/representation';
interface MembraneSphereData {
radius: number
center: Vec3
}
const MembraneSphereParams = {
...Mesh.Params,
cellColor: PD.Color(ColorNames.orange),
cellScale: PD.Numeric(2, { min: 0.1, max: 5, step: 0.1 }),
radius: PD.Numeric(2, { min: 0.1, max: 5, step: 0.1 }),
center: PD.Vec3(Vec3.create(0, 0, 0)),
quality: { ...Mesh.Params.quality, isEssential: false },
};
type MeshParams = typeof MembraneSphereParams
const MembraneSphereVisuals = {
'mesh': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<MembraneSphereData, MeshParams>) => ShapeRepresentation(getMBShape, Mesh.Utils),
};
export const MBParams = {
...MembraneSphereParams
};
export type MBParams = typeof MBParams
export type UnitcellProps = PD.Values<MBParams>
function getMBMesh(data: MembraneSphereData, props: UnitcellProps, mesh?: Mesh) {
const state = MeshBuilder.createState(256, 128, mesh);
const radius = props.radius;
const asphere = Sphere(3);
const trans: Mat4 = Mat4.identity();
Mat4.fromScaling(trans, Vec3.create(radius, radius, radius));
state.currentGroup = 1;
MeshBuilder.addPrimitive(state, trans, asphere);
const m = MeshBuilder.getMesh(state);
return m;
}
function getMBShape(ctx: RuntimeContext, data: MembraneSphereData, props: UnitcellProps, shape?: Shape<Mesh>) {
const geo = getMBMesh(data, props, shape && shape.geometry);
const label = 'mb';
return Shape.create(label, data, geo, () => props.cellColor, () => 1, () => label);
}
export type MBRepresentation = Representation<MembraneSphereData, MBParams>
export function MBRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<MembraneSphereData, MBParams>): MBRepresentation {
return Representation.createMulti('MB', ctx, getParams, Representation.StateBuilder, MembraneSphereVisuals as unknown as Representation.Def<MembraneSphereData, MBParams>);
}

View File

@@ -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 Alexander Rose <alexander.rose@weirdbyte.de>
* @author Ludovic Autin <ludovic.autin@gmail.com>
*/
import { PluginStateObject as PSO, PluginStateTransform } from '../../mol-plugin-state/objects';
@@ -15,9 +16,13 @@ import { PluginContext } from '../../mol-plugin/context';
import { CellPackInfoProvider } from './property';
import { Structure, StructureSymmetry, Unit, Model } from '../../mol-model/structure';
import { ModelSymmetry } from '../../mol-model-formats/structure/property/symmetry';
import { Vec3, Quat } from '../../mol-math/linear-algebra';
import { StateTransformer } from '../../mol-state';
import { MBRepresentation, MBParams } from './representation';
import { IsNativeEndianLittle, flipByteOrder } from '../../mol-io/common/binary';
import { getFloatValue } from './util';
export const DefaultCellPackBaseUrl = 'https://mesoscope.scripps.edu/data/cellPACK_data/cellPACK_database_1.1.0/';
export const DefaultCellPackBaseUrl = 'https://raw.githubusercontent.com/mesoscope/cellPACK_data/master/cellPACK_database_1.1.0';
export class CellPack extends PSO.Create<_CellPack>({ name: 'CellPack', typeClass: 'Object' }) { }
export { ParseCellPack };
@@ -26,26 +31,173 @@ const ParseCellPack = PluginStateTransform.BuiltIn({
name: 'parse-cellpack',
display: { name: 'Parse CellPack', description: 'Parse CellPack from JSON data' },
from: PSO.Format.Json,
to: CellPack
to: CellPack,
params: a => {
return {
resultsFile: PD.File({ accept: '.bin' }),
baseUrl: PD.Text(DefaultCellPackBaseUrl)
};
}
})({
apply({ a }) {
apply({ a, params, cache }, plugin: PluginContext) {
return Task.create('Parse CellPack', async ctx => {
const cell = a.data as Cell;
let counter_id = 0;
let fiber_counter_id = 0;
let comp_counter = 0;
const packings: CellPacking[] = [];
const { compartments, cytoplasme } = cell;
if (!cell.mapping_ids) cell.mapping_ids = {};
if (cytoplasme) {
packings.push({ name: 'Cytoplasme', location: 'cytoplasme', ingredients: cytoplasme.ingredients });
for (const iName in cytoplasme.ingredients) {
if (cytoplasme.ingredients[iName].ingtype === 'fiber') {
cell.mapping_ids[-(fiber_counter_id + 1)] = [comp_counter, iName];
if (!cytoplasme.ingredients[iName].nbCurve) cytoplasme.ingredients[iName].nbCurve = 0;
fiber_counter_id++;
} else {
cell.mapping_ids[counter_id] = [comp_counter, iName];
if (!cytoplasme.ingredients[iName].results) { cytoplasme.ingredients[iName].results = []; }
counter_id++;
}
}
comp_counter++;
}
if (compartments) {
for (const name in compartments) {
const { surface, interior } = compartments[name];
if (surface) packings.push({ name, location: 'surface', ingredients: surface.ingredients });
if (interior) packings.push({ name, location: 'interior', ingredients: interior.ingredients });
let filename = '';
if (compartments[name].geom_type === 'file') {
filename = (compartments[name].geom) ? compartments[name].geom as string : '';
}
const compartment = { filename: filename, geom_type: compartments[name].geom_type, compartment_primitives: compartments[name].mb };
if (surface) {
packings.push({ name, location: 'surface', ingredients: surface.ingredients, compartment: compartment });
for (const iName in surface.ingredients) {
if (surface.ingredients[iName].ingtype === 'fiber') {
cell.mapping_ids[-(fiber_counter_id + 1)] = [comp_counter, iName];
if (!surface.ingredients[iName].nbCurve) surface.ingredients[iName].nbCurve = 0;
fiber_counter_id++;
} else {
cell.mapping_ids[counter_id] = [comp_counter, iName];
if (!surface.ingredients[iName].results) { surface.ingredients[iName].results = []; }
counter_id++;
}
}
comp_counter++;
}
if (interior) {
if (!surface) packings.push({ name, location: 'interior', ingredients: interior.ingredients, compartment: compartment });
else packings.push({ name, location: 'interior', ingredients: interior.ingredients });
for (const iName in interior.ingredients) {
if (interior.ingredients[iName].ingtype === 'fiber') {
cell.mapping_ids[-(fiber_counter_id + 1)] = [comp_counter, iName];
if (!interior.ingredients[iName].nbCurve) interior.ingredients[iName].nbCurve = 0;
fiber_counter_id++;
} else {
cell.mapping_ids[counter_id] = [comp_counter, iName];
if (!interior.ingredients[iName].results) { interior.ingredients[iName].results = []; }
counter_id++;
}
}
comp_counter++;
}
}
}
if (cytoplasme) packings.push({ name: 'Cytoplasme', location: 'cytoplasme', ingredients: cytoplasme.ingredients });
const { options } = cell;
let resultsAsset: Asset.Wrapper<'binary'> | undefined;
if (params.resultsFile) {
resultsAsset = await plugin.runTask(plugin.managers.asset.resolve(params.resultsFile, 'binary', true));
} else if (options?.resultfile) {
const url = `${params.baseUrl}/results/${options.resultfile}`;
resultsAsset = await plugin.runTask(plugin.managers.asset.resolve(Asset.getUrlAsset(plugin.managers.asset, url), 'binary', true));
}
if (resultsAsset) {
(cache as any).asset = resultsAsset;
const results = resultsAsset.data;
// flip the byte order if needed
const buffer = IsNativeEndianLittle ? results.buffer : flipByteOrder(results, 4);
const numbers = new DataView(buffer);
const ninst = getFloatValue(numbers, 0);
const npoints = getFloatValue(numbers, 4);
const ncurve = getFloatValue(numbers, 8);
let offset = 12;
if (ninst !== 0) {
const pos = new Float32Array(buffer, offset, ninst * 4);
offset += ninst * 4 * 4;
const quat = new Float32Array(buffer, offset, ninst * 4);
offset += ninst * 4 * 4;
for (let i = 0; i < ninst; i++) {
const x: number = pos[i * 4 + 0];
const y: number = pos[i * 4 + 1];
const z: number = pos[i * 4 + 2];
const ingr_id = pos[i * 4 + 3] as number;
const pid = cell.mapping_ids![ingr_id];
if (!packings[pid[0]].ingredients[pid[1]].results) {
packings[pid[0]].ingredients[pid[1]].results = [];
}
packings[pid[0]].ingredients[pid[1]].results.push([Vec3.create(x, y, z),
Quat.create(quat[i * 4 + 0], quat[i * 4 + 1], quat[i * 4 + 2], quat[i * 4 + 3])]);
}
}
if (npoints !== 0) {
const ctr_pos = new Float32Array(buffer, offset, npoints * 4);
offset += npoints * 4 * 4;
offset += npoints * 4 * 4;
const ctr_info = new Float32Array(buffer, offset, npoints * 4);
offset += npoints * 4 * 4;
const curve_ids = new Float32Array(buffer, offset, ncurve * 4);
offset += ncurve * 4 * 4;
let counter = 0;
let ctr_points: Vec3[] = [];
let prev_ctype = 0;
let prev_cid = 0;
for (let i = 0; i < npoints; i++) {
const x: number = -ctr_pos[i * 4 + 0];
const y: number = ctr_pos[i * 4 + 1];
const z: number = ctr_pos[i * 4 + 2];
const cid: number = ctr_info[i * 4 + 0]; // curve id
const ctype: number = curve_ids[cid * 4 + 0]; // curve type
// cid 148 165 -1 0
// console.log("cid ",cid,ctype,prev_cid,prev_ctype);//165,148
if (prev_ctype !== ctype) {
const pid = cell.mapping_ids![-prev_ctype - 1];
const cname = `curve${counter}`;
packings[pid[0]].ingredients[pid[1]].nbCurve = counter + 1;
packings[pid[0]].ingredients[pid[1]][cname] = ctr_points;
ctr_points = [];
counter = 0;
} else if (prev_cid !== cid) {
ctr_points = [];
const pid = cell.mapping_ids![-prev_ctype - 1];
const cname = `curve${counter}`;
packings[pid[0]].ingredients[pid[1]][cname] = ctr_points;
counter += 1;
}
ctr_points.push(Vec3.create(x, y, z));
prev_ctype = ctype;
prev_cid = cid;
}
// do the last one
const pid = cell.mapping_ids![-prev_ctype - 1];
const cname = `curve${counter}`;
packings[pid[0]].ingredients[pid[1]].nbCurve = counter + 1;
packings[pid[0]].ingredients[pid[1]][cname] = ctr_points;
}
}
return new CellPack({ cell, packings });
});
}
},
dispose({ cache }) {
((cache as any)?.asset as Asset.Wrapper | undefined)?.dispose();
},
});
export { StructureFromCellpack };
@@ -77,9 +229,8 @@ const StructureFromCellpack = PluginStateTransform.BuiltIn({
await CellPackInfoProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset }, structure, {
info: { packingsCount: a.data.packings.length, packingIndex: params.packing, colors }
});
(cache as any).assets = assets;
return new PSO.Molecule.Structure(structure, { label: packing.name });
return new PSO.Molecule.Structure(structure, { label: packing.name + '.' + packing.location });
});
},
dispose({ b, cache }) {
@@ -125,7 +276,7 @@ const StructureFromAssemblies = PluginStateTransform.BuiltIn({
const s = await StructureSymmetry.buildAssembly(initial_structure, a.id).runInContext(ctx);
structures.push(s);
}
const builder = Structure.Builder();
const builder = Structure.Builder({ label: 'Membrane' });
let offsetInvariantId = 0;
for (const s of structures) {
let maxInvariantId = 0;
@@ -148,3 +299,28 @@ const StructureFromAssemblies = PluginStateTransform.BuiltIn({
b?.data.customPropertyDescriptors.dispose();
}
});
const CreateTransformer = StateTransformer.builderFactory('cellPACK');
export const CreateCompartmentSphere = CreateTransformer({
name: 'create-compartment-sphere',
display: 'CompartmentSphere',
from: PSO.Root, // or whatever data source
to: PSO.Shape.Representation3D,
params: {
center: PD.Vec3(Vec3()),
radius: PD.Numeric(1),
label: PD.Text(`Compartment Sphere`)
}
})({
canAutoUpdate({ oldParams, newParams }) {
return true;
},
apply({ a, params }, plugin: PluginContext) {
return Task.create('Compartment Sphere', async ctx => {
const data = params;
const repr = MBRepresentation({ webgl: plugin.canvas3d?.webgl, ...plugin.representation.structure.themes }, () => (MBParams));
await repr.createOrUpdate({ ...params, quality: 'custom', xrayShaded: true, doubleSided: true }, data).runInContext(ctx);
return new PSO.Shape.Representation3D({ repr, sourceData: a }, { label: data.label });
});
}
});

View File

@@ -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 Alexander Rose <alexander.rose@weirdbyte.de>
* @author Ludovic Autin <ludovic.autin@gmail.com>
*/
import { CIF } from '../../mol-io/reader/cif';
@@ -37,7 +38,7 @@ async function downloadPDB(plugin: PluginContext, url: string, id: string, asset
}
export async function getFromPdb(plugin: PluginContext, pdbId: string, assetManager: AssetManager) {
const { cif, asset } = await downloadCif(plugin, `https://models.rcsb.org/${pdbId.toUpperCase()}.bcif`, true, assetManager);
const { cif, asset } = await downloadCif(plugin, `https://models.rcsb.org/${pdbId}.bcif`, true, assetManager);
return { mmcif: cif.blocks[0], asset };
}
@@ -74,4 +75,35 @@ export function getStructureMean(structure: Structure) {
}
const { elementCount } = structure;
return Vec3.create(xSum / elementCount, ySum / elementCount, zSum / elementCount);
}
export function getFloatValue(value: DataView, offset: number) {
// if the last byte is a negative value (MSB is 1), the final
// float should be too
const negative = value.getInt8(offset + 2) >>> 31;
// this is how the bytes are arranged in the byte array/DataView
// buffer
const [b0, b1, b2, exponent] = [
// get first three bytes as unsigned since we only care
// about the last 8 bits of 32-bit js number returned by
// getUint8().
// Should be the same as: getInt8(offset) & -1 >>> 24
value.getUint8(offset),
value.getUint8(offset + 1),
value.getUint8(offset + 2),
// get the last byte, which is the exponent, as a signed int
// since it's already correct
value.getInt8(offset + 3)
];
let mantissa = b0 | (b1 << 8) | (b2 << 16);
if (negative) {
// need to set the most significant 8 bits to 1's since a js
// number is 32 bits but our mantissa is only 24.
mantissa |= 255 << 24;
}
return mantissa * Math.pow(10, exponent);
}

View File

@@ -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,

View File

@@ -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);
}

View File

@@ -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,6 +116,8 @@ namespace Canvas3DContext {
preserveDrawingBuffer: true,
pixelScale: 1,
pickScale: 0.25,
/** extra pixels to around target to check in case target is empty */
pickPadding: 1,
enableWboit: true,
preferWebGl1: false
};
@@ -307,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;
@@ -644,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 },
};
@@ -780,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);

View File

@@ -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');

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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']) {

View File

@@ -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 = {

View File

@@ -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

View File

@@ -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>
* @author David Sehnal <david.sehnal@gmail.com>
@@ -40,7 +40,7 @@ const v3set = Vec3.set;
const caAdd3 = ChunkedArray.add3;
const caAdd = ChunkedArray.add;
function addCap(offset: number, state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, width: number, leftHeight: number, rightHeight: number) {
function addCap(offset: number, state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, width: number, leftHeight: number, rightHeight: number, flip: boolean) {
const { vertices, normals, indices } = state;
const vertexCount = vertices.elementCount;
@@ -74,11 +74,19 @@ function addCap(offset: number, state: MeshBuilder.State, controlPoints: ArrayLi
v3copy(verticalVector, verticalLeftVector);
}
for (let i = 0; i < 4; ++i) {
caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
if (flip) {
for (let i = 0; i < 4; ++i) {
caAdd3(normals, -normalVector[0], -normalVector[1], -normalVector[2]);
}
caAdd3(indices, vertexCount, vertexCount + 1, vertexCount + 2);
caAdd3(indices, vertexCount + 2, vertexCount + 3, vertexCount);
} else {
for (let i = 0; i < 4; ++i) {
caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
}
caAdd3(indices, vertexCount + 2, vertexCount + 1, vertexCount);
caAdd3(indices, vertexCount, vertexCount + 3, vertexCount + 2);
}
caAdd3(indices, vertexCount + 2, vertexCount + 1, vertexCount);
caAdd3(indices, vertexCount, vertexCount + 3, vertexCount + 2);
}
/** set arrowHeight = 0 for no arrow */
@@ -193,19 +201,18 @@ export function addSheet(state: MeshBuilder.State, controlPoints: ArrayLike<numb
const width = widthValues[0];
const height = heightValues[0];
const h = arrowHeight === 0 ? height : arrowHeight;
addCap(0, state, controlPoints, normalVectors, binormalVectors, width, h, h);
addCap(0, state, controlPoints, normalVectors, binormalVectors, width, h, h, false);
} else if (arrowHeight > 0) {
const width = widthValues[0];
const height = heightValues[0];
addCap(0, state, controlPoints, normalVectors, binormalVectors, width, arrowHeight, -height);
addCap(0, state, controlPoints, normalVectors, binormalVectors, width, -arrowHeight, height);
addCap(0, state, controlPoints, normalVectors, binormalVectors, width, arrowHeight, -height, false);
addCap(0, state, controlPoints, normalVectors, binormalVectors, width, -arrowHeight, height, false);
}
if (endCap && arrowHeight === 0) {
const width = widthValues[linearSegments];
const height = heightValues[linearSegments];
// use negative height to flip the direction the cap's triangles are facing
addCap(linearSegments * 3, state, controlPoints, normalVectors, binormalVectors, width, -height, -height);
addCap(linearSegments * 3, state, controlPoints, normalVectors, binormalVectors, width, height, height, true);
}
const addedVertexCount = (linearSegments + 1) * 8 +

View File

@@ -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>
* @author David Sehnal <david.sehnal@gmail.com>
@@ -30,9 +30,10 @@ function add3AndScale2(out: Vec3, a: Vec3, b: Vec3, c: Vec3, sa: number, sb: num
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
const v3fromArray = Vec3.fromArray;
const v3normalize = Vec3.normalize;
const v3negate = Vec3.negate;
const v3copy = Vec3.copy;
const v3scaleAndAdd = Vec3.scaleAndAdd;
const v3cross = Vec3.cross;
const v3dot = Vec3.dot;
const v3unitX = Vec3.unitX;
const caAdd3 = ChunkedArray.add3;
const CosSinCache = new Map<number, { cos: number[], sin: number[] }>();
@@ -50,13 +51,16 @@ function getCosSin(radialSegments: number) {
return CosSinCache.get(radialSegments)!;
}
export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, radialSegments: number, widthValues: ArrayLike<number>, heightValues: ArrayLike<number>, startCap: boolean, endCap: boolean) {
export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, radialSegments: number, widthValues: ArrayLike<number>, heightValues: ArrayLike<number>, startCap: boolean, endCap: boolean, crossSection: 'elliptical' | 'rounded') {
const { currentGroup, vertices, normals, indices, groups } = state;
let vertexCount = vertices.elementCount;
const { cos, sin } = getCosSin(radialSegments);
const q1 = radialSegments / 4;
const q3 = q1 * 3;
for (let i = 0; i <= linearSegments; ++i) {
const i3 = i * 3;
v3fromArray(u, normalVectors, i3);
@@ -65,14 +69,18 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
const width = widthValues[i];
const height = heightValues[i];
const rounded = crossSection === 'rounded' && height > width;
for (let j = 0; j < radialSegments; ++j) {
add3AndScale2(surfacePoint, u, v, controlPoint, height * cos[j], width * sin[j]);
if (radialSegments === 2) {
v3copy(normalVector, v);
v3normalize(normalVector, normalVector);
if (j !== 0 || i % 2 === 0) v3negate(normalVector, normalVector);
if (rounded) {
add3AndScale2(surfacePoint, u, v, controlPoint, width * cos[j], width * sin[j]);
const h = v3dot(v, v3unitX) < 0
? (j < q1 || j >= q3) ? height - width : -height + width
: (j >= q1 && j < q3) ? -height + width : height - width;
v3scaleAndAdd(surfacePoint, surfacePoint, u, h);
add2AndScale2(normalVector, u, v, cos[j], sin[j]);
} else {
add3AndScale2(surfacePoint, u, v, controlPoint, height * cos[j], width * sin[j]);
add2AndScale2(normalVector, u, v, width * cos[j], height * sin[j]);
}
v3normalize(normalVector, normalVector);
@@ -82,19 +90,37 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
}
}
const radialSegmentsHalf = Math.round(radialSegments / 2);
for (let i = 0; i < linearSegments; ++i) {
for (let j = 0; j < radialSegments; ++j) {
// the triangles are arranged such that opposing triangles of the sheet align
// which prevents triangle intersection within tight curves
for (let j = 0; j < radialSegmentsHalf; ++j) {
caAdd3(
indices,
vertexCount + i * radialSegments + (j + 1) % radialSegments,
vertexCount + (i + 1) * radialSegments + (j + 1) % radialSegments,
vertexCount + i * radialSegments + j
vertexCount + i * radialSegments + (j + 1) % radialSegments, // a
vertexCount + (i + 1) * radialSegments + (j + 1) % radialSegments, // c
vertexCount + i * radialSegments + j // b
);
caAdd3(
indices,
vertexCount + (i + 1) * radialSegments + (j + 1) % radialSegments,
vertexCount + (i + 1) * radialSegments + j,
vertexCount + i * radialSegments + j
vertexCount + (i + 1) * radialSegments + (j + 1) % radialSegments, // c
vertexCount + (i + 1) * radialSegments + j, // d
vertexCount + i * radialSegments + j // b
);
}
for (let j = radialSegmentsHalf; j < radialSegments; ++j) {
caAdd3(
indices,
vertexCount + i * radialSegments + (j + 1) % radialSegments, // a
vertexCount + (i + 1) * radialSegments + j, // d
vertexCount + i * radialSegments + j // b
);
caAdd3(
indices,
vertexCount + (i + 1) * radialSegments + (j + 1) % radialSegments, // c
vertexCount + (i + 1) * radialSegments + j, // d
vertexCount + i * radialSegments + (j + 1) % radialSegments, // a
);
}
}
@@ -111,11 +137,18 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
const width = widthValues[0];
const height = heightValues[0];
let height = heightValues[0];
const rounded = crossSection === 'rounded' && height > width;
if (rounded) height -= width;
vertexCount = vertices.elementCount;
for (let i = 0; i < radialSegments; ++i) {
add3AndScale2(surfacePoint, u, v, controlPoint, height * cos[i], width * sin[i]);
if (rounded) {
add3AndScale2(surfacePoint, u, v, controlPoint, width * cos[i], width * sin[i]);
v3scaleAndAdd(surfacePoint, surfacePoint, u, (i < q1 || i >= q3) ? height : -height);
} else {
add3AndScale2(surfacePoint, u, v, controlPoint, height * cos[i], width * sin[i]);
}
caAdd3(vertices, surfacePoint[0], surfacePoint[1], surfacePoint[2]);
caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
@@ -141,11 +174,18 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
const width = widthValues[linearSegments];
const height = heightValues[linearSegments];
let height = heightValues[linearSegments];
const rounded = crossSection === 'rounded' && height > width;
if (rounded) height -= width;
vertexCount = vertices.elementCount;
for (let i = 0; i < radialSegments; ++i) {
add3AndScale2(surfacePoint, u, v, controlPoint, height * cos[i], width * sin[i]);
if (rounded) {
add3AndScale2(surfacePoint, u, v, controlPoint, width * cos[i], width * sin[i]);
v3scaleAndAdd(surfacePoint, surfacePoint, u, (i < q1 || i >= q3) ? height : -height);
} else {
add3AndScale2(surfacePoint, u, v, controlPoint, height * cos[i], width * sin[i]);
}
caAdd3(vertices, surfacePoint[0], surfacePoint[1], surfacePoint[2]);
caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);

View File

@@ -127,7 +127,7 @@ export namespace Points {
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),
pointStyle: PD.Select('square', PD.objectToOptions(StyleTypes)),
};

View File

@@ -245,7 +245,7 @@ export namespace Text {
...BaseGeometry.createValues(props, counts),
uSizeFactor: ValueCell.create(props.sizeFactor),
uBorderWidth: ValueCell.create(clamp(props.borderWidth / 2, 0, 0.5)),
uBorderWidth: ValueCell.create(clamp(props.borderWidth, 0, 0.5)),
uBorderColor: ValueCell.create(Color.toArrayNormalized(props.borderColor, Vec3.zero(), 0)),
uOffsetX: ValueCell.create(props.offsetX),
uOffsetY: ValueCell.create(props.offsetY),

View File

@@ -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 = {

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { createRenderObject } from '../render-object';
import { Scene } from '../scene';
import { getGLContext, tryGetGLContext } from './gl';
import { setDebugMode } from '../../mol-util/debug';
import { ColorNames } from '../../mol-util/color/names';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Cylinders } from '../../mol-geo/geometry/cylinders/cylinders';
export function createCylinders() {
const cylinders = Cylinders.createEmpty();
const props = PD.getDefaultValues(Cylinders.Params);
const values = Cylinders.Utils.createValuesSimple(cylinders, props, ColorNames.orange, 1);
const state = Cylinders.Utils.createRenderableState(props);
return createRenderObject('cylinders', values, state, -1);
}
describe('cylinders', () => {
const ctx = tryGetGLContext(32, 32, { fragDepth: true });
(ctx ? it : it.skip)('basic', async () => {
const ctx = getGLContext(32, 32);
const scene = Scene.create(ctx);
const cylinders = createCylinders();
scene.add(cylinders);
setDebugMode(true);
expect(() => scene.commit()).not.toThrow();
setDebugMode(false);
ctx.destroy();
});
});

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { createRenderObject } from '../render-object';
import { Scene } from '../scene';
import { getGLContext, tryGetGLContext } from './gl';
import { setDebugMode } from '../../mol-util/debug';
import { ColorNames } from '../../mol-util/color/names';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { DirectVolume } from '../../mol-geo/geometry/direct-volume/direct-volume';
export function createDirectVolume() {
const directVolume = DirectVolume.createEmpty();
const props = PD.getDefaultValues(DirectVolume.Params);
const values = DirectVolume.Utils.createValuesSimple(directVolume, props, ColorNames.orange, 1);
const state = DirectVolume.Utils.createRenderableState(props);
return createRenderObject('direct-volume', values, state, -1);
}
describe('direct-volume', () => {
const ctx = tryGetGLContext(32, 32);
(ctx ? it : it.skip)('basic', async () => {
const ctx = getGLContext(32, 32);
const scene = Scene.create(ctx);
const directVolume = createDirectVolume();
scene.add(directVolume);
setDebugMode(true);
expect(() => scene.commit()).not.toThrow();
setDebugMode(false);
ctx.destroy();
});
});

View File

@@ -760,8 +760,8 @@ export function createGl(width: number, height: number, contextAttributes: WebGL
validateProgram: function () { },
generateMipmap: function () { },
isContextLost: function () { return false; },
drawingBufferWidth: 1024,
drawingBufferHeight: 1024,
drawingBufferWidth: width,
drawingBufferHeight: height,
blendColor: function () { },
blendEquation: function () { },
blendEquationSeparate: function () { },

28
src/mol-gl/_spec/gl.ts Normal file
View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { createContext } from '../webgl/context';
export function getGLContext(width: number, height: number) {
const gl = require('gl')(width, height, {
alpha: true,
depth: true,
premultipliedAlpha: true,
preserveDrawingBuffer: true,
antialias: true,
});
return createContext(gl);
}
export function tryGetGLContext(width: number, height: number, requiredExtensions?: { fragDepth?: boolean }) {
try {
const ctx = getGLContext(width, height);
if (requiredExtensions?.fragDepth && !ctx.extensions.fragDepth) return;
return ctx;
} catch (e) {
return;
}
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { createRenderObject } from '../render-object';
import { Scene } from '../scene';
import { getGLContext, tryGetGLContext } from './gl';
import { setDebugMode } from '../../mol-util/debug';
import { ColorNames } from '../../mol-util/color/names';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Image } from '../../mol-geo/geometry/image/image';
export function createImage() {
const image = Image.createEmpty();
const props = PD.getDefaultValues(Image.Params);
const values = Image.Utils.createValuesSimple(image, props, ColorNames.orange, 1);
const state = Image.Utils.createRenderableState(props);
return createRenderObject('image', values, state, -1);
}
describe('image', () => {
const ctx = tryGetGLContext(32, 32);
(ctx ? it : it.skip)('basic', async () => {
const ctx = getGLContext(32, 32);
const scene = Scene.create(ctx);
const image = createImage();
scene.add(image);
setDebugMode(true);
expect(() => scene.commit()).not.toThrow();
setDebugMode(false);
ctx.destroy();
});
});

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { createRenderObject } from '../render-object';
import { Scene } from '../scene';
import { getGLContext, tryGetGLContext } from './gl';
import { setDebugMode } from '../../mol-util/debug';
import { ColorNames } from '../../mol-util/color/names';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Lines } from '../../mol-geo/geometry/lines/lines';
export function createLines() {
const lines = Lines.createEmpty();
const props = PD.getDefaultValues(Lines.Params);
const values = Lines.Utils.createValuesSimple(lines, props, ColorNames.orange, 1);
const state = Lines.Utils.createRenderableState(props);
return createRenderObject('lines', values, state, -1);
}
describe('lines', () => {
const ctx = tryGetGLContext(32, 32);
(ctx ? it : it.skip)('basic', async () => {
const ctx = getGLContext(32, 32);
const scene = Scene.create(ctx);
const lines = createLines();
scene.add(lines);
setDebugMode(true);
expect(() => scene.commit()).not.toThrow();
setDebugMode(false);
ctx.destroy();
});
});

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { createRenderObject } from '../render-object';
import { Scene } from '../scene';
import { getGLContext, tryGetGLContext } from './gl';
import { setDebugMode } from '../../mol-util/debug';
import { ColorNames } from '../../mol-util/color/names';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
export function createMesh() {
const mesh = Mesh.createEmpty();
const props = PD.getDefaultValues(Mesh.Params);
const values = Mesh.Utils.createValuesSimple(mesh, props, ColorNames.orange, 1);
const state = Mesh.Utils.createRenderableState(props);
return createRenderObject('mesh', values, state, -1);
}
describe('mesh', () => {
const ctx = tryGetGLContext(32, 32);
(ctx ? it : it.skip)('basic', async () => {
const ctx = getGLContext(32, 32);
const scene = Scene.create(ctx);
const mesh = createMesh();
scene.add(mesh);
setDebugMode(true);
expect(() => scene.commit()).not.toThrow();
setDebugMode(false);
ctx.destroy();
});
});

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { createRenderObject } from '../render-object';
import { Scene } from '../scene';
import { getGLContext, tryGetGLContext } from './gl';
import { setDebugMode } from '../../mol-util/debug';
import { ColorNames } from '../../mol-util/color/names';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Points } from '../../mol-geo/geometry/points/points';
export function createPoints() {
const points = Points.createEmpty();
const props = PD.getDefaultValues(Points.Params);
const values = Points.Utils.createValuesSimple(points, props, ColorNames.orange, 1);
const state = Points.Utils.createRenderableState(props);
return createRenderObject('points', values, state, -1);
}
describe('points', () => {
const ctx = tryGetGLContext(32, 32);
(ctx ? it : it.skip)('basic', async () => {
const ctx = getGLContext(32, 32);
const scene = Scene.create(ctx);
const points = createPoints();
scene.add(points);
setDebugMode(true);
expect(() => scene.commit()).not.toThrow();
setDebugMode(false);
ctx.destroy();
});
});

View File

@@ -5,28 +5,14 @@
*/
import { createGl } from './gl.shim';
import { Camera } from '../../mol-canvas3d/camera';
import { Vec3, Mat4, Vec4 } from '../../mol-math/linear-algebra';
import { ValueCell } from '../../mol-util';
import { Vec3 } from '../../mol-math/linear-algebra';
import { Renderer } from '../renderer';
import { createValueColor } from '../../mol-geo/geometry/color-data';
import { createValueSize } from '../../mol-geo/geometry/size-data';
import { createContext } from '../webgl/context';
import { RenderableState } from '../renderable';
import { createRenderObject } from '../render-object';
import { PointsValues } from '../renderable/points';
import { Scene } from '../scene';
import { createEmptyMarkers } from '../../mol-geo/geometry/marker-data';
import { fillSerial } from '../../mol-util/array';
import { Color } from '../../mol-util/color';
import { Sphere3D } from '../../mol-math/geometry';
import { createEmptyOverpaint } from '../../mol-geo/geometry/overpaint-data';
import { createEmptyTransparency } from '../../mol-geo/geometry/transparency-data';
import { createEmptyClipping } from '../../mol-geo/geometry/clipping-data';
import { createPoints } from './points.spec';
function createRenderer(gl: WebGLRenderingContext) {
export function createRenderer(gl: WebGLRenderingContext) {
const ctx = createContext(gl);
const camera = new Camera({
position: Vec3.create(0, 0, 50)
@@ -35,80 +21,14 @@ function createRenderer(gl: WebGLRenderingContext) {
return { ctx, camera, renderer };
}
function createPoints() {
const aPosition = ValueCell.create(new Float32Array([0, -1, 0, -1, 0, 0, 1, 1, 0]));
const aGroup = ValueCell.create(fillSerial(new Float32Array(3)));
const aInstance = ValueCell.create(fillSerial(new Float32Array(1)));
const color = createValueColor(Color(0xFF0000));
const size = createValueSize(1);
const marker = createEmptyMarkers();
const overpaint = createEmptyOverpaint();
const transparency = createEmptyTransparency();
const clipping = createEmptyClipping();
const aTransform = ValueCell.create(new Float32Array(16));
const m4 = Mat4.identity();
Mat4.toArray(m4, aTransform.ref.value, 0);
const transform = ValueCell.create(new Float32Array(aTransform.ref.value));
const extraTransform = ValueCell.create(new Float32Array(aTransform.ref.value));
const boundingSphere = ValueCell.create(Sphere3D.create(Vec3.zero(), 2));
const invariantBoundingSphere = ValueCell.create(Sphere3D.create(Vec3.zero(), 2));
const values: PointsValues = {
aPosition,
aGroup,
aTransform,
aInstance,
...color,
...marker,
...size,
...overpaint,
...transparency,
...clipping,
uAlpha: ValueCell.create(1.0),
uVertexCount: ValueCell.create(3),
uInstanceCount: ValueCell.create(1),
uGroupCount: ValueCell.create(3),
uInvariantBoundingSphere: ValueCell.create(Vec4.ofSphere(invariantBoundingSphere.ref.value)),
alpha: ValueCell.create(1.0),
drawCount: ValueCell.create(3),
instanceCount: ValueCell.create(1),
matrix: ValueCell.create(m4),
transform,
extraTransform,
hasReflection: ValueCell.create(false),
boundingSphere,
invariantBoundingSphere,
uSizeFactor: ValueCell.create(1),
dPointSizeAttenuation: ValueCell.create(true),
dPointStyle: ValueCell.create('square'),
};
const state: RenderableState = {
disposed: false,
visible: true,
alphaFactor: 1,
pickable: true,
colorOnly: false,
opaque: true,
writeDepth: true,
noClip: false,
};
return createRenderObject('points', values, state, -1);
}
describe('renderer', () => {
it('basic', () => {
const [width, height] = [32, 32];
const gl = createGl(width, height, { preserveDrawingBuffer: true });
const { ctx, renderer } = createRenderer(gl);
expect(ctx.gl.canvas.width).toBe(32);
expect(ctx.gl.canvas.height).toBe(32);
expect(ctx.gl.drawingBufferWidth).toBe(32);
expect(ctx.gl.drawingBufferHeight).toBe(32);
expect(ctx.stats.resourceCounts.attribute).toBe(0);
expect(ctx.stats.resourceCounts.texture).toBe(0);

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { createRenderObject } from '../render-object';
import { Scene } from '../scene';
import { getGLContext, tryGetGLContext } from './gl';
import { setDebugMode } from '../../mol-util/debug';
import { ColorNames } from '../../mol-util/color/names';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Spheres } from '../../mol-geo/geometry/spheres/spheres';
export function createSpheres() {
const spheres = Spheres.createEmpty();
const props = PD.getDefaultValues(Spheres.Params);
const values = Spheres.Utils.createValuesSimple(spheres, props, ColorNames.orange, 1);
const state = Spheres.Utils.createRenderableState(props);
return createRenderObject('spheres', values, state, -1);
}
describe('spheres', () => {
const ctx = tryGetGLContext(32, 32, { fragDepth: true });
(ctx ? it : it.skip)('basic', async () => {
const ctx = getGLContext(32, 32);
const scene = Scene.create(ctx);
const spheres = createSpheres();
scene.add(spheres);
setDebugMode(true);
expect(() => scene.commit()).not.toThrow();
setDebugMode(false);
ctx.destroy();
});
});

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { createRenderObject } from '../render-object';
import { Scene } from '../scene';
import { getGLContext, tryGetGLContext } from './gl';
import { setDebugMode } from '../../mol-util/debug';
import { ColorNames } from '../../mol-util/color/names';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Text } from '../../mol-geo/geometry/text/text';
export function createText() {
const text = Text.createEmpty();
const props = PD.getDefaultValues(Text.Params);
const values = Text.Utils.createValuesSimple(text, props, ColorNames.orange, 1);
const state = Text.Utils.createRenderableState(props);
return createRenderObject('text', values, state, -1);
}
describe('text', () => {
const ctx = tryGetGLContext(32, 32);
(ctx ? it : it.skip)('basic', async () => {
const ctx = getGLContext(32, 32);
const scene = Scene.create(ctx);
const text = createText();
scene.add(text);
setDebugMode(true);
expect(() => scene.commit()).not.toThrow();
setDebugMode(false);
ctx.destroy();
});
});

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { createRenderObject } from '../render-object';
import { Scene } from '../scene';
import { getGLContext, tryGetGLContext } from './gl';
import { setDebugMode } from '../../mol-util/debug';
import { ColorNames } from '../../mol-util/color/names';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { TextureMesh } from '../../mol-geo/geometry/texture-mesh/texture-mesh';
export function createTextureMesh() {
const textureMesh = TextureMesh.createEmpty();
const props = PD.getDefaultValues(TextureMesh.Params);
const values = TextureMesh.Utils.createValuesSimple(textureMesh, props, ColorNames.orange, 1);
const state = TextureMesh.Utils.createRenderableState(props);
return createRenderObject('texture-mesh', values, state, -1);
}
describe('texture-mesh', () => {
const ctx = tryGetGLContext(32, 32);
(ctx ? it : it.skip)('basic', async () => {
const ctx = getGLContext(32, 32);
const scene = Scene.create(ctx);
const textureMesh = createTextureMesh();
scene.add(textureMesh);
setDebugMode(true);
expect(() => scene.commit()).not.toThrow();
setDebugMode(false);
ctx.destroy();
});
});

View File

@@ -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
}
@@ -716,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 {

View File

@@ -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)

View File

@@ -105,6 +105,7 @@ void main(){
#else
linewidth = size * uPixelRatio;
#endif
linewidth = max(1.0, linewidth);
// adjust for linewidth
offset *= linewidth;

View File

@@ -36,6 +36,7 @@ void main(){
#else
gl_PointSize = size * uPixelRatio;
#endif
gl_PointSize = max(1.0, gl_PointSize);
gl_Position = uProjection * mvPosition;

View File

@@ -358,7 +358,10 @@ export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScal
unbindResources(gl);
// to aid GC
if (!options?.doNotForceWebGLContextLoss) gl.getExtension('WEBGL_lose_context')?.loseContext();
if (!options?.doNotForceWebGLContextLoss) {
gl.getExtension('WEBGL_lose_context')?.loseContext();
gl.getExtension('STACKGL_destroy_context')?.destroy();
}
}
};
}

View File

@@ -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 };

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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);
}

View File

@@ -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)

View File

@@ -355,8 +355,8 @@ class Structure {
return this.models.indexOf(m);
}
remapModel(m: Model) {
const { dynamicBonds, interUnitBonds } = this.state;
remapModel(m: Model): Structure {
const { dynamicBonds, interUnitBonds, parent } = this.state;
const units: Unit[] = [];
for (const ug of this.unitSymmetryGroups) {
const unit = ug.units[0].remapModel(m, dynamicBonds);
@@ -367,6 +367,7 @@ class Structure {
}
}
return Structure.create(units, {
parent: parent?.remapModel(m),
label: this.label,
interUnitBonds: dynamicBonds ? undefined : interUnitBonds,
dynamicBonds

View File

@@ -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;

View File

@@ -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];
}
}

View File

@@ -379,6 +379,8 @@ const atomicDetail = StructureRepresentationPresetProvider({
}
await update.commit({ revertOnError: true });
await updateFocusRepr(plugin, structure, params.theme?.focus?.name ?? color, params.theme?.focus?.params ?? colorParams);
return { components, representations };
}
});

View File

@@ -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);
}

View File

@@ -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,10 +16,13 @@ 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';
import { Color } from '../../../mol-util/color';
export { StructureMeasurementManager };
export const MeasurementGroupTag = 'measurement-group';
export const MeasurementOrderLabelTag = 'measurement-order-label';
export type StructureMeasurementCell = StateObjectCell<PluginStateObject.Shape.Representation3D, StateTransform<StateTransformer<PluginStateObject.Molecule.Structure.Selections, PluginStateObject.Shape.Representation3D, any>>>
@@ -35,6 +39,7 @@ export interface StructureMeasurementManagerState {
angles: StructureMeasurementCell[],
dihedrals: StructureMeasurementCell[],
orientations: StructureMeasurementCell[],
planes: StructureMeasurementCell[],
options: StructureMeasurementOptions
}
@@ -222,19 +227,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 +255,70 @@ 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 } });
}
async addOrderLabels(locis: StructureElement.Loci[]) {
const update = this.getGroup();
const current = this.plugin.state.data.select(StateSelection.Generators.ofType(PluginStateObject.Molecule.Structure.Selections).withTag(MeasurementOrderLabelTag));
for (const obj of current)
update.delete(obj);
let order = 1;
for (const loci of locis) {
const cell = this.plugin.helpers.substructureParent.get(loci.structure);
if (!cell) continue;
const dependsOn = [cell.transform.ref];
update
.apply(StateTransforms.Model.MultiStructureSelectionFromExpression, {
selections: [
{ key: 'a', ref: cell.transform.ref, expression: StructureElement.Loci.toExpression(loci) },
],
isTransitive: true,
label: 'Order'
}, { dependsOn, tags: MeasurementOrderLabelTag })
.apply(StateTransforms.Representation.StructureSelectionsLabel3D, {
textColor: Color.fromRgb(255, 255, 255),
borderColor: Color.fromRgb(0, 0, 0),
textSize: 0.33,
borderWidth: 0.3,
offsetZ: 0.75,
customText: `${order++}`
}, { tags: MeasurementOrderLabelTag });
}
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;
@@ -254,18 +329,26 @@ class StructureMeasurementManager extends StatefulPluginComponent<StructureMeasu
}
private sync() {
const labels = [];
for (const cell of this.getTransforms(StateTransforms.Representation.StructureSelectionsLabel3D) as StructureMeasurementCell[]) {
const tags = (cell.obj as any)['tags'] as string[];
if (!tags || !tags.includes(MeasurementOrderLabelTag))
labels.push(cell);
}
const updated = this.updateState({
labels: this.getTransforms(StateTransforms.Representation.StructureSelectionsLabel3D),
labels,
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;

View File

@@ -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) {

View File

@@ -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) };
}

View File

@@ -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;

View File

@@ -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;
});
},
});

View File

@@ -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>
* @author David Sehnal <david.sehnal@gmail.com>
@@ -25,6 +25,9 @@ import { StructureSelectionManager } from '../mol-plugin-state/manager/structure
import { arrayEqual } from '../mol-util/array';
const MaxDisplaySequenceLength = 5000;
// TODO: add virtualized Select controls (at best with a search box)?
const MaxSelectOptionsCount = 1000;
const MaxSequenceWrappersCount = 30;
function opKey(l: StructureElement.Location) {
const ids = SP.unit.pdbx_struct_oper_list_ids(l);
@@ -94,7 +97,7 @@ function getSequenceWrapper(state: { structure: Structure, modelEntityId: string
}
}
function getModelEntityOptions(structure: Structure, polymersOnly = false) {
function getModelEntityOptions(structure: Structure, polymersOnly = false): [string, string][] {
const options: [string, string][] = [];
const l = StructureElement.Location.create(structure);
const seen = new Set<string>();
@@ -118,13 +121,17 @@ function getModelEntityOptions(structure: Structure, polymersOnly = false) {
const label = `${id}: ${description}`;
options.push([key, label]);
seen.add(key);
if (options.length > MaxSelectOptionsCount) {
return [['', 'Too many entities']];
}
}
if (options.length === 0) options.push(['', 'No entities']);
return options;
}
function getChainOptions(structure: Structure, modelEntityId: string) {
function getChainOptions(structure: Structure, modelEntityId: string): [number, string][] {
const options: [number, string][] = [];
const l = StructureElement.Location.create(structure);
const seen = new Set<number>();
@@ -144,13 +151,17 @@ function getChainOptions(structure: Structure, modelEntityId: string) {
options.push([id, label]);
seen.add(id);
if (options.length > MaxSelectOptionsCount) {
return [[-1, 'Too many chains']];
}
}
if (options.length === 0) options.push([-1, 'No units']);
if (options.length === 0) options.push([-1, 'No chains']);
return options;
}
function getOperatorOptions(structure: Structure, modelEntityId: string, chainGroupId: number) {
function getOperatorOptions(structure: Structure, modelEntityId: string, chainGroupId: number): [string, string][] {
const options: [string, string][] = [];
const l = StructureElement.Location.create(structure);
const seen = new Set<string>();
@@ -168,6 +179,10 @@ function getOperatorOptions(structure: Structure, modelEntityId: string, chainGr
const label = unit.conformation.operator.name;
options.push([id, label]);
seen.add(id);
if (options.length > MaxSelectOptionsCount) {
return [['', 'Too many operators']];
}
}
if (options.length === 0) options.push(['', 'No operators']);
@@ -266,6 +281,7 @@ export class SequenceView extends PluginUIComponent<{ defaultMode?: SequenceView
}, this.plugin.managers.structure.selection),
label: `${cLabel} | ${eLabel}`
});
if (wrappers.length > MaxSequenceWrappersCount) return [];
}
}
}

View File

@@ -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';
@@ -61,12 +62,13 @@ export class MeasurementList extends PurePluginUIComponent {
render() {
const measurements = this.plugin.managers.structure.measurement.state;
return <div style={{ marginTop: '6px' }}>
{this.renderGroup(measurements.labels, 'Labels')}
{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>;
}
}
@@ -77,6 +79,7 @@ export class MeasurementControls extends PurePluginUIComponent<{}, { isBusy: boo
componentDidMount() {
this.subscribe(this.selection.events.additionsHistoryUpdated, () => {
this.forceUpdate();
this.updateOrderLabels();
});
this.subscribe(this.plugin.behaviors.state.isBusy, v => {
@@ -84,6 +87,33 @@ export class MeasurementControls extends PurePluginUIComponent<{}, { isBusy: boo
});
}
componentWillUnmount() {
this.clearOrderLabels();
super.componentWillUnmount();
}
componentDidUpdate(prevProps: {}, prevState: { isBusy: boolean, action?: 'add' | 'options' }) {
if (this.state.action !== prevState.action)
this.updateOrderLabels();
}
clearOrderLabels() {
this.plugin.managers.structure.measurement.addOrderLabels([]);
}
updateOrderLabels() {
if (this.state.action !== 'add') {
this.clearOrderLabels();
return;
}
const locis = [];
const history = this.selection.additionsHistory;
for (let idx = 0; idx < history.length && idx < 4; idx++)
locis.push(history[idx].loci);
this.plugin.managers.structure.measurement.addOrderLabels(locis);
}
get selection() {
return this.plugin.managers.structure.selection;
}
@@ -108,13 +138,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;
}
@@ -142,8 +190,8 @@ export class MeasurementControls extends PurePluginUIComponent<{}, { isBusy: boo
historyEntry(e: StructureSelectionHistoryEntry, idx: number) {
const history = this.plugin.managers.structure.selection.additionsHistory;
return <div className='msp-flex-row' key={e.id}>
<Button noOverflow title='Click to focus. Hover to highlight.' onClick={() => this.focusLoci(e.loci)} style={{ width: 'auto', textAlign: 'left' }} onMouseEnter={() => this.highlight(e.loci)} onMouseLeave={() => this.plugin.managers.interactivity.lociHighlights.clearHighlights()}>
return <div className='msp-flex-row' key={e.id} onMouseEnter={() => this.highlight(e.loci)} onMouseLeave={() => this.plugin.managers.interactivity.lociHighlights.clearHighlights()}>
<Button noOverflow title='Click to focus. Hover to highlight.' onClick={() => this.focusLoci(e.loci)} style={{ width: 'auto', textAlign: 'left' }}>
{idx}. <span dangerouslySetInnerHTML={{ __html: e.label }} />
</Button>
{history.length > 1 && <IconButton svg={ArrowUpwardSvg} small={true} className='msp-form-control' onClick={() => this.moveHistory(e, 'up')} flex='20px' title={'Move up'} />}
@@ -219,7 +267,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 +314,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 +326,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>';
}

View File

@@ -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;
}
}

View File

@@ -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',

View File

@@ -20,9 +20,19 @@ 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() {
function preferWebGl1() {
if (typeof navigator === 'undefined' || typeof window === 'undefined') return false;
// WebGL2 isn't working in MacOS 12.0.1 Safari 15.1 (but is working in Safari tech preview)
// prefer webgl 1 based on the userAgent substring
if (navigator.userAgent.indexOf('Version/15.1 Safari') > 0) {
return true;
}
// Check for iOS device which enabled WebGL2 recently but it doesn't seem
// to be full up to speed yet.
// adapted from https://stackoverflow.com/questions/9038625/detect-if-device-is-ios
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)
@@ -37,10 +47,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()),
PreferWebGl1: item('plugin-config.prefer-webgl1', preferWebGl1()),
},
State: {
DefaultServer: item('plugin-state.server', 'https://webchem.ncbr.muni.cz/molstar-state'),

View File

@@ -196,9 +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;
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);

View File

@@ -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() {

View File

@@ -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]);
const principalAxes = StructureElement.Loci.getPrincipalAxesMany(data.locis);
state.currentGroup = i;
addEllipsoid(state, origin, dirA, dirB, radiusScale, 2);
}
}
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);
}
//

View File

@@ -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;
}

View File

@@ -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),

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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];

View File

@@ -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];

View File

@@ -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);
}

View File

@@ -27,8 +27,9 @@ import { StructureGroup } from './util/common';
export const PolymerTraceMeshParams = {
sizeFactor: PD.Numeric(0.2, { min: 0, max: 10, step: 0.01 }),
aspectRatio: PD.Numeric(5, { min: 0.1, max: 10, step: 0.1 }),
arrowFactor: PD.Numeric(1.5, { min: 0, max: 3, step: 0.1 }),
tubularHelices: PD.Boolean(false),
arrowFactor: PD.Numeric(1.5, { min: 0, max: 3, step: 0.1 }, { description: 'Size factor for sheet arrows' }),
tubularHelices: PD.Boolean(false, { description: 'Draw alpha helices as tubes' }),
helixProfile: PD.Select('elliptical', PD.arrayToOptions(['elliptical', 'rounded', 'square'] as const), { description: 'Protein and nucleic helix trace profile' }),
detail: PD.Numeric(0, { min: 0, max: 3, step: 1 }, BaseGeometry.CustomQualityParamInfo),
linearSegments: PD.Numeric(8, { min: 1, max: 48, step: 1 }, BaseGeometry.CustomQualityParamInfo),
radialSegments: PD.Numeric(16, { min: 2, max: 56, step: 2 }, BaseGeometry.CustomQualityParamInfo)
@@ -42,7 +43,7 @@ function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure: Struc
const polymerElementCount = unit.polymerElements.length;
if (!polymerElementCount) return Mesh.createEmpty(mesh);
const { sizeFactor, detail, linearSegments, radialSegments, aspectRatio, arrowFactor, tubularHelices } = props;
const { sizeFactor, detail, linearSegments, radialSegments, aspectRatio, arrowFactor, tubularHelices, helixProfile } = props;
const vertexCount = linearSegments * radialSegments * polymerElementCount + (radialSegments + 1) * polymerElementCount * 2;
const builderState = MeshBuilder.createState(vertexCount, vertexCount / 10, mesh);
@@ -131,9 +132,6 @@ function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure: Struc
h0 = w0 * aspectRatio;
h1 = w1 * aspectRatio;
h2 = w2 * aspectRatio;
[w0, h0] = [h0, w0];
[w1, h1] = [h1, w1];
[w2, h2] = [h2, w2];
} else {
h0 = w0;
h1 = w1;
@@ -142,18 +140,26 @@ function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure: Struc
interpolateSizes(state, w0, w1, w2, h0, h1, h2, shift);
const [normals, binormals] = isNucleicType && !v.isCoarseBackbone ? [binormalVectors, normalVectors] : [normalVectors, binormalVectors];
if (isNucleicType && !v.isCoarseBackbone) {
// TODO: find a cleaner way to swap normal and binormal for nucleic types
for (let i = 0, il = normals.length; i < il; i++) normals[i] *= -1;
}
if (radialSegments === 2) {
if (isNucleicType && !v.isCoarseBackbone) {
// TODO find a cleaner way to swap normal and binormal for nucleic types
for (let i = 0, il = binormalVectors.length; i < il; i++) binormalVectors[i] *= -1;
addRibbon(builderState, curvePoints, binormalVectors, normalVectors, segmentCount, heightValues, widthValues, 0);
addRibbon(builderState, curvePoints, normals, binormals, segmentCount, heightValues, widthValues, 0);
} else {
addRibbon(builderState, curvePoints, normalVectors, binormalVectors, segmentCount, widthValues, heightValues, 0);
addRibbon(builderState, curvePoints, normals, binormals, segmentCount, widthValues, heightValues, 0);
}
} else if (radialSegments === 4) {
addSheet(builderState, curvePoints, normalVectors, binormalVectors, segmentCount, widthValues, heightValues, 0, startCap, endCap);
addSheet(builderState, curvePoints, normals, binormals, segmentCount, widthValues, heightValues, 0, startCap, endCap);
} else if (h1 === w1) {
addTube(builderState, curvePoints, normals, binormals, segmentCount, radialSegments, widthValues, heightValues, startCap, endCap, 'elliptical');
} else if (helixProfile === 'square') {
addSheet(builderState, curvePoints, normals, binormals, segmentCount, widthValues, heightValues, 0, startCap, endCap);
} else {
addTube(builderState, curvePoints, normalVectors, binormalVectors, segmentCount, radialSegments, widthValues, heightValues, startCap, endCap);
addTube(builderState, curvePoints, normals, binormals, segmentCount, radialSegments, widthValues, heightValues, startCap, endCap, helixProfile);
}
}
@@ -189,7 +195,8 @@ export function PolymerTraceVisual(materialId: number): UnitsVisual<PolymerTrace
newProps.linearSegments !== currentProps.linearSegments ||
newProps.radialSegments !== currentProps.radialSegments ||
newProps.aspectRatio !== currentProps.aspectRatio ||
newProps.arrowFactor !== currentProps.arrowFactor
newProps.arrowFactor !== currentProps.arrowFactor ||
newProps.helixProfile !== currentProps.helixProfile
);
const secondaryStructureHash = SecondaryStructureProvider.get(newStructureGroup.structure).version;

View File

@@ -93,7 +93,7 @@ function createPolymerTubeMesh(ctx: VisualContext, unit: Unit, structure: Struct
} else if (radialSegments === 4) {
addSheet(builderState, curvePoints, normalVectors, binormalVectors, segmentCount, widthValues, heightValues, 0, startCap, endCap);
} else {
addTube(builderState, curvePoints, normalVectors, binormalVectors, segmentCount, radialSegments, widthValues, heightValues, startCap, endCap);
addTube(builderState, curvePoints, normalVectors, binormalVectors, segmentCount, radialSegments, widthValues, heightValues, startCap, endCap, 'elliptical');
}
++i;

View File

@@ -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

View File

@@ -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, 3, cylinderProps);
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);
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);

View File

@@ -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;

View File

@@ -230,7 +230,6 @@ function addJsonConfigArgs(parser: argparse.ArgumentParser) {
'If a property is not specified, cmd line param/OS variable/default value are used.'
].join('\n'),
required: false,
action: 'store_true'
});
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' });

View File

@@ -90,7 +90,6 @@ function addJsonConfigArgs(parser: argparse.ArgumentParser) {
'If a property is not specified, cmd line param/OS variable/default value are used.'
].join('\n'),
required: false,
action: 'store_true'
});
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' });

View File

@@ -14,6 +14,7 @@
"moduleResolution": "node",
"importHelpers": true,
"noEmitHelpers": true,
"allowSyntheticDefaultImports": true,
"jsx": "react-jsx",
"lib": [ "es6", "dom", "esnext.asynciterable", "es2016" ],
"rootDir": "src",

View File

@@ -14,6 +14,7 @@
"moduleResolution": "node",
"importHelpers": true,
"noEmitHelpers": true,
"allowSyntheticDefaultImports": true,
"jsx": "react-jsx",
"lib": [ "es6", "dom", "esnext.asynciterable", "es2016" ],
"rootDir": "src",