Compare commits

...

30 Commits

Author SHA1 Message Date
Alexander Rose
40347e3e46 0.6.0-dev.5 2020-03-18 21:16:44 -07:00
Alexander Rose
ed277f6e16 Merge remote-tracking branch 'origin/master' into objects 2020-03-18 21:12:52 -07:00
Alexander Rose
8f2085d8b4 0.6.0-dev.4 2020-03-18 20:59:42 -07:00
Alexander Rose
b5a48ad201 package updates 2020-03-18 20:59:01 -07:00
Alexander Rose
14b900791f wip, unitcell ref 2020-03-18 20:49:30 -07:00
Alexander Rose
de80799e68 allow any Loci in CameraManager.focusLoci 2020-03-18 20:44:54 -07:00
Alexander Rose
53eca387fc take renderable visibility into account for scene bounding-sphere 2020-03-18 20:35:14 -07:00
Alexander Rose
fc3005f271 ui tweaks 2020-03-18 16:47:57 -07:00
Alexander Rose
c95fdff00c tweaked StructureSelectionCategorys 2020-03-18 16:04:09 -07:00
Alexander Rose
3cd9042c72 added helpers to Model, better InitVolumeStreaming.isApplicable check 2020-03-18 15:47:58 -07:00
Alexander Rose
4d3914426e improved LociLabelManager to handle multiple loci 2020-03-18 15:46:29 -07:00
David Sehnal
af1fb7041e removed apps/model-server-query 2020-03-18 18:28:19 +01:00
David Sehnal
e2eb1bf223 model-server: config tweak 2020-03-18 13:49:54 +01:00
David Sehnal
5a64a6f1a3 mol-model: fixed a bug in secondary structure export 2020-03-18 13:45:43 +01:00
David Sehnal
88aa9303d7 model-server: fixed data_source bug, allow fetching source data over http(s) 2020-03-18 13:20:08 +01:00
Alexander Rose
5c77eec184 bounding-sphere fixes
- fixed renderable.spec
- simplify extrema for calculateInvariantBoundingSphere when possible
- fixed Sphere3D.expand
2020-03-17 23:19:26 -07:00
Alexander Rose
a931ed7c01 Merge branch 'master' into objects 2020-03-17 21:46:22 -07:00
Alexander Rose
94cd2b618c wip 2020-03-17 20:17:41 -07:00
Alexander Rose
9c97fc258d better box3d calculation in boundary-helper 2020-03-17 16:35:57 -07:00
Alexander Rose
0ac1cfe555 improved bounding sphere- drop hierarchy in favor of extrema points 2020-03-17 16:28:34 -07:00
Alexander Rose
3942f1bc33 added PickRequired type helper 2020-03-17 15:21:06 -07:00
Alexander Rose
a66e38a901 use unit/structure bounding-sphere in visuals geometry 2020-03-17 12:15:39 -07:00
Alexander Rose
f65f4f4aeb seperate grid and boundary calculation 2020-03-17 12:14:36 -07:00
Alexander Rose
f28b13bf87 shader: fixed broken flat-shaded & flip-sided 2020-03-17 09:48:28 -07:00
David Sehnal
8f211a0785 servers readme tweak 2020-03-17 15:50:05 +01:00
David Sehnal
ba1dfb2851 remove preprocess app from webpack config 2020-03-17 15:35:06 +01:00
Alexander Rose
7ccf36a0fa moved unitcell representation to mol-repr/shape 2020-03-16 22:38:46 -07:00
Alexander Rose
b0ee640c12 calc bespoke boundingSphere for unitcell 2020-03-16 22:18:09 -07:00
Alexander Rose
cd6d41a035 allow direct setting of geo bounding sphere
- support mesh, lines, text, spheres, points
2020-03-16 21:20:07 -07:00
Alexander Rose
9c8f1f3e11 wip 2020-03-16 17:20:20 -07:00
89 changed files with 1605 additions and 1450 deletions

View File

@@ -30,7 +30,7 @@ node lib/servers/model/server/server
## From NPM
```
npm install molstar
npm install --production molstar
./model-server
```

View File

@@ -34,7 +34,7 @@ node lib/servers/volume/server
## From NPM
```
npm install molstar
npm install --production molstar
./volume-server
```

1011
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "molstar",
"version": "0.6.0-dev.3",
"version": "0.6.0-dev.5",
"description": "A comprehensive macromolecular library.",
"homepage": "https://github.com/molstar/molstar#readme",
"repository": {
@@ -74,16 +74,16 @@
],
"license": "MIT",
"devDependencies": {
"@graphql-codegen/add": "^1.13.0",
"@graphql-codegen/cli": "^1.13.0",
"@graphql-codegen/time": "^1.13.0",
"@graphql-codegen/typescript": "^1.13.0",
"@graphql-codegen/typescript-graphql-files-modules": "^1.13.0",
"@graphql-codegen/typescript-graphql-request": "^1.13.0",
"@graphql-codegen/typescript-operations": "^1.13.0",
"@graphql-codegen/add": "^1.13.1",
"@graphql-codegen/cli": "^1.13.1",
"@graphql-codegen/time": "^1.13.1",
"@graphql-codegen/typescript": "^1.13.1",
"@graphql-codegen/typescript-graphql-files-modules": "^1.13.1",
"@graphql-codegen/typescript-graphql-request": "^1.13.1",
"@graphql-codegen/typescript-operations": "^1.13.1",
"@types/cors": "^2.8.6",
"@typescript-eslint/eslint-plugin": "^2.23.0",
"@typescript-eslint/parser": "^2.23.0",
"@typescript-eslint/eslint-plugin": "^2.24.0",
"@typescript-eslint/parser": "^2.24.0",
"benchmark": "^2.1.4",
"circular-dependency-plugin": "^5.2.0",
"concurrently": "^5.1.0",
@@ -91,7 +91,7 @@
"css-loader": "^3.4.2",
"eslint": "^6.8.0",
"extra-watch-webpack-plugin": "^1.0.3",
"file-loader": "^5.1.0",
"file-loader": "^6.0.0",
"fs-extra": "^8.1.0",
"http-server": "^0.12.1",
"jest": "^25.1.0",
@@ -115,7 +115,7 @@
"@types/compression": "1.7.0",
"@types/express": "^4.17.3",
"@types/jest": "^25.1.4",
"@types/node": "^13.9.0",
"@types/node": "^13.9.2",
"@types/node-fetch": "^2.5.5",
"@types/react": "^16.9.23",
"@types/react-dom": "^16.9.5",
@@ -126,7 +126,7 @@
"cors": "^2.8.5",
"express": "^4.17.1",
"graphql": "^14.6.0",
"immer": "^6.0.1",
"immer": "^6.0.2",
"immutable": "^3.8.2",
"node-fetch": "^2.6.0",
"react": "^16.13.0",

View File

@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<title>Mol* ModelServer Query Builder</title>
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src="./index.js"></script>
</body>
</html>

View File

@@ -1,134 +0,0 @@
/**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import * as Rx from 'rxjs'
import { QueryDefinition, QueryList } from '../../servers/model/server/api'
import './index.html'
interface State {
query: Rx.BehaviorSubject<QueryDefinition>,
id: Rx.BehaviorSubject<string>,
params: Rx.BehaviorSubject<any>,
isBinary: Rx.BehaviorSubject<boolean>,
models: Rx.BehaviorSubject<number[]>,
url: Rx.Subject<string>
}
class Root extends React.Component<{ state: State }, { }> {
render() {
return <div>
<div>
Query: <QuerySelect state={this.props.state} />
</div>
<div>
ID: <input type='text' onChange={t => this.props.state.id.next(t.currentTarget.value)} />
</div>
<div>
Params:<br/>
<QueryParams state={this.props.state} />
</div>
<div>
Model numbers (empty for all): <ModelNums state={this.props.state} />
</div>
<div>
<input type='checkbox' onChange={t => this.props.state.isBinary.next(!!t.currentTarget.checked)} /> Binary
</div>
<div>
Query string:
<QueryUrl state={this.props.state} />
</div>
</div>
}
}
class QuerySelect extends React.Component<{ state: State }> {
render() {
return <select onChange={s => this.props.state.query.next(QueryList[+s.currentTarget.value].definition)}>
{ QueryList.map((q, i) => <option value={i} key={i} selected={i === 1}>{q.definition.niceName}</option>) }
</select>
}
}
class QueryParams extends React.Component<{ state: State }, { prms: string }> {
state = { prms: '' };
parseParams(str: string) {
this.setState({ prms: str });
try {
const params = JSON.parse(str);
this.props.state.params.next(params);
} catch {
this.props.state.params.next({});
}
}
componentDidMount() {
this.props.state.query.subscribe(q => this.setState({ prms: formatParams(q) }))
}
render() {
return <textarea style={{height: '300px'}} value={this.state.prms} cols={80} onChange={t => this.parseParams(t.currentTarget.value)} />;
}
}
class QueryUrl extends React.Component<{ state: State }, { queryString: string }> {
state = { queryString: '' };
componentDidMount() {
this.props.state.url.subscribe(url => this.setState({ queryString: url }))
}
render() {
return <input type='text' value={this.state.queryString} style={{ width: '800px' }} />
}
}
class ModelNums extends React.Component<{ state: State }> {
render() {
return <input type='text' defaultValue='1' style={{ width: '300px' }} onChange={t =>
this.props.state.models.next(t.currentTarget.value.split(',')
.map(v => v.trim())
.filter(v => !!v)
.map(v => +v)
)} />
}
}
const state: State = {
query: new Rx.BehaviorSubject(QueryList[1].definition),
id: new Rx.BehaviorSubject('1cbs'),
params: new Rx.BehaviorSubject({ }),
isBinary: new Rx.BehaviorSubject<boolean>(false),
models: new Rx.BehaviorSubject<number[]>([]),
url: new Rx.Subject()
}
function formatParams(def: QueryDefinition) {
const prms = Object.create(null);
for (const p of def.jsonParams) {
prms[p.name] = p.exampleValues ? p.exampleValues[0] : void 0;
}
return JSON.stringify(prms, void 0, 2);
}
function formatUrl() {
const json = JSON.stringify({
name: state.query.value.name,
id: state.id.value,
modelNums: state.models.value.length ? state.models.value : void 0,
binary: state.isBinary.value,
params: state.params.value
});
state.url.next(encodeURIComponent(json));
}
Rx.merge(state.query, state.id, state.params, state.isBinary, state.models).subscribe(s => formatUrl());
ReactDOM.render(<Root state={state} />, document.getElementById('app'));

View File

@@ -74,6 +74,7 @@ interface Canvas3D {
commit(isSynchronous?: boolean): void
update(repr?: Representation.Any, keepBoundingSphere?: boolean): void
clear(): void
syncVisibility(): void
requestDraw(force?: boolean): void
animate(): void
@@ -397,6 +398,11 @@ namespace Canvas3D {
requestDraw(true)
reprCount.next(reprRenderObjects.size)
},
syncVisibility: () => {
if (scene.syncVisibility()) {
camera.setState({ radiusMax: scene.boundingSphere.radius })
}
},
// draw,
requestDraw,

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -45,16 +45,16 @@ export class BoundingSphereHelper {
}
update() {
const newSceneData = updateBoundingSphereData(this.scene, this.parent.boundingSphere, this.sceneData, ColorNames.grey)
const newSceneData = updateBoundingSphereData(this.scene, this.parent.boundingSphere, this.sceneData, ColorNames.grey, sceneMaterialId)
if (newSceneData) this.sceneData = newSceneData
this.parent.forEach((r, ro) => {
const objectData = this.objectsData.get(ro)
const newObjectData = updateBoundingSphereData(this.scene, r.values.boundingSphere.ref.value, objectData, ColorNames.tomato)
const newObjectData = updateBoundingSphereData(this.scene, r.values.boundingSphere.ref.value, objectData, ColorNames.tomato, objectMaterialId)
if (newObjectData) this.objectsData.set(ro, newObjectData)
const instanceData = this.instancesData.get(ro)
const newInstanceData = updateBoundingSphereData(this.scene, r.values.invariantBoundingSphere.ref.value, instanceData, ColorNames.skyblue, {
const newInstanceData = updateBoundingSphereData(this.scene, r.values.invariantBoundingSphere.ref.value, instanceData, ColorNames.skyblue, instanceMaterialId, {
aTransform: ro.values.aTransform,
matrix: ro.values.matrix,
transform: ro.values.transform,
@@ -114,10 +114,10 @@ export class BoundingSphereHelper {
}
}
function updateBoundingSphereData(scene: Scene, boundingSphere: Sphere3D, data: BoundingSphereData | undefined, color: Color, transform?: TransformData) {
function updateBoundingSphereData(scene: Scene, boundingSphere: Sphere3D, data: BoundingSphereData | undefined, color: Color, materialId: number, transform?: TransformData) {
if (!data || !Sphere3D.equals(data.boundingSphere, boundingSphere)) {
const mesh = createBoundingSphereMesh(boundingSphere, data && data.mesh)
const renderObject = data ? data.renderObject : createBoundingSphereRenderObject(mesh, color, transform)
const renderObject = data ? data.renderObject : createBoundingSphereRenderObject(mesh, color, materialId, transform)
if (data) {
ValueCell.update(renderObject.values.drawCount, Geometry.getDrawCount(mesh))
} else {
@@ -132,15 +132,19 @@ function createBoundingSphereMesh(boundingSphere: Sphere3D, mesh?: Mesh) {
const vertexCount = sphereVertexCount(detail)
const builderState = MeshBuilder.createState(vertexCount, vertexCount / 2, mesh)
if (boundingSphere.radius) {
for (const b of Sphere3D.getList(boundingSphere)) {
addSphere(builderState, b.center, b.radius, detail)
addSphere(builderState, boundingSphere.center, boundingSphere.radius, detail)
if (Sphere3D.hasExtrema(boundingSphere)) {
for (const e of boundingSphere.extrema) addSphere(builderState, e, 1.0, 0)
}
}
return MeshBuilder.getMesh(builderState)
}
const boundingSphereHelberMaterialId = getNextMaterialId()
function createBoundingSphereRenderObject(mesh: Mesh, color: Color, transform?: TransformData) {
const sceneMaterialId = getNextMaterialId()
const objectMaterialId = getNextMaterialId()
const instanceMaterialId = getNextMaterialId()
function createBoundingSphereRenderObject(mesh: Mesh, color: Color, materialId: number, transform?: TransformData) {
const values = Mesh.Utils.createValuesSimple(mesh, { alpha: 0.1, doubleSided: false }, color, 1, transform)
return createRenderObject('mesh', values, { visible: true, alphaFactor: 1, pickable: false, opaque: false }, boundingSphereHelberMaterialId)
return createRenderObject('mesh', values, { visible: true, alphaFactor: 1, pickable: false, opaque: false }, materialId)
}

View File

@@ -6,9 +6,7 @@
import { ValueCell } from '../../../mol-util'
import { Mat4 } from '../../../mol-math/linear-algebra'
import { transformPositionArray,/* , transformDirectionArray, getNormalMatrix */
GroupMapping,
createGroupMapping} from '../../util';
import { transformPositionArray, GroupMapping, createGroupMapping} from '../../util';
import { GeometryUtils } from '../geometry';
import { createColors } from '../color-data';
import { createMarkers } from '../marker-data';
@@ -50,6 +48,8 @@ export interface Lines {
readonly boundingSphere: Sphere3D
/** Maps group ids to line indices */
readonly groupMapping: GroupMapping
setBoundingSphere(boundingSphere: Sphere3D): void
}
export namespace Lines {
@@ -129,6 +129,10 @@ export namespace Lines {
currentGroup = lines.groupBuffer.ref.version
}
return groupMapping
},
setBoundingSphere(sphere: Sphere3D) {
Sphere3D.copy(boundingSphere, sphere)
currentHash = hashCode(lines)
}
}
return lines

View File

@@ -45,6 +45,8 @@ export interface Mesh {
readonly boundingSphere: Sphere3D
/** Maps group ids to vertex indices */
readonly groupMapping: GroupMapping
setBoundingSphere(boundingSphere: Sphere3D): void
}
export namespace Mesh {
@@ -101,6 +103,10 @@ export namespace Mesh {
currentGroup = mesh.groupBuffer.ref.version
}
return groupMapping
},
setBoundingSphere(sphere: Sphere3D) {
Sphere3D.copy(boundingSphere, sphere)
currentHash = hashCode(mesh)
}
}
return mesh

View File

@@ -6,9 +6,7 @@
import { ValueCell } from '../../../mol-util'
import { Mat4 } from '../../../mol-math/linear-algebra'
import { transformPositionArray,/* , transformDirectionArray, getNormalMatrix */
GroupMapping,
createGroupMapping} from '../../util';
import { transformPositionArray, GroupMapping, createGroupMapping} from '../../util';
import { GeometryUtils } from '../geometry';
import { createColors } from '../color-data';
import { createMarkers } from '../marker-data';
@@ -43,6 +41,8 @@ export interface Points {
readonly boundingSphere: Sphere3D
/** Maps group ids to point indices */
readonly groupMapping: GroupMapping
setBoundingSphere(boundingSphere: Sphere3D): void
}
export namespace Points {
@@ -92,6 +92,10 @@ export namespace Points {
currentGroup = points.groupBuffer.ref.version
}
return groupMapping
},
setBoundingSphere(sphere: Sphere3D) {
Sphere3D.copy(boundingSphere, sphere)
currentHash = hashCode(points)
}
}
return points

View File

@@ -42,6 +42,8 @@ export interface Spheres {
readonly boundingSphere: Sphere3D
/** Maps group ids to sphere indices */
readonly groupMapping: GroupMapping
setBoundingSphere(boundingSphere: Sphere3D): void
}
export namespace Spheres {
@@ -97,6 +99,10 @@ export namespace Spheres {
currentGroup = spheres.groupBuffer.ref.version
}
return groupMapping
},
setBoundingSphere(sphere: Sphere3D) {
Sphere3D.copy(boundingSphere, sphere)
currentHash = hashCode(spheres)
}
}
return spheres

View File

@@ -62,6 +62,8 @@ export interface Text {
readonly boundingSphere: Sphere3D
/** Maps group ids to text indices */
readonly groupMapping: GroupMapping
setBoundingSphere(boundingSphere: Sphere3D): void
}
export namespace Text {
@@ -124,6 +126,10 @@ export namespace Text {
currentGroup = text.groupBuffer.ref.version
}
return groupMapping
},
setBoundingSphere(sphere: Sphere3D) {
Sphere3D.copy(boundingSphere, sphere)
currentHash = hashCode(text)
}
}
return text

View File

@@ -1,42 +1,44 @@
/**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
// import { calculateBoundingSphere } from '../renderable/util';
// import { Vec3 } from '../../mol-math/linear-algebra';
import { calculateBoundingSphere } from '../renderable/util';
describe('renderable', () => {
it('calculateBoundingSphere', () => {
// const position = new Float32Array([
// 0, 0, 0,
// 1, 0, 0
// ])
// const transform = new Float32Array([
// 1, 0, 0, 0,
// 0, 1, 0, 0,
// 0, 0, 1, 0,
// 0, 0, 0, 0,
const position = new Float32Array([
0, 0, 0,
1, 0, 0,
-1, 0, 0,
])
const transform = new Float32Array([
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 0,
// 1, 0, 0, 0,
// 0, 1, 0, 0,
// 0, 0, 1, 0,
// 1, 0, 0, 0,
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
1, 0, 0, 0,
// 1, 0, 0, 0,
// 0, 1, 0, 0,
// 0, 0, 1, 0,
// 2, 0, 0, 0
// ])
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
-1, 0, 0, 0
])
// const { boundingSphere } = calculateBoundingSphere(
// position, position.length / 3,
// transform, transform.length / 16
// )
const { boundingSphere, invariantBoundingSphere } = calculateBoundingSphere(
position, position.length / 3,
transform, transform.length / 16
)
// TODO:
// expect(boundingSphere.radius).toBeCloseTo(1.58, 2)
// expect(Vec3.equals(boundingSphere.center, Vec3.create(1.418367, 0, 0))).toBe(true)
expect(invariantBoundingSphere.extrema).toEqual([[0, 0, 0], [1, 0, 0], [-1, 0, 0]])
expect(invariantBoundingSphere.radius).toBe(1)
expect(invariantBoundingSphere.center).toEqual([0, 0, 0])
expect(boundingSphere.radius).toBe(2)
expect(boundingSphere.center).toEqual([0, 0, 0])
})
})

View File

@@ -6,7 +6,7 @@
import { Sphere3D } from '../../mol-math/geometry'
import { Vec3 } from '../../mol-math/linear-algebra'
import { BoundaryHelper, HierarchyHelper } from '../../mol-math/geometry/boundary-helper';
import { BoundaryHelper } from '../../mol-math/geometry/boundary-helper';
export function calculateTextureInfo (n: number, itemSize: number) {
const sqN = Math.sqrt(n)
@@ -83,22 +83,14 @@ export function printImageData(imageData: ImageData, scale = 1, pixelated = fals
const v = Vec3.zero()
const boundaryHelperCoarse = new BoundaryHelper('14')
const boundaryHelperFine = new BoundaryHelper('98')
const hierarchyHelperCoarse = new HierarchyHelper('14')
const hierarchyHelperFine = new HierarchyHelper('98')
function getHelper(count: number) {
return count > 500_000 ? {
boundaryHelper: boundaryHelperCoarse,
hierarchyHelper: hierarchyHelperCoarse
} : {
boundaryHelper: boundaryHelperFine,
hierarchyHelper: hierarchyHelperFine
}
return count > 500_000 ? boundaryHelperCoarse : boundaryHelperFine
}
export function calculateInvariantBoundingSphere(position: Float32Array, positionCount: number, stepFactor: number): Sphere3D {
const step = stepFactor * 3
const { boundaryHelper, hierarchyHelper } = getHelper(positionCount)
const boundaryHelper = getHelper(positionCount)
boundaryHelper.reset()
for (let i = 0, _i = positionCount * 3; i < _i; i += step) {
@@ -111,49 +103,52 @@ export function calculateInvariantBoundingSphere(position: Float32Array, positio
boundaryHelper.radiusStep(v)
}
const hierarchyInput = boundaryHelper.getHierarchyInput()
if (hierarchyInput) {
hierarchyHelper.reset(hierarchyInput.sphere, hierarchyInput.normal)
const sphere = boundaryHelper.getSphere()
if (positionCount <= 98) {
const extrema: Vec3[] = []
for (let i = 0, _i = positionCount * 3; i < _i; i += step) {
Vec3.fromArray(v, position, i)
hierarchyHelper.includeStep(v)
extrema.push(Vec3.fromArray(Vec3(), position, i));
}
hierarchyHelper.finishedIncludeStep()
for (let i = 0, _i = positionCount * 3; i < _i; i += step) {
Vec3.fromArray(v, position, i)
hierarchyHelper.radiusStep(v)
}
return hierarchyHelper.getSphere()
} else {
return boundaryHelper.getSphere()
Sphere3D.setExtrema(sphere, extrema)
}
return sphere
}
export function calculateTransformBoundingSphere(invariantBoundingSphere: Sphere3D, transform: Float32Array, transformCount: number): Sphere3D {
const { boundaryHelper } = getHelper(transformCount)
const boundaryHelper = getHelper(transformCount)
boundaryHelper.reset()
const transformedSpheres: Sphere3D[] = []
for (const b of Sphere3D.getList(invariantBoundingSphere)) {
const { center, radius, extrema } = invariantBoundingSphere
if (extrema) {
for (let i = 0, _i = transformCount; i < _i; ++i) {
const c = Vec3.transformMat4Offset(Vec3(), b.center, transform, 0, 0, i * 16)
transformedSpheres.push(Sphere3D.create(c as Vec3, b.radius))
for (const e of extrema) {
Vec3.transformMat4Offset(v, e, transform, 0, 0, i * 16)
boundaryHelper.includeStep(v)
}
}
boundaryHelper.finishedIncludeStep()
for (let i = 0, _i = transformCount; i < _i; ++i) {
for (const e of extrema) {
Vec3.transformMat4Offset(v, e, transform, 0, 0, i * 16)
boundaryHelper.radiusStep(v)
}
}
} else {
for (let i = 0, _i = transformCount; i < _i; ++i) {
Vec3.transformMat4Offset(v, center, transform, 0, 0, i * 16)
boundaryHelper.includeSphereStep(v, radius)
}
boundaryHelper.finishedIncludeStep()
for (let i = 0, _i = transformCount; i < _i; ++i) {
Vec3.transformMat4Offset(v, center, transform, 0, 0, i * 16)
boundaryHelper.radiusSphereStep(v, radius)
}
}
for (const b of transformedSpheres) {
boundaryHelper.includeSphereStep(b.center, b.radius)
}
boundaryHelper.finishedIncludeStep()
for (const b of transformedSpheres) {
boundaryHelper.radiusSphereStep(b.center, b.radius)
}
const sphere = boundaryHelper.getSphere()
if (transformedSpheres.length > 1) {
(sphere as Sphere3D.Hierarchy).hierarchy = transformedSpheres
}
return sphere
return boundaryHelper.getSphere()
}
export function calculateBoundingSphere(position: Float32Array, positionCount: number, transform: Float32Array, transformCount: number, padding = 0, stepFactor = 1): { boundingSphere: Sphere3D, invariantBoundingSphere: Sphere3D } {

View File

@@ -15,6 +15,7 @@ import { CommitQueue } from './commit-queue';
import { now } from '../mol-util/now';
import { arraySetRemove } from '../mol-util/array';
import { BoundaryHelper } from '../mol-math/geometry/boundary-helper';
import { hash1 } from '../mol-data/util';
const boundaryHelper = new BoundaryHelper('98')
@@ -22,20 +23,32 @@ function calculateBoundingSphere(renderables: Renderable<RenderableValues & Base
boundaryHelper.reset();
for (let i = 0, il = renderables.length; i < il; ++i) {
if (!renderables[i].state.visible) continue;
const boundingSphere = renderables[i].values.boundingSphere.ref.value
if (!boundingSphere.radius) continue;
for (const b of Sphere3D.getList(boundingSphere)) {
boundaryHelper.includeSphereStep(b.center, b.radius);
if (Sphere3D.hasExtrema(boundingSphere)) {
for (const e of boundingSphere.extrema) {
boundaryHelper.includeStep(e)
}
} else {
boundaryHelper.includeSphereStep(boundingSphere.center, boundingSphere.radius);
}
}
boundaryHelper.finishedIncludeStep();
for (let i = 0, il = renderables.length; i < il; ++i) {
if (!renderables[i].state.visible) continue;
const boundingSphere = renderables[i].values.boundingSphere.ref.value
if (!boundingSphere.radius) continue;
for (const b of Sphere3D.getList(boundingSphere)) {
boundaryHelper.radiusSphereStep(b.center, b.radius);
if (Sphere3D.hasExtrema(boundingSphere)) {
for (const e of boundingSphere.extrema) {
boundaryHelper.radiusStep(e)
}
} else {
boundaryHelper.radiusSphereStep(boundingSphere.center, boundingSphere.radius);
}
}
@@ -62,6 +75,8 @@ interface Scene extends Object3D {
readonly renderables: ReadonlyArray<Renderable<RenderableValues & BaseValues>>
readonly boundingSphere: Sphere3D
/** Returns `true` if some visibility has changed, `false` otherwise. */
syncVisibility: () => boolean
update: (objects: ArrayLike<GraphicsRenderObject> | undefined, keepBoundingSphere: boolean, isRemoving?: boolean) => void
add: (o: GraphicsRenderObject) => void // Renderable<any>
remove: (o: GraphicsRenderObject) => void
@@ -76,7 +91,7 @@ namespace Scene {
export function create(ctx: WebGLContext): Scene {
const renderableMap = new Map<GraphicsRenderObject, Renderable<RenderableValues & BaseValues>>()
const renderables: Renderable<RenderableValues & BaseValues>[] = []
const boundingSphere = Sphere3D.zero()
const boundingSphere = Sphere3D()
let boundingSphereDirty = true
@@ -131,13 +146,35 @@ namespace Scene {
const commitQueue = new CommitQueue();
let visibleHash = -1
function computeVisibleHash() {
let hash = 23
for (let i = 0, il = renderables.length; i < il; ++i) {
if (!renderables[i].state.visible) continue;
hash = (31 * hash + renderables[i].id) | 0;
}
hash = hash1(hash);
if (hash === -1) hash = 0;
return hash
}
function syncVisibility() {
const newVisibleHash = computeVisibleHash()
if (newVisibleHash !== visibleHash) {
boundingSphereDirty = true
return true
} else {
return false
}
}
return {
get view () { return object3d.view },
get position () { return object3d.position },
get direction () { return object3d.direction },
get up () { return object3d.up },
// get isCommiting () { return commitQueue.length > 0 },
syncVisibility,
update(objects, keepBoundingSphere, isRemoving) {
Object3D.update(object3d)
if (objects) {
@@ -151,7 +188,11 @@ namespace Scene {
renderables[i].update()
}
}
if (!keepBoundingSphere) boundingSphereDirty = true
if (!keepBoundingSphere) {
boundingSphereDirty = true
} else {
syncVisibility()
}
},
add: (o: GraphicsRenderObject) => commitQueue.add(o),
remove: (o: GraphicsRenderObject) => commitQueue.remove(o),
@@ -176,8 +217,11 @@ namespace Scene {
},
renderables,
get boundingSphere() {
if (boundingSphereDirty) calculateBoundingSphere(renderables, boundingSphere)
boundingSphereDirty = false
if (boundingSphereDirty) {
calculateBoundingSphere(renderables, boundingSphere)
boundingSphereDirty = false
visibleHash = computeVisibleHash()
}
return boundingSphere
}
}

View File

@@ -37,7 +37,6 @@ import assign_color_varying from './shader/chunks/assign-color-varying.glsl'
import assign_group from './shader/chunks/assign-group.glsl'
import assign_marker_varying from './shader/chunks/assign-marker-varying.glsl'
import assign_material_color from './shader/chunks/assign-material-color.glsl'
import assign_normal from './shader/chunks/assign-normal.glsl'
import assign_position from './shader/chunks/assign-position.glsl'
import assign_size from './shader/chunks/assign-size.glsl'
import check_picking_alpha from './shader/chunks/check-picking-alpha.glsl'
@@ -63,7 +62,6 @@ const ShaderChunks: { [k: string]: string } = {
assign_group,
assign_marker_varying,
assign_material_color,
assign_normal,
assign_position,
assign_size,
check_picking_alpha,

View File

@@ -1,12 +0,0 @@
export default `
#if defined(dFlatShaded) && defined(enabledStandardDerivatives)
vec3 fdx = dFdx(vViewPosition);
vec3 fdy = dFdy(vViewPosition);
vec3 normal = -normalize(cross(fdx, fdy));
#else
vec3 normal = -normalize(vNormal);
#ifdef dDoubleSided
normal = normal * (float(frontFacing) * 2.0 - 1.0);
#endif
#endif
`

View File

@@ -25,7 +25,11 @@ void main() {
bool frontFacing = dot(vNormal, vViewPosition) < 0.0;
#endif
interior = !frontFacing; // TODO take dFlipSided into account
#if defined(dFlipSided)
interior = frontFacing;
#else
interior = !frontFacing;
#endif
#include assign_material_color
@@ -38,7 +42,14 @@ void main() {
#ifdef dIgnoreLight
gl_FragColor = material;
#else
#include assign_normal
#if defined(dFlatShaded) && defined(enabledStandardDerivatives)
vec3 normal = -faceNormal;
#else
vec3 normal = -normalize(vNormal);
#ifdef dDoubleSided
normal = normal * (float(frontFacing) * 2.0 - 1.0);
#endif
#endif
#include apply_light_color
#endif

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
@@ -7,6 +7,7 @@
import { GridLookup3D } from '../../geometry';
import { sortArray } from '../../../mol-data/util';
import { OrderedSet } from '../../../mol-data/int';
import { getBoundary } from '../boundary';
const xs = [0, 0, 1];
const ys = [0, 1, 0];
@@ -15,7 +16,9 @@ const rs = [0, 0.5, 1/3];
describe('GridLookup3d', () => {
it('basic', () => {
const grid = GridLookup3D({ x: xs, y: ys, z: zs, indices: OrderedSet.ofBounds(0, 3) });
const position = { x: xs, y: ys, z: zs, indices: OrderedSet.ofBounds(0, 3) }
const boundary = getBoundary(position)
const grid = GridLookup3D(position, boundary);
let r = grid.find(0, 0, 0, 0);
expect(r.count).toBe(1);
@@ -27,7 +30,9 @@ describe('GridLookup3d', () => {
});
it('radius', () => {
const grid = GridLookup3D({ x: xs, y: ys, z: zs, radius: [0, 0.5, 1 / 3], indices: OrderedSet.ofBounds(0, 3) });
const position = { x: xs, y: ys, z: zs, radius: [0, 0.5, 1 / 3], indices: OrderedSet.ofBounds(0, 3) }
const boundary = getBoundary(position)
const grid = GridLookup3D(position, boundary);
let r = grid.find(0, 0, 0, 0);
expect(r.count).toBe(1);
@@ -39,7 +44,9 @@ describe('GridLookup3d', () => {
});
it('indexed', () => {
const grid = GridLookup3D({ x: xs, y: ys, z: zs, indices: OrderedSet.ofSingleton(1), radius: rs });
const position = { x: xs, y: ys, z: zs, indices: OrderedSet.ofSingleton(1), radius: rs }
const boundary = getBoundary(position)
const grid = GridLookup3D(position, boundary);
let r = grid.find(0, 0, 0, 0);
expect(r.count).toBe(0);

View File

@@ -11,8 +11,6 @@ import { Box3D } from './primitives/box3d';
// implementing http://www.ep.liu.se/ecp/034/009/ecp083409.pdf
const MinThresholdDist = 0.1
export class BoundaryHelper {
private dir: Vec3[]
@@ -74,36 +72,17 @@ export class BoundaryHelper {
this.centroidHelper.radiusSphereStep(center, radius);
}
getHierarchyInput() {
if (this.centroidHelper.getCount() < 2) return false
const sphere = this.centroidHelper.getSphere();
const normal = Vec3()
const t = sphere.radius * this.hierarchyThresholdFactor
let maxDist = -Infinity
let belowThreshold = false
for (let i = 0; i < this.extrema.length; i += 2) {
const halfDist = Vec3.distance(this.extrema[i], this.extrema[i + 1]) / 2
if (halfDist > maxDist) {
maxDist = halfDist
Vec3.normalize(normal, Vec3.sub(normal, this.extrema[i], this.extrema[i + 1]))
}
if (halfDist < t) belowThreshold = true
}
return (belowThreshold && maxDist > MinThresholdDist) ? { sphere, normal } : false
}
getSphere(sphere?: Sphere3D) {
return this.centroidHelper.getSphere(sphere)
return Sphere3D.setExtrema(this.centroidHelper.getSphere(sphere), this.extrema)
}
getBox(box?: Box3D) {
// TODO can we get a tighter box from the extrema???
if (!box) box = Box3D()
return Box3D.fromSphere3D(box, this.centroidHelper.getSphere())
Box3D.setEmpty(box)
for (let i = 0; i < this.extrema.length; i++) {
Box3D.add(box, this.extrema[i])
}
return box
}
reset() {
@@ -116,69 +95,12 @@ export class BoundaryHelper {
this.centroidHelper.reset()
}
constructor(quality: EposQuality, private hierarchyThresholdFactor = 0.66) {
constructor(quality: EposQuality) {
this.dir = getEposDir(quality)
this.reset()
}
}
export class HierarchyHelper {
private tmpV = Vec3()
private tmpS = Sphere3D()
private sphere = Sphere3D()
private normal = Vec3()
private helperA = new BoundaryHelper(this.quality)
private helperB = new BoundaryHelper(this.quality)
private checkSide(p: Vec3) {
return Vec3.dot(this.normal, Vec3.sub(this.tmpV, this.sphere.center, p)) > 0
}
includeStep(p: Vec3) {
if (this.checkSide(p)) {
this.helperA.includeStep(p)
} else {
this.helperB.includeStep(p)
}
}
finishedIncludeStep() {
this.helperA.finishedIncludeStep();
this.helperB.finishedIncludeStep();
}
radiusStep(p: Vec3) {
if (this.checkSide(p)) {
this.helperA.radiusStep(p)
} else {
this.helperB.radiusStep(p)
}
}
getSphere(): Sphere3D {
const sphereA = this.helperA.getSphere()
const sphereB = this.helperB.getSphere()
Sphere3D.expandBySphere(this.tmpS, this.sphere, sphereA)
Sphere3D.expandBySphere(this.tmpS, this.tmpS, sphereB)
// check if the split spheres actually result in a smaller radius
return this.tmpS.radius < this.sphere.radius ? {
center: Vec3.clone(this.sphere.center),
radius: this.sphere.radius,
hierarchy: [this.helperA.getSphere(), this.helperB.getSphere()]
} : Sphere3D.clone(this.sphere)
}
reset(sphere: Sphere3D, normal: Vec3) {
Sphere3D.copy(this.sphere, sphere)
Vec3.copy(this.normal, normal)
this.helperA.reset()
this.helperB.reset()
}
constructor(private quality: EposQuality) { }
}
type EposQuality = '6' | '14' | '26' | '98'
function getEposDir(quality: EposQuality) {

View File

@@ -0,0 +1,52 @@
/**
* Copyright (c) 2018-2020 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 { PositionData } from './common';
import { Vec3 } from '../linear-algebra';
import { OrderedSet } from '../../mol-data/int';
import { BoundaryHelper } from './boundary-helper';
import { Box3D, Sphere3D } from '../geometry';
const boundaryHelperCoarse = new BoundaryHelper('14');
const boundaryHelperFine = new BoundaryHelper('98');
function getBoundaryHelper(count: number) {
return count > 100_000 ? boundaryHelperCoarse : boundaryHelperFine
}
export type Boundary = { readonly box: Box3D, readonly sphere: Sphere3D }
export function getBoundary(data: PositionData): Boundary {
const { x, y, z, radius, indices } = data;
const p = Vec3();
const boundaryHelper = getBoundaryHelper(OrderedSet.size(indices));
boundaryHelper.reset();
for (let t = 0, _t = OrderedSet.size(indices); t < _t; t++) {
const i = OrderedSet.getAt(indices, t);
Vec3.set(p, x[i], y[i], z[i]);
boundaryHelper.includeSphereStep(p, (radius && radius[i]) || 0);
}
boundaryHelper.finishedIncludeStep();
for (let t = 0, _t = OrderedSet.size(indices); t < _t; t++) {
const i = OrderedSet.getAt(indices, t);
Vec3.set(p, x[i], y[i], z[i]);
boundaryHelper.radiusSphereStep(p, (radius && radius[i]) || 0);
}
const sphere = boundaryHelper.getSphere()
if (!radius && OrderedSet.size(indices) <= 98) {
const extrema: Vec3[] = []
for (let t = 0, _t = OrderedSet.size(indices); t < _t; t++) {
const i = OrderedSet.getAt(indices, t);
extrema.push(Vec3.create(x[i], y[i], z[i]));
}
Sphere3D.setExtrema(sphere, extrema)
}
return { box: boundaryHelper.getBox(), sphere };
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2020 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,14 +11,14 @@ import { Sphere3D } from '../primitives/sphere3d';
import { PositionData } from '../common';
import { Vec3 } from '../../linear-algebra';
import { OrderedSet } from '../../../mol-data/int';
import { BoundaryHelper } from '../boundary-helper';
import { Boundary } from '../../../mol-model/structure/structure/util/boundary';
interface GridLookup3D<T = number> extends Lookup3D<T> {
readonly buckets: { readonly offset: ArrayLike<number>, readonly count: ArrayLike<number>, readonly array: ArrayLike<number> }
}
function GridLookup3D<T extends number = number>(data: PositionData, cellSizeOrCount?: Vec3 | number): GridLookup3D<T> {
return new GridLookup3DImpl<T>(data, cellSizeOrCount);
function GridLookup3D<T extends number = number>(data: PositionData, boundary: Boundary, cellSizeOrCount?: Vec3 | number): GridLookup3D<T> {
return new GridLookup3DImpl<T>(data, boundary, cellSizeOrCount);
}
export { GridLookup3D }
@@ -48,8 +48,8 @@ class GridLookup3DImpl<T extends number = number> implements GridLookup3D<T> {
return query(this.ctx);
}
constructor(data: PositionData, cellSizeOrCount?: Vec3 | number) {
const structure = build(data, cellSizeOrCount);
constructor(data: PositionData, boundary: Boundary, cellSizeOrCount?: Vec3 | number) {
const structure = build(data, boundary, cellSizeOrCount);
this.ctx = createContext<T>(structure);
this.boundary = { box: structure.boundingBox, sphere: structure.boundingSphere };
this.buckets = { offset: structure.bucketOffset, count: structure.bucketCounts, array: structure.bucketArray };
@@ -166,36 +166,9 @@ function _build(state: BuildState): Grid3D {
}
}
const boundaryHelperCoarse = new BoundaryHelper('14');
const boundaryHelperFine = new BoundaryHelper('98');
function getBoundaryHelper(count: number) {
return count > 500_000 ? boundaryHelperCoarse : boundaryHelperFine
}
function getBoundary(data: PositionData) {
const { x, y, z, radius, indices } = data;
const p = Vec3();
const boundaryHelper = getBoundaryHelper(OrderedSet.size(indices));
boundaryHelper.reset();
for (let t = 0, _t = OrderedSet.size(indices); t < _t; t++) {
const i = OrderedSet.getAt(indices, t);
Vec3.set(p, x[i], y[i], z[i]);
boundaryHelper.includeSphereStep(p, (radius && radius[i]) || 0);
}
boundaryHelper.finishedIncludeStep();
for (let t = 0, _t = OrderedSet.size(indices); t < _t; t++) {
const i = OrderedSet.getAt(indices, t);
Vec3.set(p, x[i], y[i], z[i]);
boundaryHelper.radiusSphereStep(p, (radius && radius[i]) || 0);
}
return { boundingBox: boundaryHelper.getBox(), boundingSphere: boundaryHelper.getSphere() };
}
function build(data: PositionData, cellSizeOrCount?: Vec3 | number) {
const { boundingBox, boundingSphere } = getBoundary(data);
function build(data: PositionData, boundary: Boundary, cellSizeOrCount?: Vec3 | number) {
// need to expand the grid bounds to avoid rounding errors
const expandedBox = Box3D.expand(Box3D.empty(), boundingBox, Vec3.create(0.5, 0.5, 0.5));
const expandedBox = Box3D.expand(Box3D.empty(), boundary.box, Vec3.create(0.5, 0.5, 0.5));
const { indices } = data;
const S = Box3D.size(Vec3.zero(), expandedBox);
@@ -233,8 +206,8 @@ function build(data: PositionData, cellSizeOrCount?: Vec3 | number) {
size,
data: inputData,
expandedBox,
boundingBox,
boundingSphere,
boundingBox: boundary.box,
boundingSphere: boundary.sphere,
elementCount,
delta
}

View File

@@ -15,6 +15,7 @@ import { PositionData } from './common';
import { Mat4 } from '../../mol-math/linear-algebra/3d';
import { Box3D, GridLookup3D, fillGridDim } from '../../mol-math/geometry';
import { BaseGeometry } from '../../mol-geo/geometry/base';
import { Boundary } from '../../mol-model/structure/structure/util/boundary';
function normalToLine (out: Vec3, p: Vec3) {
out[0] = out[1] = out[2] = 1.0
@@ -54,7 +55,7 @@ export const DefaultMolecularSurfaceCalculationProps = PD.getDefaultValues(Molec
export type MolecularSurfaceCalculationProps = typeof DefaultMolecularSurfaceCalculationProps
export async function calcMolecularSurface(ctx: RuntimeContext, position: Required<PositionData>, maxRadius: number, box: Box3D | null, props: MolecularSurfaceCalculationProps) {
export async function calcMolecularSurface(ctx: RuntimeContext, position: Required<PositionData>, boundary: Boundary, maxRadius: number, box: Box3D | null, props: MolecularSurfaceCalculationProps) {
// Field generation method adapted from AstexViewer (Mike Hartshorn) by Fred Ludlow.
// Other parts based heavily on NGL (Alexander Rose) EDT Surface class
@@ -320,7 +321,7 @@ export async function calcMolecularSurface(ctx: RuntimeContext, position: Requir
const cellSize = Vec3.create(maxRadius, maxRadius, maxRadius)
Vec3.scale(cellSize, cellSize, 2)
const lookup3d = GridLookup3D(position, cellSize)
const lookup3d = GridLookup3D(position, boundary, cellSize)
const neighbours = lookup3d.result
if (box === null) box = lookup3d.boundary.box

View File

@@ -8,24 +8,23 @@
import { Vec3, Mat4, EPSILON } from '../../linear-algebra'
import { PositionData } from '../common'
import { OrderedSet } from '../../../mol-data/int';
import { NumberArray } from '../../../mol-util/type-helpers';
import { NumberArray, PickRequired } from '../../../mol-util/type-helpers';
import { Box3D } from './box3d';
import { Axes3D } from './axes3d';
type Sphere3D = Sphere3D.Data | Sphere3D.Hierarchy
interface Sphere3D {
center: Vec3,
radius: number,
extrema?: Vec3[]
}
function Sphere3D() {
return Sphere3D.zero();
}
namespace Sphere3D {
export interface Data { center: Vec3, radius: number }
export interface Hierarchy extends Data { hierarchy: Sphere3D[] }
export function isHierarchy(x: Sphere3D | Hierarchy): x is Hierarchy {
return 'hierarchy' in x
}
export function getList(sphere: Sphere3D) {
return Sphere3D.isHierarchy(sphere) ? sphere.hierarchy : [sphere]
export function hasExtrema(sphere: Sphere3D): sphere is PickRequired<Sphere3D, 'extrema'> {
return sphere.extrema !== undefined
}
export function create(center: Vec3, radius: number): Sphere3D { return { center, radius }; }
@@ -33,24 +32,27 @@ namespace Sphere3D {
export function clone(a: Sphere3D): Sphere3D {
const out = create(Vec3.clone(a.center), a.radius)
if (isHierarchy(a)) (out as Hierarchy).hierarchy = a.hierarchy
if (hasExtrema(a)) out.extrema = a.extrema
return out;
}
export function copy(out: Sphere3D, a: Sphere3D) {
Vec3.copy(out.center, a.center)
out.radius = a.radius
if (isHierarchy(a)) {
if (isHierarchy(out)) {
out.hierarchy.length = 0
out.hierarchy.push(...a.hierarchy)
} else {
(out as Hierarchy).hierarchy = a.hierarchy
}
}
if (hasExtrema(a)) setExtrema(out, a.extrema)
return out;
}
export function setExtrema(out: Sphere3D, extrema: Vec3[]): Sphere3D {
if (out.extrema !== undefined) {
out.extrema.length = 0
out.extrema.push(...extrema)
} else {
out.extrema = [...extrema]
}
return out
}
export function computeBounding(data: PositionData): Sphere3D {
const { x, y, z, indices } = data;
let cx = 0, cy = 0, cz = 0;
@@ -129,18 +131,18 @@ namespace Sphere3D {
return out
}
const tmpDir = Vec3()
/** Expand sphere radius by delta */
export function expand(out: Sphere3D, sphere: Sphere3D, delta: number): Sphere3D {
Vec3.copy(out.center, sphere.center)
out.radius = sphere.radius + delta
if (isHierarchy(sphere)) {
const hierarchy = sphere.hierarchy.map(s => expand(Sphere3D(), s, delta))
if (isHierarchy(out)) {
out.hierarchy.length = 0
out.hierarchy.push(...hierarchy)
} else {
(out as Hierarchy).hierarchy = hierarchy
}
if (hasExtrema(sphere)) {
setExtrema(out, sphere.extrema.map(e => {
Vec3.sub(tmpDir, e, sphere.center)
const dist = Vec3.distance(sphere.center, e)
Vec3.normalize(tmpDir, tmpDir)
return Vec3.scaleAndAdd(Vec3(), sphere.center, tmpDir, dist + delta)
}))
}
return out
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -11,6 +11,7 @@ import { OrderedSet, SortedArray } from '../../../mol-data/int';
import { FeatureGroup, FeatureType } from './common';
import { ValenceModelProvider } from '../valence-model';
import { Vec3 } from '../../../mol-math/linear-algebra';
import { getBoundary } from '../../../mol-math/geometry/boundary';
export { Features }
@@ -105,7 +106,11 @@ namespace Features {
return {
...data,
get lookup3d() {
return lookup3d || (lookup3d = GridLookup3D({ x: data.x, y: data.y, z: data.z, indices: OrderedSet.ofBounds(0 as FeatureIndex, data.count as FeatureIndex) }))
if (!lookup3d) {
const position = { x: data.x, y: data.y, z: data.z, indices: OrderedSet.ofBounds(0 as FeatureIndex, data.count as FeatureIndex) }
lookup3d = GridLookup3D(position, getBoundary(position))
}
return lookup3d
},
get elementsIndex() {
return elementsIndex || (elementsIndex = createElementsIndex(data, elementsCount))
@@ -128,7 +133,11 @@ namespace Features {
return {
indices,
get lookup3d() {
return lookup3d || (lookup3d = GridLookup3D({ x: data.x, y: data.y, z: data.z, indices }))
if (!lookup3d) {
const position = { x: data.x, y: data.y, z: data.z, indices }
lookup3d = GridLookup3D(position, getBoundary(position))
}
return lookup3d
}
}
}

View File

@@ -12,8 +12,8 @@ import { Unit } from '../../mol-model/structure/structure';
import { CustomStructureProperty } from '../common/custom-structure-property';
import { CustomProperty } from '../common/custom-property';
import { ModelSecondaryStructure } from '../../mol-model-formats/structure/property/secondary-structure';
import { MmcifFormat } from '../../mol-model-formats/structure/mmcif';
import { CustomPropertyDescriptor } from '../../mol-model/structure/common/custom-property';
import { Model } from '../../mol-model/structure/model';
function getSecondaryStructureParams(data?: Structure) {
let defaultType = 'model' as 'model' | 'dssp'
@@ -21,16 +21,11 @@ function getSecondaryStructureParams(data?: Structure) {
defaultType = 'dssp'
for (let i = 0, il = data.models.length; i < il; ++i) {
const m = data.models[i]
if (MmcifFormat.is(m.sourceData)) {
if (m.sourceData.data.db.struct_conf.id.isDefined ||
m.sourceData.data.db.struct_sheet_range.id.isDefined ||
m.sourceData.data.db.database_2.database_id.isDefined
) {
// if there is any secondary structure definition given or if there is
// an archival model, don't calculate dssp by default
defaultType = 'model'
break
}
if (Model.isFromPdbArchive(m) || Model.hasSecondaryStructure(m)) {
// if there is any secondary structure definition given or if there is
// an archival model, don't calculate dssp by default
defaultType = 'model'
break
}
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -8,6 +8,7 @@ import { GridLookup3D } from '../../../../mol-math/geometry';
import { SortedArray } from '../../../../mol-data/int';
import { Unit } from '../../../../mol-model/structure/structure';
import { ResidueIndex } from '../../../../mol-model/structure';
import { getBoundary } from '../../../../mol-math/geometry/boundary';
export function calcUnitProteinTraceLookup3D(unit: Unit.Atomic, unitProteinResidues: SortedArray<ResidueIndex>): GridLookup3D {
const { x, y, z } = unit.model.atomicConformation;
@@ -16,5 +17,6 @@ export function calcUnitProteinTraceLookup3D(unit: Unit.Atomic, unitProteinResid
for (let i = 0, il = unitProteinResidues.length; i < il; ++i) {
indices[i] = traceElementIndex[unitProteinResidues[i]]
}
return GridLookup3D({ x, y, z, indices: SortedArray.ofSortedArray(indices) });
const position = { x, y, z, indices: SortedArray.ofSortedArray(indices) }
return GridLookup3D(position, getBoundary(position));
}

View File

@@ -241,7 +241,7 @@ 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)) {
// convert Bond.Loci to a StructureElement.Loci so granularity can be applied

View File

@@ -51,7 +51,7 @@ const struct_conf_fields: CifField[] = [
CifField.str<number, SSElement<SecondaryStructure.Helix>[]>('details', (i, data) => data[i].element.details || '', {
valueKind: (i, d) => !!d[i].element.details ? Column.ValueKind.Present : Column.ValueKind.Unknown
}),
CifField.int<number, SSElement<SecondaryStructure.Helix>[]>('pdbx_PDB_helix_class', (i, data) => data[i].length)
CifField.int<number, SSElement<SecondaryStructure.Helix>[]>('pdbx_PDB_helix_length', (i, data) => data[i].length)
];
const struct_sheet_range_fields: CifField[] = [

View File

@@ -21,6 +21,7 @@ import { Topology } from '../topology';
import { Task } from '../../../mol-task';
import { IndexPairBonds } from '../../../mol-model-formats/structure/property/bonds/index-pair';
import { createModels } from '../../../mol-model-formats/structure/basic/parser';
import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
/**
* Interface to the "source data" of the molecule.
@@ -135,4 +136,82 @@ export namespace Model {
model._dynamicPropertyData[CenterProp] = center
return center
}
//
export function isFromPdbArchive(model: Model) {
if (!MmcifFormat.is(model.sourceData)) return false
const { db } = model.sourceData.data
return (
db.database_2.database_id.isDefined
)
}
export function hasSecondaryStructure(model: Model) {
if (!MmcifFormat.is(model.sourceData)) return false
const { db } = model.sourceData.data
return (
db.struct_conf.id.isDefined ||
db.struct_sheet_range.id.isDefined
)
}
export function hasCrystalSymmetry(model: Model) {
if (!MmcifFormat.is(model.sourceData)) return false
const { db } = model.sourceData.data
return (
db.symmetry._rowCount === 1 && db.cell._rowCount === 1 && !(
db.symmetry.Int_Tables_number.value(0) === 1 &&
db.cell.angle_alpha.value(0) === 90 &&
db.cell.angle_beta.value(0) === 90 &&
db.cell.angle_gamma.value(0) === 90 &&
db.cell.length_a.value(0) === 1 &&
db.cell.length_b.value(0) === 1 &&
db.cell.length_c.value(0) === 1
)
)
}
export function isFromXray(model: Model) {
if (!MmcifFormat.is(model.sourceData)) return false
const { db } = model.sourceData.data
for (let i = 0; i < db.exptl.method.rowCount; i++) {
const v = db.exptl.method.value(i).toUpperCase()
if (v.indexOf('DIFFRACTION') >= 0) return true
}
return false
}
export function isFromNmr(model: Model) {
if (!MmcifFormat.is(model.sourceData)) return false
const { db } = model.sourceData.data
for (let i = 0; i < db.exptl.method.rowCount; i++) {
const v = db.exptl.method.value(i).toUpperCase()
if (v.indexOf('NMR') >= 0) return true
}
return false
}
export function hasXrayMap(model: Model) {
if (!MmcifFormat.is(model.sourceData)) return false
const { db } = model.sourceData.data
return db.pdbx_database_status.status_code_sf.value(0) === 'REL'
}
export function hasEmMap(model: Model) {
if (!MmcifFormat.is(model.sourceData)) return false
const { db } = model.sourceData.data
let hasEmMap = false
for (let i = 0, il = db.pdbx_database_related._rowCount; i < il; ++i) {
if (db.pdbx_database_related.db_name.value(i).toUpperCase() === 'EMDB') {
hasEmMap = true
break
}
}
return hasEmMap
}
export function hasDensityMap(model: Model) {
return hasXrayMap(model) || hasEmMap(model)
}
}

View File

@@ -28,6 +28,7 @@ import { UUID } from '../../../mol-util';
import { CustomProperties } from '../common/custom-property';
import { AtomicHierarchy } from '../model/properties/atomic';
import { StructureSelection } from '../query/selection';
import { getBoundary } from '../../../mol-math/geometry/boundary';
class Structure {
/** Maps unit.id to unit */
@@ -696,7 +697,8 @@ namespace Structure {
function partitionAtomicUnitByAtom(model: Model, indices: SortedArray, builder: StructureBuilder, multiChain: boolean) {
const { x, y, z } = model.atomicConformation;
const lookup = GridLookup3D({ x, y, z, indices }, 8192);
const position = { x, y, z, indices }
const lookup = GridLookup3D(position, getBoundary(position), 8192);
const { offset, count, array } = lookup.buckets;
const traits = (multiChain ? Unit.Trait.MultiChain : Unit.Trait.None) | (offset.length > 1 ? Unit.Trait.Partitioned : Unit.Trait.None);
@@ -731,7 +733,8 @@ namespace Structure {
const gridCellCount = 512 * firstResidueAtomCount
const { x, y, z } = model.atomicConformation;
const lookup = GridLookup3D({ x, y, z, indices: SortedArray.ofSortedArray(startIndices) }, gridCellCount);
const position = { x, y, z, indices: SortedArray.ofSortedArray(startIndices) }
const lookup = GridLookup3D(position, getBoundary(position), gridCellCount);
const { offset, count, array } = lookup.buckets;
const traits = (multiChain ? Unit.Trait.MultiChain : Unit.Trait.None) | (offset.length > 1 ? Unit.Trait.Partitioned : Unit.Trait.None);

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2017-2020 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>
@@ -20,6 +20,7 @@ import { getAtomicPolymerElements, getCoarsePolymerElements, getAtomicGapElement
import { mmCIF_Schema } from '../../../mol-io/reader/cif/schema/mmcif';
import { PrincipalAxes } from '../../../mol-math/linear-algebra/matrix/principal-axes';
import { getPrincipalAxes } from './util/principal-axes';
import { Boundary, getBoundary } from '../../../mol-math/geometry/boundary';
/**
* A building block of a structure that corresponds to an atomic or
@@ -128,6 +129,7 @@ namespace Unit {
getChild(elements: StructureElement.Set): Unit,
applyOperator(id: number, operator: SymmetryOperator, dontCompose?: boolean /* = false */): Unit,
readonly boundary: Boundary
readonly lookup3d: Lookup3D<StructureElement.UnitIndex>
readonly polymerElements: SortedArray<ElementIndex>
readonly gapElements: SortedArray<ElementIndex>
@@ -138,6 +140,7 @@ namespace Unit {
}
interface BaseProperties {
boundary: ValueRef<Boundary | undefined>,
lookup3d: ValueRef<Lookup3D<StructureElement.UnitIndex> | undefined>,
principalAxes: ValueRef<PrincipalAxes | undefined>,
polymerElements: ValueRef<SortedArray<ElementIndex> | undefined>
@@ -147,6 +150,7 @@ namespace Unit {
function BaseProperties(): BaseProperties {
return {
boundary: ValueRef.create(void 0),
lookup3d: ValueRef.create(void 0),
principalAxes: ValueRef.create(void 0),
polymerElements: ValueRef.create(void 0),
@@ -203,10 +207,17 @@ namespace Unit {
return new Atomic(id, this.invariantId, this.chainGroupId, this.traits, this.model, this.elements, SymmetryOperator.createMapping(op, this.model.atomicConformation, this.conformation.r), this.props);
}
get boundary() {
if (this.props.boundary.ref) return this.props.boundary.ref;
const { x, y, z } = this.model.atomicConformation;
this.props.boundary.ref = getBoundary({ x, y, z, indices: this.elements });
return this.props.boundary.ref;
}
get lookup3d() {
if (this.props.lookup3d.ref) return this.props.lookup3d.ref;
const { x, y, z } = this.model.atomicConformation;
this.props.lookup3d.ref = GridLookup3D({ x, y, z, indices: this.elements });
this.props.lookup3d.ref = GridLookup3D({ x, y, z, indices: this.elements }, this.boundary);
return this.props.lookup3d.ref;
}
@@ -333,11 +344,19 @@ namespace Unit {
return ret;
}
get boundary() {
if (this.props.boundary.ref) return this.props.boundary.ref;
// TODO: support sphere radius?
const { x, y, z } = this.getCoarseConformation();
this.props.boundary.ref = getBoundary({ x, y, z, indices: this.elements });
return this.props.boundary.ref;
}
get lookup3d() {
if (this.props.lookup3d.ref) return this.props.lookup3d.ref;
// TODO: support sphere radius?
const { x, y, z } = this.getCoarseConformation();
this.props.lookup3d.ref = GridLookup3D({ x, y, z, indices: this.elements });
this.props.lookup3d.ref = GridLookup3D({ x, y, z, indices: this.elements }, this.boundary);
return this.props.lookup3d.ref;
}

View File

@@ -13,6 +13,7 @@ import { OrderedSet } from '../../../../mol-data/int';
import { StructureUniqueSubsetBuilder } from './unique-subset-builder';
import StructureElement from '../element';
import Unit from '../unit';
import { getBoundary } from '../../../../mol-math/geometry/boundary';
export interface StructureResult extends Result<StructureElement.UnitIndex> {
units: Unit[]
@@ -174,6 +175,7 @@ export class StructureLookup3D {
radius[i] = s.radius;
}
this.unitLookup = GridLookup3D({ x: xs, y: ys, z: zs, radius, indices: OrderedSet.ofBounds(0, unitCount) });
const position = { x: xs, y: ys, z: zs, radius, indices: OrderedSet.ofBounds(0, unitCount) }
this.unitLookup = GridLookup3D(position, getBoundary(position));
}
}

View File

@@ -19,6 +19,8 @@ import { Download, ParsePsf } from '../transforms/data';
import { CoordinatesFromDcd, CustomModelProperties, CustomStructureProperties, TopologyFromPsf, TrajectoryFromModelAndCoordinates } from '../transforms/model';
import { DataFormatProvider, guessCifVariant } from './data-format';
// TODO make unitcell creation part of preset
export const MmcifProvider: DataFormatProvider<PluginStateObject.Data.String | PluginStateObject.Data.Binary> = {
label: 'mmCIF',
description: 'mmCIF',
@@ -32,9 +34,10 @@ export const MmcifProvider: DataFormatProvider<PluginStateObject.Data.String | P
},
getDefaultBuilder: (ctx: PluginContext, data, options) => {
return Task.create('mmCIF default builder', async taskCtx => {
const { structure } = await ctx.builders.structure.parseStructure({ data, dataFormat: 'mmcif' });
const { structure, model } = await ctx.builders.structure.parseStructure({ data, dataFormat: 'mmcif' });
if (options.visuals) {
await ctx.builders.structure.representation.applyPreset(structure, 'auto');
await ctx.builders.structure.createUnitcell(model, undefined, { isHidden: true })
}
})
}
@@ -50,9 +53,10 @@ export const PdbProvider: DataFormatProvider<any> = {
},
getDefaultBuilder: (ctx: PluginContext, data, options) => {
return Task.create('PDB default builder', async () => {
const { structure } = await ctx.builders.structure.parseStructure({ data, dataFormat: 'pdb' });
const { structure, model } = await ctx.builders.structure.parseStructure({ data, dataFormat: 'pdb' });
if (options.visuals) {
await ctx.builders.structure.representation.applyPreset(structure, 'auto');
await ctx.builders.structure.createUnitcell(model, undefined, { isHidden: true })
}
})
}
@@ -68,9 +72,10 @@ export const GroProvider: DataFormatProvider<any> = {
},
getDefaultBuilder: (ctx: PluginContext, data, options) => {
return Task.create('GRO default builder', async () => {
const { structure } = await ctx.builders.structure.parseStructure({ data, dataFormat: 'gro' });
const { structure, model } = await ctx.builders.structure.parseStructure({ data, dataFormat: 'gro' });
if (options.visuals) {
await ctx.builders.structure.representation.applyPreset(structure, 'auto');
await ctx.builders.structure.createUnitcell(model, undefined, { isHidden: true })
}
})
}
@@ -86,9 +91,10 @@ export const Provider3dg: DataFormatProvider<any> = {
},
getDefaultBuilder: (ctx: PluginContext, data, options) => {
return Task.create('3DG default builder', async () => {
const { structure } = await ctx.builders.structure.parseStructure({ data, dataFormat: '3dg' });
const { structure, model } = await ctx.builders.structure.parseStructure({ data, dataFormat: '3dg' });
if (options.visuals) {
await ctx.builders.structure.representation.applyPreset(structure, 'auto');
await ctx.builders.structure.createUnitcell(model, undefined, { isHidden: true })
}
})
}
@@ -238,8 +244,8 @@ const DownloadStructure = StateAction.build({
const blob = await plugin.builders.data.downloadBlob({
sources: downloadParams.map((src, i) => ({ id: '' + i, url: src.url, isBinary: src.isBinary })),
maxConcurrency: 6
}, { state: { isGhost: true } });
const { structure } = await plugin.builders.structure.parseStructure({
}, { state: { isGhost: true } });
const { structure, model } = await plugin.builders.structure.parseStructure({
blob,
blobParams: { formats: downloadParams.map((_, i) => ({ id: '' + i, format: 'cif' as 'cif' })) },
modelProperties: supportProps,
@@ -247,11 +253,12 @@ const DownloadStructure = StateAction.build({
});
if (createRepr) {
await plugin.builders.structure.representation.applyPreset(structure, 'auto');
await plugin.builders.structure.createUnitcell(model, undefined, { isHidden: true })
}
} else {
for (const download of downloadParams) {
const data = await plugin.builders.data.download(download, { state: { isGhost: true } });
const { structure } = await plugin.builders.structure.parseStructure({
const { structure, model } = await plugin.builders.structure.parseStructure({
data,
dataFormat: format,
modelProperties: supportProps,
@@ -259,6 +266,7 @@ const DownloadStructure = StateAction.build({
});
if (createRepr) {
await plugin.builders.structure.representation.applyPreset(structure, 'auto');
await plugin.builders.structure.createUnitcell(model, undefined, { isHidden: true })
}
}
}

View File

@@ -26,7 +26,7 @@ export class DataBuilder {
return data.selector;
}
async downloadBlob(params: StateTransformer.Params<DownloadBlob>, options?: Partial<StateTransform.Options>) {
async downloadBlob(params: StateTransformer.Params<DownloadBlob>, options?: Partial<StateTransform.Options>) {
const data = this.dataState.build().toRoot().apply(DownloadBlob, params, options);
await this.plugin.updateDataState(data, { revertOnError: true });
return data.selector;

View File

@@ -21,7 +21,9 @@ export type TrajectoryFormat = 'pdb' | 'cif' | 'gro' | '3dg'
export enum StructureBuilderTags {
Trajectory = 'trajectory',
Model = 'model',
ModelUnitcell = 'model-unitcell',
ModelProperties = 'model-properties',
ModelGenericRepresentation = 'model-generic-representation',
Structure = 'structure',
StructureProperties = 'structure-properties',
Component = 'structure-component'
@@ -43,7 +45,7 @@ export class StructureBuilder {
const state = this.dataState;
const trajectory = state.build().to(data)
.apply(StateTransforms.Data.ParseBlob, params, { state: { isGhost: true } })
.apply(StateTransforms.Model.TrajectoryFromBlob, void 0, { tags: StructureBuilderTags.Trajectory });
.apply(StateTransforms.Model.TrajectoryFromBlob, void 0, { tags: StructureBuilderTags.Trajectory });
await this.plugin.updateDataState(trajectory, { revertOnError: true });
return trajectory.selector;
}
@@ -60,18 +62,18 @@ export class StructureBuilder {
structure?: RootStructureDefinition.Params,
structureProperties?: boolean | StateTransformer.Params<StateTransforms['Model']['CustomStructureProperties']>
}) {
const trajectory = params.data
const trajectory = params.data
? await this.parseTrajectory(params.data, params.dataFormat! || 'cif')
: await this.parseTrajectoryBlob(params.blob!, params.blobParams!);
const model = await this.createModel(trajectory, params.model);
const modelProperties = !!params.modelProperties
const modelProperties = !!params.modelProperties
? await this.insertModelProperties(model, typeof params?.modelProperties !== 'boolean' ? params?.modelProperties : void 0) : void 0;
const structure = await this.createStructure(modelProperties || model, params.structure);
const structureProperties = !!params.structureProperties
const structureProperties = !!params.structureProperties
? await this.insertStructureProperties(structure, typeof params?.structureProperties !== 'boolean' ? params?.structureProperties : void 0) : void 0;
return {
trajectory,
model: modelProperties || model,
@@ -113,10 +115,19 @@ export class StructureBuilder {
return props.selector;
}
async createUnitcell(model: StateObjectRef<SO.Molecule.Model>, params?: StateTransformer.Params<StateTransforms['Representation']['ModelUnitcell3D']>, initialState?: Partial<StateTransform.State>) {
const state = this.plugin.state.data;
const unitcell = state.build().to(model)
.apply(StateTransforms.Representation.ModelUnitcell3D, params, { tags: StructureBuilderTags.ModelUnitcell, state: initialState });
await this.plugin.updateDataState(unitcell, { revertOnError: true });
return unitcell.selector;
}
async createStructure(model: StateObjectRef<SO.Molecule.Model>, params?: RootStructureDefinition.Params, initialState?: Partial<StateTransform.State>) {
const state = this.dataState;
const structure = state.build().to(model)
.apply(StateTransforms.Model.StructureFromModel, { type: params || { name: 'assembly', params: { } } }, { tags: StructureBuilderTags.Structure, state: initialState });
.apply(StateTransforms.Model.StructureFromModel, { type: params || { name: 'assembly', params: { } } }, { tags: StructureBuilderTags.Structure, state: initialState });
await this.plugin.updateDataState(structure, { revertOnError: true });
return structure.selector;
@@ -141,7 +152,7 @@ export class StructureBuilder {
const root = state.build().to(structure);
const keyTag = `structure-component-${key}`;
const component = root.applyOrUpdateTagged(keyTag, StateTransforms.Model.StructureComponent, params, {
const component = root.applyOrUpdateTagged(keyTag, StateTransforms.Model.StructureComponent, params, {
tags: tags ? [...tags, StructureBuilderTags.Component, keyTag] : [StructureBuilderTags.Component, keyTag]
});
@@ -158,7 +169,7 @@ export class StructureBuilder {
return selector;
}
tryCreateStaticComponent(params: { structure: StateObjectRef<SO.Molecule.Structure>, type: StaticStructureComponentType, key: string, label?: string, tags?: string[] }) {
tryCreateStaticComponent(params: { structure: StateObjectRef<SO.Molecule.Structure>, type: StaticStructureComponentType, key: string, label?: string, tags?: string[] }) {
return this.tryCreateComponent(params.structure, {
type: { name: 'static', params: params.type },
nullIfEmpty: true,
@@ -168,13 +179,13 @@ export class StructureBuilder {
tryCreateQueryComponent(params: { structure: StateObjectRef<SO.Molecule.Structure>, query: StructureSelectionQuery, key: string, label?: string, tags?: string[] }): Promise<StateObjectRef<SO.Molecule.Structure> | undefined> {
return this.plugin.runTask(Task.create('Query Component', async taskCtx => {
let { structure, query, key, label, tags } = params;
let { structure, query, key, label, tags } = params;
label = (label || '').trim();
const structureData = StateObjectRef.resolveAndCheck(this.dataState, structure)?.obj?.data;
if (!structureData) return;
const transformParams: StructureComponentParams = query.referencesCurrent
? {
type: {
@@ -191,25 +202,25 @@ export class StructureBuilder {
if (query.ensureCustomProperties) {
await query.ensureCustomProperties({ fetch: this.plugin.fetch, runtime: taskCtx }, structureData);
}
const state = this.dataState;
const root = state.build().to(structure);
const keyTag = `structure-component-${key}`;
const component = root.applyOrUpdateTagged(keyTag, StateTransforms.Model.StructureComponent, transformParams, {
const component = root.applyOrUpdateTagged(keyTag, StateTransforms.Model.StructureComponent, transformParams, {
tags: tags ? [...tags, StructureBuilderTags.Component, keyTag] : [StructureBuilderTags.Component, keyTag]
});
await this.dataState.updateTree(component).runInContext(taskCtx);
const selector = component.selector;
if (!selector.isOk || selector.cell?.obj?.data.elementCount === 0) {
const del = state.build().delete(selector.ref);
await this.plugin.updateDataState(del);
return;
}
return selector;
return selector;
}))
}

View File

@@ -1,92 +0,0 @@
/**
* Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Model, Symmetry } from '../../mol-model/structure';
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 { BoxCage } from '../../mol-geo/primitive/box';
import { Mat4, Vec3 } from '../../mol-math/linear-algebra';
import { transformCage, cloneCage } from '../../mol-geo/primitive/cage';
import { radToDeg } from '../../mol-math/misc';
import { ModelSymmetry } from '../../mol-model-formats/structure/property/symmetry';
const translate05 = Mat4.fromTranslation(Mat4(), Vec3.create(0.5, 0.5, 0.5))
const unitCage = transformCage(cloneCage(BoxCage()), translate05)
const tmpRef = Vec3()
const tmpTranslate = Mat4()
interface UnitcellData {
symmetry: Symmetry
ref: Vec3
}
export const UnitcellParams = {
...Mesh.Params,
cellColor: PD.Color(ColorNames.orange),
cellScale: PD.Numeric(2, { min: 0.1, max: 5, step: 0.1 })
}
export type UnitcellParams = typeof UnitcellParams
export type UnitcellProps = PD.Values<UnitcellParams>
function getUnitcellMesh(data: UnitcellData, props: UnitcellProps, mesh?: Mesh) {
const state = MeshBuilder.createState(256, 128, mesh)
const { fromFractional } = data.symmetry.spacegroup.cell
Vec3.floor(tmpRef, data.ref)
Mat4.fromTranslation(tmpTranslate, tmpRef)
const cellCage = transformCage(cloneCage(unitCage), tmpTranslate)
const radius = (Math.cbrt(data.symmetry.spacegroup.cell.volume) / 300) * props.cellScale
state.currentGroup = 1
MeshBuilder.addCage(state, fromFractional, cellCage, radius, 2, 20)
return MeshBuilder.getMesh(state)
}
export async function getUnitcellRepresentation(ctx: RuntimeContext, model: Model, params: UnitcellProps, prev?: ShapeRepresentation<UnitcellData, Mesh, Mesh.Params>) {
const repr = prev || ShapeRepresentation(getUnitcellShape, Mesh.Utils);
const symmetry = ModelSymmetry.Provider.get(model)
if (symmetry) {
const data = {
symmetry,
ref: Vec3.transformMat4(Vec3(), Model.getCenter(model), symmetry.spacegroup.cell.toFractional)
}
await repr.createOrUpdate(params, data).runInContext(ctx);
}
return repr;
}
function getUnitcellLabel(data: UnitcellData) {
const { cell, name, num } = data.symmetry.spacegroup
const { size, anglesInRadians } = cell
const a = size[0].toFixed(2)
const b = size[1].toFixed(2)
const c = size[2].toFixed(2)
const alpha = radToDeg(anglesInRadians[0]).toFixed(2)
const beta = radToDeg(anglesInRadians[1]).toFixed(2)
const gamma = radToDeg(anglesInRadians[2]).toFixed(2)
const label: string[] = []
// name
label.push(`${name} #${num}`)
// sizes
label.push(`${a}\u00D7${b}\u00D7${c} \u212B`)
// angles
label.push(`\u03b1=${alpha}\u00B0 \u03b2=${beta}\u00B0 \u03b3=${gamma}\u00B0`)
return label.join(' | ')
}
function getUnitcellShape(ctx: RuntimeContext, data: UnitcellData, props: UnitcellProps, shape?: Shape<Mesh>) {
const geo = getUnitcellMesh(data, props, shape && shape.geometry);
const label = getUnitcellLabel(data)
return Shape.create('Unitcell', data, geo, () => props.cellColor, () => 1, () => label)
}

View File

@@ -41,12 +41,8 @@ export interface StructureRepresentationProps<
sizeParams?: Partial<SizeTheme.ParamValues<S>>
}
export function createStructureRepresentationParams
<R extends StructureRepresentationRegistry.BuiltIn, C extends ColorTheme.BuiltIn, S extends SizeTheme.BuiltIn>
(ctx: PluginContext, structure?: Structure, props?: StructureRepresentationBuiltInProps<R, C, S>): StateTransformer.Params<StructureRepresentation3D>
export function createStructureRepresentationParams
<R extends RepresentationProvider<Structure>, C extends ColorTheme.Provider, S extends SizeTheme.Provider>
(ctx: PluginContext, structure?: Structure, props?: StructureRepresentationProps<R, C, S>): StateTransformer.Params<StructureRepresentation3D>
export function createStructureRepresentationParams<R extends StructureRepresentationRegistry.BuiltIn, C extends ColorTheme.BuiltIn, S extends SizeTheme.BuiltIn>(ctx: PluginContext, structure?: Structure, props?: StructureRepresentationBuiltInProps<R, C, S>): StateTransformer.Params<StructureRepresentation3D>
export function createStructureRepresentationParams<R extends RepresentationProvider<Structure>, C extends ColorTheme.Provider, S extends SizeTheme.Provider>(ctx: PluginContext, structure?: Structure, props?: StructureRepresentationProps<R, C, S>): StateTransformer.Params<StructureRepresentation3D>
export function createStructureRepresentationParams(ctx: PluginContext, structure?: Structure, props: any = {}): StateTransformer.Params<StructureRepresentation3D> {
const p = props as StructureRepresentationBuiltInProps;
if (typeof p.type === 'string' || typeof p.color === 'string' || typeof p.size === 'string') return createParamsByName(ctx, structure || Structure.Empty, props);
@@ -59,8 +55,7 @@ export function getStructureThemeTypes(ctx: PluginContext, structure?: Structure
return themeCtx.colorThemeRegistry.getApplicableTypes({ structure });
}
export function createStructureColorThemeParams<T extends ColorTheme.BuiltIn>
(ctx: PluginContext, structure: Structure | undefined, typeName: string | undefined, themeName: T, params?: ColorTheme.BuiltInParams<T>): StateTransformer.Params<StructureRepresentation3D>['colorTheme']
export function createStructureColorThemeParams<T extends ColorTheme.BuiltIn>(ctx: PluginContext, structure: Structure | undefined, typeName: string | undefined, themeName: T, params?: ColorTheme.BuiltInParams<T>): StateTransformer.Params<StructureRepresentation3D>['colorTheme']
export function createStructureColorThemeParams(ctx: PluginContext, structure: Structure | undefined, typeName: string | undefined, themeName?: string, params?: any): StateTransformer.Params<StructureRepresentation3D>['colorTheme']
export function createStructureColorThemeParams(ctx: PluginContext, structure: Structure | undefined, typeName: string | undefined, themeName?: string, params?: any): StateTransformer.Params<StructureRepresentation3D>['colorTheme'] {
const { registry, themes } = ctx.representation.structure;
@@ -71,8 +66,7 @@ export function createStructureColorThemeParams(ctx: PluginContext, structure: S
return { name: color.name, params: Object.assign(colorDefaultParams, params) };
}
export function createStructureSizeThemeParams<T extends SizeTheme.BuiltIn>
(ctx: PluginContext, structure: Structure | undefined, typeName: string | undefined, themeName: T, params?: SizeTheme.BuiltInParams<T>): StateTransformer.Params<StructureRepresentation3D>['sizeTheme']
export function createStructureSizeThemeParams<T extends SizeTheme.BuiltIn>(ctx: PluginContext, structure: Structure | undefined, typeName: string | undefined, themeName: T, params?: SizeTheme.BuiltInParams<T>): StateTransformer.Params<StructureRepresentation3D>['sizeTheme']
export function createStructureSizeThemeParams(ctx: PluginContext, structure: Structure | undefined, typeName: string | undefined, themeName?: string, params?: any): StateTransformer.Params<StructureRepresentation3D>['sizeTheme']
export function createStructureSizeThemeParams(ctx: PluginContext, structure: Structure | undefined, typeName: string | undefined, themeName?: string, params?: any): StateTransformer.Params<StructureRepresentation3D>['sizeTheme'] {
const { registry, themes } = ctx.representation.structure;
@@ -86,7 +80,7 @@ export function createStructureSizeThemeParams(ctx: PluginContext, structure: St
function createParamsByName(ctx: PluginContext, structure: Structure, props: StructureRepresentationBuiltInProps): StateTransformer.Params<StructureRepresentation3D> {
const typeProvider = (props.type && ctx.representation.structure.registry.get(props.type))
|| ctx.representation.structure.registry.default.provider;
const colorProvider = (props.color && ctx.representation.structure.themes.colorThemeRegistry.get(props.color))
const colorProvider = (props.color && ctx.representation.structure.themes.colorThemeRegistry.get(props.color))
|| ctx.representation.structure.themes.colorThemeRegistry.get(typeProvider.defaultColorTheme.name);
const sizeProvider = (props.size && ctx.representation.structure.themes.sizeThemeRegistry.get(props.size))
|| ctx.representation.structure.themes.sizeThemeRegistry.get(typeProvider.defaultSizeTheme.name);
@@ -104,11 +98,11 @@ function createParamsByName(ctx: PluginContext, structure: Structure, props: Str
function createParamsProvider(ctx: PluginContext, structure: Structure, props: StructureRepresentationProps = {}): StateTransformer.Params<StructureRepresentation3D> {
const { themes: themeCtx } = ctx.representation.structure
const themeDataCtx = { structure };
const repr = props.type || ctx.representation.structure.registry.default.provider;
const reprDefaultParams = PD.getDefaultValues(repr.getParams(themeCtx, structure));
const reprParams = Object.assign(reprDefaultParams, props.typeParams);
const color = props.color || themeCtx.colorThemeRegistry.get(repr.defaultColorTheme.name);
const colorDefaultParams = PD.getDefaultValues(color.getParams(themeDataCtx));
if (color.name === repr.defaultColorTheme.name) Object.assign(colorDefaultParams, repr.defaultColorTheme.props);

View File

@@ -77,7 +77,7 @@ function StructureSelectionQuery(label: string, expression: Expression, props: S
if (props.ensureCustomProperties) {
await props.ensureCustomProperties({ fetch: plugin.fetch, runtime }, structure)
}
if (!_query) _query = compile<StructureSelection>(expression)
if (!_query) _query = compile<StructureSelection>(expression)
return _query(new QueryContext(structure, { currentSelection }));
}
}
@@ -188,7 +188,7 @@ const helix = StructureSelectionQuery('Helix', MS.struct.modifier.union([
MS.core.type.bitflags([SecondaryStructureType.Flag.Helix])
])
})
]), { category: StructureSelectionCategory.Residue })
]), { category: StructureSelectionCategory.Structure })
const beta = StructureSelectionQuery('Beta Strand/Sheet', MS.struct.modifier.union([
MS.struct.generator.atomGroups({
@@ -204,7 +204,7 @@ const beta = StructureSelectionQuery('Beta Strand/Sheet', MS.struct.modifier.uni
MS.core.type.bitflags([SecondaryStructureType.Flag.Beta])
])
})
]), { category: StructureSelectionCategory.Residue })
]), { category: StructureSelectionCategory.Structure })
const water = StructureSelectionQuery('Water', MS.struct.modifier.union([
MS.struct.generator.atomGroups({

View File

@@ -1,14 +1,15 @@
/**
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2020 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 { Sphere3D } from '../../mol-math/geometry';
import { StructureElement } from '../../mol-model/structure';
import { PluginContext } from '../../mol-plugin/context';
import { PrincipalAxes } from '../../mol-math/linear-algebra/matrix/principal-axes';
import { Camera } from '../../mol-canvas3d/camera';
import { Loci } from '../../mol-model/loci';
// TODO: make this customizable somewhere?
const DefaultCameraFocusOptions = {
@@ -20,14 +21,16 @@ const DefaultCameraFocusOptions = {
export type CameraFocusOptions = typeof DefaultCameraFocusOptions
export class CameraManager {
focusLoci(loci: StructureElement.Loci, options?: Partial<CameraFocusOptions>) {
focusLoci(loci: Loci, options?: Partial<CameraFocusOptions>) {
// TODO: allow computation of principal axes here?
// perhaps have an optimized function, that does exact axes small Loci and approximate/sampled from big ones?
const { extraRadius, minRadius, durationMs } = { ...DefaultCameraFocusOptions, ...options };
const { sphere } = StructureElement.Loci.getBoundary(loci);
const radius = Math.max(sphere.radius + extraRadius, minRadius);
this.plugin.canvas3d?.camera.focus(sphere.center, radius, durationMs);
const sphere = Loci.getBoundingSphere(loci);
if (sphere) {
const radius = Math.max(sphere.radius + extraRadius, minRadius);
this.plugin.canvas3d?.camera.focus(sphere.center, radius, durationMs);
}
}
focusSphere(sphere: Sphere3D, options?: Partial<CameraFocusOptions> & { principalAxes?: PrincipalAxes }) {

View File

@@ -1,11 +1,11 @@
/**
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2020 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>
*/
import { EmptyLoci, EveryLoci, isEmptyLoci, Loci as ModelLoci } from '../../mol-model/loci';
import { EveryLoci, isEmptyLoci, Loci } from '../../mol-model/loci';
import { Structure, StructureElement } from '../../mol-model/structure';
import { PluginContext } from '../../mol-plugin/context';
import { Representation } from '../../mol-repr/representation';
@@ -63,25 +63,16 @@ class InteractivityManager extends PluginComponent<InteractivityManagerState> {
}
namespace InteractivityManager {
export interface Loci<T extends ModelLoci = ModelLoci> { loci: T, repr?: Representation.Any }
export namespace Loci {
export function areEqual(a: Loci, b: Loci) {
return a.repr === b.repr && ModelLoci.areEqual(a.loci, b.loci);
}
export const Empty: Loci = { loci: EmptyLoci };
}
export const Params = {
granularity: PD.Select('residue', ModelLoci.GranularityOptions, { label: 'Picking Level', description: 'Controls if selections are expanded upon picking to whole residues, chains, structures, instances, or left as atoms and coarse elements' }),
granularity: PD.Select('residue', Loci.GranularityOptions, { label: 'Picking Level', description: 'Controls if selections are expanded upon picking to whole residues, chains, structures, instances, or left as atoms and coarse elements' }),
}
export type Params = typeof Params
export type Props = PD.Values<Params>
export interface HoverEvent { current: Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys }
export interface ClickEvent { current: Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys }
export interface HoverEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys }
export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys }
export type LociMarkProvider = (loci: Loci, action: MarkerAction) => void
export type LociMarkProvider = (loci: Representation.Loci, action: MarkerAction) => void
export abstract class LociMarkManager {
protected providers: LociMarkProvider[] = [];
@@ -102,13 +93,13 @@ namespace InteractivityManager {
// TODO clear, then re-apply remaining providers
}
protected normalizedLoci(interactivityLoci: Loci, applyGranularity = true) {
const { loci, repr } = interactivityLoci
protected normalizedLoci(reprLoci: Representation.Loci, applyGranularity = true) {
const { loci, repr } = reprLoci
const granularity = applyGranularity ? this.props.granularity : undefined
return { loci: ModelLoci.normalize(loci, granularity), repr }
return { loci: Loci.normalize(loci, granularity), repr }
}
protected mark(current: Loci<ModelLoci>, action: MarkerAction) {
protected mark(current: Representation.Loci, action: MarkerAction) {
for (let p of this.providers) p(current, action);
}
@@ -121,16 +112,16 @@ namespace InteractivityManager {
//
export class LociHighlightManager extends LociMarkManager {
private prev: Loci[] = [];
private prev: Representation.Loci[] = [];
private isHighlighted(loci: Loci) {
private isHighlighted(loci: Representation.Loci) {
for (const p of this.prev) {
if (Loci.areEqual(p, loci)) return true
if (Representation.Loci.areEqual(p, loci)) return true
}
return false
}
private addHighlight(loci: Loci) {
private addHighlight(loci: Representation.Loci) {
this.mark(loci, MarkerAction.Highlight);
this.prev.push(loci)
}
@@ -142,14 +133,14 @@ namespace InteractivityManager {
this.prev.length = 0
}
highlight(current: Loci, applyGranularity = true) {
highlight(current: Representation.Loci, applyGranularity = true) {
const normalized = this.normalizedLoci(current, applyGranularity)
if (!this.isHighlighted(normalized)) {
this.addHighlight(normalized)
}
}
highlightOnly(current: Loci, applyGranularity = true) {
highlightOnly(current: Representation.Loci, applyGranularity = true) {
const normalized = this.normalizedLoci(current, applyGranularity)
if (!this.isHighlighted(normalized)) {
this.clearHighlights()
@@ -157,7 +148,7 @@ namespace InteractivityManager {
}
}
highlightOnlyExtend(current: Loci, applyGranularity = true) {
highlightOnlyExtend(current: Representation.Loci, applyGranularity = true) {
const normalized = this.normalizedLoci(current, applyGranularity)
if (StructureElement.Loci.is(normalized.loci)) {
const loci = {
@@ -175,8 +166,8 @@ namespace InteractivityManager {
//
export class LociSelectManager extends LociMarkManager {
toggle(current: Loci<ModelLoci>, applyGranularity = true) {
if (ModelLoci.isEmpty(current.loci)) return;
toggle(current: Representation.Loci, applyGranularity = true) {
if (Loci.isEmpty(current.loci)) return;
const normalized = this.normalizedLoci(current, applyGranularity)
if (StructureElement.Loci.is(normalized.loci)) {
@@ -186,8 +177,8 @@ namespace InteractivityManager {
}
}
toggleExtend(current: Loci<ModelLoci>, applyGranularity = true) {
if (ModelLoci.isEmpty(current.loci)) return;
toggleExtend(current: Representation.Loci, applyGranularity = true) {
if (Loci.isEmpty(current.loci)) return;
const normalized = this.normalizedLoci(current, applyGranularity)
if (StructureElement.Loci.is(normalized.loci)) {
@@ -196,7 +187,7 @@ namespace InteractivityManager {
}
}
select(current: Loci<ModelLoci>, applyGranularity = true) {
select(current: Representation.Loci, applyGranularity = true) {
const normalized = this.normalizedLoci(current, applyGranularity)
if (StructureElement.Loci.is(normalized.loci)) {
this.sel.modify('add', normalized.loci);
@@ -204,7 +195,7 @@ namespace InteractivityManager {
this.mark(normalized, MarkerAction.Select);
}
selectJoin(current: Loci<ModelLoci>, applyGranularity = true) {
selectJoin(current: Representation.Loci, applyGranularity = true) {
const normalized = this.normalizedLoci(current, applyGranularity)
if (StructureElement.Loci.is(normalized.loci)) {
this.sel.modify('intersect', normalized.loci);
@@ -212,7 +203,7 @@ namespace InteractivityManager {
this.mark(normalized, MarkerAction.Select);
}
selectOnly(current: Loci<ModelLoci>, applyGranularity = true) {
selectOnly(current: Representation.Loci, applyGranularity = true) {
this.deselectAll()
const normalized = this.normalizedLoci(current, applyGranularity)
if (StructureElement.Loci.is(normalized.loci)) {
@@ -221,7 +212,7 @@ namespace InteractivityManager {
this.mark(normalized, MarkerAction.Select);
}
deselect(current: Loci<ModelLoci>, applyGranularity = true) {
deselect(current: Representation.Loci, applyGranularity = true) {
const normalized = this.normalizedLoci(current, applyGranularity)
if (StructureElement.Loci.is(normalized.loci)) {
this.sel.modify('remove', normalized.loci);
@@ -234,11 +225,11 @@ namespace InteractivityManager {
this.mark({ loci: EveryLoci }, MarkerAction.Deselect);
}
deselectAllOnEmpty(current: Loci<ModelLoci>) {
deselectAllOnEmpty(current: Representation.Loci) {
if (isEmptyLoci(current.loci)) this.deselectAll()
}
protected mark(current: Loci<ModelLoci>, action: MarkerAction.Select | MarkerAction.Deselect) {
protected mark(current: Representation.Loci, action: MarkerAction.Select | MarkerAction.Deselect) {
const { loci } = current
if (StructureElement.Loci.is(loci)) {
// do a full deselect/select for the current structure so visuals that are
@@ -251,7 +242,7 @@ namespace InteractivityManager {
}
}
private toggleSel(current: Loci<ModelLoci>) {
private toggleSel(current: Representation.Loci) {
if (this.sel.has(current.loci)) {
this.sel.modify('remove', current.loci);
this.mark(current, MarkerAction.Deselect);

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2020 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,6 +9,7 @@ import { PluginContext } from '../../mol-plugin/context';
import { Loci } from '../../mol-model/loci';
import { Representation } from '../../mol-repr/representation';
import { MarkerAction } from '../../mol-util/marker-action';
import { arrayRemoveAtInPlace } from '../../mol-util/array';
export type LociLabelEntry = JSX.Element | string
export type LociLabelProvider = (info: Loci, repr?: Representation<any>) => LociLabelEntry | undefined
@@ -18,25 +19,55 @@ export class LociLabelManager {
addProvider(provider: LociLabelProvider) {
this.providers.push(provider);
this.isDirty = true
this.showLabels()
}
removeProvider(provider: LociLabelProvider) {
this.providers = this.providers.filter(p => p !== provider);
// Event.Interactivity.Highlight.dispatch(this.ctx, []);
this.isDirty = true
this.showLabels()
}
private empty: LociLabelEntry[] = [];
private getInfo({ loci, repr }: Representation.Loci, action: MarkerAction) {
if (Loci.isEmpty(loci) || action !== MarkerAction.Highlight) return this.empty;
const info: LociLabelEntry[] = [];
for (let p of this.providers) {
const e = p(loci, repr);
if (e) info.push(e);
private locis: Representation.Loci[] = []
private mark(loci: Representation.Loci, action: MarkerAction) {
const idx = this.locis.findIndex(l => Representation.Loci.areEqual(loci, l))
if (idx === -1 && action === MarkerAction.Highlight) {
this.locis.push(loci)
this.isDirty = true
} else if(idx !== -1 && action === MarkerAction.RemoveHighlight) {
arrayRemoveAtInPlace(this.locis, idx)
this.isDirty = true
}
return info;
}
private isDirty = false
private entries: LociLabelEntry[] = []
private showLabels() {
this.ctx.behaviors.labels.highlight.next({ entries: this.getEntries() })
}
private getEntries() {
if (this.isDirty) {
this.entries.length = 0
for (const provider of this.providers) {
for (const loci of this.locis) {
if (Loci.isEmpty(loci.loci)) continue
const entry = provider(loci.loci, loci.repr)
if (entry) this.entries.push(entry)
}
}
this.isDirty = false
}
return this.entries
}
constructor(public ctx: PluginContext) {
ctx.managers.interactivity.lociHighlights.addProvider((loci, action) => ctx.behaviors.labels.highlight.next({ entries: this.getInfo(loci, action) }))
ctx.managers.interactivity.lociHighlights.addProvider((loci, action) => {
this.mark(loci, action)
this.showLabels()
})
}
}

View File

@@ -86,7 +86,7 @@ class StructureComponentManager extends PluginComponent<StructureComponentManage
private async updateInterationProps() {
for (const s of this.currentStructures) {
const interactionParams = InteractionsProvider.getParams(s.cell.obj?.data!);
if (s.properties) {
const params = s.properties.cell.transform.params;
if (PD.areEqual(interactionParams, params, this.state.options.interactions)) continue;
@@ -137,12 +137,12 @@ class StructureComponentManager extends PluginComponent<StructureComponentManage
modifyByCurrentSelection(components: ReadonlyArray<StructureComponentRef>, action: StructureComponentManager.ModifyAction) {
return this.plugin.runTask(Task.create('Modify Component', async taskCtx => {
const b = this.dataState.build();
const b = this.dataState.build();
for (const c of components) {
if (!this.canBeModified(c)) continue;
const selection = this.plugin.managers.structure.selection.getStructure(c.structure.cell.obj!.data);
if (!selection || selection.elementCount === 0) continue;
if (!selection || selection.elementCount === 0) continue;
this.modifyComponent(b, c, selection, action);
}
await this.dataState.updateTree(b, { canUndo: 'Modify Selection' }).runInContext(taskCtx);
@@ -157,7 +157,7 @@ class StructureComponentManager extends PluginComponent<StructureComponentManage
for (const c of components) {
setSubtreeVisibility(this.dataState, c.cell.transform.ref, isHidden);
}
} else {
} else {
const index = components[0].representations.indexOf(reprPivot);
const isHidden = !reprPivot.cell.state.isHidden;
@@ -192,14 +192,14 @@ class StructureComponentManager extends PluginComponent<StructureComponentManage
return this.plugin.managers.structure.hierarchy.remove(toRemove, true);
}
updateRepresentations(components: ReadonlyArray<StructureComponentRef>, pivot: StructureRepresentationRef, params: StateTransformer.Params<StructureRepresentation3D>) {
updateRepresentations(components: ReadonlyArray<StructureComponentRef>, pivot: StructureRepresentationRef, params: StateTransformer.Params<StructureRepresentation3D>) {
if (components.length === 0) return Promise.resolve();
const index = components[0].representations.indexOf(pivot);
if (index < 0) return Promise.resolve();
const update = this.dataState.build();
for (const c of components) {
// TODO: is it ok to use just the index here? Could possible lead to ugly edge cases, but perhaps not worth the trouble to "fix".
const repr = c.representations[index];
@@ -214,19 +214,19 @@ class StructureComponentManager extends PluginComponent<StructureComponentManage
updateRepresentationsTheme<C extends ColorTheme.BuiltIn, S extends SizeTheme.BuiltIn>(components: ReadonlyArray<StructureComponentRef>, params: StructureComponentManager.UpdateThemeParams<C, S>): Promise<any> {
if (components.length === 0) return Promise.resolve();
const update = this.dataState.build();
for (const c of components) {
for (const repr of c.representations) {
const old = repr.cell.transform.params;
const colorTheme = params.color
const colorTheme = params.color
? createStructureColorThemeParams(this.plugin, c.structure.cell.obj?.data, old?.type.name, params.color, params.colorParams)
: void 0;
const sizeTheme = params.color
const sizeTheme = params.color
? createStructureSizeThemeParams(this.plugin, c.structure.cell.obj?.data, old?.type.name, params.size, params.sizeParams)
: void 0;
update.to(repr.cell).update(prev => {
update.to(repr.cell).update(prev => {
if (colorTheme) prev.colorTheme = colorTheme;
if (sizeTheme) prev.sizeTheme = sizeTheme;
});
@@ -260,7 +260,7 @@ class StructureComponentManager extends PluginComponent<StructureComponentManage
const componentKey = UUID.create22();
for (const s of xs) {
const component = await this.plugin.builders.structure.tryCreateQueryComponent({
const component = await this.plugin.builders.structure.tryCreateQueryComponent({
structure: s.childRoot,
query: params.selection,
key: componentKey,
@@ -297,11 +297,11 @@ class StructureComponentManager extends PluginComponent<StructureComponentManage
if ((action === 'subtract' || action === 'intersect') && !structureAreIntersecting(structure, by)) return;
const parent = component.structure.cell.obj?.data!;
const modified = action === 'union'
? structureUnion(parent, [structure, by])
const modified = action === 'union'
? structureUnion(parent, [structure, by])
: action === 'intersect'
? structureIntersect(structure, by)
: structureSubtract(structure, by);
? structureIntersect(structure, by)
: structureSubtract(structure, by);
if (modified.elementCount === 0) {
builder.delete(component.cell.transform.ref);
@@ -346,9 +346,9 @@ namespace StructureComponentManager {
interactions: PD.Group(InteractionsProvider.defaultParams, { label: 'Non-covalent Interactions' }),
}
export type Options = PD.Values<typeof OptionsParams>
const SelectionParam = PD.Select(StructureSelectionQueryOptions[1][0], StructureSelectionQueryOptions)
export function getAddParams(plugin: PluginContext) {
return {
selection: SelectionParam,
@@ -389,7 +389,7 @@ namespace StructureComponentManager {
export type ModifyAction = 'union' | 'subtract' | 'intersect'
export interface UpdateThemeParams<C extends ColorTheme.BuiltIn, S extends SizeTheme.BuiltIn> {
/**
/**
* this works for any theme name (use 'name as any'), but code completion will break
*/
color?: C,

View File

@@ -15,7 +15,7 @@ import { CreateVolumeStreamingBehavior } from '../../../mol-plugin/behavior/dyna
export function buildStructureHierarchy(state: State, previous?: StructureHierarchy) {
const build = BuildState(state, previous || StructureHierarchy());
// StateTree.doPreOrder(state.tree, state.tree.root, build, visitCell);
// StateTree.doPreOrder(state.tree, state.tree.root, build, visitCell);
doPreOrder(state.tree, build);
if (previous) previous.refs.forEach(isRemoved, build);
return { hierarchy: build.hierarchy, added: build.added, updated: build.updated, removed: build.removed };
@@ -38,9 +38,9 @@ interface RefBase<K extends string = string, O extends StateObject = StateObject
version: StateTransform['version']
}
export type HierarchyRef =
export type HierarchyRef =
| TrajectoryRef
| ModelRef | ModelPropertiesRef
| ModelRef | ModelPropertiesRef | ModelUnitcellRef
| StructureRef | StructurePropertiesRef | StructureVolumeStreamingRef | StructureComponentRef | StructureRepresentationRef
| GenericRepresentationRef
@@ -48,7 +48,7 @@ export interface TrajectoryRef extends RefBase<'trajectory', SO.Molecule.Traject
models: ModelRef[]
}
function TrajectoryRef(cell: StateObjectCell<SO.Molecule.Trajectory>): TrajectoryRef {
function TrajectoryRef(cell: StateObjectCell<SO.Molecule.Trajectory>): TrajectoryRef {
return { kind: 'trajectory', cell, version: cell.transform.version, models: [] };
}
@@ -57,6 +57,7 @@ export interface ModelRef extends RefBase<'model', SO.Molecule.Model> {
properties?: ModelPropertiesRef,
structures: StructureRef[],
genericRepresentations?: GenericRepresentationRef[],
unitcell?: ModelUnitcellRef,
/** to support decorators */
childRoot: StateObjectCell<SO.Molecule.Model>
}
@@ -73,6 +74,14 @@ function ModelPropertiesRef(cell: StateObjectCell<SO.Molecule.Model>, model: Mod
return { kind: 'model-properties', cell, version: cell.transform.version, model };
}
export interface ModelUnitcellRef extends RefBase<'model-unitcell', SO.Shape.Representation3D, StateTransforms['Representation']['ModelUnitcell3D']> {
model: ModelRef
}
function ModelUnitcellRef(cell: StateObjectCell<SO.Shape.Representation3D>, model: ModelRef): ModelUnitcellRef {
return { kind: 'model-unitcell', cell, version: cell.transform.version, model };
}
export interface StructureRef extends RefBase<'structure', SO.Molecule.Structure> {
model?: ModelRef,
properties?: StructurePropertiesRef,
@@ -200,6 +209,15 @@ const tagMap: [string, (state: BuildState, cell: StateObjectCell) => boolean | v
if (!state.currentModel) return false;
state.currentModel.properties = createOrUpdateRef(state, cell, ModelPropertiesRef, cell, state.currentModel);
}, state => { }],
[StructureBuilderTags.ModelUnitcell, (state, cell) => {
if (!state.currentModel) return false;
state.currentModel.unitcell = createOrUpdateRef(state, cell, ModelUnitcellRef, cell, state.currentModel);
}, state => { }],
[StructureBuilderTags.ModelGenericRepresentation, (state, cell) => {
if (!state.currentModel) return false;
if (!state.currentModel.genericRepresentations) state.currentModel.genericRepresentations = []
createOrUpdateRefList(state, cell, state.currentModel.genericRepresentations, GenericRepresentationRef, cell, state.currentModel);
}, state => { }],
[StructureBuilderTags.Structure, (state, cell) => {
if (state.currentModel) {
state.currentStructure = createOrUpdateRefList(state, cell, state.currentModel.structures, StructureRef, cell, state.currentModel);

View File

@@ -60,7 +60,7 @@ class StructureMeasurementManager extends PluginComponent<StructureMeasurementMa
const update = this.plugin.state.data.build();
for (const cell of this.state.distances) {
update.to(cell).update((old: any) => {
update.to(cell).update((old: any) => {
old.unitLabel = options.distanceUnitLabel;
old.textColor = options.textColor;
});
@@ -74,7 +74,7 @@ class StructureMeasurementManager extends PluginComponent<StructureMeasurementMa
for (const cell of this.state.dihedrals) {
update.to(cell).update((old: any) => { old.textColor = options.textColor; });
}
if (update.editInfo.count === 0) return;
await PluginCommands.State.Update(this.plugin, { state: this.plugin.state.data, tree: update, options: { doNotLogTiming: true } });

View File

@@ -26,7 +26,7 @@ import { Overpaint } from '../../mol-theme/overpaint';
import { Transparency } from '../../mol-theme/transparency';
import { BaseGeometry } from '../../mol-geo/geometry/base';
import { Script } from '../../mol-script/script';
import { getUnitcellRepresentation, UnitcellParams } from '../helpers/model-unitcell';
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 { LabelParams, LabelRepresentation } from '../../mol-repr/shape/loci/label';
@@ -579,17 +579,24 @@ const ModelUnitcell3D = PluginStateTransform.BuiltIn({
canAutoUpdate({ oldParams, newParams }) {
return true;
},
apply({ a, params }) {
apply({ a, params }, plugin: PluginContext) {
return Task.create('Model Unitcell', async ctx => {
const symmetry = ModelSymmetry.Provider.get(a.data)
if (!symmetry) return StateObject.Null
const repr = await getUnitcellRepresentation(ctx, a.data, params);
const data = getUnitcellData(a.data, symmetry)
const repr = UnitcellRepresentation({ webgl: plugin.canvas3d?.webgl, ...plugin.representation.structure.themes }, () => UnitcellParams)
await repr.createOrUpdate(params, data).runInContext(ctx);
return new SO.Shape.Representation3D({ repr, source: a }, { label: `Unitcell`, description: symmetry.spacegroup.name });
});
},
update({ a, b, newParams }) {
return Task.create('Model Unitcell', async ctx => {
await getUnitcellRepresentation(ctx, a.data, newParams, b.data.repr);
const symmetry = ModelSymmetry.Provider.get(a.data)
if (!symmetry) return StateTransformer.UpdateResult.Null
const props = { ...b.data.repr.props, ...newParams }
const data = getUnitcellData(a.data, symmetry)
await b.data.repr.createOrUpdate(props, data).runInContext(ctx);
b.data.source = a
return StateTransformer.UpdateResult.Updated;
});
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2020 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>
@@ -7,7 +7,6 @@
import * as React from 'react'
import { PluginUIComponent } from '../base';
import { InteractivityManager } from '../../mol-plugin-state/manager/interactivity';
import { MarkerAction } from '../../mol-util/marker-action';
import { ButtonsType, ModifiersKeys, getButtons, getModifiers, getButton } from '../../mol-util/input/input-observer';
import { SequenceWrapper } from './wrapper';
@@ -16,6 +15,7 @@ import { Subject } from 'rxjs';
import { debounceTime } from 'rxjs/operators';
import { Color } from '../../mol-util/color';
import { OrderedSet } from '../../mol-data/int';
import { Representation } from '../../mol-repr/representation';
type SequenceProps = {
sequenceWrapper: SequenceWrapper.Any,
@@ -32,12 +32,12 @@ export class Sequence<P extends SequenceProps> extends PluginUIComponent<P> {
private lastMouseOverSeqIdx = -1;
private highlightQueue = new Subject<{ seqIdx: number, buttons: number, button: number, modifiers: ModifiersKeys }>();
private lociHighlightProvider = (loci: InteractivityManager.Loci, action: MarkerAction) => {
private lociHighlightProvider = (loci: Representation.Loci, action: MarkerAction) => {
const changed = this.props.sequenceWrapper.markResidue(loci.loci, action)
if (changed) this.updateMarker();
}
private lociSelectionProvider = (loci: InteractivityManager.Loci, action: MarkerAction) => {
private lociSelectionProvider = (loci: Representation.Loci, action: MarkerAction) => {
const changed = this.props.sequenceWrapper.markResidue(loci.loci, action)
if (changed) this.updateMarker();
}
@@ -88,7 +88,7 @@ export class Sequence<P extends SequenceProps> extends PluginUIComponent<P> {
}
hover(loci: StructureElement.Loci | undefined, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys) {
const ev = { current: InteractivityManager.Loci.Empty, buttons, button, modifiers }
const ev = { current: Representation.Loci.Empty, buttons, button, modifiers }
if (loci !== undefined && !StructureElement.Loci.isEmpty(loci)) {
ev.current = { loci };
}
@@ -96,7 +96,7 @@ export class Sequence<P extends SequenceProps> extends PluginUIComponent<P> {
}
click(loci: StructureElement.Loci | undefined, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys) {
const ev = { current: InteractivityManager.Loci.Empty, buttons, button, modifiers }
const ev = { current: Representation.Loci.Empty, buttons, button, modifiers }
if (loci !== undefined && !StructureElement.Loci.isEmpty(loci)) {
ev.current = { loci };
}

View File

@@ -195,7 +195,8 @@
stroke: $font-color;
}
.msp-no-overflow {
.msp-no-overflow {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

View File

@@ -38,6 +38,7 @@
height: $row-height;
margin-top: 1px;
width: 100%;
background: $default-background;
> button {
margin: 0;
@@ -49,6 +50,10 @@
> button:last-child {
margin-right: 0;
}
> button.msp-control-button-label {
background: $default-background;
}
}
.msp-select-row {
@@ -327,9 +332,9 @@
text-align: left;
}
.msp-action-menu-button {
margin-top: 1px;
margin-top: 1px;
.msp-icon {
font-size: 80%;
margin-right: 6px;

View File

@@ -246,9 +246,9 @@ class StructureComponentGroup extends PurePluginUIComponent<{ group: StructureCo
const repr = this.pivot.representations[0];
const name = repr.cell.transform.params?.colorTheme.name;
const themes = getStructureThemeTypes(this.plugin, this.pivot.cell.obj?.data);
return ActionMenu.createItemsFromSelectOptions(themes, {
return ActionMenu.createItemsFromSelectOptions(themes, {
value: o => () => mng.updateRepresentationsTheme(this.props.group, { color: o[0] }),
selected: o => o[0] === name
selected: o => o[0] === name
}) as ActionMenu.Item[];
}
@@ -282,7 +282,7 @@ class StructureComponentGroup extends PurePluginUIComponent<{ group: StructureCo
return ret;
}
selectAction: ActionMenu.OnSelect = item => {
if (!item) return;
this.setState({ action: void 0 });
@@ -333,15 +333,13 @@ class StructureComponentGroup extends PurePluginUIComponent<{ group: StructureCo
const cell = component.cell;
const label = cell.obj?.label;
return <>
<div className='msp-control-row'>
<button className='msp-control-button-label' title={`${label}. Click to focus.`} onClick={this.focus} onMouseEnter={this.highlight} onMouseLeave={this.clearHighlight} style={{ textAlign: 'left' }}>
<div className='msp-btn-row-group'>
<button className='msp-form-control msp-control-button-label' title={`${label}. Click to focus.`} onClick={this.focus} onMouseEnter={this.highlight} onMouseLeave={this.clearHighlight} style={{ textAlign: 'left' }}>
{label}
</button>
<div className='msp-select-row'>
<IconButton onClick={this.toggleVisible} icon='visual-visibility' toggleState={!cell.state.isHidden} title={`${cell.state.isHidden ? 'Show' : 'Hide'} component`} small />
<IconButton onClick={this.toggleRemove} icon='remove' title='Remove' small toggleState={this.state.action === 'remove'} />
<IconButton onClick={this.toggleAction} icon='dot-3' title='Actions' toggleState={this.state.action === 'action'} />
</div>
<IconButton onClick={this.toggleVisible} icon='visual-visibility' toggleState={!cell.state.isHidden} title={`${cell.state.isHidden ? 'Show' : 'Hide'} component`} small customClass='msp-form-control' style={{ flex: '0 0 32px' }} />
<IconButton onClick={this.toggleRemove} icon='remove' title='Remove' small toggleState={this.state.action === 'remove'} customClass='msp-form-control' style={{ flex: '0 0 32px' }} />
<IconButton onClick={this.toggleAction} icon='dot-3' title='Actions' toggleState={this.state.action === 'action'} customClass='msp-form-control' style={{ flex: '0 0 32px', padding: '0px' }} />
</div>
{this.state.action === 'remove' && <ActionMenu items={this.removeActions} onSelect={this.selectRemoveAction} />}
{this.state.action === 'action' && <>

View File

@@ -276,7 +276,7 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen
this.props.cell.sourceRef
return [ActionMenu.Item('Select This', 'flash', () => this.plugin.managers.structure.selection.fromSelections(this.props.cell.sourceRef!))];
}
selectAction: ActionMenu.OnSelect = item => {
if (!item) return;
this.setState({ showUpdate: false });
@@ -290,18 +290,18 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen
return <>
<div className='msp-btn-row-group' key={obj.id} onMouseEnter={this.highlight} onMouseLeave={this.clearHighlight}>
<button className='msp-btn msp-btn-block msp-form-control msp-no-overflow' title='Click to focus. Hover to highlight.' onClick={this.focus} style={{ width: 'auto', textAlign: 'left' }}>
<button className='msp-form-control msp-control-button-label msp-no-overflow' title='Click to focus. Hover to highlight.' onClick={this.focus} style={{ width: 'auto', textAlign: 'left' }}>
<span dangerouslySetInnerHTML={{ __html: this.label }} />
</button>
<IconButton small={true} customClass='msp-form-control' onClick={this.toggleVisibility} icon='eye' style={{ flex: '0 0 32px' }} title={cell.state.isHidden ? 'Show' : 'Hide'} toggleState={!cell.state.isHidden} />
<IconButton small={true} customClass='msp-form-control' onClick={this.delete} icon='remove' style={{ flex: '0 0 32px' }} title='Delete' />
<IconButton small={true} customClass='msp-form-control' onClick={this.toggleUpdate} icon='dot-3' style={{ flex: '0 0 32px' }} title={cell.state.isHidden ? 'Show' : 'Hide'} toggleState={this.state.showUpdate} />
<IconButton small customClass='msp-form-control' onClick={this.toggleVisibility} icon='eye' style={{ flex: '0 0 32px' }} title={cell.state.isHidden ? 'Show' : 'Hide'} toggleState={!cell.state.isHidden} />
<IconButton small customClass='msp-form-control' onClick={this.delete} icon='remove' style={{ flex: '0 0 32px' }} title='Delete' />
<IconButton customClass='msp-form-control' onClick={this.toggleUpdate} icon='dot-3' style={{ flex: '0 0 32px', padding: '0px' }} title='Actions' toggleState={this.state.showUpdate} />
</div>
{this.state.showUpdate && <>
<ActionMenu items={this.actions} onSelect={this.selectAction} />
<div className='msp-control-offset'>
<ExpandGroup header='Options' noOffset>
<UpdateTransformControl state={cell.parent} transform={cell.transform} customHeader='none' />
<UpdateTransformControl state={cell.parent} transform={cell.transform} customHeader='none' autoHideApply />
</ExpandGroup>
</div>
</>}

View File

@@ -14,6 +14,7 @@ import { PluginCommands } from '../../mol-plugin/commands';
import { StateTransforms } from '../../mol-plugin-state/transforms';
import { memoize1 } from '../../mol-util/memoize';
import { StructureHierarchyManager } from '../../mol-plugin-state/manager/structure/hierarchy';
import { UnitcellEntry } from './unitcell';
interface StructureSourceControlState extends CollapsableState {
isBusy: boolean,
@@ -197,7 +198,7 @@ export class StructureSourceControls extends CollapsableControls<{}, StructureSo
if (selection.trajectories.some(t => t.models.length > 1)) {
ret.push(ActionMenu.Item('Load single model', () => this.plugin.managers.structure.hierarchy.createModels(selection.trajectories, 'single')));
}
// TODO: remove actions?
return ret;
}
@@ -245,6 +246,24 @@ export class StructureSourceControls extends CollapsableControls<{}, StructureSo
return <ParameterControls params={params} values={s.cell.params?.values} onChangeValues={this.updateStructure} isDisabled={this.state.isBusy} />
}
get unitcell() {
const { selection } = this.plugin.managers.structure.hierarchy;
if (selection.structures.length !== 1) return null;
const model = selection.structures[0].model
if (!model) return null
const unitcell = model.unitcell
if (!unitcell) {
// this.plugin.builders.structure.createUnitcell(model.cell, undefined, { isHidden: true })
return null
} else if (!unitcell.cell.obj) {
return null;
}
return <UnitcellEntry key={unitcell.cell.obj.id} cell={unitcell.cell} />
}
renderControls() {
const disabled = this.state.isBusy || this.isEmpty;
const actions = this.actions(this.plugin.managers.structure.hierarchy.selection);
@@ -260,6 +279,7 @@ export class StructureSourceControls extends CollapsableControls<{}, StructureSo
{this.state.show === 'actions' && <ActionMenu items={actions} onSelect={this.selectAction} />}
{this.modelIndex}
{this.structureType}
{this.unitcell}
</>;
}
}

View File

@@ -0,0 +1,74 @@
/**
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import * as React from 'react';
import { PurePluginUIComponent } from '../base';
import { StateTransformer, StateTransform, StateObjectCell } from '../../mol-state';
import { IconButton } from '../controls/common';
import { UpdateTransformControl } from '../state/update-transform';
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { PluginCommands } from '../../mol-plugin/commands';
export type UnitcellCell = StateObjectCell<PluginStateObject.Shape.Representation3D, StateTransform<StateTransformer<PluginStateObject.Molecule.Model, PluginStateObject.Shape.Representation3D, any>>>
export class UnitcellEntry extends PurePluginUIComponent<{ cell: UnitcellCell }, { showOptions: boolean }> {
state = { showOptions: false }
componentDidMount() {
this.subscribe(this.plugin.events.state.cell.stateUpdated, e => {
this.forceUpdate();
});
}
toggleVisibility = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
PluginCommands.State.ToggleVisibility(this.plugin, { state: this.props.cell.parent, ref: this.props.cell.transform.ref });
e.currentTarget.blur();
}
highlight = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
if (!this.props.cell.state.isHidden) {
PluginCommands.Interactivity.Object.Highlight(this.plugin, { state: this.props.cell.parent, ref: this.props.cell.transform.ref });
}
}
clearHighlight = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
PluginCommands.Interactivity.ClearHighlights(this.plugin);
}
focus = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
if (!this.props.cell.state.isHidden) {
const loci = this.props.cell.obj?.data.repr.getLoci()
if (loci) this.plugin.managers.camera.focusLoci(loci);
}
}
toggleOptions = () => this.setState({ showOptions: !this.state.showOptions })
render() {
const { cell } = this.props;
const { obj } = cell;
if (!obj) return null;
return <>
<div className='msp-btn-row-group' style={{ marginTop: '3px' }}>
<button className='msp-form-control msp-control-button-label' title={`Unitcell. Click to focus.`} onClick={this.focus} onMouseEnter={this.highlight} onMouseLeave={this.clearHighlight} style={{ textAlign: 'left' }}>
Unitcell
</button>
<IconButton customClass='msp-form-control' onClick={this.toggleVisibility} icon='visual-visibility' toggleState={!cell.state.isHidden} title={`${cell.state.isHidden ? 'Show' : 'Hide'}`} small style={{ flex: '0 0 32px' }} />
<IconButton customClass='msp-form-control' onClick={this.toggleOptions} icon='dot-3' title='Options' toggleState={this.state.showOptions} style={{ flex: '0 0 32px', padding: '0px' }} />
</div>
{this.state.showOptions && <>
<div className='msp-control-offset'>
<UpdateTransformControl state={cell.parent} transform={cell.transform} customHeader='none' autoHideApply />
</div>
</>}
</>;
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -10,7 +10,6 @@ import { PluginContext } from '../../../mol-plugin/context';
import { PluginStateObject as SO } from '../../../mol-plugin-state/objects';
import { lociLabel } from '../../../mol-theme/label';
import { PluginBehavior } from '../behavior';
import { InteractivityManager } from '../../../mol-plugin-state/manager/interactivity';
import { StateTreeSpine } from '../../../mol-state/tree/spine';
import { StateSelection } from '../../../mol-state';
import { ButtonsType, ModifiersKeys } from '../../../mol-util/input/input-observer';
@@ -19,6 +18,7 @@ import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { EmptyLoci, Loci } from '../../../mol-model/loci';
import { Structure } from '../../../mol-model/structure';
import { arrayMax } from '../../../mol-util/array';
import { Representation } from '../../../mol-repr/representation';
const B = ButtonsType
const M = ModifiersKeys
@@ -39,7 +39,7 @@ export const HighlightLoci = PluginBehavior.create({
name: 'representation-highlight-loci',
category: 'interaction',
ctor: class extends PluginBehavior.Handler<HighlightLociProps> {
private lociMarkProvider = (interactionLoci: InteractivityManager.Loci, action: MarkerAction) => {
private lociMarkProvider = (interactionLoci: Representation.Loci, action: MarkerAction) => {
if (!this.ctx.canvas3d) return;
this.ctx.canvas3d.mark({ loci: interactionLoci.loci }, action)
}
@@ -92,9 +92,9 @@ export const SelectLoci = PluginBehavior.create({
category: 'interaction',
ctor: class extends PluginBehavior.Handler<SelectLociProps> {
private spine: StateTreeSpine.Impl
private lociMarkProvider = (interactionLoci: InteractivityManager.Loci, action: MarkerAction) => {
private lociMarkProvider = (reprLoci: Representation.Loci, action: MarkerAction) => {
if (!this.ctx.canvas3d) return;
this.ctx.canvas3d.mark({ loci: interactionLoci.loci }, action)
this.ctx.canvas3d.mark({ loci: reprLoci.loci }, action)
}
private applySelectMark(ref: string, clear?: boolean) {
const cell = this.ctx.state.data.cells.get(ref)
@@ -111,10 +111,10 @@ export const SelectLoci = PluginBehavior.create({
}
}
register() {
const lociIsEmpty = (current: InteractivityManager.Loci) => Loci.isEmpty(current.loci)
const lociIsNotEmpty = (current: InteractivityManager.Loci) => !Loci.isEmpty(current.loci)
const lociIsEmpty = (current: Representation.Loci) => Loci.isEmpty(current.loci)
const lociIsNotEmpty = (current: Representation.Loci) => !Loci.isEmpty(current.loci)
const actions: [keyof typeof DefaultSelectLociBindings, (current: InteractivityManager.Loci) => void, ((current: InteractivityManager.Loci) => boolean) | undefined][] = [
const actions: [keyof typeof DefaultSelectLociBindings, (current: Representation.Loci) => void, ((current: Representation.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],

View File

@@ -22,6 +22,7 @@ import { Theme } from '../../../../mol-theme/theme';
import { Box3D } from '../../../../mol-math/geometry';
import { Vec3 } from '../../../../mol-math/linear-algebra';
import { PluginConfig } from '../../../config';
import { Model } from '../../../../mol-model/structure';
function addEntry(entries: InfoEntryProps[], method: VolumeServerInfo.Kind, dataId: string, emDefaultContourLevel: number) {
entries.push({
@@ -54,7 +55,7 @@ export const InitVolumeStreaming = StateAction.build({
isApplicable: (a, _, plugin: PluginContext) => {
const canStreamTest = plugin.config.get(PluginConfig.VolumeStreaming.CanStream);
if (canStreamTest) return canStreamTest(a.data, plugin);
return a.data.models.length === 1;
return a.data.models.length === 1 && Model.hasDensityMap(a.data.models[0]);
}
})(({ ref, state, params }, plugin: PluginContext) => Task.create('Volume Streaming', async taskCtx => {
const entries: InfoEntryProps[] = []

View File

@@ -81,6 +81,7 @@ export function UpdateRepresentationVisibility(ctx: PluginContext) {
const cell = e.state.cells.get(e.ref)!;
if (!SO.isRepresentation3D(cell.obj)) return;
updateVisibility(cell, cell.obj.data.repr);
ctx.canvas3d?.syncVisibility();
ctx.canvas3d?.requestDraw(true);
})
}

View File

@@ -2,9 +2,10 @@
* Copyright (c) 2020 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 { Structure } from '../mol-model/structure';
import { Structure, Model } from '../mol-model/structure';
import { PluginContext } from './context';
export class PluginConfigItem<T = any> {
@@ -23,7 +24,11 @@ export const PluginConfig = {
},
VolumeStreaming: {
DefaultServer: item('volume-streaming.server', 'https://ds.litemol.org'),
CanStream: item('volume-streaming.can-stream', (s: Structure, plugin: PluginContext) => s.models.length === 1)
CanStream: item('volume-streaming.can-stream', (s: Structure, plugin: PluginContext) => {
return s.models.length === 1 && (Model.hasDensityMap(s.models[0])
// the following test is to include e.g. 'updated' files from PDBe
|| (!Model.isFromPdbArchive(s.models[0]) && s.models[0].entryId.length === 4))
})
}
}

View File

@@ -49,6 +49,7 @@ import { TaskManager } from './util/task-manager';
import { PluginToastManager } from './util/toast';
import { ViewportScreenshotHelper } from './util/viewport-screenshot';
import { PLUGIN_VERSION, PLUGIN_VERSION_DATE } from './version';
import { Representation } from '../mol-repr/representation';
export class PluginContext {
private disposed = false;
@@ -90,8 +91,8 @@ export class PluginContext {
isBusy: this.ev.behavior<boolean>(false)
},
interaction: {
hover: this.ev.behavior<InteractivityManager.HoverEvent>({ current: InteractivityManager.Loci.Empty, modifiers: ModifiersKeys.None, buttons: 0, button: 0 }),
click: this.ev.behavior<InteractivityManager.ClickEvent>({ current: InteractivityManager.Loci.Empty, modifiers: ModifiersKeys.None, buttons: 0, button: 0 })
hover: this.ev.behavior<InteractivityManager.HoverEvent>({ current: Representation.Loci.Empty, modifiers: ModifiersKeys.None, buttons: 0, button: 0 }),
click: this.ev.behavior<InteractivityManager.ClickEvent>({ current: Representation.Loci.Empty, modifiers: ModifiersKeys.None, buttons: 0, button: 0 })
},
labels: {
highlight: this.ev.behavior<{ entries: ReadonlyArray<LociLabelEntry> }>({ entries: [] })

View File

@@ -0,0 +1,120 @@
/**
* Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Model, Symmetry } from '../../../mol-model/structure';
import { ShapeRepresentation } from '../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 { BoxCage } from '../../../mol-geo/primitive/box';
import { Mat4, Vec3 } from '../../../mol-math/linear-algebra';
import { transformCage, cloneCage } from '../../../mol-geo/primitive/cage';
import { radToDeg } from '../../../mol-math/misc';
import { Sphere3D } from '../../../mol-math/geometry';
import { RepresentationParamsGetter, Representation, RepresentationContext } from '../../representation';
const translate05 = Mat4.fromTranslation(Mat4(), Vec3.create(0.5, 0.5, 0.5))
const unitCage = transformCage(cloneCage(BoxCage()), translate05)
const tmpRef = Vec3()
const tmpTranslate = Mat4()
interface UnitcellData {
symmetry: Symmetry
ref: Vec3
}
const CellParams = {
...Mesh.Params,
cellColor: PD.Color(ColorNames.orange),
cellScale: PD.Numeric(2, { min: 0.1, max: 5, step: 0.1 })
}
type MeshParams = typeof CellParams
const UnitcellVisuals = {
'mesh': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<UnitcellData, MeshParams>) => ShapeRepresentation(getUnitcellShape, Mesh.Utils),
}
export const UnitcellParams = {
...CellParams
}
export type UnitcellParams = typeof UnitcellParams
export type UnitcellProps = PD.Values<UnitcellParams>
function getUnitcellMesh(data: UnitcellData, props: UnitcellProps, mesh?: Mesh) {
const state = MeshBuilder.createState(256, 128, mesh)
const { fromFractional } = data.symmetry.spacegroup.cell
Vec3.floor(tmpRef, data.ref)
Mat4.fromTranslation(tmpTranslate, tmpRef)
const cellCage = transformCage(cloneCage(unitCage), tmpTranslate)
const radius = (Math.cbrt(data.symmetry.spacegroup.cell.volume) / 300) * props.cellScale
state.currentGroup = 1
MeshBuilder.addCage(state, fromFractional, cellCage, radius, 2, 20)
const cpA = Vec3.create(0, 0, 0)
Vec3.transformMat4(cpA, Vec3.add(cpA, cpA, tmpRef), fromFractional)
const cpB = Vec3.create(1, 1, 1)
Vec3.transformMat4(cpB, Vec3.add(cpB, cpB, tmpRef), fromFractional)
const cpC = Vec3.create(1, 0, 0)
Vec3.transformMat4(cpC, Vec3.add(cpC, cpC, tmpRef), fromFractional)
const cpD = Vec3.create(0, 1, 1)
Vec3.transformMat4(cpD, Vec3.add(cpD, cpD, tmpRef), fromFractional)
const center = Vec3()
Vec3.add(center, cpA, cpB)
Vec3.scale(center, center, 0.5)
const d = Math.max(Vec3.distance(cpA, cpB), Vec3.distance(cpC, cpD))
const sphere = Sphere3D.create(center, d / 2 + radius)
const m = MeshBuilder.getMesh(state)
m.setBoundingSphere(sphere)
return m
}
function getUnitcellLabel(data: UnitcellData) {
const { cell, name, num } = data.symmetry.spacegroup
const { size, anglesInRadians } = cell
const a = size[0].toFixed(2)
const b = size[1].toFixed(2)
const c = size[2].toFixed(2)
const alpha = radToDeg(anglesInRadians[0]).toFixed(2)
const beta = radToDeg(anglesInRadians[1]).toFixed(2)
const gamma = radToDeg(anglesInRadians[2]).toFixed(2)
const label: string[] = []
// name
label.push(`${name} #${num}`)
// sizes
label.push(`${a}\u00D7${b}\u00D7${c} \u212B`)
// angles
label.push(`\u03b1=${alpha}\u00B0 \u03b2=${beta}\u00B0 \u03b3=${gamma}\u00B0`)
return label.join(' | ')
}
function getUnitcellShape(ctx: RuntimeContext, data: UnitcellData, props: UnitcellProps, shape?: Shape<Mesh>) {
const geo = getUnitcellMesh(data, props, shape && shape.geometry);
const label = getUnitcellLabel(data)
return Shape.create('Unitcell', data, geo, () => props.cellColor, () => 1, () => label)
}
//
export function getUnitcellData(model: Model, symmetry: Symmetry) {
return {
symmetry,
ref: Vec3.transformMat4(Vec3(), Model.getCenter(model), symmetry.spacegroup.cell.toFractional)
}
}
export type UnitcellRepresentation = Representation<UnitcellData, UnitcellParams>
export function UnitcellRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<UnitcellData, UnitcellParams>): UnitcellRepresentation {
return Representation.createMulti('Unitcell', ctx, getParams, Representation.StateBuilder, UnitcellVisuals as unknown as Representation.Def<UnitcellData, UnitcellParams>)
}

View File

@@ -21,6 +21,7 @@ import { Interval, OrderedSet } from '../../../mol-data/int';
import { isHydrogen } from './util/common';
import { BondType } from '../../../mol-model/structure/model/types';
import { ignoreBondType, BondCylinderParams, BondIterator } from './util/bond';
import { Sphere3D } from '../../../mol-math/geometry';
function createIntraUnitBondCylinderMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<IntraUnitBondParams>, mesh?: Mesh) {
if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh)
@@ -92,7 +93,12 @@ function createIntraUnitBondCylinderMesh(ctx: VisualContext, unit: Unit, structu
ignore: (edgeIndex: number) => ignoreHydrogen(edgeIndex) || ignoreBondType(include, exclude, _flags[edgeIndex])
}
return createLinkCylinderMesh(ctx, builderProps, props, mesh)
const m = createLinkCylinderMesh(ctx, builderProps, props, mesh)
const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1 * sizeFactor)
m.setBoundingSphere(sphere)
return m
}
export const IntraUnitBondParams = {

View File

@@ -14,6 +14,7 @@ import { PointsBuilder } from '../../../mol-geo/geometry/points/points-builder';
import { Vec3 } from '../../../mol-math/linear-algebra';
import { ElementIterator, getElementLoci, eachElement } from './util/element';
import { VisualUpdateState } from '../../util';
import { Sphere3D } from '../../../mol-math/geometry';
export const ElementPointParams = {
...UnitsPointsParams,
@@ -39,7 +40,13 @@ export function createElementPoint(ctx: VisualContext, unit: Unit, structure: St
pos(elements[i], p)
builder.add(p[0], p[1], p[2], i)
}
return builder.getPoints()
const pt = builder.getPoints()
const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1 * props.sizeFactor)
pt.setBoundingSphere(sphere)
return pt
}
export function ElementPointVisual(materialId: number): UnitsVisual<ElementPointParams> {

View File

@@ -20,6 +20,7 @@ import { addEllipsoid } from '../../../mol-geo/geometry/mesh/builder/ellipsoid';
import { AtomSiteAnisotrop } from '../../../mol-model-formats/structure/property/anisotropic'
import { equalEps } from '../../../mol-math/linear-algebra/3d/common';
import { addSphere } from '../../../mol-geo/geometry/mesh/builder/sphere';
import { Sphere3D } from '../../../mol-math/geometry';
export const EllipsoidMeshParams = {
...UnitsMeshParams,
@@ -66,7 +67,7 @@ export function createEllipsoidMesh(ctx: VisualContext, unit: Unit, structure: S
if (!atomSiteAnisotrop) return Mesh.createEmpty(mesh)
const v = Vec3()
const m = Mat3()
const mat = Mat3()
const eigvals = Vec3()
const eigvec1 = Vec3()
const eigvec2 = Vec3()
@@ -87,11 +88,11 @@ export function createEllipsoidMesh(ctx: VisualContext, unit: Unit, structure: S
pos(ei, v)
builderState.currentGroup = i
Tensor.toMat3(m, space, U.value(ai))
Mat3.symmtricFromLower(m, m)
Mat3.symmetricEigenvalues(eigvals, m)
Mat3.eigenvector(eigvec1, m, eigvals[1])
Mat3.eigenvector(eigvec2, m, eigvals[2])
Tensor.toMat3(mat, space, U.value(ai))
Mat3.symmtricFromLower(mat, mat)
Mat3.symmetricEigenvalues(eigvals, mat)
Mat3.eigenvector(eigvec1, mat, eigvals[1])
Mat3.eigenvector(eigvec2, mat, eigvals[2])
for (let j = 0; j < 3; ++j) {
// show 50% probability surface, needs sqrt as U matrix is in angstrom-squared
// take abs of eigenvalue to avoid reflection
@@ -106,5 +107,10 @@ export function createEllipsoidMesh(ctx: VisualContext, unit: Unit, structure: S
}
}
return MeshBuilder.getMesh(builderState)
const m = MeshBuilder.getMesh(builderState)
const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1 * sizeFactor)
m.setBoundingSphere(sphere)
return m
}

View File

@@ -49,6 +49,9 @@ async function createGaussianSurfaceMesh(ctx: VisualContext, unit: Unit, structu
Mesh.transform(surface, transform)
if (ctx.webgl && !ctx.webgl.isWebGL2) Mesh.uniformTriangleGroup(surface)
const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, props.radiusOffset)
surface.setBoundingSphere(sphere)
return surface
}
@@ -93,6 +96,9 @@ async function createStructureGaussianSurfaceMesh(ctx: VisualContext, structure:
Mesh.transform(surface, transform)
if (ctx.webgl && !ctx.webgl.isWebGL2) Mesh.uniformTriangleGroup(surface)
const sphere = Sphere3D.expand(Sphere3D(), structure.boundary.sphere, props.radiusOffset)
surface.setBoundingSphere(sphere)
return surface
}

View File

@@ -14,6 +14,7 @@ import { computeMarchingCubesLines } from '../../../mol-geo/util/marching-cubes/
import { UnitsLinesParams, UnitsVisual, UnitsLinesVisual } from '../units-visual';
import { ElementIterator, getElementLoci, eachElement } from './util/element';
import { VisualUpdateState } from '../../util';
import { Sphere3D } from '../../../mol-math/geometry';
async function createGaussianWireframe(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: GaussianDensityProps, lines?: Lines): Promise<Lines> {
const { smoothness } = props
@@ -28,6 +29,9 @@ async function createGaussianWireframe(ctx: VisualContext, unit: Unit, structure
Lines.transform(wireframe, transform)
const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, props.radiusOffset)
wireframe.setBoundingSphere(sphere)
return wireframe
}

View File

@@ -16,6 +16,7 @@ import { computeMarchingCubesMesh } from '../../../mol-geo/util/marching-cubes/a
import { ElementIterator, getElementLoci, eachElement } from './util/element';
import { VisualUpdateState } from '../../util';
import { CommonSurfaceParams } from './util/common';
import { Sphere3D } from '../../../mol-math/geometry';
export const MolecularSurfaceMeshParams = {
...UnitsMeshParams,
@@ -38,6 +39,9 @@ async function createMolecularSurfaceMesh(ctx: VisualContext, unit: Unit, struct
Mesh.transform(surface, transform)
if (ctx.webgl && !ctx.webgl.isWebGL2) Mesh.uniformTriangleGroup(surface)
const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, props.probeRadius)
surface.setBoundingSphere(sphere)
return surface
}

View File

@@ -16,6 +16,7 @@ import { computeMarchingCubesLines } from '../../../mol-geo/util/marching-cubes/
import { ElementIterator, getElementLoci, eachElement } from './util/element';
import { VisualUpdateState } from '../../util';
import { CommonSurfaceParams } from './util/common';
import { Sphere3D } from '../../../mol-math/geometry';
export const MolecularSurfaceWireframeParams = {
...UnitsLinesParams,
@@ -38,6 +39,9 @@ async function createMolecularSurfaceWireframe(ctx: VisualContext, unit: Unit, s
Lines.transform(wireframe, transform)
const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, props.probeRadius)
wireframe.setBoundingSphere(sphere)
return wireframe
}

View File

@@ -20,6 +20,7 @@ import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual } from '../units-visual';
import { NucleotideLocationIterator, getNucleotideElementLoci, eachNucleotideElement } from './util/nucleotide';
import { VisualUpdateState } from '../../util';
import { BaseGeometry } from '../../../mol-geo/geometry/base';
import { Sphere3D } from '../../../mol-math/geometry';
const p1 = Vec3.zero()
const p2 = Vec3.zero()
@@ -134,7 +135,12 @@ function createNucleotideBlockMesh(ctx: VisualContext, unit: Unit, structure: St
}
}
return MeshBuilder.getMesh(builderState)
const m = MeshBuilder.getMesh(builderState)
const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1 * props.sizeFactor)
m.setBoundingSphere(sphere)
return m
}
export const NucleotideBlockParams = {

View File

@@ -21,6 +21,7 @@ import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual } from '../units-visual';
import { NucleotideLocationIterator, getNucleotideElementLoci, eachNucleotideElement } from './util/nucleotide';
import { VisualUpdateState } from '../../util';
import { BaseGeometry } from '../../../mol-geo/geometry/base';
import { Sphere3D } from '../../../mol-math/geometry';
const pTrace = Vec3.zero()
const pN1 = Vec3.zero()
@@ -189,7 +190,12 @@ function createNucleotideRingMesh(ctx: VisualContext, unit: Unit, structure: Str
}
}
return MeshBuilder.getMesh(builderState)
const m = MeshBuilder.getMesh(builderState)
const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1 * props.sizeFactor)
m.setBoundingSphere(sphere)
return m
}
export const NucleotideRingParams = {

View File

@@ -14,7 +14,7 @@ import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
import { Vec3 } from '../../../mol-math/linear-algebra';
import { addEllipsoid } from '../../../mol-geo/geometry/mesh/builder/ellipsoid';
import { Axes3D } from '../../../mol-math/geometry';
import { Axes3D, Sphere3D } from '../../../mol-math/geometry';
import { PickingId } from '../../../mol-geo/geometry/picking';
import { OrderedSet, Interval } from '../../../mol-data/int';
import { EmptyLoci, Loci } from '../../../mol-model/loci';
@@ -82,7 +82,12 @@ export function createOrientationEllipsoidMesh(ctx: VisualContext, unit: Unit, s
builderState.currentGroup = 0
addEllipsoid(builderState, origin, dirA, dirB, radiusScale, detail)
return MeshBuilder.getMesh(builderState)
const m = MeshBuilder.getMesh(builderState)
const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1 * props.sizeFactor)
m.setBoundingSphere(sphere)
return m
}
//

View File

@@ -19,6 +19,7 @@ import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual } from '../units-visual';
import { ElementIterator, getElementLoci, eachElement } from './util/element';
import { VisualUpdateState } from '../../util';
import { BaseGeometry } from '../../../mol-geo/geometry/base';
import { Sphere3D } from '../../../mol-math/geometry';
export const PolymerBackboneCylinderParams = {
sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
@@ -58,7 +59,12 @@ function createPolymerBackboneCylinderMesh(ctx: VisualContext, unit: Unit, struc
addCylinder(builderState, pB, pA, 0.5, cylinderProps)
}
return MeshBuilder.getMesh(builderState)
const m = MeshBuilder.getMesh(builderState)
const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1 * props.sizeFactor)
m.setBoundingSphere(sphere)
return m
}
export const PolymerBackboneParams = {

View File

@@ -16,6 +16,7 @@ import { createCurveSegmentState, PolymerTraceIterator, interpolateCurveSegment,
import { isNucleic, SecondaryStructureType } from '../../../mol-model/structure/model/types';
import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual } from '../units-visual';
import { VisualUpdateState } from '../../util';
import { Sphere3D } from '../../../mol-math/geometry';
const t = Mat4.identity()
const sVec = Vec3.zero()
@@ -82,7 +83,12 @@ function createPolymerDirectionWedgeMesh(ctx: VisualContext, unit: Unit, structu
++i
}
return MeshBuilder.getMesh(builderState)
const m = MeshBuilder.getMesh(builderState)
const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1 * props.sizeFactor)
m.setBoundingSphere(sphere)
return m
}
export const PolymerDirectionParams = {

View File

@@ -17,6 +17,7 @@ import { addFixedCountDashedCylinder } from '../../../mol-geo/geometry/mesh/buil
import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual } from '../units-visual';
import { VisualUpdateState } from '../../util';
import { BaseGeometry } from '../../../mol-geo/geometry/base';
import { Sphere3D } from '../../../mol-math/geometry';
// import { TriangularPyramid } from '../../../mol-geo/primitive/pyramid';
const segmentCount = 10
@@ -77,7 +78,12 @@ function createPolymerGapCylinderMesh(ctx: VisualContext, unit: Unit, structure:
i += 2
}
return MeshBuilder.getMesh(builderState)
const m = MeshBuilder.getMesh(builderState)
const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1 * props.sizeFactor)
m.setBoundingSphere(sphere)
return m
}
export const PolymerGapParams = {

View File

@@ -21,6 +21,7 @@ import { addRibbon } from '../../../mol-geo/geometry/mesh/builder/ribbon';
import { addSphere } from '../../../mol-geo/geometry/mesh/builder/sphere';
import { Vec3 } from '../../../mol-math/linear-algebra';
import { BaseGeometry } from '../../../mol-geo/geometry/base';
import { Sphere3D } from '../../../mol-math/geometry';
export const PolymerTraceMeshParams = {
sizeFactor: PD.Numeric(0.2, { min: 0, max: 10, step: 0.01 }),
@@ -147,7 +148,12 @@ function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure: Struc
++i
}
return MeshBuilder.getMesh(builderState)
const m = MeshBuilder.getMesh(builderState)
const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1 * props.sizeFactor)
m.setBoundingSphere(sphere)
return m
}
export const PolymerTraceParams = {

View File

@@ -20,6 +20,7 @@ import { addRibbon } from '../../../mol-geo/geometry/mesh/builder/ribbon';
import { Vec3 } from '../../../mol-math/linear-algebra';
import { addSphere } from '../../../mol-geo/geometry/mesh/builder/sphere';
import { BaseGeometry } from '../../../mol-geo/geometry/base';
import { Sphere3D } from '../../../mol-math/geometry';
export const PolymerTubeMeshParams = {
sizeFactor: PD.Numeric(0.2, { min: 0, max: 10, step: 0.01 }),
@@ -100,7 +101,12 @@ function createPolymerTubeMesh(ctx: VisualContext, unit: Unit, structure: Struct
++i
}
return MeshBuilder.getMesh(builderState)
const m = MeshBuilder.getMesh(builderState)
const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1 * props.sizeFactor)
m.setBoundingSphere(sphere)
return m
}
export const PolymerTubeParams = {

View File

@@ -14,6 +14,7 @@ import { AtomicNumbers, AtomNumber } from '../../../../mol-model/structure/model
import { fillSerial } from '../../../../mol-util/array';
import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
import { AssignableArrayLike } from '../../../../mol-util/type-helpers';
import { getBoundary } from '../../../../mol-math/geometry/boundary';
/** Return a Loci for the elements of a whole residue the elementIndex belongs to. */
export function getResidueLoci(structure: Structure, unit: Unit.Atomic, elementIndex: ElementIndex): Loci {
@@ -139,7 +140,7 @@ export function getUnitConformationAndRadius(structure: Structure, unit: Unit, p
const { x, y, z } = getConformation(rootUnit)
const { elements } = rootUnit
const { center, radius: sphereRadius } = unit.lookup3d.boundary.sphere
const { center, radius: sphereRadius } = unit.boundary.sphere
const extraRadius = (2 + 1.5) * 2 // TODO should be twice (the max vdW/sphere radius plus the probe radius)
const radiusSq = (sphereRadius + extraRadius) * (sphereRadius + extraRadius)
@@ -169,6 +170,7 @@ export function getUnitConformationAndRadius(structure: Structure, unit: Unit, p
}
const position = { indices, x, y, z, id }
const boundary = unit === rootUnit ? unit.boundary : getBoundary(position)
const l = StructureElement.Location.create(structure, rootUnit)
const sizeTheme = PhysicalSizeTheme({}, {})
@@ -177,7 +179,7 @@ export function getUnitConformationAndRadius(structure: Structure, unit: Unit, p
return sizeTheme.size(l)
}
return { position, radius }
return { position, boundary, radius }
}
export function getStructureConformationAndRadius(structure: Structure, ignoreHydrogens: boolean) {

View File

@@ -21,6 +21,7 @@ import { StructureGroup } from '../../../../mol-repr/structure/units-visual';
import { Spheres } from '../../../../mol-geo/geometry/spheres/spheres';
import { SpheresBuilder } from '../../../../mol-geo/geometry/spheres/spheres-builder';
import { isHydrogen } from './common';
import { Sphere3D } from '../../../../mol-math/geometry';
export interface ElementSphereMeshProps {
detail: number,
@@ -51,7 +52,12 @@ export function createElementSphereMesh(ctx: VisualContext, unit: Unit, structur
addSphere(builderState, v, theme.size.size(l) * sizeFactor, detail)
}
return MeshBuilder.getMesh(builderState)
const m = MeshBuilder.getMesh(builderState)
const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1 * sizeFactor)
m.setBoundingSphere(sphere)
return m
}
export interface ElementSphereImpostorProps {
@@ -74,7 +80,12 @@ export function createElementSphereImpostor(ctx: VisualContext, unit: Unit, stru
builder.add(v[0], v[1], v[2], i)
}
return builder.getSpheres()
const s = builder.getSpheres()
const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1)
s.setBoundingSphere(sphere)
return s
}
export function eachElement(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {

View File

@@ -10,12 +10,13 @@ import { getUnitConformationAndRadius, CommonSurfaceProps } from './common';
import { PositionData, DensityData, Box3D } from '../../../../mol-math/geometry';
import { MolecularSurfaceCalculationProps, calcMolecularSurface } from '../../../../mol-math/geometry/molecular-surface';
import { OrderedSet } from '../../../../mol-data/int';
import { Boundary } from '../../../../mol-math/geometry/boundary';
export type MolecularSurfaceProps = MolecularSurfaceCalculationProps & CommonSurfaceProps
function getPositionDataAndMaxRadius(structure: Structure, unit: Unit, props: MolecularSurfaceProps) {
const { probeRadius } = props
const { position, radius } = getUnitConformationAndRadius(structure, unit, props)
const { position, boundary, radius } = getUnitConformationAndRadius(structure, unit, props)
const { indices } = position
const n = OrderedSet.size(indices)
const radii = new Float32Array(OrderedSet.end(indices))
@@ -28,19 +29,19 @@ function getPositionDataAndMaxRadius(structure: Structure, unit: Unit, props: Mo
radii[j] = r + probeRadius
}
return { position: { ...position, radius: radii }, maxRadius }
return { position: { ...position, radius: radii }, boundary, maxRadius }
}
export function computeUnitMolecularSurface(structure: Structure, unit: Unit, props: MolecularSurfaceProps) {
const { box } = unit.lookup3d.boundary
const { position, maxRadius } = getPositionDataAndMaxRadius(structure, unit, props)
const { position, boundary, maxRadius } = getPositionDataAndMaxRadius(structure, unit, props)
return Task.create('Molecular Surface', async ctx => {
return await MolecularSurface(ctx, position, maxRadius, box, props);
return await MolecularSurface(ctx, position, boundary, maxRadius, box, props);
});
}
//
async function MolecularSurface(ctx: RuntimeContext, position: Required<PositionData>, maxRadius: number, box: Box3D | null, props: MolecularSurfaceCalculationProps): Promise<DensityData> {
return calcMolecularSurface(ctx, position, maxRadius, box, props)
async function MolecularSurface(ctx: RuntimeContext, position: Required<PositionData>, boundary: Boundary, maxRadius: number, box: Box3D | null, props: MolecularSurfaceCalculationProps): Promise<DensityData> {
return calcMolecularSurface(ctx, position, boundary, maxRadius, box, props)
}

View File

@@ -7,11 +7,14 @@
export async function retryIf<T>(promiseProvider: () => Promise<T>, params: {
retryThenIf?: (result: T) => boolean,
retryCatchIf?: (error: any) => boolean,
onRetry?: () => void,
retryCount: number
}) {
let count = 0;
while (count <= params.retryCount) {
try {
if (count > 0) params.onRetry?.();
const result = await promiseProvider();
if (params.retryThenIf && params.retryThenIf(result)) {
count++;

View File

@@ -7,6 +7,7 @@
export type Mutable<T> = { -readonly [P in keyof T]: T[P] }
export type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
export type PickRequired<T, K extends keyof T> = T & Required<Pick<T, K>>
export type TypedIntArray = Int8Array | Int16Array | Int32Array | Uint8Array | Uint16Array | Uint32Array
export type TypedFloatArray = Float32Array | Float64Array

View File

@@ -8,6 +8,7 @@ import { GridLookup3D } from '../mol-math/geometry';
// import { sortArray } from 'mol-data/util';
import { OrderedSet } from '../mol-data/int';
import { trajectoryFromMmCIF, MmcifFormat } from '../mol-model-formats/structure/mmcif';
import { getBoundary } from '../mol-math/geometry/boundary';
require('util.promisify').shim();
const readFileAsync = util.promisify(fs.readFile);
@@ -41,11 +42,12 @@ export async function readCIF(path: string) {
export async function test() {
const { mmcif, structures } = await readCIF('e:/test/quick/1tqn_updated.cif');
const lookup = GridLookup3D({ x: mmcif.db.atom_site.Cartn_x.toArray(), y: mmcif.db.atom_site.Cartn_y.toArray(), z: mmcif.db.atom_site.Cartn_z.toArray(),
const position = { x: mmcif.db.atom_site.Cartn_x.toArray(), y: mmcif.db.atom_site.Cartn_y.toArray(), z: mmcif.db.atom_site.Cartn_z.toArray(),
indices: OrderedSet.ofBounds(0, mmcif.db.atom_site._rowCount),
// radius: [1, 1, 1, 1]
// indices: [1]
});
}
const lookup = GridLookup3D(position, getBoundary(position));
console.log(lookup.boundary.box, lookup.boundary.sphere);
const result = lookup.find(-30.07, 8.178, -13.897, 10);

View File

@@ -86,17 +86,24 @@ const DefaultModelServerConfig = {
defaultSource: 'pdb-cif' as string,
/**
* Maps a request identifier to a filename given a 'source' and 'id' variables.
* Maps a request identifier to either:
* - filename [source, mapping]
* - URI [source, mapping, format]
*
* Mapping is provided 'source' and 'id' variables to interpolate.
*
* /static query uses 'pdb-cif' and 'pdb-bcif' source names.
*/
sourceMap: [
['pdb-cif', 'e:/test/quick/${id}_updated.cif'],
// ['pdb-bcif', 'e:/test/quick/${id}.bcif'],
] as [string, string][]
] as ([string, string] | [string, string, ModelServerFetchFormats])[]
};
export let mapSourceAndIdToFilename: (source: string, id: string) => string = () => {
export const ModelServerFetchFormats = ['cif', 'bcif', 'cif.gz', 'bcif.gz'] as const
export type ModelServerFetchFormats = (typeof ModelServerFetchFormats)[number]
export let mapSourceAndIdToFilename: (source: string, id: string) => [string, ModelServerFetchFormats] = () => {
throw new Error('call setupConfig & validateConfigAndSetupSourceMap to initialize this function');
}
@@ -156,7 +163,18 @@ function addServerArgs(parser: argparse.ArgumentParser) {
'Example: pdb-bcif \'../../data/bcif/${id}.bcif\' ',
'JS expressions can be used inside ${}, e.g. \'${id.substr(1, 2)}/${id}.mdb\'',
'Can be specified multiple times.',
'The `SOURCE` variable (e.g. `pdb-bcif`) is arbitrary and depends on how you plan to use the server.'
'The `SOURCE` variable (e.g. `pdb-bcif`) is arbitrary and depends on how you plan to use the server.',
`Supported formats: ${ModelServerFetchFormats.join(', ')}`
].join('\n'),
});
parser.addArgument([ '--sourceMapUrl' ], {
nargs: 3,
action: 'append',
metavar: ['SOURCE', 'PATH', 'SOURCE_MAP_FORMAT'] as any,
help: [
'Same as --sourceMap but for URL. \'--sourceMapUrl src url format\'',
'Example: \'pdb-cif "https://www.ebi.ac.uk/pdbe/entry-files/download/${id}_updated.cif" cif\'',
`Supported formats: ${ModelServerFetchFormats.join(', ')}`
].join('\n'),
});
}
@@ -170,7 +188,7 @@ export const ModelServerConfigTemplate: ModelServerConfig = {
sourceMap: [
['pdb-bcif', './path-to-binary-cif/${id.substr(1, 2)}/${id}.bcif'],
['pdb-cif', './path-to-text-cif/${id.substr(1, 2)}/${id}.cif'],
['pdb-updated', './path-to-updated-cif/${id}.bcif']
['pdb-updated', 'https://www.ebi.ac.uk/pdbe/entry-files/download/${id}_updated.cif', 'cif']
] as [string, string][]
}
@@ -199,6 +217,11 @@ function setConfig(config: ModelServerConfig) {
for (const k of ObjectKeys(ModelServerConfig)) {
if (config[k] !== void 0) (ModelServerConfig as any)[k] = config[k];
}
if ((config as any).sourceMapUrl) {
if (!ModelServerConfig.sourceMap) ModelServerConfig.sourceMap = [];
ModelServerConfig.sourceMap.push(...(config as any).sourceMapUrl);
}
}
function validateConfigAndSetupSourceMap() {
@@ -208,7 +231,7 @@ function validateConfigAndSetupSourceMap() {
mapSourceAndIdToFilename = new Function('source', 'id', [
'switch (source.toLowerCase()) {',
...ModelServerConfig.sourceMap.map(([source, path]) => `case '${source.toLowerCase()}': return \`${path}\`;`),
...ModelServerConfig.sourceMap.map(([source, path, format]) => `case '${source.toLowerCase()}': return [\`${path}\`, '${format}'];`),
'}',
].join('\n')) as any;
}

View File

@@ -22,7 +22,7 @@ export function preprocessFile(filename: string, propertyProvider?: ModelPropert
}
async function preprocess(filename: string, propertyProvider?: ModelPropertiesProvider, outputCif?: string, outputBcif?: string) {
const input = await readStructureWrapper('entry', '_local_', filename, propertyProvider);
const input = await readStructureWrapper('entry', '_local_', filename, void 0, propertyProvider);
const categories = await classifyCif(input.cifFrame);
const inputStructures = (await resolveStructures(input))!;
const exportCtx = CifExportContext.create(inputStructures);

View File

@@ -119,36 +119,46 @@ function mapQuery(app: express.Express, queryName: string, queryDefinition: Quer
});
}
export function initWebApi(app: express.Express) {
app.use(bodyParser.json({ limit: '1mb' }));
function serveStatic(req: express.Request, res: express.Response) {
const source = req.params.source === 'bcif'
? 'pdb-bcif'
: req.params.source === 'cif'
? 'pdb-cif'
: req.params.source;
const id = req.params.id;
const [fn, format] = mapSourceAndIdToFilename(source, id);
const binary = format === 'bcif' || fn.indexOf('.bcif') > 0;
app.get(makePath('static/:format/:id'), async (req, res) => {
const binary = req.params.format === 'bcif';
const id = req.params.id;
const fn = mapSourceAndIdToFilename(binary ? 'pdb-bcif' : 'pdb-cif', id);
if (!fn || !fs.existsSync(fn)) {
if (!fn || !fs.existsSync(fn)) {
res.status(404);
res.end();
return;
}
fs.readFile(fn, (err, data) => {
if (err) {
res.status(404);
res.end();
return;
}
fs.readFile(fn, (err, data) => {
if (err) {
res.status(404);
res.end();
return;
}
const f = path.parse(fn);
res.writeHead(200, {
'Content-Type': binary ? 'application/octet-stream' : 'text/plain; charset=utf-8',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'X-Requested-With',
'Content-Disposition': `inline; filename="${f.name}${f.ext}"`
});
res.write(data);
res.end();
const f = path.parse(fn);
res.writeHead(200, {
'Content-Type': binary ? 'application/octet-stream' : 'text/plain; charset=utf-8',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'X-Requested-With',
'Content-Disposition': `inline; filename="${f.name}${f.ext}"`
});
})
res.write(data);
res.end();
});
}
export function initWebApi(app: express.Express) {
app.use(bodyParser.json({ limit: '1mb' }));
app.get(makePath('static/:source/:id'), (req, res) => serveStatic(req, res));
app.get(makePath('v1/static/:source/:id'), (req, res) => serveStatic(req, res));
// app.get(makePath('v1/json'), (req, res) => {
// const query = /\?(.*)$/.exec(req.url)![1];

View File

@@ -44,7 +44,7 @@ export interface QueryDefinition<Params = any> {
export const CommonQueryParamsInfo: QueryParamInfo[] = [
{ name: 'model_nums', type: QueryParamType.String, description: `A comma-separated list of model ids (i.e. 1,2). If set, only include atoms with the corresponding '_atom_site.pdbx_PDB_model_num' field.` },
{ name: 'encoding', type: QueryParamType.String, defaultValue: 'cif', description: `Determines the output encoding (text based 'CIF' or binary 'BCIF').`, supportedValues: ['cif', 'bcif'] },
{ name: 'data_Source', type: QueryParamType.String, defaultValue: '', description: 'Allows to control how the provided data source ID maps to input file (as specified by the server instance config).' }
{ name: 'data_source', type: QueryParamType.String, defaultValue: '', description: 'Allows to control how the provided data source ID maps to input file (as specified by the server instance config).' }
];
export interface CommonQueryParamsInfo {

View File

@@ -1,93 +0,0 @@
/**
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import Version from '../version'
const examples = [{
name: 'Atoms',
params: {
id: '1cbs',
name: 'atoms',
params: { atom_site: { label_comp_id: 'ALA' } }
}
}, {
name: 'Residue Interaction',
params: {
id: '1cbs',
name: 'residueInteraction',
params: {
radius: 5,
atom_site: { 'label_comp_id': 'REA' }
}
}
}, {
name: 'Full',
params: {
id: '1tqn',
name: 'full'
}
}, {
name: 'Full (binary)',
params: {
id: '1tqn',
name: 'full',
binary: true
}
}, {
name: 'Full (specific models)',
params: {
id: '1grm',
name: 'full',
modelNums: [ 2, 3 ]
}
}];
function create() {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<title>Mol* ModelServer ${Version}</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css" />
</head>
<body>
<h1>Mol* Model Server ${Version}</h1>
<select id='example'>
<option value='-1'>Select example...</option>
${examples.map((e, i) => `<option value=${i}>${e.name}</option>`)}
</select>
<br/>
<textarea style="height: 280px; width: 600px; font-family: monospace" id="query-text"></textarea><br>
<button class="button button-primary" style="width: 600px" id="query">Query</button>
<div id='error' style='color: red; font-weight: blue'></div>
<div>Static input files available as CIF and BinaryCIF at <a href='/ModelServer/static/cif/1cbs' target='_blank'>static/cif/id</a> and <a href='/ModelServer/static/bcif/1cbs' target='_blank'>static/bcif/id</a> respectively.</div>
<script>
var Examples = ${JSON.stringify(examples)};
var err = document.getElementById('error');
var exampleEl = document.getElementById('example'), queryTextEl = document.getElementById('query-text');
exampleEl.onchange = function () {
var i = +exampleEl.value;
if (i < 0) return;
queryTextEl.value = JSON.stringify(Examples[i].params, null, 2);
};
document.getElementById('query').onclick = function () {
err.innerText = '';
try {
var q = JSON.parse(queryTextEl.value);
var path = '/ModelServer/api/v1?' + encodeURIComponent(JSON.stringify(q));
console.log(path);
window.open(path, '_blank');
} catch (e) {
err.innerText = '' + e;
}
};
</script>
</body>
</html>`;
}
export const LandingPage = create();

View File

@@ -7,7 +7,7 @@
import { Structure, Model } from '../../../mol-model/structure';
import { PerformanceMonitor } from '../../../mol-util/performance-monitor';
import { Cache } from './cache';
import { ModelServerConfig as Config, mapSourceAndIdToFilename } from '../config';
import { ModelServerConfig as Config, mapSourceAndIdToFilename, ModelServerFetchFormats } from '../config';
import { CIF, CifFrame, CifBlock } from '../../../mol-io/reader/cif'
import * as util from 'util'
import * as fs from 'fs'
@@ -16,6 +16,7 @@ import { Job } from './jobs';
import { ConsoleLogger } from '../../../mol-util/console-logger';
import { ModelPropertiesProvider } from '../property-provider';
import { trajectoryFromMmCIF } from '../../../mol-model-formats/structure/mmcif';
import { fetchRetry } from '../utils/fetch-retry';
require('util.promisify').shim();
@@ -53,7 +54,7 @@ export async function createStructureWrapperFromJob(job: Job, propertyProvider:
const ret = StructureCache.get(job.key);
if (ret) return ret;
}
const ret = await readStructureWrapper(job.key, job.sourceId, job.entryId, propertyProvider);
const ret = await readStructureWrapper(job.key, job.sourceId, job.entryId, job.id, propertyProvider);
if (allowCache && Config.cacheMaxSizeInBytes > 0) {
StructureCache.add(ret);
}
@@ -73,13 +74,13 @@ async function readFile(filename: string) {
if (isGz) input = await unzipAsync(input);
const data = new Uint8Array(input.byteLength);
for (let i = 0; i < input.byteLength; i++) data[i] = input[i];
return data;
return { data, isBinary: true };
} else {
if (isGz) {
const data = await unzipAsync(await readFileAsync(filename));
return data.toString('utf8');
return { data: data.toString('utf8'), isBinary: false };
}
return readFileAsync(filename, 'utf8');
return { data: await readFileAsync(filename, 'utf8'), isBinary: false };
}
}
@@ -90,11 +91,13 @@ async function parseCif(data: string|Uint8Array) {
return parsed.result;
}
export async function readDataAndFrame(filename: string, key?: string): Promise<{ data: string | Uint8Array, frame: CifBlock }> {
export async function readDataAndFrame(filename: string, key?: string): Promise<{ data: string | Uint8Array, frame: CifBlock, isBinary: boolean }> {
perf.start('read');
let data;
let data, isBinary;
try {
data = await readFile(filename);
const read = await readFile(filename);
data = read.data;
isBinary = read.isBinary;
} catch (e) {
ConsoleLogger.error(key || filename, '' + e);
throw new Error(`Could not read the file for '${key || filename}' from disk.`);
@@ -105,15 +108,57 @@ export async function readDataAndFrame(filename: string, key?: string): Promise<
const frame = (await parseCif(data)).blocks[0];
perf.end('parse');
return { data, frame };
return { data, frame, isBinary };
}
export async function readStructureWrapper(key: string, sourceId: string | '_local_', entryId: string, propertyProvider: ModelPropertiesProvider | undefined) {
const filename = sourceId === '_local_' ? entryId : mapSourceAndIdToFilename(sourceId, entryId);
if (!filename) throw new Error(`Cound not map '${key}' to a valid filename.`);
if (!fs.existsSync(filename)) throw new Error(`Could not find source file for '${key}'.`);
async function fetchDataAndFrame(jobId: string, uri: string, format: ModelServerFetchFormats, key?: string): Promise<{ data: string | Uint8Array, frame: CifBlock, isBinary: boolean }> {
perf.start('read');
const isBinary = format.startsWith('bcif');
let data;
try {
ConsoleLogger.logId(jobId, 'Fetch', `${uri}`);
const response = await fetchRetry(uri, 500, 3, () => ConsoleLogger.logId(jobId, 'Fetch', `Retrying to fetch '${uri}'`));
const { data, frame } = await readDataAndFrame(filename, key);
if (format.endsWith('.gz')) {
const input = await unzipAsync(await response.arrayBuffer());
if (isBinary) {
data = new Uint8Array(input.byteLength);
for (let i = 0; i < input.byteLength; i++) data[i] = input[i];
} else {
data = input.toString('utf8');
}
} else {
data = isBinary ? new Uint8Array(await response.arrayBuffer()) : await response.text();
}
} catch (e) {
ConsoleLogger.error(key || uri, '' + e);
throw new Error(`Could not fetch the file for '${key || uri}'.`);
}
perf.end('read');
perf.start('parse');
const frame = (await parseCif(data)).blocks[0];
perf.end('parse');
return { data, frame, isBinary };
}
function readOrFetch(jobId: string, key: string, sourceId: string | '_local_', entryId: string) {
const mapped = sourceId === '_local_' ? [entryId] as const : mapSourceAndIdToFilename(sourceId, entryId);
if (!mapped) throw new Error(`Cound not map '${key}' for a resource.`);
const uri = mapped[0].toLowerCase();
if (uri.startsWith('http://') || uri.startsWith('https://') || uri.startsWith('ftp://')) {
return fetchDataAndFrame(jobId, mapped[0], (mapped[1] || 'cif').toLowerCase() as any, key);
}
if (!fs.existsSync(mapped[0])) throw new Error(`Could not find source file for '${key}'.`);
return readDataAndFrame(mapped[0], key);
}
export async function readStructureWrapper(key: string, sourceId: string | '_local_', entryId: string, jobId: string | undefined, propertyProvider: ModelPropertiesProvider | undefined) {
const { data, frame, isBinary } = await readOrFetch(jobId || '', key, sourceId, entryId);
perf.start('createModel');
const models = await trajectoryFromMmCIF(frame).run();
perf.end('createModel');
@@ -133,7 +178,7 @@ export async function readStructureWrapper(key: string, sourceId: string | '_loc
sourceId,
entryId
},
isBinary: /\.bcif/.test(filename),
isBinary,
key,
approximateSize: typeof data === 'string' ? 2 * data.length : data.length,
models,

View File

@@ -16,11 +16,12 @@ function isRetriableNetworkError(error: any) {
return error && RETRIABLE_NETWORK_ERRORS.includes(error.code);
}
export async function fetchRetry(url: string, timeout: number, retryCount: number): Promise<Response> {
export async function fetchRetry(url: string, timeout: number, retryCount: number, onRetry?: () => void): Promise<Response> {
const result = await retryIf(() => fetch(url, { timeout }), {
retryThenIf: r => r.status >= 500 && r.status < 600,
retryThenIf: r => r.status === 408 /** timeout */ || r.status === 429 /** too mant requests */ || (r.status >= 500 && r.status < 600),
// TODO test retryCatchIf
retryCatchIf: e => isRetriableNetworkError(e),
onRetry,
retryCount
});

View File

@@ -2,17 +2,12 @@ const common = require('./webpack.config.common.js');
const createApp = common.createApp;
const createEntry = common.createEntry;
const createBrowserTest = common.createBrowserTest;
const createNodeEntryPoint = common.createNodeEntryPoint;
const createNodeApp = common.createNodeApp;
module.exports = [
createApp('viewer'),
createApp('basic-wrapper'),
createEntry('examples/proteopedia-wrapper/index', 'examples/proteopedia-wrapper', 'index'),
createEntry('apps/demos/lighting/index', 'demos/lighting', 'index'),
createNodeApp('state-docs'),
createNodeEntryPoint('preprocess', 'servers/model', 'model-server'),
createApp('model-server-query'),
createBrowserTest('font-atlas'),
createBrowserTest('marching-cubes'),