diff --git a/.vscode/tasks.json b/.vscode/tasks.json index c59f2cf6c7d1f739351734f9e300c144ced9e9c9..b3225a1f2aa3536b48e7f68f44220d42958af6eb 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -9,11 +9,6 @@ "problemMatcher": [ "$tsc" ] - }, - { - "type": "npm", - "script": "app-render-test", - "problemMatcher": [] } ] } \ No newline at end of file diff --git a/docs/state/readme.md b/docs/state/readme.md index bc20ea3df433e837a729edb60fe5da749ceef05b..22ae1ccc62d1f76e148bfbf672ac0b0ca2365d23 100644 --- a/docs/state/readme.md +++ b/docs/state/readme.md @@ -8,6 +8,8 @@ interface Snapshot { data?: State.Snapshot, // Snapshot of behavior state tree behaviour?: State.Snapshot, + // Snapshot for current animation, + animation?: PluginAnimationManager.Snapshot, // Saved camera positions cameraSnapshots?: CameraSnapshotManager.StateSnapshot, canvas3d?: { @@ -69,6 +71,10 @@ interface Transform.Props { "Built-in" data state transforms and description of their parameters are defined in ``mol-plugin/state/transforms``. Behavior transforms are defined in ``mol-plugin/behavior``. Auto-generated documentation for the transforms is also [available](transforms.md). +# Animation State + +Defined by ``CameraSnapshotManager.StateSnapshot`` in ``mol-plugin/state/animation/manager.ts``. + # Canvas3D State Defined by ``Canvas3DParams`` in ``mol-canvas3d/canvas3d.ts``. diff --git a/docs/state/transforms.md b/docs/state/transforms.md index f02e38b687426456cbaf3738956c41d75058e835..473030fade96825b498d9dc66bb053382da39c1d 100644 --- a/docs/state/transforms.md +++ b/docs/state/transforms.md @@ -7,9 +7,11 @@ * [ms-plugin.parse-ccp4](#ms-plugin-parse-ccp4) * [ms-plugin.parse-dsn6](#ms-plugin-parse-dsn6) * [ms-plugin.trajectory-from-mmcif](#ms-plugin-trajectory-from-mmcif) +* [ms-plugin.trajectory-from-pdb](#ms-plugin-trajectory-from-pdb) * [ms-plugin.model-from-trajectory](#ms-plugin-model-from-trajectory) * [ms-plugin.structure-from-model](#ms-plugin-structure-from-model) * [ms-plugin.structure-assembly-from-model](#ms-plugin-structure-assembly-from-model) +* [ms-plugin.structure-symmetry-from-model](#ms-plugin-structure-symmetry-from-model) * [ms-plugin.structure-selection](#ms-plugin-structure-selection) * [ms-plugin.structure-complex-element](#ms-plugin-structure-complex-element) * [ms-plugin.custom-model-properties](#ms-plugin-custom-model-properties) @@ -65,7 +67,7 @@ ---------------------------- ## <a name="ms-plugin-parse-ccp4"></a>ms-plugin.parse-ccp4 :: Binary -> Ccp4 -*Parse CCP4/MRC from Binary data* +*Parse CCP4/MRC/MAP from Binary data* ---------------------------- ## <a name="ms-plugin-parse-dsn6"></a>ms-plugin.parse-dsn6 :: Binary -> Dsn6 @@ -82,6 +84,9 @@ ```js {} ``` +---------------------------- +## <a name="ms-plugin-trajectory-from-pdb"></a>ms-plugin.trajectory-from-pdb :: String -> Trajectory + ---------------------------- ## <a name="ms-plugin-model-from-trajectory"></a>ms-plugin.model-from-trajectory :: Trajectory -> Model *Create a molecular structure from the specified model.* @@ -104,13 +109,36 @@ *Create a molecular structure assembly.* ### Parameters -- **id**?: String *(Assembly Id. If none specified (undefined or empty string), the asymmetric unit is used.)* +- **id**?: String *(Assembly Id. Value 'deposited' can be used to specify deposited asymmetric unit.)* ### Default Parameters ```js {} ``` ---------------------------- +## <a name="ms-plugin-structure-symmetry-from-model"></a>ms-plugin.structure-symmetry-from-model :: Model -> Structure +*Create a molecular structure symmetry.* + +### Parameters +- **ijkMin**: 3D vector [x, y, z] +- **ijkMax**: 3D vector [x, y, z] + +### Default Parameters +```js +{ + "ijkMin": [ + -1, + -1, + -1 + ], + "ijkMax": [ + 1, + 1, + 1 + ] +} +``` +---------------------------- ## <a name="ms-plugin-structure-selection"></a>ms-plugin.structure-selection :: Structure -> Structure *Create a molecular structure from the specified query expression.* @@ -149,7 +177,7 @@ ``` ---------------------------- ## <a name="ms-plugin-volume-from-ccp4"></a>ms-plugin.volume-from-ccp4 :: Ccp4 -> Data -*Create Volume from CCP4/MRC data* +*Create Volume from CCP4/MRC/MAP data* ### Parameters - **voxelSize**: 3D vector [x, y, z] @@ -294,7 +322,7 @@ Object with: - **highlightColor**: Color as 0xrrggbb - **selectColor**: Color as 0xrrggbb - **quality**: One of 'custom', 'auto', 'highest', 'higher', 'high', 'medium', 'low', 'lower', 'lowest' - - **isoValue**: Numeric value + - **isoValueNorm**: Numeric value *(Normalized Isolevel Value)* - **renderMode**: One of 'isosurface', 'volume' - **controlPoints**: A list of 2d vectors [xi, yi][] - **list**: One of 'OrangeRed', 'PurpleBlue', 'BluePurple', 'Oranges', 'BlueGreen', 'YellowOrangeBrown', 'YellowGreen', 'Reds', 'RedPurple', 'Greens', 'YellowGreenBlue', 'Purples', 'GreenBlue', 'Greys', 'YellowOrangeRed', 'PurpleRed', 'Blues', 'PurpleBlueGreen', 'Spectral', 'RedYellowGreen', 'RedBlue', 'PinkYellowGreen', 'PurpleGreen', 'RedYellowBlue', 'BrownWhiteGreen', 'RedGrey', 'PurpleOrange', 'Set2', 'Accent', 'Set1', 'Set3', 'Dark2', 'Paired', 'Pastel2', 'Pastel1', 'Magma', 'Inferno', 'Plasma', 'Viridis', 'Cividis', 'Twilight', 'Rainbow', 'RedWhiteBlue' @@ -480,7 +508,7 @@ Object with: - **highlightColor**: Color as 0xrrggbb - **selectColor**: Color as 0xrrggbb - **quality**: One of 'custom', 'auto', 'highest', 'higher', 'high', 'medium', 'low', 'lower', 'lowest' - - **isoValue**: Numeric value + - **isoValueNorm**: Numeric value *(Normalized Isolevel Value)* - **renderMode**: One of 'isosurface', 'volume' - **controlPoints**: A list of 2d vectors [xi, yi][] - **list**: One of 'OrangeRed', 'PurpleBlue', 'BluePurple', 'Oranges', 'BlueGreen', 'YellowOrangeBrown', 'YellowGreen', 'Reds', 'RedPurple', 'Greens', 'YellowGreenBlue', 'Purples', 'GreenBlue', 'Greys', 'YellowOrangeRed', 'PurpleRed', 'Blues', 'PurpleBlueGreen', 'Spectral', 'RedYellowGreen', 'RedBlue', 'PinkYellowGreen', 'PurpleGreen', 'RedYellowBlue', 'BrownWhiteGreen', 'RedGrey', 'PurpleOrange', 'Set2', 'Accent', 'Set1', 'Set3', 'Dark2', 'Paired', 'Pastel2', 'Pastel1', 'Magma', 'Inferno', 'Plasma', 'Viridis', 'Cividis', 'Twilight', 'Rainbow', 'RedWhiteBlue' diff --git a/package-lock.json b/package-lock.json index 6558c4bc4fcf8c6f35afdf000c314437ffb22ffc..763951320022faa79a1de613aa74388c26a6ec04 100644 Binary files a/package-lock.json and b/package-lock.json differ diff --git a/package.json b/package.json index 58063c0f529d6e7e66126062b60c766bf529dfca..2ba5f74e9f1abf9992be6e25283dc89d4aead432 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "watch": "concurrently --kill-others \"npm:watch-ts\" \"npm:watch-extra\" \"npm:watch-webpack\"", "watch-ts": "tsc -watch", "watch-extra": "cpx \"src/**/*.{vert,frag,glsl,scss,woff,woff2,ttf,otf,eot,svg,html,gql}\" build/src/ --watch", - "build-webpack": "webpack --mode development", + "build-webpack": "webpack --mode production", "watch-webpack": "webpack -w --mode development", "model-server": "node build/src/servers/model/server.js", "model-server-watch": "nodemon --watch build/src build/src/servers/model/server.js" @@ -79,11 +79,11 @@ "@types/benchmark": "^1.0.31", "@types/compression": "0.0.36", "@types/express": "^4.16.1", - "@types/jest": "^23.3.14", - "@types/node": "^10.12.23", - "@types/node-fetch": "^2.1.4", - "@types/react": "^16.8.2", - "@types/react-dom": "^16.8.0", + "@types/jest": "^24.0.6", + "@types/node": "^11.9.4", + "@types/node-fetch": "^2.1.6", + "@types/react": "^16.8.4", + "@types/react-dom": "^16.8.2", "@types/webgl2": "0.0.4", "benchmark": "^2.1.4", "circular-dependency-plugin": "^5.0.2", @@ -96,22 +96,22 @@ "glslify-import": "^3.1.0", "glslify-loader": "^2.0.0", "graphql-code-generator": "^0.16.1", - "graphql-codegen-time": "^0.16.0", + "graphql-codegen-time": "^0.16.1", "graphql-codegen-typescript-template": "^0.16.1", - "jest": "^23.6.0", + "jest": "^24.1.0", "jest-raw-loader": "^1.0.1", "mini-css-extract-plugin": "^0.5.0", "node-sass": "^4.11.0", "raw-loader": "^1.0.0", - "resolve-url-loader": "^3.0.0", + "resolve-url-loader": "^3.0.1", "sass-loader": "^7.1.0", "style-loader": "^0.23.1", - "ts-jest": "^23.10.5", + "ts-jest": "^24.0.0", "tslint": "^5.12.1", - "typescript": "^3.3.1", + "typescript": "^3.3.3", "uglify-js": "^3.4.9", "util.promisify": "^1.0.0", - "webpack": "^4.29.3", + "webpack": "^4.29.5", "webpack-cli": "^3.2.3" }, "dependencies": { @@ -121,8 +121,8 @@ "graphql": "^14.1.1", "immutable": "^3.8.2", "node-fetch": "^2.3.0", - "react": "^16.7.0", - "react-dom": "^16.7.0", - "rxjs": "^6.3.3" + "react": "^16.8.2", + "react-dom": "^16.8.2", + "rxjs": "^6.4.0" } } diff --git a/src/apps/basic-wrapper/index.html b/src/apps/basic-wrapper/index.html index b4a202a2dddd64da6f63e70dab00eb33de5e9744..07592b99d310d9376a10be671fa594b4314e01e0 100644 --- a/src/apps/basic-wrapper/index.html +++ b/src/apps/basic-wrapper/index.html @@ -12,37 +12,94 @@ } #app { position: absolute; - left: 100px; + left: 160px; top: 100px; width: 600px; height: 600px; border: 1px solid #ccc; } + + #controls { + position: absolute; + width: 130px; + top: 10px; + left: 10px; + } + + #controls > button { + display: block; + width: 100%; + text-align: left; + } + + #controls > hr { + margin: 5px 0; + } </style> <link rel="stylesheet" type="text/css" href="app.css" /> <script type="text/javascript" src="./index.js"></script> </head> <body> - <button id='spin'>Toggle Spin</button> - <button id='asym'>Load Asym Unit</button> - <button id='asm'>Load Assemly 1</button> + <div id='controls'> + + </div> <div id="app"></div> <script> - var pdbId = '5ire', assemblyId= '1'; + var pdbId = '1grm', assemblyId= '1'; var url = 'https://www.ebi.ac.uk/pdbe/static/entry/' + pdbId + '_updated.cif'; var format = 'cif'; // var url = 'https://www.ebi.ac.uk/pdbe/entry-files/pdb' + pdbId + '.ent'; // var format = 'pdb'; + // var assemblyId = 'deposited'; BasicMolStarWrapper.init('app' /** or document.getElementById('app') */); BasicMolStarWrapper.setBackground(0xffffff); BasicMolStarWrapper.load({ url: url, format: format, assemblyId: assemblyId }); BasicMolStarWrapper.toggleSpin(); - document.getElementById('spin').onclick = () => BasicMolStarWrapper.toggleSpin(); - document.getElementById('asym').onclick = () => BasicMolStarWrapper.load({ url: url, format: format }); - document.getElementById('asm').onclick = () => BasicMolStarWrapper.load({ url: url, format: format, assemblyId: assemblyId }); + addHeader('Source'); + addControl('Load Asym Unit', () => BasicMolStarWrapper.load({ url: url, format: format })); + addControl('Load Assembly 1', () => BasicMolStarWrapper.load({ url: url, format: format, assemblyId: assemblyId })); + + addSeparator(); + + addHeader('Camera'); + addControl('Toggle Spin', () => BasicMolStarWrapper.toggleSpin()); + + addSeparator(); + + addHeader('Animation'); + + // adjust this number to make the animation faster or slower + // requires to "restart" the animation if changed + BasicMolStarWrapper.animate.modelIndex.maxFPS = 4; + + addControl('Play To End', () => BasicMolStarWrapper.animate.modelIndex.onceForward()); + addControl('Play To Start', () => BasicMolStarWrapper.animate.modelIndex.onceBackward()); + addControl('Play Palindrome', () => BasicMolStarWrapper.animate.modelIndex.palindrome()); + addControl('Play Loop', () => BasicMolStarWrapper.animate.modelIndex.loop()); + addControl('Stop', () => BasicMolStarWrapper.animate.modelIndex.stop()); + + //////////////////////////////////////////////////////// + + function addControl(label, action) { + var btn = document.createElement('button'); + btn.onclick = action; + btn.innerText = label; + document.getElementById('controls').appendChild(btn); + } + + function addSeparator() { + var hr = document.createElement('hr'); + document.getElementById('controls').appendChild(hr); + } + + function addHeader(header) { + var h = document.createElement('h3'); + h.innerText = header; + document.getElementById('controls').appendChild(h); + } </script> </body> </html> \ No newline at end of file diff --git a/src/apps/basic-wrapper/index.ts b/src/apps/basic-wrapper/index.ts index b45753f912eb4053dc8cf6730f085a2bdb635042..ec4ed52e3ce2bc31bfb56da127e952ca6000f459 100644 --- a/src/apps/basic-wrapper/index.ts +++ b/src/apps/basic-wrapper/index.ts @@ -11,8 +11,9 @@ import { PluginCommands } from 'mol-plugin/command'; import { StateTransforms } from 'mol-plugin/state/transforms'; import { StructureRepresentation3DHelpers } from 'mol-plugin/state/transforms/representation'; import { Color } from 'mol-util/color'; -import { StateTreeBuilder } from 'mol-state/tree/builder'; import { PluginStateObject as PSO } from 'mol-plugin/state/objects'; +import { AnimateModelIndex } from 'mol-plugin/state/animation/built-in'; +import { StateBuilder } from 'mol-state'; require('mol-plugin/skin/light.scss') type SupportedFormats = 'cif' | 'pdb' @@ -31,11 +32,11 @@ class BasicWrapper { }); } - private download(b: StateTreeBuilder.To<PSO.Root>, url: string) { + private download(b: StateBuilder.To<PSO.Root>, url: string) { return b.apply(StateTransforms.Data.Download, { url, isBinary: false }) } - private parse(b: StateTreeBuilder.To<PSO.Data.Binary | PSO.Data.String>, format: SupportedFormats, assemblyId: string) { + private parse(b: StateBuilder.To<PSO.Data.Binary | PSO.Data.String>, format: SupportedFormats, assemblyId: string) { const parsed = format === 'cif' ? b.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif) : b.apply(StateTransforms.Model.TrajectoryFromPDB); @@ -45,7 +46,7 @@ class BasicWrapper { .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: 'asm' }); } - private visual(visualRoot: StateTreeBuilder.To<PSO.Molecule.Structure>) { + private visual(visualRoot: StateBuilder.To<PSO.Molecule.Structure>) { visualRoot.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' }) .apply(StateTransforms.Representation.StructureRepresentation3D, StructureRepresentation3DHelpers.getDefaultParamsStatic(this.plugin, 'cartoon')); @@ -73,7 +74,7 @@ class BasicWrapper { if (state.select('asm').length > 0) loadType = 'update'; } - let tree: StateTreeBuilder.Root; + let tree: StateBuilder.Root; if (loadType === 'full') { await PluginCommands.State.RemoveObject.dispatch(this.plugin, { state, ref: state.tree.root.ref }); tree = state.build(); @@ -98,6 +99,17 @@ class BasicWrapper { PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { trackball: { ...trackball, spin: !trackball.spin } } }); if (!spinning) PluginCommands.Camera.Reset.dispatch(this.plugin, { }); } + + animate = { + modelIndex: { + maxFPS: 8, + onceForward: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'once', params: { direction: 'forward' } } }) }, + onceBackward: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'once', params: { direction: 'backward' } } }) }, + palindrome: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'palindrome', params: {} } }) }, + loop: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'loop', params: {} } }) }, + stop: () => this.plugin.state.animation.stop() + } + } } (window as any).BasicMolStarWrapper = new BasicWrapper(); \ No newline at end of file diff --git a/src/apps/state-docs/index.ts b/src/apps/state-docs/index.ts index b9db93eb963b47defc3f5ba6c4ad9d3835f4af48..1cd02da05e62fae0441d5f98c27f2bdf887a8bab 100644 --- a/src/apps/state-docs/index.ts +++ b/src/apps/state-docs/index.ts @@ -5,7 +5,7 @@ */ import * as _ from 'mol-plugin/state/transforms' -import { Transformer, StateObject } from 'mol-state'; +import { StateTransformer, StateObject } from 'mol-state'; import { StringBuilder } from 'mol-util'; import * as fs from 'fs'; import { paramsToMd } from './pd-to-md'; @@ -28,7 +28,7 @@ function typeToString(o: StateObject.Ctor[]) { return o.map(o => o.name).join(' | '); } -function writeTransformer(t: Transformer) { +function writeTransformer(t: StateTransformer) { StringBuilder.write(builder, `## <a name="${t.id.replace('.', '-')}"></a>${t.id} :: ${typeToString(t.definition.from)} -> ${typeToString(t.definition.to)}`); StringBuilder.newline(builder); if (t.definition.display.description) { @@ -52,7 +52,7 @@ function writeTransformer(t: Transformer) { StringBuilder.newline(builder); } -const transformers = Transformer.getAll(); +const transformers = StateTransformer.getAll(); StringBuilder.write(builder, '# Mol* Plugin State Transformer Reference'); StringBuilder.newline(builder); diff --git a/src/apps/structure-info/volume.ts b/src/apps/structure-info/volume.ts index 59029e155acce0ccede581f7ece1f1b585e34ddd..4b3e54895e3e92f3b46c9abd8295652a16381ff7 100644 --- a/src/apps/structure-info/volume.ts +++ b/src/apps/structure-info/volume.ts @@ -40,7 +40,7 @@ function print(data: Volume) { } async function doMesh(data: Volume, filename: string) { - const mesh = await Task.create('', runtime => createVolumeIsosurfaceMesh({ runtime }, data.volume, createEmptyTheme(), { isoValue: VolumeIsoValue.absolute(data.volume.dataStats, 1.5) } )).run(); + const mesh = await Task.create('', runtime => createVolumeIsosurfaceMesh({ runtime }, data.volume, createEmptyTheme(), { isoValue: VolumeIsoValue.absolute(1.5) } )).run(); console.log({ vc: mesh.vertexCount, tc: mesh.triangleCount }); // Export the mesh in OBJ format. diff --git a/src/mol-canvas3d/canvas3d.ts b/src/mol-canvas3d/canvas3d.ts index 156219dead421927c46c3de787dc0a2740423850..5169f3f294581527fd08c255efce39de6923e58b 100644 --- a/src/mol-canvas3d/canvas3d.ts +++ b/src/mol-canvas3d/canvas3d.ts @@ -192,7 +192,7 @@ namespace Canvas3D { if (isIdentifying || isUpdating) return false let didRender = false - controls.update() + controls.update(currentTime); // TODO: is this a good fix? Also, setClipping does not work if the user has manually set a clipping plane. if (!camera.transition.inTransition) setClipping(); const cameraChanged = camera.updateMatrices(); @@ -230,6 +230,7 @@ namespace Canvas3D { } let forceNextDraw = false; + let currentTime = 0; function draw(force?: boolean) { if (render('draw', !!force || forceNextDraw)) { @@ -246,8 +247,8 @@ namespace Canvas3D { } function animate() { - const t = now(); - camera.transition.tick(t); + currentTime = now(); + camera.transition.tick(currentTime); draw(false) window.requestAnimationFrame(animate) } diff --git a/src/mol-canvas3d/controls/trackball.ts b/src/mol-canvas3d/controls/trackball.ts index 7955ab4c5976d0c137f2a88c9ed878cf1632a902..b2e01c66d3206c082d0cbdd20b0da58f10eaf874 100644 --- a/src/mol-canvas3d/controls/trackball.ts +++ b/src/mol-canvas3d/controls/trackball.ts @@ -39,12 +39,12 @@ interface TrackballControls { readonly props: Readonly<TrackballControlsProps> setProps: (props: Partial<TrackballControlsProps>) => void - update: () => void + update: (t: number) => void reset: () => void dispose: () => void } namespace TrackballControls { - export function create (input: InputObserver, object: Object3D & { target: Vec3 }, props: Partial<TrackballControlsProps> = {}): TrackballControls { + export function create(input: InputObserver, object: Object3D & { target: Vec3 }, props: Partial<TrackballControlsProps> = {}): TrackballControls { const p = { ...PD.getDefaultValues(TrackballControlsParams), ...props } const viewport: Viewport = { x: 0, y: 0, width: 0, height: 0 } @@ -131,7 +131,7 @@ namespace TrackballControls { Vec3.normalize(rotAxis, Vec3.cross(rotAxis, rotMoveDir, _eye)) angle *= p.rotateSpeed; - Quat.setAxisAngle(rotQuat, rotAxis, angle ) + Quat.setAxisAngle(rotQuat, rotAxis, angle) Vec3.transformQuat(_eye, _eye, rotQuat) Vec3.transformQuat(object.up, object.up, rotQuat) @@ -150,7 +150,7 @@ namespace TrackballControls { Vec2.copy(_movePrev, _moveCurr) } - function zoomCamera () { + function zoomCamera() { const factor = 1.0 + (_zoomEnd[1] - _zoomStart[1]) * p.zoomSpeed if (factor !== 1.0 && factor > 0.0) { Vec3.scale(_eye, _eye, factor) @@ -207,8 +207,12 @@ namespace TrackballControls { } } + let lastUpdated = -1; /** Update the object's position, direction and up vectors */ - function update() { + function update(t: number) { + if (lastUpdated === t) return; + if (p.spin) spin(t - lastUpdated); + Vec3.sub(_eye, object.position, target) rotateCamera() @@ -224,6 +228,8 @@ namespace TrackballControls { if (Vec3.squaredDistance(lastPosition, object.position) > EPSILON.Value) { Vec3.copy(lastPosition, object.position) } + + lastUpdated = t; } /** Reset object's vectors and the target vector to their initial values */ @@ -297,25 +303,21 @@ namespace TrackballControls { } const _spinSpeed = Vec2.create(0.005, 0); - function spin() { - _spinSpeed[0] = (p.spinSpeed || 0) / 1000; + function spin(deltaT: number) { + const frameSpeed = (p.spinSpeed || 0) / 1000; + _spinSpeed[0] = 60 * Math.min(Math.abs(deltaT), 1000 / 8) / 1000 * frameSpeed; if (!_isInteracting) Vec2.add(_moveCurr, _movePrev, _spinSpeed); - if (p.spin) requestAnimationFrame(spin); } // force an update at start - update(); - - if (props.spin) { spin(); } + update(0); return { viewport, get props() { return p as Readonly<TrackballControlsProps> }, setProps: (props: Partial<TrackballControlsProps>) => { - const wasSpinning = p.spin Object.assign(p, props) - if (p.spin && !wasSpinning) requestAnimationFrame(spin) }, update, diff --git a/src/mol-io/common/file-handle.ts b/src/mol-io/common/file-handle.ts index b3ae0fd64d1adb4f74c4f93482f2af7a75f8469a..90c6deebfa9466b9c1097d511f67478e6370db2a 100644 --- a/src/mol-io/common/file-handle.ts +++ b/src/mol-io/common/file-handle.ts @@ -1,46 +1,132 @@ /** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2019 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 { defaults } from 'mol-util'; +import { defaults, noop } from 'mol-util'; +import { SimpleBuffer } from './simple-buffer'; +// only import 'fs' in node.js +const fs = typeof document === 'undefined' ? require('fs') as typeof import('fs') : void 0; export interface FileHandle { - /** The number of bytes in the file */ - length: number /** + * Asynchronously reads data, returning buffer and number of bytes read + * * @param position The offset from the beginning of the file from which data should be read. - * @param sizeOrBuffer The buffer the data will be written to. If a number a buffer of that size will be created. - * @param size The number of bytes to read. + * @param sizeOrBuffer The buffer the data will be read from. + * @param length The number of bytes to read. * @param byteOffset The offset in the buffer at which to start writing. */ - readBuffer(position: number, sizeOrBuffer: Uint8Array | number, size?: number, byteOffset?: number): Promise<{ bytesRead: number, buffer: Uint8Array }> + readBuffer(position: number, sizeOrBuffer: SimpleBuffer | number, length?: number, byteOffset?: number): Promise<{ bytesRead: number, buffer: SimpleBuffer }> + + /** + * Asynchronously writes buffer, returning the number of bytes written. + * + * @param position — The offset from the beginning of the file where this data should be written. + * @param buffer - The buffer data to be written. + * @param length — The number of bytes to write. If not supplied, defaults to buffer.length + */ + writeBuffer(position: number, buffer: SimpleBuffer, length?: number): Promise<number> + + /** + * Synchronously writes buffer, returning the number of bytes written. + * + * @param position — The offset from the beginning of the file where this data should be written. + * @param buffer - The buffer data to be written. + * @param length — The number of bytes to write. If not supplied, defaults to buffer.length + */ + writeBufferSync(position: number, buffer: SimpleBuffer, length?: number): number + + /** Closes a file handle */ + close(): void } export namespace FileHandle { - export function fromBuffer(buffer: Uint8Array): FileHandle { + export function fromBuffer(buffer: SimpleBuffer): FileHandle { return { - length: buffer.length, - readBuffer: (position: number, sizeOrBuffer: Uint8Array | number, size?: number, byteOffset?: number) => { + readBuffer: (position: number, sizeOrBuffer: SimpleBuffer | number, size?: number, byteOffset?: number) => { + let bytesRead: number + let outBuffer: SimpleBuffer if (typeof sizeOrBuffer === 'number') { + size = defaults(size, sizeOrBuffer) const start = position - const end = Math.min(buffer.length, start + (defaults(size, sizeOrBuffer))) - return Promise.resolve({ - bytesRead: end - start, - buffer: buffer.subarray(start, end), - }) + const end = Math.min(buffer.length, start + size) + bytesRead = end - start + outBuffer = SimpleBuffer.fromUint8Array(new Uint8Array(buffer.buffer, start, end - start)) } else { - if (size === void 0) { - return Promise.reject('readBuffer: Specify size.'); - } + size = defaults(size, sizeOrBuffer.length) const start = position - const end = Math.min(buffer.length, start + defaults(size, sizeOrBuffer.length)) + const end = Math.min(buffer.length, start + size) sizeOrBuffer.set(buffer.subarray(start, end), byteOffset) - return Promise.resolve({ - bytesRead: end - start, - buffer: sizeOrBuffer, + bytesRead = end - start + outBuffer = sizeOrBuffer + } + if (size !== bytesRead) { + console.warn(`byteCount ${size} and bytesRead ${bytesRead} differ`) + } + return Promise.resolve({ bytesRead, buffer: outBuffer }) + }, + writeBuffer: (position: number, buffer: SimpleBuffer, length?: number) => { + length = defaults(length, buffer.length) + console.error('.writeBuffer not implemented for FileHandle.fromBuffer') + return Promise.resolve(0) + }, + writeBufferSync: (position: number, buffer: SimpleBuffer, length?: number, ) => { + length = defaults(length, buffer.length) + console.error('.writeSync not implemented for FileHandle.fromBuffer') + return 0 + }, + close: noop + } + } + + export function fromDescriptor(file: number): FileHandle { + if (fs === undefined) throw new Error('fs module not available') + return { + readBuffer: (position: number, sizeOrBuffer: SimpleBuffer | number, length?: number, byteOffset?: number) => { + return new Promise((res, rej) => { + let outBuffer: SimpleBuffer + if (typeof sizeOrBuffer === 'number') { + byteOffset = defaults(byteOffset, 0) + length = defaults(length, sizeOrBuffer) + outBuffer = SimpleBuffer.fromArrayBuffer(new ArrayBuffer(sizeOrBuffer)); + } else { + byteOffset = defaults(byteOffset, 0) + length = defaults(length, sizeOrBuffer.length) + outBuffer = sizeOrBuffer + } + fs.read(file, outBuffer, byteOffset, length, position, (err, bytesRead, buffer) => { + if (err) { + rej(err); + return; + } + if (length !== bytesRead) { + console.warn(`byteCount ${length} and bytesRead ${bytesRead} differ`) + } + res({ bytesRead, buffer }); + }); + }) + }, + writeBuffer: (position: number, buffer: SimpleBuffer, length?: number) => { + length = defaults(length, buffer.length) + return new Promise<number>((res, rej) => { + fs.write(file, buffer, 0, length, position, (err, written) => { + if (err) rej(err); + else res(written); }) + }) + }, + writeBufferSync: (position: number, buffer: Uint8Array, length?: number) => { + length = defaults(length, buffer.length) + return fs.writeSync(file, buffer, 0, length, position); + }, + close: () => { + try { + if (file !== void 0) fs.close(file, noop); + } catch (e) { + } } } diff --git a/src/mol-io/common/simple-buffer.ts b/src/mol-io/common/simple-buffer.ts new file mode 100644 index 0000000000000000000000000000000000000000..9f44ee1dcff10a575d9a19aacf949573ef7c3294 --- /dev/null +++ b/src/mol-io/common/simple-buffer.ts @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2019 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 { defaults } from 'mol-util'; + +export interface SimpleBuffer extends Uint8Array { + readInt8: (offset: number) => number + readUInt8: (offset: number) => number + + writeInt8: (value: number, offset: number) => void + writeUInt8: (value: number, offset: number) => void + + readInt16LE: (offset: number) => number + readInt32LE: (offset: number) => number + readUInt16LE: (offset: number) => number + readUInt32LE: (offset: number) => number + readFloatLE: (offset: number) => number + readDoubleLE: (offset: number) => number + + writeInt16LE: (value: number, offset: number) => void + writeInt32LE: (value: number, offset: number) => void + writeUInt16LE: (value: number, offset: number) => void + writeUInt32LE: (value: number, offset: number) => void + writeFloatLE: (value: number, offset: number) => void + writeDoubleLE: (value: number, offset: number) => void + + readInt16BE: (offset: number) => number + readInt32BE: (offset: number) => number + readUInt16BE: (offset: number) => number + readUInt32BE: (offset: number) => number + readFloatBE: (offset: number) => number + readDoubleBE: (offset: number) => number + + writeInt16BE: (value: number, offset: number) => void + writeInt32BE: (value: number, offset: number) => void + writeUInt16BE: (value: number, offset: number) => void + writeUInt32BE: (value: number, offset: number) => void + writeFloatBE: (value: number, offset: number) => void + writeDoubleBE: (value: number, offset: number) => void + + copy: (targetBuffer: Uint8Array, targetStart?: number, sourceStart?: number, sourceEnd?: number) => number +} + +export namespace SimpleBuffer { + export function fromUint8Array(array: Uint8Array): SimpleBuffer { + const dv = new DataView(array.buffer) + return Object.assign(array.subarray(0), { + readInt8: (offset: number) => dv.getInt8(offset), + readUInt8: (offset: number) => dv.getUint8(offset), + writeInt8: (value: number, offset: number) => dv.setInt8(offset, value), + writeUInt8: (value: number, offset: number) => dv.setUint8(offset, value), + + readInt16LE: (offset: number) => dv.getInt16(offset, true), + readInt32LE: (offset: number) => dv.getInt32(offset, true), + readUInt16LE: (offset: number) => dv.getUint16(offset, true), + readUInt32LE: (offset: number) => dv.getUint32(offset, true), + readFloatLE: (offset: number) => dv.getFloat32(offset, true), + readDoubleLE: (offset: number) => dv.getFloat64(offset, true), + + writeInt16LE: (value: number, offset: number) => dv.setInt16(offset, value, true), + writeInt32LE: (value: number, offset: number) => dv.setInt32(offset, value, true), + writeUInt16LE: (value: number, offset: number) => dv.setUint16(offset, value, true), + writeUInt32LE: (value: number, offset: number) => dv.setUint32(offset, value, true), + writeFloatLE: (value: number, offset: number) => dv.setFloat32(offset, value, true), + writeDoubleLE: (value: number, offset: number) => dv.setFloat64(offset, value, true), + + readInt16BE: (offset: number) => dv.getInt16(offset, false), + readInt32BE: (offset: number) => dv.getInt32(offset, false), + readUInt16BE: (offset: number) => dv.getUint16(offset, false), + readUInt32BE: (offset: number) => dv.getUint32(offset, false), + readFloatBE: (offset: number) => dv.getFloat32(offset, false), + readDoubleBE: (offset: number) => dv.getFloat64(offset, false), + + writeInt16BE: (value: number, offset: number) => dv.setInt16(offset, value, false), + writeInt32BE: (value: number, offset: number) => dv.setInt32(offset, value, false), + writeUInt16BE: (value: number, offset: number) => dv.setUint16(offset, value, false), + writeUInt32BE: (value: number, offset: number) => dv.setUint32(offset, value, false), + writeFloatBE: (value: number, offset: number) => dv.setFloat32(offset, value, false), + writeDoubleBE: (value: number, offset: number) => dv.setFloat64(offset, value, false), + + copy: (targetBuffer: Uint8Array, targetStart?: number, sourceStart?: number, sourceEnd?: number) => { + targetStart = defaults(targetStart, 0) + sourceStart = defaults(sourceStart, 0) + sourceEnd = defaults(sourceEnd, array.length) + targetBuffer.set(array.subarray(sourceStart, sourceEnd), targetStart) + return sourceEnd - sourceStart + } + }) + } + + export function fromArrayBuffer(arrayBuffer: ArrayBuffer): SimpleBuffer { + return fromUint8Array(new Uint8Array(arrayBuffer)) + } + + export function fromBuffer(buffer: Buffer): SimpleBuffer { + return buffer + } + + export const IsNativeEndianLittle = new Uint16Array(new Uint8Array([0x12, 0x34]).buffer)[0] === 0x3412; + + /** source and target can't be the same */ + export function flipByteOrder(source: SimpleBuffer, target: Uint8Array, byteCount: number, elementByteSize: number, offset: number) { + for (let i = 0, n = byteCount; i < n; i += elementByteSize) { + for (let j = 0; j < elementByteSize; j++) { + target[offset + i + elementByteSize - j - 1] = source[offset + i + j]; + } + } + } + + export function flipByteOrderInPlace2(buffer: ArrayBuffer, byteOffset = 0, length?: number) { + const intView = new Int16Array(buffer, byteOffset, length) + for (let i = 0, n = intView.length; i < n; ++i) { + const val = intView[i] + intView[i] = ((val & 0xff) << 8) | ((val >> 8) & 0xff) + } + } + + export function ensureLittleEndian(source: SimpleBuffer, target: SimpleBuffer, byteCount: number, elementByteSize: number, offset: number) { + if (IsNativeEndianLittle) return; + if (!byteCount || elementByteSize <= 1) return; + flipByteOrder(source, target, byteCount, elementByteSize, offset); + } +} \ No newline at end of file diff --git a/src/mol-io/common/typed-array.ts b/src/mol-io/common/typed-array.ts new file mode 100644 index 0000000000000000000000000000000000000000..f07e982c367e059bd1c0a6cbedcb6d6dd2c7c7a8 --- /dev/null +++ b/src/mol-io/common/typed-array.ts @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * Taken/adapted from DensityServer (https://github.com/dsehnal/DensityServer) + * + * @author David Sehnal <david.sehnal@gmail.com> + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { FileHandle } from 'mol-io/common/file-handle'; +import { SimpleBuffer } from 'mol-io/common/simple-buffer'; + +export type TypedArrayValueType = 'float32' | 'int8' | 'int16' + +export namespace TypedArrayValueType { + export const Float32: TypedArrayValueType = 'float32'; + export const Int8: TypedArrayValueType = 'int8'; + export const Int16: TypedArrayValueType = 'int16'; +} + +export type TypedArrayValueArray = Float32Array | Int8Array | Int16Array + +export interface TypedArrayBufferContext { + type: TypedArrayValueType, + elementByteSize: number, + readBuffer: SimpleBuffer, + valuesBuffer: Uint8Array, + values: TypedArrayValueArray +} + +export function getElementByteSize(type: TypedArrayValueType) { + if (type === TypedArrayValueType.Float32) return 4; + if (type === TypedArrayValueType.Int16) return 2; + return 1; +} + +export function makeTypedArray(type: TypedArrayValueType, buffer: ArrayBuffer, byteOffset = 0, length?: number): TypedArrayValueArray { + if (type === TypedArrayValueType.Float32) return new Float32Array(buffer, byteOffset, length); + if (type === TypedArrayValueType.Int16) return new Int16Array(buffer, byteOffset, length); + return new Int8Array(buffer, byteOffset, length); +} + +export function createTypedArray(type: TypedArrayValueType, size: number) { + switch (type) { + case TypedArrayValueType.Float32: return new Float32Array(new ArrayBuffer(4 * size)); + case TypedArrayValueType.Int8: return new Int8Array(new ArrayBuffer(1 * size)); + case TypedArrayValueType.Int16: return new Int16Array(new ArrayBuffer(2 * size)); + } + throw Error(`${type} is not a supported value format.`); +} + +export function createTypedArrayBufferContext(size: number, type: TypedArrayValueType): TypedArrayBufferContext { + let elementByteSize = getElementByteSize(type); + let arrayBuffer = new ArrayBuffer(elementByteSize * size); + let readBuffer = SimpleBuffer.fromArrayBuffer(arrayBuffer); + let valuesBuffer = SimpleBuffer.IsNativeEndianLittle ? arrayBuffer : new ArrayBuffer(elementByteSize * size); + return { + type, + elementByteSize, + readBuffer, + valuesBuffer: new Uint8Array(valuesBuffer), + values: makeTypedArray(type, valuesBuffer) + }; +} + +export async function readTypedArray(ctx: TypedArrayBufferContext, file: FileHandle, position: number, byteCount: number, valueByteOffset: number, littleEndian?: boolean) { + await file.readBuffer(position, ctx.readBuffer, byteCount, valueByteOffset); + if (ctx.elementByteSize > 1 && ((littleEndian !== void 0 && littleEndian !== SimpleBuffer.IsNativeEndianLittle) || !SimpleBuffer.IsNativeEndianLittle)) { + // fix the endian + SimpleBuffer.flipByteOrder(ctx.readBuffer, ctx.valuesBuffer, byteCount, ctx.elementByteSize, valueByteOffset); + } + return ctx.values; +} diff --git a/src/mol-io/reader/ccp4/parser.ts b/src/mol-io/reader/ccp4/parser.ts index d48c343e5a63e4df3623551bd71b0f95f9aff4ce..832135f4a8ba2a867b5f8d3ec23d603dc20da6be 100644 --- a/src/mol-io/reader/ccp4/parser.ts +++ b/src/mol-io/reader/ccp4/parser.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ @@ -8,122 +8,164 @@ import { Task, RuntimeContext } from 'mol-task'; import { Ccp4File, Ccp4Header } from './schema' import { ReaderResult as Result } from '../result' import { FileHandle } from '../../common/file-handle'; +import { SimpleBuffer } from 'mol-io/common/simple-buffer'; +import { TypedArrayValueType, getElementByteSize, TypedArrayBufferContext, readTypedArray, createTypedArrayBufferContext } from 'mol-io/common/typed-array'; -async function parseInternal(file: FileHandle, ctx: RuntimeContext): Promise<Result<Ccp4File>> { - await ctx.update({ message: 'Parsing CCP4/MRC file...' }); - - const { buffer } = await file.readBuffer(0, file.length) - const bin = buffer.buffer - - const intView = new Int32Array(bin, 0, 56) - const floatView = new Float32Array(bin, 0, 56) - const dv = new DataView(bin) +export async function readCcp4Header(file: FileHandle): Promise<{ header: Ccp4Header, littleEndian: boolean }> { + const headerSize = 1024; + const { buffer } = await file.readBuffer(0, headerSize) // 53 MAP Character string 'MAP ' to identify file type const MAP = String.fromCharCode( - dv.getUint8(52 * 4), dv.getUint8(52 * 4 + 1), - dv.getUint8(52 * 4 + 2), dv.getUint8(52 * 4 + 3) + buffer.readUInt8(52 * 4), buffer.readUInt8(52 * 4 + 1), + buffer.readUInt8(52 * 4 + 2), buffer.readUInt8(52 * 4 + 3) ) if (MAP !== 'MAP ') { - return Result.error('ccp4 format error, missing "MAP " string'); + throw new Error('ccp4 format error, missing "MAP " string'); } // 54 MACHST Machine stamp indicating machine type which wrote file // 17 and 17 for big-endian or 68 and 65 for little-endian - const MACHST = [ dv.getUint8(53 * 4), dv.getUint8(53 * 4 + 1) ] + const MACHST = [ buffer.readUInt8(53 * 4), buffer.readUInt8(53 * 4 + 1) ] + let littleEndian = true // found MRC files that don't have the MACHST stamp set and are big-endian if (MACHST[0] !== 68 && MACHST[1] !== 65) { - // flip byte order in-place - for (let i = 0, il = bin.byteLength; i < il; i += 4) { - dv.setFloat32(i, dv.getFloat32(i), true) - } + littleEndian = false; } + const readInt = littleEndian ? (o: number) => buffer.readInt32LE(o * 4) : (o: number) => buffer.readInt32BE(o * 4) + const readFloat = littleEndian ? (o: number) => buffer.readFloatLE(o * 4) : (o: number) => buffer.readFloatBE(o * 4) + const header: Ccp4Header = { - NC: intView[0], - NR: intView[1], - NS: intView[2], + NC: readInt(0), + NR: readInt(1), + NS: readInt(2), - MODE: intView[3], + MODE: readInt(3), - NCSTART: intView[4], - NRSTART: intView[5], - NSSTART: intView[6], + NCSTART: readInt(4), + NRSTART: readInt(5), + NSSTART: readInt(6), - NX: intView[7], - NY: intView[8], - NZ: intView[9], + NX: readInt(7), + NY: readInt(8), + NZ: readInt(9), - xLength: floatView[10], - yLength: floatView[11], - zLength: floatView[12], + xLength: readFloat(10), + yLength: readFloat(11), + zLength: readFloat(12), - alpha: floatView[13], - beta: floatView[14], - gamma: floatView[15], + alpha: readFloat(13), + beta: readFloat(14), + gamma: readFloat(15), - MAPC: intView[16], - MAPR: intView[17], - MAPS: intView[18], + MAPC: readInt(16), + MAPR: readInt(17), + MAPS: readInt(18), - AMIN: floatView[19], - AMAX: floatView[20], - AMEAN: floatView[21], + AMIN: readFloat(19), + AMAX: readFloat(20), + AMEAN: readFloat(21), - ISPG: intView[22], + ISPG: readInt(22), - NSYMBT: intView[23], + NSYMBT: readInt(23), - LSKFLG: intView[24], + LSKFLG: readInt(24), SKWMAT: [], // TODO bytes 26-34 SKWTRN: [], // TODO bytes 35-37 + userFlag1: readInt(39), + userFlag2: readInt(40), + // bytes 50-52 origin in X,Y,Z used for transforms - originX: floatView[49], - originY: floatView[50], - originZ: floatView[51], + originX: readFloat(49), + originY: readFloat(50), + originZ: readFloat(51), MAP, // bytes 53 MAP MACHST, // bytes 54 MACHST - ARMS: floatView[54], + ARMS: readFloat(54), // TODO bytes 56 NLABL // TODO bytes 57-256 LABEL } - const offset = 256 * 4 + header.NSYMBT - const count = header.NC * header.NR * header.NS - let values - if (header.MODE === 2) { - values = new Float32Array(bin, offset, count) - } else if (header.MODE === 0) { - values = new Int8Array(bin, offset, count) - } else { - return Result.error(`ccp4 mode '${header.MODE}' unsupported`); - } + return { header, littleEndian } +} - // if the file was converted by mapmode2to0 - scale the data - // based on uglymol (https://github.com/uglymol/uglymol) by Marcin Wojdyr (wojdyr) - if (intView[39] === -128 && intView[40] === 127) { - values = new Float32Array(values) +export async function readCcp4Slices(header: Ccp4Header, buffer: TypedArrayBufferContext, file: FileHandle, byteOffset: number, length: number, littleEndian: boolean) { + if (isMapmode2to0(header)) { + // data from mapmode2to0 is in MODE 0 (Int8) and needs to be scaled and written as float32 + const valueByteOffset = 3 * length + // read int8 data to last quarter of the read buffer + await file.readBuffer(byteOffset, buffer.readBuffer, length, valueByteOffset); + // get int8 view of last quarter of the read buffer + const int8 = new Int8Array(buffer.valuesBuffer.buffer, valueByteOffset) // scaling f(x)=b1*x+b0 such that f(-128)=min and f(127)=max const b1 = (header.AMAX - header.AMIN) / 255.0 const b0 = 0.5 * (header.AMIN + header.AMAX + b1) - for (let j = 0, jl = values.length; j < jl; ++j) { - values[j] = b1 * values[j] + b0 + for (let j = 0, jl = length; j < jl; ++j) { + buffer.values[j] = b1 * int8[j] + b0 } + } else { + await readTypedArray(buffer, file, byteOffset, length, 0, littleEndian); + } +} + +function getCcp4DataType(mode: number) { + switch (mode) { + case 2: return TypedArrayValueType.Float32 + case 1: return TypedArrayValueType.Int16 + case 0: return TypedArrayValueType.Int8 } + throw new Error(`ccp4 mode '${mode}' unsupported`); +} + +/** check if the file was converted by mapmode2to0, see https://github.com/uglymol/uglymol */ +function isMapmode2to0(header: Ccp4Header) { + return header.userFlag1 === -128 && header.userFlag2 === 127 +} - const result: Ccp4File = { header, values }; - return Result.success(result); +export function getCcp4ValueType(header: Ccp4Header) { + return isMapmode2to0(header) ? TypedArrayValueType.Float32 : getCcp4DataType(header.MODE) } -export function parseFile(file: FileHandle) { - return Task.create<Result<Ccp4File>>('Parse CCP4/MRC', ctx => parseInternal(file, ctx)); +export function getCcp4DataOffset(header: Ccp4Header) { + return 256 * 4 + header.NSYMBT +} + +async function parseInternal(file: FileHandle, size: number, ctx: RuntimeContext): Promise<Ccp4File> { + await ctx.update({ message: 'Parsing CCP4/MRC/MAP file...' }); + + const { header, littleEndian } = await readCcp4Header(file) + const offset = getCcp4DataOffset(header) + const dataType = getCcp4DataType(header.MODE) + const valueType = getCcp4ValueType(header) + + const count = header.NC * header.NR * header.NS + const elementByteSize = getElementByteSize(dataType) + const byteCount = count * elementByteSize + + const buffer = createTypedArrayBufferContext(count, valueType) + readCcp4Slices(header, buffer, file, offset, byteCount, littleEndian) + + const result: Ccp4File = { header, values: buffer.values }; + return result +} + +export function parseFile(file: FileHandle, size: number) { + return Task.create<Result<Ccp4File>>('Parse CCP4/MRC/MAP', async ctx => { + try { + return Result.success(await parseInternal(file, size, ctx)); + } catch (e) { + return Result.error(e); + } + }) } export function parse(buffer: Uint8Array) { - return parseFile(FileHandle.fromBuffer(buffer)) + return parseFile(FileHandle.fromBuffer(SimpleBuffer.fromUint8Array(buffer)), buffer.length) } \ No newline at end of file diff --git a/src/mol-io/reader/ccp4/schema.ts b/src/mol-io/reader/ccp4/schema.ts index 0ef828cf3b26e05ea79d7ce954f3f610770fa9cc..f896f81d7df462218a164bb293308f313dc74180 100644 --- a/src/mol-io/reader/ccp4/schema.ts +++ b/src/mol-io/reader/ccp4/schema.ts @@ -81,6 +81,9 @@ export interface Ccp4Header { * May be used in CCP4 but not in MRC */ SKWTRN: number[] + /** see https://github.com/uglymol/uglymol/blob/master/tools/mapmode2to0#L69 */ + userFlag1: number, + userFlag2: number, /** x axis origin transformation (not used in CCP4) */ originX: number /** y axis origin transformation (not used in CCP4) */ @@ -112,5 +115,5 @@ export interface Ccp4Header { */ export interface Ccp4File { header: Ccp4Header - values: Float32Array | Int8Array + values: Float32Array | Int16Array | Int8Array } \ No newline at end of file diff --git a/src/mol-io/reader/dsn6/parser.ts b/src/mol-io/reader/dsn6/parser.ts index 35416d7a82ec686f5846ce84eabacc448ced77d3..131de466842f8a912655b9086d2ecbc219cba0c9 100644 --- a/src/mol-io/reader/dsn6/parser.ts +++ b/src/mol-io/reader/dsn6/parser.ts @@ -8,6 +8,9 @@ import { Task, RuntimeContext } from 'mol-task'; import { Dsn6File, Dsn6Header } from './schema' import { ReaderResult as Result } from '../result' import { FileHandle } from '../../common/file-handle'; +import { SimpleBuffer } from 'mol-io/common/simple-buffer'; + +export const dsn6HeaderSize = 512; function parseBrixHeader(str: string): Dsn6Header { return { @@ -32,61 +35,58 @@ function parseBrixHeader(str: string): Dsn6Header { } } -function parseDsn6Header(int: Int16Array): Dsn6Header { - const factor = 1 / int[ 17 ] +function parseDsn6Header(buffer: SimpleBuffer, littleEndian: boolean): Dsn6Header { + const readInt = littleEndian ? (o: number) => buffer.readInt16LE(o * 2) : (o: number) => buffer.readInt16BE(o * 2) + const factor = 1 / readInt(17) return { - xStart: int[ 0 ], - yStart: int[ 1 ], - zStart: int[ 2 ], - xExtent: int[ 3 ], - yExtent: int[ 4 ], - zExtent: int[ 5 ], - xRate: int[ 6 ], - yRate: int[ 7 ], - zRate: int[ 8 ], - xlen: int[ 9 ] * factor, - ylen: int[ 10 ] * factor, - zlen: int[ 11 ] * factor, - alpha: int[ 12 ] * factor, - beta: int[ 13 ] * factor, - gamma: int[ 14 ] * factor, - divisor: int[ 15 ] / 100, - summand: int[ 16 ], + xStart: readInt(0), + yStart: readInt(1), + zStart: readInt(2), + xExtent: readInt(3), + yExtent: readInt(4), + zExtent: readInt(5), + xRate: readInt(6), + yRate: readInt(7), + zRate: readInt(8), + xlen: readInt(9) * factor, + ylen: readInt(10) * factor, + zlen: readInt(11) * factor, + alpha: readInt(12) * factor, + beta: readInt(13) * factor, + gamma: readInt(14) * factor, + divisor: readInt(15) / 100, + summand: readInt(16), sigma: undefined } } -async function parseInternal(file: FileHandle, ctx: RuntimeContext): Promise<Result<Dsn6File>> { - await ctx.update({ message: 'Parsing DSN6/BRIX file...' }); - - const { buffer } = await file.readBuffer(0, file.length) - const bin = buffer.buffer +function getBlocks(header: Dsn6Header) { + const { xExtent, yExtent, zExtent } = header + const xBlocks = Math.ceil(xExtent / 8) + const yBlocks = Math.ceil(yExtent / 8) + const zBlocks = Math.ceil(zExtent / 8) + return { xBlocks, yBlocks, zBlocks } +} - const intView = new Int16Array(bin) - const byteView = new Uint8Array(bin) - const brixStr = String.fromCharCode.apply(null, byteView.subarray(0, 512)) +export async function readDsn6Header(file: FileHandle): Promise<{ header: Dsn6Header, littleEndian: boolean }> { + const { buffer } = await file.readBuffer(0, dsn6HeaderSize) + const brixStr = String.fromCharCode.apply(null, buffer) as string const isBrix = brixStr.startsWith(':-)') + const littleEndian = isBrix || buffer.readInt16LE(18 * 2) === 100 + const header = isBrix ? parseBrixHeader(brixStr) : parseDsn6Header(buffer, littleEndian) + return { header, littleEndian } +} - if (!isBrix) { - // for DSN6, swap byte order when big endian - if (intView[18] !== 100) { - for (let i = 0, n = intView.length; i < n; ++i) { - const val = intView[i] - intView[i] = ((val & 0xff) << 8) | ((val >> 8) & 0xff) - } - } +export async function parseDsn6Values(header: Dsn6Header, source: Uint8Array, target: Float32Array, littleEndian: boolean) { + if (!littleEndian) { + // even though the values are one byte they need to be swapped like they are 2 + SimpleBuffer.flipByteOrderInPlace2(source.buffer) } - const header = isBrix ? parseBrixHeader(brixStr) : parseDsn6Header(intView) - const { divisor, summand } = header - - const values = new Float32Array(header.xExtent * header.yExtent * header.zExtent) - - let offset = 512 - const xBlocks = Math.ceil(header.xExtent / 8) - const yBlocks = Math.ceil(header.yExtent / 8) - const zBlocks = Math.ceil(header.zExtent / 8) + const { divisor, summand, xExtent, yExtent, zExtent } = header + const { xBlocks, yBlocks, zBlocks } = getBlocks(header) + let offset = 0 // loop over blocks for (let zz = 0; zz < zBlocks; ++zz) { for (let yy = 0; yy < yBlocks; ++yy) { @@ -99,9 +99,9 @@ async function parseInternal(file: FileHandle, ctx: RuntimeContext): Promise<Res for (let i = 0; i < 8; ++i) { const x = 8 * xx + i // check if remaining slice-part contains values - if (x < header.xExtent && y < header.yExtent && z < header.zExtent) { - const idx = ((((x * header.yExtent) + y) * header.zExtent) + z) - values[ idx ] = (byteView[ offset ] - summand) / divisor + if (x < xExtent && y < yExtent && z < zExtent) { + const idx = ((((x * yExtent) + y) * zExtent) + z) + target[idx] = (source[offset] - summand) / divisor ++offset } else { offset += 8 - i @@ -113,15 +113,41 @@ async function parseInternal(file: FileHandle, ctx: RuntimeContext): Promise<Res } } } +} + +export function getDsn6Counts(header: Dsn6Header) { + const { xExtent, yExtent, zExtent } = header + const { xBlocks, yBlocks, zBlocks } = getBlocks(header) + const valueCount = xExtent * yExtent * zExtent + const count = xBlocks * 8 * yBlocks * 8 * zBlocks * 8 + const elementByteSize = 1 + const byteCount = count * elementByteSize + return { count, byteCount, valueCount } +} + +async function parseInternal(file: FileHandle, size: number, ctx: RuntimeContext): Promise<Dsn6File> { + await ctx.update({ message: 'Parsing DSN6/BRIX file...' }); + const { header, littleEndian } = await readDsn6Header(file) + const { buffer } = await file.readBuffer(dsn6HeaderSize, size - dsn6HeaderSize) + const { valueCount } = getDsn6Counts(header) + + const values = new Float32Array(valueCount) + await parseDsn6Values(header, buffer, values, littleEndian) const result: Dsn6File = { header, values }; - return Result.success(result); + return result; } -export function parseFile(file: FileHandle) { - return Task.create<Result<Dsn6File>>('Parse DSN6/BRIX', ctx => parseInternal(file, ctx)); +export function parseFile(file: FileHandle, size: number) { + return Task.create<Result<Dsn6File>>('Parse DSN6/BRIX', async ctx => { + try { + return Result.success(await parseInternal(file, size, ctx)); + } catch (e) { + return Result.error(e); + } + }) } export function parse(buffer: Uint8Array) { - return parseFile(FileHandle.fromBuffer(buffer)) + return parseFile(FileHandle.fromBuffer(SimpleBuffer.fromUint8Array(buffer)), buffer.length) } \ No newline at end of file diff --git a/src/mol-io/reader/dsn6/schema.ts b/src/mol-io/reader/dsn6/schema.ts index 7b185656e365d5daad08f0a5e372334179b16469..6bd612093ebb7a1454120f9a04e1b7917c7f8553 100644 --- a/src/mol-io/reader/dsn6/schema.ts +++ b/src/mol-io/reader/dsn6/schema.ts @@ -40,5 +40,5 @@ export interface Dsn6Header { */ export interface Dsn6File { header: Dsn6Header - values: Float32Array | Int8Array + values: Float32Array } \ No newline at end of file diff --git a/src/mol-model-formats/volume/ccp4.ts b/src/mol-model-formats/volume/ccp4.ts index 5a6d9cfac971938ed881439ac32d9c1df00111e9..118205173abad9610ea2f2784990a23e29de2728 100644 --- a/src/mol-model-formats/volume/ccp4.ts +++ b/src/mol-model-formats/volume/ccp4.ts @@ -8,12 +8,36 @@ import { VolumeData } from 'mol-model/volume/data' import { Task } from 'mol-task'; import { SpacegroupCell, Box3D } from 'mol-math/geometry'; import { Tensor, Vec3 } from 'mol-math/linear-algebra'; -import { Ccp4File } from 'mol-io/reader/ccp4/schema'; +import { Ccp4File, Ccp4Header } from 'mol-io/reader/ccp4/schema'; import { degToRad } from 'mol-math/misc'; +import { getCcp4ValueType } from 'mol-io/reader/ccp4/parser'; +import { TypedArrayValueType } from 'mol-io/common/typed-array'; -function volumeFromCcp4(source: Ccp4File, params?: { voxelSize?: Vec3 }): Task<VolumeData> { +/** When available (e.g. in MRC files) use ORIGIN records instead of N[CRS]START */ +export function getCcp4Origin(header: Ccp4Header) { + let gridOrigin: number[] + if (header.originX === 0.0 && header.originY === 0.0 && header.originZ === 0.0) { + gridOrigin = [header.NCSTART, header.NRSTART, header.NSSTART]; + } else { + gridOrigin = [header.originX, header.originY, header.originZ]; + } + return gridOrigin +} + +function getTypedArrayCtor(header: Ccp4Header) { + const valueType = getCcp4ValueType(header) + switch (valueType) { + case TypedArrayValueType.Float32: return Float32Array; + case TypedArrayValueType.Int8: return Int8Array; + case TypedArrayValueType.Int16: return Int16Array; + } + throw Error(`${valueType} is not a supported value format.`); +} + +export function volumeFromCcp4(source: Ccp4File, params?: { voxelSize?: Vec3 }): Task<VolumeData> { return Task.create<VolumeData>('Create Volume Data', async ctx => { const { header, values } = source; + console.log({ header, values }) const size = Vec3.create(header.xLength, header.yLength, header.zLength) if (params && params.voxelSize) Vec3.mul(size, size, params.voxelSize) const angles = Vec3.create(degToRad(header.alpha), degToRad(header.beta), degToRad(header.gamma)) @@ -24,19 +48,12 @@ function volumeFromCcp4(source: Ccp4File, params?: { voxelSize?: Vec3 }): Task<V const grid = [header.NX, header.NY, header.NZ]; const extent = normalizeOrder([header.NC, header.NR, header.NS]); - - let gridOrigin: number[] - if (header.originX === 0.0 && header.originY === 0.0 && header.originZ === 0.0) { - gridOrigin = normalizeOrder([header.NCSTART, header.NRSTART, header.NSSTART]); - } else { - // When available (e.g. in MRC files) use ORIGIN records instead of N[CRS]START - gridOrigin = [header.originX, header.originY, header.originZ]; - } + const gridOrigin = normalizeOrder(getCcp4Origin(header)); const origin_frac = Vec3.create(gridOrigin[0] / grid[0], gridOrigin[1] / grid[1], gridOrigin[2] / grid[2]); const dimensions_frac = Vec3.create(extent[0] / grid[0], extent[1] / grid[1], extent[2] / grid[2]); - const space = Tensor.Space(extent, Tensor.invertAxisOrder(axis_order_fast_to_slow), header.MODE === 0 ? Int8Array : Float32Array); + const space = Tensor.Space(extent, Tensor.invertAxisOrder(axis_order_fast_to_slow), getTypedArrayCtor(header)); const data = Tensor.create(space, Tensor.Data1(values)); // TODO Calculate stats? When to trust header data? @@ -55,6 +72,4 @@ function volumeFromCcp4(source: Ccp4File, params?: { voxelSize?: Vec3 }): Task<V } }; }); -} - -export { volumeFromCcp4 } \ No newline at end of file +} \ No newline at end of file diff --git a/src/mol-model/volume/data.ts b/src/mol-model/volume/data.ts index 6e9ab3173c50b897bb4eb2ec144a0a327e8a2eaa..97572d0ef23dc697e20f3747316ab2276da75987 100644 --- a/src/mol-model/volume/data.ts +++ b/src/mol-model/volume/data.ts @@ -21,10 +21,10 @@ interface VolumeData { } namespace VolumeData { - export const Empty: VolumeData = { + export const One: VolumeData = { cell: SpacegroupCell.Zero, fractionalBox: Box3D.empty(), - data: Tensor.create(Tensor.Space([0, 0, 0], [0, 1, 2]), Tensor.Data1([])), + data: Tensor.create(Tensor.Space([1, 1, 1], [0, 1, 2]), Tensor.Data1([0])), dataStats: { min: 0, max: 0, mean: 0, sigma: 0 } } @@ -44,11 +44,11 @@ namespace VolumeData { type VolumeIsoValue = VolumeIsoValue.Absolute | VolumeIsoValue.Relative namespace VolumeIsoValue { - export type Relative = Readonly<{ kind: 'relative', stats: VolumeData['dataStats'], relativeValue: number }> - export type Absolute = Readonly<{ kind: 'absolute', stats: VolumeData['dataStats'], absoluteValue: number }> + export type Relative = Readonly<{ kind: 'relative', relativeValue: number }> + export type Absolute = Readonly<{ kind: 'absolute', absoluteValue: number }> - export function absolute(stats: VolumeData['dataStats'], value: number): Absolute { return { kind: 'absolute', stats, absoluteValue: value }; } - export function relative(stats: VolumeData['dataStats'], value: number): Relative { return { kind: 'relative', stats, relativeValue: value }; } + export function absolute(value: number): Absolute { return { kind: 'absolute', absoluteValue: value }; } + export function relative(value: number): Relative { return { kind: 'relative', relativeValue: value }; } export function calcAbsolute(stats: VolumeData['dataStats'], relativeValue: number): number { return relativeValue * stats.sigma + stats.mean @@ -58,22 +58,18 @@ namespace VolumeIsoValue { return stats.sigma === 0 ? 0 : ((absoluteValue - stats.mean) / stats.sigma) } - export function toAbsolute(value: VolumeIsoValue): Absolute { - if (value.kind === 'absolute') return value; - return { - kind: 'absolute', - stats: value.stats, - absoluteValue: calcAbsolute(value.stats, value.relativeValue) - } + export function toAbsolute(value: VolumeIsoValue, stats: VolumeData['dataStats']): Absolute { + return value.kind === 'absolute' ? value : { kind: 'absolute', absoluteValue: VolumeIsoValue.calcAbsolute(stats, value.relativeValue) } } - export function toRelative(value: VolumeIsoValue): Relative { - if (value.kind === 'relative') return value; - return { - kind: 'relative', - stats: value.stats, - relativeValue: calcRelative(value.stats, value.absoluteValue) - } + export function toRelative(value: VolumeIsoValue, stats: VolumeData['dataStats']): Relative { + return value.kind === 'relative' ? value : { kind: 'relative', relativeValue: VolumeIsoValue.calcRelative(stats, value.absoluteValue) } + } + + export function toString(value: VolumeIsoValue) { + return value.kind === 'relative' + ? `${value.relativeValue} σ` + : `${value.absoluteValue}` } } diff --git a/src/mol-plugin/behavior/behavior.ts b/src/mol-plugin/behavior/behavior.ts index 4d764da26697e3d69b3422a2787baabfb1e802a9..f3fae6e80fe4442f4df5c2c7d799d8222a29cdf0 100644 --- a/src/mol-plugin/behavior/behavior.ts +++ b/src/mol-plugin/behavior/behavior.ts @@ -5,7 +5,7 @@ */ import { PluginStateTransform, PluginStateObject } from '../state/objects'; -import { Transformer, Transform } from 'mol-state'; +import { StateTransformer, StateTransform } from 'mol-state'; import { Task } from 'mol-task'; import { PluginContext } from 'mol-plugin/context'; import { PluginCommand } from '../command'; @@ -16,7 +16,7 @@ import { shallowEqual } from 'mol-util'; export { PluginBehavior } interface PluginBehavior<P = unknown> { - register(ref: Transform.Ref): void, + register(ref: StateTransform.Ref): void, unregister(): void, /** Update params in place. Optionally return a promise if it depends on an async action. */ @@ -25,14 +25,24 @@ interface PluginBehavior<P = unknown> { namespace PluginBehavior { export class Root extends PluginStateObject.Create({ name: 'Root', typeClass: 'Root' }) { } + export class Category extends PluginStateObject.Create({ name: 'Category', typeClass: 'Object' }) { } export class Behavior extends PluginStateObject.CreateBehavior<PluginBehavior>({ name: 'Behavior' }) { } export interface Ctor<P = undefined> { new(ctx: PluginContext, params: P): PluginBehavior<P> } + export const Categories = { + 'common': 'Common', + 'representation': 'Representation', + 'interaction': 'Interaction', + 'custom-props': 'Custom Properties', + 'misc': 'Miscellaneous' + }; + export interface CreateParams<P> { name: string, + category: keyof typeof Categories, ctor: Ctor<P>, - canAutoUpdate?: Transformer.Definition<Root, Behavior, P>['canAutoUpdate'], + canAutoUpdate?: StateTransformer.Definition<Root, Behavior, P>['canAutoUpdate'], label?: (params: P) => { label: string, description?: string }, display: { name: string, @@ -42,9 +52,28 @@ namespace PluginBehavior { params?(a: Root, globalCtx: PluginContext): { [K in keyof P]: ParamDefinition.Any } } + export type CreateCategory = typeof CreateCategory + export const CreateCategory = PluginStateTransform.BuiltIn({ + name: 'create-behavior-category', + display: { name: 'Behavior Category' }, + from: Root, + to: Category, + params: { + label: ParamDefinition.Text('', { isHidden: true }), + } + })({ + apply({ params }) { + return new Category({}, { label: params.label }); + } + }); + + const categoryMap = new Map<string, string>(); + export function getCategoryId(t: StateTransformer) { + return categoryMap.get(t.id)!; + } + export function create<P>(params: CreateParams<P>) { - // TODO: cache groups etc - return PluginStateTransform.CreateBuiltIn<Root, Behavior, P>({ + const t = PluginStateTransform.CreateBuiltIn<Category, Behavior, P>({ name: params.name, display: params.display, from: [Root], @@ -56,13 +85,15 @@ namespace PluginBehavior { }, update({ b, newParams }) { return Task.create('Update Behavior', async () => { - if (!b.data.update) return Transformer.UpdateResult.Unchanged; + if (!b.data.update) return StateTransformer.UpdateResult.Unchanged; const updated = await b.data.update(newParams); - return updated ? Transformer.UpdateResult.Updated : Transformer.UpdateResult.Unchanged; + return updated ? StateTransformer.UpdateResult.Updated : StateTransformer.UpdateResult.Unchanged; }) }, canAutoUpdate: params.canAutoUpdate }); + categoryMap.set(t.id, params.category); + return t; } export function simpleCommandHandler<T>(cmd: PluginCommand<T>, action: (data: T, ctx: PluginContext) => void | Promise<void>) { diff --git a/src/mol-plugin/behavior/dynamic/animation.ts b/src/mol-plugin/behavior/dynamic/animation.ts index 2b3a1cee14b172862acd3a79390ce0b660ef8c41..a2c863ed388f5a86bd2bf3be4d56a4243435fb97 100644 --- a/src/mol-plugin/behavior/dynamic/animation.ts +++ b/src/mol-plugin/behavior/dynamic/animation.ts @@ -10,8 +10,7 @@ import { ParamDefinition as PD } from 'mol-util/param-definition' import { degToRad } from 'mol-math/misc'; import { Mat4, Vec3 } from 'mol-math/linear-algebra'; import { PluginStateObject as SO, PluginStateObject } from '../../state/objects'; -import { StateSelection } from 'mol-state/state/selection'; -import { StateObjectCell, State } from 'mol-state'; +import { StateObjectCell, State, StateSelection } from 'mol-state'; import { StructureUnitTransforms } from 'mol-model/structure/structure/util/unit-transforms'; import { UUID } from 'mol-util'; @@ -30,6 +29,7 @@ type StructureAnimationProps = PD.Values<typeof StructureAnimationParams> */ export const StructureAnimation = PluginBehavior.create<StructureAnimationProps>({ name: 'structure-animation', + category: 'representation', display: { name: 'Structure Animation', group: 'Animation' }, canAutoUpdate: () => true, ctor: class extends PluginBehavior.Handler<StructureAnimationProps> { diff --git a/src/mol-plugin/behavior/dynamic/camera.ts b/src/mol-plugin/behavior/dynamic/camera.ts index 4431eec954a3fcc90d73705940c338339fbe2e23..b2caee3fefa673d61b9bcf5eb6cacc1c485bd4d5 100644 --- a/src/mol-plugin/behavior/dynamic/camera.ts +++ b/src/mol-plugin/behavior/dynamic/camera.ts @@ -10,6 +10,7 @@ import { PluginBehavior } from '../behavior'; export const FocusLociOnSelect = PluginBehavior.create<{ minRadius: number, extraRadius: number }>({ name: 'focus-loci-on-select', + category: 'interaction', ctor: class extends PluginBehavior.Handler<{ minRadius: number, extraRadius: number }> { register(): void { this.subscribeObservable(this.ctx.behaviors.canvas.selectLoci, current => { diff --git a/src/mol-plugin/behavior/dynamic/custom-props/pdbe/structure-quality-report.ts b/src/mol-plugin/behavior/dynamic/custom-props/pdbe/structure-quality-report.ts index 6c09982015da5a9d71bba13f2351d6dd8d40df83..dc882b0efb0991435ff6f0ff618ecdbdf17ca957 100644 --- a/src/mol-plugin/behavior/dynamic/custom-props/pdbe/structure-quality-report.ts +++ b/src/mol-plugin/behavior/dynamic/custom-props/pdbe/structure-quality-report.ts @@ -16,6 +16,7 @@ import { ThemeDataContext } from 'mol-theme/theme'; export const PDBeStructureQualityReport = PluginBehavior.create<{ autoAttach: boolean }>({ name: 'pdbe-structure-quality-report-prop', + category: 'custom-props', display: { name: 'PDBe Structure Quality Report', group: 'Custom Props' }, ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> { private attach = StructureQualityReport.createAttachTask( diff --git a/src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts b/src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts index 3079bbcab9e21bcf2cb79c2b3f9cc4ef11bd5627..fc131da26a344fac0b72dc545072c2b8263f50f1 100644 --- a/src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts +++ b/src/mol-plugin/behavior/dynamic/custom-props/rcsb/assembly-symmetry.ts @@ -16,6 +16,7 @@ import { Table } from 'mol-data/db'; export const RCSBAssemblySymmetry = PluginBehavior.create<{ autoAttach: boolean }>({ name: 'rcsb-assembly-symmetry-prop', + category: 'custom-props', display: { name: 'RCSB Assembly Symmetry', group: 'Custom Props' }, ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> { private attach = AssemblySymmetry.createAttachTask(this.ctx.fetch); diff --git a/src/mol-plugin/behavior/dynamic/labels.ts b/src/mol-plugin/behavior/dynamic/labels.ts index 7b8ccd1ba92cb413457de3fe1e6ae15b85944084..f3f17ea828a20e69695883d059d8597bca38a0bf 100644 --- a/src/mol-plugin/behavior/dynamic/labels.ts +++ b/src/mol-plugin/behavior/dynamic/labels.ts @@ -9,8 +9,7 @@ import { PluginBehavior } from '../behavior'; import { ParamDefinition as PD } from 'mol-util/param-definition' import { Mat4, Vec3 } from 'mol-math/linear-algebra'; import { PluginStateObject as SO, PluginStateObject } from '../../state/objects'; -import { StateSelection } from 'mol-state/state/selection'; -import { StateObjectCell, State } from 'mol-state'; +import { StateObjectCell, State, StateSelection } from 'mol-state'; import { RuntimeContext } from 'mol-task'; import { Shape } from 'mol-model/shape'; import { Text } from 'mol-geo/geometry/text/text'; @@ -74,6 +73,7 @@ function getLabelsText(data: LabelsData, props: PD.Values<Text.Params>, text?: T export const SceneLabels = PluginBehavior.create<SceneLabelsProps>({ name: 'scene-labels', + category: 'representation', display: { name: 'Scene Labels', group: 'Labels' }, canAutoUpdate: () => true, ctor: class extends PluginBehavior.Handler<SceneLabelsProps> { diff --git a/src/mol-plugin/behavior/dynamic/representation.ts b/src/mol-plugin/behavior/dynamic/representation.ts index b604e2142b3b7a1adacd02cf67cd9e8e2a529742..e5566019b12186bbe2de08e2ca3f34dd49b28a93 100644 --- a/src/mol-plugin/behavior/dynamic/representation.ts +++ b/src/mol-plugin/behavior/dynamic/representation.ts @@ -10,14 +10,14 @@ import { EmptyLoci, Loci } from 'mol-model/loci'; import { StructureUnitTransforms } from 'mol-model/structure/structure/util/unit-transforms'; import { PluginContext } from 'mol-plugin/context'; import { PluginStateObject } from 'mol-plugin/state/objects'; -import { StateObjectTracker } from 'mol-state'; -import { StateSelection } from 'mol-state/state/selection'; +import { StateObjectTracker, StateSelection } from 'mol-state'; import { labelFirst } from 'mol-theme/label'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import { PluginBehavior } from '../behavior'; export const HighlightLoci = PluginBehavior.create({ name: 'representation-highlight-loci', + category: 'interaction', ctor: class extends PluginBehavior.Handler { register(): void { let prevLoci: Loci = EmptyLoci, prevRepr: any = void 0; @@ -38,6 +38,7 @@ export const HighlightLoci = PluginBehavior.create({ export const SelectLoci = PluginBehavior.create({ name: 'representation-select-loci', + category: 'interaction', ctor: class extends PluginBehavior.Handler { register(): void { let prevLoci: Loci = EmptyLoci, prevRepr: any = void 0; @@ -60,6 +61,7 @@ export const SelectLoci = PluginBehavior.create({ export const DefaultLociLabelProvider = PluginBehavior.create({ name: 'default-loci-label-provider', + category: 'interaction', ctor: class implements PluginBehavior<undefined> { private f = labelFirst; register(): void { this.ctx.lociLabels.addProvider(this.f); } diff --git a/src/mol-plugin/behavior/dynamic/volume.ts b/src/mol-plugin/behavior/dynamic/volume.ts new file mode 100644 index 0000000000000000000000000000000000000000..b826f37c0a8f7a90bd00785bd5414d6066d561f2 --- /dev/null +++ b/src/mol-plugin/behavior/dynamic/volume.ts @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import CIF from 'mol-io/reader/cif'; +import { Box3D } from 'mol-math/geometry'; +import { Vec3 } from 'mol-math/linear-algebra'; +import { volumeFromDensityServerData } from 'mol-model-formats/volume/density-server'; +import { VolumeData, VolumeIsoValue } from 'mol-model/volume'; +import { PluginContext } from 'mol-plugin/context'; +import { PluginStateObject } from 'mol-plugin/state/objects'; +import { createIsoValueParam } from 'mol-repr/volume/isosurface'; +import { Color } from 'mol-util/color'; +import { LRUCache } from 'mol-util/lru-cache'; +import { ParamDefinition as PD } from 'mol-util/param-definition'; +import { PluginBehavior } from '../behavior'; +import { Structure } from 'mol-model/structure'; + +export namespace VolumeStreaming { + function channelParam(label: string, color: Color, defaultValue: number) { + return PD.Group({ + color: PD.Color(color), + isoValue: createIsoValueParam(VolumeIsoValue.relative(defaultValue)) + }, { label }); + } + + export const Params = { + id: PD.Text('1tqn'), + levels: PD.MappedStatic('x-ray', { + 'em': channelParam('EM', Color(0x638F8F), 1.5), + 'x-ray': PD.Group({ + '2fo-fc': channelParam('2Fo-Fc', Color(0x3362B2), 1.5), + 'fo-fc(+ve)': channelParam('Fo-Fc(+ve)', Color(0x33BB33), 3), + 'fo-fc(-ve)': channelParam('Fo-Fc(-ve)', Color(0xBB3333), -3), + }) + }), + box: PD.MappedStatic('static-box', { + 'static-box': PD.Group({ + bottomLeft: PD.Vec3(Vec3.create(-22.4, -33.4, -21.6)), + topRight: PD.Vec3(Vec3.create(-7.1, -10, -0.9)) + }, { description: 'Static box defined by cartesian coords.' }), + // 'around-selection': PD.Group({ radius: PD.Numeric(5, { min: 0, max: 10 }) }), + 'cell': PD.Group({ }), + // 'auto': PD.Group({ }), // based on camera distance/active selection/whatever, show whole structure or slice. + }), + detailLevel: PD.Numeric(3, { min: 0, max: 7 }), + serverUrl: PD.Text('https://webchem.ncbr.muni.cz/DensityServer'), + } + export type Params = PD.Values<typeof Params> + + export type ChannelData = { [name in 'EM' | '2FO-FC' | 'FO-FC']?: VolumeData } + export type LevelType = 'em' | '2fo-fc' | 'fo-fc(+ve)' | 'fo-fc(-ve)' + + export class Behavior implements PluginBehavior<Params> { + // TODO: have special value for "cell"? + private cache = LRUCache.create<ChannelData>(25); + // private ref: string = ''; + + currentData: ChannelData = { } + + private async queryData(box?: Box3D) { + let url = `${this.params.serverUrl}/${this.params.levels.name}/${this.params.id}` + + if (box) { + const { min: a, max: b } = box; + url += `/box` + + `/${a.map(v => Math.round(1000 * v) / 1000).join(',')}` + + `/${b.map(v => Math.round(1000 * v) / 1000).join(',')}`; + } else { + url += `/cell`; + } + url += `?detail=${this.params.detailLevel}`; + + let data = LRUCache.get(this.cache, url); + if (data) { + return data; + } + + const cif = await this.ctx.runTask(this.ctx.fetch(url, 'binary')); + data = await this.parseCif(cif as Uint8Array); + if (!data) { + return; + } + + LRUCache.set(this.cache, url, data); + return data; + } + + private async parseCif(data: Uint8Array): Promise<ChannelData | undefined> { + const parsed = await this.ctx.runTask(CIF.parseBinary(data)); + if (parsed.isError) { + this.ctx.log.error('VolumeStreaming, parsing CIF: ' + parsed.toString()); + return; + } + if (parsed.result.blocks.length < 2) { + this.ctx.log.error('VolumeStreaming: Invalid data.'); + return; + } + + const ret: ChannelData = { }; + for (let i = 1; i < parsed.result.blocks.length; i++) { + const block = parsed.result.blocks[i]; + + const densityServerCif = CIF.schema.densityServer(block); + const volume = await this.ctx.runTask(await volumeFromDensityServerData(densityServerCif)); + (ret as any)[block.header as any] = volume; + } + return ret; + } + + register(ref: string): void { + // TODO: register camera movement/loci so that "around selection box works" + // alternatively, and maybe a better solution, write a global behavior that modifies this node from the outside + } + + async update(params: Params): Promise<boolean> { + this.params = params; + + let box: Box3D | undefined = void 0; + + switch (params.box.name) { + case 'static-box': + box = Box3D.create(params.box.params.bottomLeft, params.box.params.topRight); + break; + case 'cell': + box = this.params.levels.name === 'x-ray' + ? this.structure.boundary.box + : void 0; + break; + } + + const data = await this.queryData(box); + this.currentData = data || { }; + + return true; + } + + unregister(): void { + // TODO unsubscribe to events + } + + constructor(public ctx: PluginContext, public params: Params, private structure: Structure) { + } + } + + export class Obj extends PluginStateObject.CreateBehavior<Behavior>({ name: 'Volume Streaming' }) { } +} \ No newline at end of file diff --git a/src/mol-plugin/behavior/static/state.ts b/src/mol-plugin/behavior/static/state.ts index 2e53ede8c6ae73956b50ce2fc5061749bfd8a3a1..6ff152876d98c634ba8b8c76e1a453bdcca379f4 100644 --- a/src/mol-plugin/behavior/static/state.ts +++ b/src/mol-plugin/behavior/static/state.ts @@ -6,7 +6,7 @@ import { PluginCommands } from '../../command'; import { PluginContext } from '../../context'; -import { StateTree, Transform, State } from 'mol-state'; +import { StateTree, StateTransform, State } from 'mol-state'; import { PluginStateSnapshotManager } from 'mol-plugin/state/snapshots'; import { PluginStateObject as SO, PluginStateObject } from '../../state/objects'; import { EmptyLoci, EveryLoci } from 'mol-model/loci'; @@ -51,7 +51,7 @@ export function SetCurrentObject(ctx: PluginContext) { } export function Update(ctx: PluginContext) { - PluginCommands.State.Update.subscribe(ctx, ({ state, tree }) => ctx.runTask(state.updateTree(tree))); + PluginCommands.State.Update.subscribe(ctx, ({ state, tree, doNotLogTiming }) => ctx.runTask(state.updateTree(tree, doNotLogTiming))); } export function ApplyAction(ctx: PluginContext) { @@ -59,9 +59,27 @@ export function ApplyAction(ctx: PluginContext) { } export function RemoveObject(ctx: PluginContext) { - PluginCommands.State.RemoveObject.subscribe(ctx, ({ state, ref }) => { - const tree = state.tree.build().delete(ref).getTree(); + function remove(state: State, ref: string) { + const tree = state.build().delete(ref).getTree(); return ctx.runTask(state.updateTree(tree)); + } + + PluginCommands.State.RemoveObject.subscribe(ctx, ({ state, ref, removeParentGhosts }) => { + if (removeParentGhosts) { + const tree = state.tree; + let curr = tree.transforms.get(ref); + if (curr.parent === ref) return remove(state, ref); + + while (true) { + const children = tree.children.get(curr.parent); + if (curr.parent === curr.ref || children.size > 1) return remove(state, curr.ref); + const parent = tree.transforms.get(curr.parent); + if (!parent.props || !parent.props.isGhost) return remove(state, curr.ref); + curr = parent; + } + } else { + remove(state, ref); + } }); } @@ -73,11 +91,11 @@ export function ToggleVisibility(ctx: PluginContext) { PluginCommands.State.ToggleVisibility.subscribe(ctx, ({ state, ref }) => setVisibility(state, ref, !state.cellStates.get(ref).isHidden)); } -function setVisibility(state: State, root: Transform.Ref, value: boolean) { +function setVisibility(state: State, root: StateTransform.Ref, value: boolean) { StateTree.doPreOrder(state.tree, state.transforms.get(root), { state, value }, setVisibilityVisitor); } -function setVisibilityVisitor(t: Transform, tree: StateTree, ctx: { state: State, value: boolean }) { +function setVisibilityVisitor(t: StateTransform, tree: StateTree, ctx: { state: State, value: boolean }) { ctx.state.updateCellState(t.ref, { isHidden: ctx.value }); } diff --git a/src/mol-plugin/command.ts b/src/mol-plugin/command.ts index d17b98dae56c01efee9f206175a789062c7e10a2..c188f4cb3c8a2f005ec3576f76ad0e4c240c250e 100644 --- a/src/mol-plugin/command.ts +++ b/src/mol-plugin/command.ts @@ -6,8 +6,7 @@ import { Camera } from 'mol-canvas3d/camera'; import { PluginCommand } from './command/base'; -import { Transform, State } from 'mol-state'; -import { StateAction } from 'mol-state/action'; +import { StateTransform, State, StateAction } from 'mol-state'; import { Canvas3DProps } from 'mol-canvas3d/canvas3d'; import { PluginLayoutStateProps } from './layout'; @@ -15,16 +14,16 @@ export * from './command/base'; export const PluginCommands = { State: { - SetCurrentObject: PluginCommand<{ state: State, ref: Transform.Ref }>(), - ApplyAction: PluginCommand<{ state: State, action: StateAction.Instance, ref?: Transform.Ref }>(), - Update: PluginCommand<{ state: State, tree: State.Tree | State.Builder }>(), + SetCurrentObject: PluginCommand<{ state: State, ref: StateTransform.Ref }>(), + ApplyAction: PluginCommand<{ state: State, action: StateAction.Instance, ref?: StateTransform.Ref }>(), + Update: PluginCommand<{ state: State, tree: State.Tree | State.Builder, doNotLogTiming?: boolean }>(), - RemoveObject: PluginCommand<{ state: State, ref: Transform.Ref }>(), + RemoveObject: PluginCommand<{ state: State, ref: StateTransform.Ref, removeParentGhosts?: boolean }>(), - ToggleExpanded: PluginCommand<{ state: State, ref: Transform.Ref }>({ isImmediate: true }), - ToggleVisibility: PluginCommand<{ state: State, ref: Transform.Ref }>({ isImmediate: true }), - Highlight: PluginCommand<{ state: State, ref: Transform.Ref }>({ isImmediate: true }), - ClearHighlight: PluginCommand<{ state: State, ref: Transform.Ref }>({ isImmediate: true }), + ToggleExpanded: PluginCommand<{ state: State, ref: StateTransform.Ref }>({ isImmediate: true }), + ToggleVisibility: PluginCommand<{ state: State, ref: StateTransform.Ref }>({ isImmediate: true }), + Highlight: PluginCommand<{ state: State, ref: StateTransform.Ref }>({ isImmediate: true }), + ClearHighlight: PluginCommand<{ state: State, ref: StateTransform.Ref }>({ isImmediate: true }), Snapshots: { Add: PluginCommand<{ name?: string, description?: string }>({ isImmediate: true }), diff --git a/src/mol-plugin/component.ts b/src/mol-plugin/component.ts index 56577179e12cfb870169c6cee6b0088762182276..b49f80c1bb2d219fe334bbbf9b093bf37e66fa1e 100644 --- a/src/mol-plugin/component.ts +++ b/src/mol-plugin/component.ts @@ -4,39 +4,37 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { BehaviorSubject, Observable, Subject } from 'rxjs'; -import { PluginContext } from './context'; import { shallowMergeArray } from 'mol-util/object'; +import { RxEventHelper } from 'mol-util/rx-event-helper'; export class PluginComponent<State> { - private _state: BehaviorSubject<State>; - private _updated = new Subject(); + private _ev: RxEventHelper; - updateState(...states: Partial<State>[]) { - const latest = this.latestState; - const s = shallowMergeArray(latest, states); - if (s !== latest) { - this._state.next(s); - } + protected get ev() { + return this._ev || (this._ev = RxEventHelper.create()); } - get states() { - return <Observable<State>>this._state; - } + private _state: State; - get latestState() { - return this._state.value; + protected updateState(...states: Partial<State>[]): boolean { + const latest = this.state; + const s = shallowMergeArray(latest, states); + if (s !== latest) { + this._state = s; + return true; + } + return false; } - get updated() { - return <Observable<{}>>this._updated; + get state() { + return this._state; } - triggerUpdate() { - this._updated.next({}); + dispose() { + if (this._ev) this._ev.dispose(); } - constructor(public context: PluginContext, initialState: State) { - this._state = new BehaviorSubject<State>(initialState); + constructor(initialState: State) { + this._state = initialState; } } \ No newline at end of file diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index 5895a6c6dfa731ed5240e2b9ff6423a6e6588ffe..25bd3648cfee27be1f62c3cf5753f41f3c761411 100644 --- a/src/mol-plugin/context.ts +++ b/src/mol-plugin/context.ts @@ -8,7 +8,7 @@ import { Canvas3D } from 'mol-canvas3d/canvas3d'; import { EmptyLoci, Loci } from 'mol-model/loci'; import { Representation } from 'mol-repr/representation'; import { StructureRepresentationRegistry } from 'mol-repr/structure/registry'; -import { State, Transform, Transformer } from 'mol-state'; +import { State, StateTransform, StateTransformer } from 'mol-state'; import { Task } from 'mol-task'; import { ColorTheme } from 'mol-theme/color'; import { SizeTheme } from 'mol-theme/size'; @@ -30,7 +30,8 @@ import { PLUGIN_VERSION, PLUGIN_VERSION_DATE } from './version'; import { PluginLayout } from './layout'; import { List } from 'immutable'; import { StateTransformParameters } from './ui/state/common'; -import { DataFormatRegistry } from './state/actions/basic'; +import { DataFormatRegistry } from './state/actions/volume'; +import { PluginBehavior } from './behavior/behavior'; export class PluginContext { private disposed = false; @@ -98,7 +99,7 @@ export class PluginContext { initViewer(canvas: HTMLCanvasElement, container: HTMLDivElement) { try { this.layout.setRoot(container); - if (this.spec.initialLayout) this.layout.updateState(this.spec.initialLayout); + if (this.spec.initialLayout) this.layout.setProps(this.spec.initialLayout); (this.canvas3d as Canvas3D) = Canvas3D.create(canvas, container); PluginCommands.Canvas3D.SetSettings.dispatch(this, { settings: { backgroundColor: Color(0xFCFBF9) } }); this.canvas3d.animate(); @@ -140,15 +141,16 @@ export class PluginContext { this.ev.dispose(); this.state.dispose(); this.tasks.dispose(); + this.layout.dispose(); this.disposed = true; } - applyTransform(state: State, a: Transform.Ref, transformer: Transformer, params: any) { - const tree = state.tree.build().to(a).apply(transformer, params); + applyTransform(state: State, a: StateTransform.Ref, transformer: StateTransformer, params: any) { + const tree = state.build().to(a).apply(transformer, params); return PluginCommands.State.Update.dispatch(this, { state, tree }); } - updateTransform(state: State, a: Transform.Ref, params: any) { + updateTransform(state: State, a: StateTransform.Ref, params: any) { const tree = state.build().to(a).update(params); return PluginCommands.State.Update.dispatch(this, { state, tree }); } @@ -163,10 +165,14 @@ export class PluginContext { } private async initBehaviors() { - const tree = this.state.behaviorState.tree.build(); + const tree = this.state.behaviorState.build(); + + for (const cat of Object.keys(PluginBehavior.Categories)) { + tree.toRoot().apply(PluginBehavior.CreateCategory, { label: (PluginBehavior.Categories as any)[cat] }, { ref: cat, props: { isLocked: true } }); + } for (const b of this.spec.behaviors) { - tree.toRoot().apply(b.transformer, b.defaultParams, { ref: b.transformer.id }); + tree.to(PluginBehavior.getCategoryId(b.transformer)).apply(b.transformer, b.defaultParams, { ref: b.transformer.id }); } await this.runTask(this.state.behaviorState.updateTree(tree, true)); @@ -178,6 +184,13 @@ export class PluginContext { } } + private initAnimations() { + if (!this.spec.animations) return; + for (const anim of this.spec.animations) { + this.state.animation.register(anim); + } + } + private initCustomParamEditors() { if (!this.spec.customParamEditors) return; @@ -193,6 +206,7 @@ export class PluginContext { this.initBehaviors(); this.initDataActions(); + this.initAnimations(); this.initCustomParamEditors(); this.lociLabels = new LociLabelManager(this); diff --git a/src/mol-plugin/index.ts b/src/mol-plugin/index.ts index 54e7e1590e56e0970754f3e5083f11a53a6f8bb6..b701c7828ba25bf5fe1b6a3d4044059c4c5780b4 100644 --- a/src/mol-plugin/index.ts +++ b/src/mol-plugin/index.ts @@ -11,9 +11,10 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { PluginCommands } from './command'; import { PluginSpec } from './spec'; -import { DownloadStructure, CreateComplexRepresentation, OpenStructure, OpenVolume, DownloadDensity } from './state/actions/basic'; import { StateTransforms } from './state/transforms'; import { PluginBehaviors } from './behavior'; +import { AnimateModelIndex } from './state/animation/built-in'; +import { StateActions } from './state/actions'; function getParam(name: string, regex: string): string { let r = new RegExp(`${name}=(${regex})[&]?`, 'i'); @@ -22,11 +23,15 @@ function getParam(name: string, regex: string): string { export const DefaultPluginSpec: PluginSpec = { actions: [ - PluginSpec.Action(DownloadStructure), - PluginSpec.Action(DownloadDensity), - PluginSpec.Action(OpenStructure), - PluginSpec.Action(OpenVolume), - PluginSpec.Action(CreateComplexRepresentation), + PluginSpec.Action(StateActions.Structure.DownloadStructure), + PluginSpec.Action(StateActions.Volume.DownloadDensity), + PluginSpec.Action(StateActions.Structure.OpenStructure), + PluginSpec.Action(StateActions.Volume.OpenVolume), + PluginSpec.Action(StateActions.Structure.CreateComplexRepresentation), + PluginSpec.Action(StateActions.Structure.EnableModelCustomProps), + + PluginSpec.Action(StateActions.Volume.InitVolumeStreaming), + PluginSpec.Action(StateTransforms.Data.Download), PluginSpec.Action(StateTransforms.Data.ParseCif), PluginSpec.Action(StateTransforms.Data.ParseCcp4), @@ -34,7 +39,7 @@ export const DefaultPluginSpec: PluginSpec = { PluginSpec.Action(StateTransforms.Model.StructureSymmetryFromModel), PluginSpec.Action(StateTransforms.Model.StructureFromModel), PluginSpec.Action(StateTransforms.Model.ModelFromTrajectory), - PluginSpec.Action(StateTransforms.Model.VolumeFromCcp4), + PluginSpec.Action(StateTransforms.Volume.VolumeFromCcp4), PluginSpec.Action(StateTransforms.Representation.StructureRepresentation3D), PluginSpec.Action(StateTransforms.Representation.ExplodeStructureRepresentation3D), PluginSpec.Action(StateTransforms.Representation.VolumeRepresentation3D), @@ -48,6 +53,9 @@ export const DefaultPluginSpec: PluginSpec = { PluginSpec.Behavior(PluginBehaviors.Labels.SceneLabels), PluginSpec.Behavior(PluginBehaviors.CustomProps.PDBeStructureQualityReport, { autoAttach: true }), PluginSpec.Behavior(PluginBehaviors.CustomProps.RCSBAssemblySymmetry, { autoAttach: true }), + ], + animations: [ + AnimateModelIndex ] } diff --git a/src/mol-plugin/layout.ts b/src/mol-plugin/layout.ts index bca2e2cd1da0b3570afb58a0b9f620c2e7530ca4..63b8f19be005def858b246ed5d7e31317289b9e4 100644 --- a/src/mol-plugin/layout.ts +++ b/src/mol-plugin/layout.ts @@ -42,21 +42,29 @@ interface RootState { } export class PluginLayout extends PluginComponent<PluginLayoutStateProps> { + readonly events = { + updated: this.ev() + } + private updateProps(state: Partial<PluginLayoutStateProps>) { - let prevExpanded = !!this.latestState.isExpanded; + let prevExpanded = !!this.state.isExpanded; this.updateState(state); if (this.root && typeof state.isExpanded === 'boolean' && state.isExpanded !== prevExpanded) this.handleExpand(); - this.triggerUpdate(); + this.events.updated.next(); } private root: HTMLElement; private rootState: RootState | undefined = void 0; private expandedViewport: HTMLMetaElement; + setProps(props: PluginLayoutStateProps) { + this.updateState(props); + } + setRoot(root: HTMLElement) { this.root = root; - if (this.latestState.isExpanded) this.handleExpand(); + if (this.state.isExpanded) this.handleExpand(); } private getScrollElement() { @@ -72,7 +80,7 @@ export class PluginLayout extends PluginComponent<PluginLayoutStateProps> { if (!body || !head) return; - if (this.latestState.isExpanded) { + if (this.state.isExpanded) { let children = head.children; let hasExp = false; let viewports: HTMLElement[] = []; @@ -167,8 +175,8 @@ export class PluginLayout extends PluginComponent<PluginLayoutStateProps> { } } - constructor(context: PluginContext) { - super(context, { ...PD.getDefaultValues(PluginLayoutStateParams), ...context.spec.initialLayout }); + constructor(private context: PluginContext) { + super({ ...PD.getDefaultValues(PluginLayoutStateParams), ...context.spec.initialLayout }); PluginCommands.Layout.Update.subscribe(context, e => this.updateProps(e.state)); diff --git a/src/mol-plugin/providers/custom-prop.ts b/src/mol-plugin/providers/custom-prop.ts deleted file mode 100644 index 0ffdd02fcbce683e436c0030ffe0517135c6ceda..0000000000000000000000000000000000000000 --- a/src/mol-plugin/providers/custom-prop.ts +++ /dev/null @@ -1 +0,0 @@ -// TODO \ No newline at end of file diff --git a/src/mol-plugin/providers/theme.ts b/src/mol-plugin/providers/theme.ts deleted file mode 100644 index 0ffdd02fcbce683e436c0030ffe0517135c6ceda..0000000000000000000000000000000000000000 --- a/src/mol-plugin/providers/theme.ts +++ /dev/null @@ -1 +0,0 @@ -// TODO \ No newline at end of file diff --git a/src/mol-plugin/skin/base/components/controls.scss b/src/mol-plugin/skin/base/components/controls.scss index 3605b3cd9d918adac14de6a5b84ee5c093bfd20f..bd527f69b3c6fa427c0ebe08921aa5539b5374b2 100644 --- a/src/mol-plugin/skin/base/components/controls.scss +++ b/src/mol-plugin/skin/base/components/controls.scss @@ -76,19 +76,10 @@ > div:first-child { position: absolute; top: 0; - left: 0; + left: 18px; bottom: 0; - right: 50px; - width: 100%; - padding-right: 50px; - display: table; - - > div { - height: $row-height; - display: table-cell; - vertical-align: middle; - padding: 0 ($control-spacing + 4px); - } + right: 62px; + display: grid; } > div:last-child { position: absolute; @@ -101,9 +92,12 @@ bottom: 0; } - // input[type=text] { - // text-align: right; - // } + input[type=text] { + padding-right: 6px; + padding-left: 4px; + font-size: 80%; + text-align: right; + } // input[type=range] { // width: 100%; @@ -125,20 +119,10 @@ > div:nth-child(2) { position: absolute; top: 0; - left: 0; + left: 35px; bottom: 0; - right: 25px; - width: 100%; - padding-left: 20px; - padding-right: 25px; - display: table; - - > div { - height: $row-height; - display: table-cell; - vertical-align: middle; - padding: 0 ($control-spacing + 4px); - } + right: 37px; + display: grid; } > div:last-child { position: absolute; @@ -152,9 +136,12 @@ font-size: 80%; } - // input[type=text] { - // text-align: right; - // } + input[type=text] { + padding-right: 4px; + padding-left: 4px; + font-size: 80%; + text-align: center; + } // input[type=range] { // width: 100%; diff --git a/src/mol-plugin/skin/base/components/misc.scss b/src/mol-plugin/skin/base/components/misc.scss index d11284bcf2feee85acc1e334685872c053da564d..f75ff2101ae6bc1261b1fb69f418f0875365c3b2 100644 --- a/src/mol-plugin/skin/base/components/misc.scss +++ b/src/mol-plugin/skin/base/components/misc.scss @@ -66,4 +66,8 @@ background: white; cursor: inherit; display: block; +} + +.msp-animation-section { + margin-bottom: $control-spacing; } \ No newline at end of file diff --git a/src/mol-plugin/skin/base/components/slider.scss b/src/mol-plugin/skin/base/components/slider.scss index 3d879558045cb147effefb5861d7eb6e457c57c2..cc5c2c689c48808b44927d8b015600e9d29c90e4 100644 --- a/src/mol-plugin/skin/base/components/slider.scss +++ b/src/mol-plugin/skin/base/components/slider.scss @@ -14,6 +14,7 @@ padding: 5px 0; width: 100%; border-radius: $slider-border-radius-base; + align-self: center; @include borderBox; &-rail { diff --git a/src/mol-plugin/skin/base/components/temp.scss b/src/mol-plugin/skin/base/components/temp.scss index 84624ff7e3e050ed85c8ed67f911b260419f572c..8c6521668790495cbad78c722baf0f41e1c96ae4 100644 --- a/src/mol-plugin/skin/base/components/temp.scss +++ b/src/mol-plugin/skin/base/components/temp.scss @@ -14,6 +14,15 @@ // border-bottom: 1px solid $entity-color-Group; // TODO separate color } +.msp-current-header { + height: $row-height; + line-height: $row-height; + margin-bottom: $control-spacing; + text-align: center; + font-weight: bold; + background: $default-background; +} + .msp-btn-row-group { display:flex; flex-direction:row; @@ -69,10 +78,32 @@ margin-bottom: 1px; padding-left: $row-height; padding-right: 2 * $row-height + $control-spacing; - border-bottom-left-radius: $control-spacing; + border-left: 1px solid $entity-color-Group; // TODO custom color + // border-bottom-left-radius: $control-spacing; &-current { - background: $control-background + // background: $control-background; + + a { + color: $font-color; + } + + a:hover, a:hover > small { + color: color-lower-contrast($font-color, 24%); + } + } + + a { + display: block; + } + + a > small { + color: $font-color; + } + + a:hover { + font-weight: bold; + text-decoration: none; } } @@ -90,6 +121,7 @@ left: 0; top: 0; width: $row-height; + padding: 0; color: color-lower-contrast($font-color, 24%); } diff --git a/src/mol-plugin/skin/base/components/transformer.scss b/src/mol-plugin/skin/base/components/transformer.scss index aca44d435bbafbec2138e5da13b8974509273875..baa6efb5b350bc8af6d8f3c9934c48d183e99731 100644 --- a/src/mol-plugin/skin/base/components/transformer.scss +++ b/src/mol-plugin/skin/base/components/transformer.scss @@ -10,7 +10,7 @@ } } -.msp-layout-right { +.msp-layout-right, .msp-layout-left { background: $control-background; } @@ -42,14 +42,44 @@ margin-bottom: $control-spacing; } +.msp-transform-update-wrapper { + margin-bottom: 1px; +} + +.msp-transform-update-wrapper-collapsed { + margin-bottom: 1px; +} + +.msp-transform-update-wrapper, .msp-transform-update-wrapper-collapsed { + > .msp-transform-header > button { + text-align: left; + padding-left: $row-height; + line-height: 24px; + background: color-lower-contrast($control-background, 4%); // $control-background; // color-lower-contrast($default-background, 4%); + // font-weight: bold; + } +} + +.msp-transform-wrapper > .msp-transform-header > button { + text-align: left; + background: color-lower-contrast($default-background, 4%); + font-weight: bold; +} + .msp-transform-header { position: relative; - border-top: 1px solid $entity-color-Behaviour; // TODO: separate color + // border-top: 1px solid $entity-color-Behaviour; // TODO: separate color - > button { - text-align: left; - background: color-lower-contrast($default-background, 4%); - font-weight: bold; + // > button { + // text-align: left; + // padding-left: $row-height; + // background: $control-background; // color-lower-contrast($default-background, 4%); + // font-weight: bold; + // } + + > button > small { + font-weight: normal; + float: right; } > button:hover { @@ -58,9 +88,11 @@ } .msp-transform-default-params { + background: $default-background; position: absolute; - right: 0; + left: 0; top: 0; + width: $row-height; } .msp-transform-default-params:hover { @@ -74,7 +106,8 @@ } .msp-transform-refresh { - width: $control-label-width + $control-spacing; + width: $control-label-width + $control-spacing - $row-height - 1; + margin-left: $row-height + 1; background: $default-background; text-align: right; } diff --git a/src/mol-plugin/spec.ts b/src/mol-plugin/spec.ts index 7475b6ebd0446cd623ad7de994031eb321530218..2bfb64ecabb86f3b97d3fbdf0a748c65cd0bfa7f 100644 --- a/src/mol-plugin/spec.ts +++ b/src/mol-plugin/spec.ts @@ -4,37 +4,38 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { StateAction } from 'mol-state/action'; -import { Transformer } from 'mol-state'; +import { StateTransformer, StateAction } from 'mol-state'; import { StateTransformParameters } from './ui/state/common'; import { PluginLayoutStateProps } from './layout'; +import { PluginStateAnimation } from './state/animation/model'; export { PluginSpec } interface PluginSpec { actions: PluginSpec.Action[], behaviors: PluginSpec.Behavior[], - customParamEditors?: [StateAction | Transformer, StateTransformParameters.Class][] + animations?: PluginStateAnimation[], + customParamEditors?: [StateAction | StateTransformer, StateTransformParameters.Class][], initialLayout?: PluginLayoutStateProps } namespace PluginSpec { export interface Action { - action: StateAction | Transformer, + action: StateAction | StateTransformer, customControl?: StateTransformParameters.Class, autoUpdate?: boolean } - export function Action(action: StateAction | Transformer, params?: { customControl?: StateTransformParameters.Class, autoUpdate?: boolean }): Action { + export function Action(action: StateAction | StateTransformer, params?: { customControl?: StateTransformParameters.Class, autoUpdate?: boolean }): Action { return { action, customControl: params && params.customControl, autoUpdate: params && params.autoUpdate }; } export interface Behavior { - transformer: Transformer, + transformer: StateTransformer, defaultParams?: any } - export function Behavior<T extends Transformer>(transformer: T, defaultParams?: Transformer.Params<T>): Behavior { + export function Behavior<T extends StateTransformer>(transformer: T, defaultParams?: StateTransformer.Params<T>): Behavior { return { transformer, defaultParams }; } } \ No newline at end of file diff --git a/src/mol-plugin/state.ts b/src/mol-plugin/state.ts index f94af6388e24c853c4f9ce7e2a39e0a45bd10d90..9c5e4c13bb433290fbd7b215c558612eda44224e 100644 --- a/src/mol-plugin/state.ts +++ b/src/mol-plugin/state.ts @@ -13,6 +13,7 @@ import { PluginStateSnapshotManager } from './state/snapshots'; import { RxEventHelper } from 'mol-util/rx-event-helper'; import { Canvas3DProps } from 'mol-canvas3d/canvas3d'; import { PluginCommands } from './command'; +import { PluginAnimationManager } from './state/animation/manager'; export { PluginState } class PluginState { @@ -20,8 +21,8 @@ class PluginState { readonly dataState: State; readonly behaviorState: State; + readonly animation: PluginAnimationManager; readonly cameraSnapshots = new CameraSnapshotManager(); - readonly snapshots = new PluginStateSnapshotManager(); readonly behavior = { @@ -43,6 +44,7 @@ class PluginState { return { data: this.dataState.getSnapshot(), behaviour: this.behaviorState.getSnapshot(), + animation: this.animation.getSnapshot(), cameraSnapshots: this.cameraSnapshots.getStateSnapshot(), canvas3d: { camera: this.plugin.canvas3d.camera.getSnapshot(), @@ -60,6 +62,9 @@ class PluginState { if (snapshot.canvas3d.camera) this.plugin.canvas3d.camera.setState(snapshot.canvas3d.camera); } this.plugin.canvas3d.requestDraw(true); + if (snapshot.animation) { + this.animation.setSnapshot(snapshot.animation); + } } dispose() { @@ -67,11 +72,12 @@ class PluginState { this.dataState.dispose(); this.behaviorState.dispose(); this.cameraSnapshots.dispose(); + this.animation.dispose(); } constructor(private plugin: import('./context').PluginContext) { this.dataState = State.create(new SO.Root({ }), { globalContext: plugin }); - this.behaviorState = State.create(new PluginBehavior.Root({ }), { globalContext: plugin }); + this.behaviorState = State.create(new PluginBehavior.Root({ }), { globalContext: plugin, rootProps: { isLocked: true } }); this.dataState.behaviors.currentObject.subscribe(o => { if (this.behavior.kind.value === 'data') this.behavior.currentObject.next(o); @@ -81,6 +87,8 @@ class PluginState { }); this.behavior.currentObject.next(this.dataState.behaviors.currentObject.value); + + this.animation = new PluginAnimationManager(plugin); } } @@ -90,6 +98,7 @@ namespace PluginState { export interface Snapshot { data?: State.Snapshot, behaviour?: State.Snapshot, + animation?: PluginAnimationManager.Snapshot, cameraSnapshots?: CameraSnapshotManager.StateSnapshot, canvas3d?: { camera?: Camera.Snapshot, diff --git a/src/mol-plugin/state/actions.ts b/src/mol-plugin/state/actions.ts new file mode 100644 index 0000000000000000000000000000000000000000..c5b1fb6a04dfc810abadefeab19d3ef266a90826 --- /dev/null +++ b/src/mol-plugin/state/actions.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import * as Structure from './actions/structure' +import * as Volume from './actions/volume' + +export const StateActions = { + Structure, + Volume +} \ No newline at end of file diff --git a/src/mol-plugin/state/actions/basic.ts b/src/mol-plugin/state/actions/basic.ts deleted file mode 100644 index db954502b1e46603f0f8a64fc22ca81aa598c42b..0000000000000000000000000000000000000000 --- a/src/mol-plugin/state/actions/basic.ts +++ /dev/null @@ -1,391 +0,0 @@ -/** - * Copyright (c) 2018-2019 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 { PluginContext } from 'mol-plugin/context'; -import { StateTree, Transformer, StateObject } from 'mol-state'; -import { StateAction } from 'mol-state/action'; -import { StateSelection } from 'mol-state/state/selection'; -import { StateTreeBuilder } from 'mol-state/tree/builder'; -import { ParamDefinition as PD } from 'mol-util/param-definition'; -import { PluginStateObject } from '../objects'; -import { StateTransforms } from '../transforms'; -import { Download } from '../transforms/data'; -import { StructureRepresentation3DHelpers } from '../transforms/representation'; -import { getFileInfo, FileInfo } from 'mol-util/file-info'; -import { Task } from 'mol-task'; - -// TODO: "structure/volume parser provider" - -export { DownloadStructure }; -type DownloadStructure = typeof DownloadStructure -const DownloadStructure = StateAction.build({ - from: PluginStateObject.Root, - display: { name: 'Download Structure', description: 'Load a structure from the provided source and create its default Assembly and visual.' }, - params: { - source: PD.MappedStatic('bcif-static', { - 'pdbe-updated': PD.Group({ - id: PD.Text('1cbs', { label: 'Id' }), - supportProps: PD.Boolean(false) - }, { isFlat: true }), - 'rcsb': PD.Group({ - id: PD.Text('1tqn', { label: 'Id' }), - supportProps: PD.Boolean(false) - }, { isFlat: true }), - 'bcif-static': PD.Group({ - id: PD.Text('1tqn', { label: 'Id' }), - supportProps: PD.Boolean(false) - }, { isFlat: true }), - 'url': PD.Group({ - url: PD.Text(''), - format: PD.Select('cif', [['cif', 'CIF'], ['pdb', 'PDB']]), - isBinary: PD.Boolean(false), - supportProps: PD.Boolean(false) - }, { isFlat: true }) - }, { - options: [ - ['pdbe-updated', 'PDBe Updated'], - ['rcsb', 'RCSB'], - ['bcif-static', 'BinaryCIF (static PDBe Updated)'], - ['url', 'URL'] - ] - }) - } -})(({ params, state }, ctx: PluginContext) => { - const b = state.build(); - const src = params.source; - let downloadParams: Transformer.Params<Download>; - - switch (src.name) { - case 'url': - downloadParams = { url: src.params.url, isBinary: src.params.isBinary }; - break; - case 'pdbe-updated': - downloadParams = { url: `https://www.ebi.ac.uk/pdbe/static/entry/${src.params.id.toLowerCase()}_updated.cif`, isBinary: false, label: `PDBe: ${src.params.id}` }; - break; - case 'rcsb': - downloadParams = { url: `https://files.rcsb.org/download/${src.params.id.toUpperCase()}.cif`, isBinary: false, label: `RCSB: ${src.params.id}` }; - break; - case 'bcif-static': - downloadParams = { url: `https://webchem.ncbr.muni.cz/ModelServer/static/bcif/${src.params.id.toLowerCase()}`, isBinary: true, label: `BinaryCIF: ${src.params.id}` }; - break; - default: throw new Error(`${(src as any).name} not supported.`); - } - - const data = b.toRoot().apply(StateTransforms.Data.Download, downloadParams); - const traj = createModelTree(data, src.name === 'url' ? src.params.format : 'cif'); - return state.updateTree(createStructureTree(ctx, traj, params.source.params.supportProps)); -}); - -export const OpenStructure = StateAction.build({ - display: { name: 'Open Structure', description: 'Load a structure from file and create its default Assembly and visual' }, - from: PluginStateObject.Root, - params: { file: PD.File({ accept: '.cif,.bcif' }) } -})(({ params, state }, ctx: PluginContext) => { - const b = state.build(); - const data = b.toRoot().apply(StateTransforms.Data.ReadFile, { file: params.file, isBinary: /\.bcif$/i.test(params.file.name) }); - const traj = createModelTree(data, 'cif'); - return state.updateTree(createStructureTree(ctx, traj, false)); -}); - -function createModelTree(b: StateTreeBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, format: 'pdb' | 'cif' = 'cif') { - const parsed = format === 'cif' - ? b.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif) - : b.apply(StateTransforms.Model.TrajectoryFromPDB); - - return parsed.apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 }); -} - -function createStructureTree(ctx: PluginContext, b: StateTreeBuilder.To<PluginStateObject.Molecule.Model>, supportProps: boolean): StateTree { - let root = b; - if (supportProps) { - root = root.apply(StateTransforms.Model.CustomModelProperties); - } - const structure = root.apply(StateTransforms.Model.StructureAssemblyFromModel); - complexRepresentation(ctx, structure); - - return root.getTree(); -} - -function complexRepresentation(ctx: PluginContext, root: StateTreeBuilder.To<PluginStateObject.Molecule.Structure>) { - root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' }) - .apply(StateTransforms.Representation.StructureRepresentation3D, - StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'cartoon')); - root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' }) - .apply(StateTransforms.Representation.StructureRepresentation3D, - StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick')); - root.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' }) - .apply(StateTransforms.Representation.StructureRepresentation3D, - StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick', { alpha: 0.51 })); - root.apply(StateTransforms.Model.StructureComplexElement, { type: 'spheres' }) - .apply(StateTransforms.Representation.StructureRepresentation3D, - StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'spacefill')); -} - -export const CreateComplexRepresentation = StateAction.build({ - display: { name: 'Create Complex', description: 'Split the structure into Sequence/Water/Ligands/... ' }, - from: PluginStateObject.Molecule.Structure -})(({ ref, state }, ctx: PluginContext) => { - const root = state.build().to(ref); - complexRepresentation(ctx, root); - return state.updateTree(root.getTree()); -}); - -export const UpdateTrajectory = StateAction.build({ - display: { name: 'Update Trajectory' }, - params: { - action: PD.Select<'advance' | 'reset'>('advance', [['advance', 'Advance'], ['reset', 'Reset']]), - by: PD.makeOptional(PD.Numeric(1, { min: -1, max: 1, step: 1 })) - } -})(({ params, state }) => { - const models = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Model) - .filter(c => c.transform.transformer === StateTransforms.Model.ModelFromTrajectory)); - - const update = state.build(); - - if (params.action === 'reset') { - for (const m of models) { - update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory, - () => ({ modelIndex: 0 })); - } - } else { - for (const m of models) { - const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, [PluginStateObject.Molecule.Trajectory]); - if (!parent || !parent.obj) continue; - const traj = parent.obj as PluginStateObject.Molecule.Trajectory; - update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory, - old => { - let modelIndex = (old.modelIndex + params.by!) % traj.data.length; - if (modelIndex < 0) modelIndex += traj.data.length; - return { modelIndex }; - }); - } - } - - return state.updateTree(update); -}); - -// - -export class DataFormatRegistry<D extends PluginStateObject.Data.Binary | PluginStateObject.Data.String, M extends StateObject> { - private _list: { name: string, provider: DataFormatProvider<D, M> }[] = [] - private _map = new Map<string, DataFormatProvider<D, M>>() - - get default() { return this._list[0]; } - get types(): [string, string][] { - return this._list.map(e => [e.name, e.provider.label] as [string, string]); - } - - constructor() { - this.add('ccp4', Ccp4Provider) - this.add('dsn6', Dsn6Provider) - this.add('dscif', DscifProvider) - }; - - add(name: string, provider: DataFormatProvider<D, M>) { - this._list.push({ name, provider }) - this._map.set(name, provider) - } - - remove(name: string) { - this._list.splice(this._list.findIndex(e => e.name === name), 1) - this._map.delete(name) - } - - auto(info: FileInfo, dataStateObject: D) { - for (let i = 0, il = this.list.length; i < il; ++i) { - const { provider } = this._list[i] - if (provider.isApplicable(info, dataStateObject.data)) return provider - } - throw new Error('no compatible data format provider available') - } - - get(name: string): DataFormatProvider<D, M> { - if (this._map.has(name)) { - return this._map.get(name)! - } else { - throw new Error(`unknown data format name '${name}'`) - } - } - - get list() { - return this._list - } -} - -interface DataFormatProvider<D extends PluginStateObject.Data.Binary | PluginStateObject.Data.String, M extends StateObject> { - label: string - description: string - fileExtensions: string[] - isApplicable(info: FileInfo, data: string | Uint8Array): boolean - getDefaultBuilder(b: StateTreeBuilder.To<D>): StateTreeBuilder.To<M> -} - -const Ccp4Provider: DataFormatProvider<any, any> = { - label: 'CCP4/MRC/BRIX', - description: 'CCP4/MRC/BRIX', - fileExtensions: ['ccp4', 'mrc', 'map'], - isApplicable: (info: FileInfo, data: Uint8Array) => { - return info.ext === 'ccp4' || info.ext === 'mrc' || info.ext === 'map' - }, - getDefaultBuilder: (b: StateTreeBuilder.To<PluginStateObject.Data.Binary>) => { - return b.apply(StateTransforms.Data.ParseCcp4) - .apply(StateTransforms.Model.VolumeFromCcp4) - .apply(StateTransforms.Representation.VolumeRepresentation3D) - } -} - -const Dsn6Provider: DataFormatProvider<any, any> = { - label: 'DSN6/BRIX', - description: 'DSN6/BRIX', - fileExtensions: ['dsn6', 'brix'], - isApplicable: (info: FileInfo, data: Uint8Array) => { - return info.ext === 'dsn6' || info.ext === 'brix' - }, - getDefaultBuilder: (b: StateTreeBuilder.To<PluginStateObject.Data.Binary>) => { - return b.apply(StateTransforms.Data.ParseDsn6) - .apply(StateTransforms.Model.VolumeFromDsn6) - .apply(StateTransforms.Representation.VolumeRepresentation3D) - } -} - -const DscifProvider: DataFormatProvider<any, any> = { - label: 'DensityServer CIF', - description: 'DensityServer CIF', - fileExtensions: ['cif'], - isApplicable: (info: FileInfo, data: Uint8Array) => { - return info.ext === 'cif' - }, - getDefaultBuilder: (b: StateTreeBuilder.To<PluginStateObject.Data.Binary>) => { - return b.apply(StateTransforms.Data.ParseCif, { }) - .apply(StateTransforms.Model.VolumeFromDensityServerCif) - .apply(StateTransforms.Representation.VolumeRepresentation3D) - } -} - -// - -function getDataFormatExtensionsOptions(dataFormatRegistry: DataFormatRegistry<any, any>) { - const extensions: string[] = [] - const options: [string, string][] = [['auto', 'Automatic']] - dataFormatRegistry.list.forEach(({ name, provider }) => { - extensions.push(...provider.fileExtensions) - options.push([ name, provider.label ]) - }) - return { extensions, options } -} - -export const OpenVolume = StateAction.build({ - display: { name: 'Open Volume', description: 'Load a volume from file and create its default visual' }, - from: PluginStateObject.Root, - params: (a, ctx: PluginContext) => { - const { extensions, options } = getDataFormatExtensionsOptions(ctx.dataFormat.registry) - return { - file: PD.File({ accept: extensions.map(e => `.${e}`).join(',')}), - format: PD.Select('auto', options), - isBinary: PD.Boolean(true), // TOOD should take selected format into account - } - } -})(({ params, state }, ctx: PluginContext) => Task.create('Open Volume', async taskCtx => { - const data = state.build().toRoot().apply(StateTransforms.Data.ReadFile, { file: params.file, isBinary: params.isBinary }); - const dataStateObject = await state.updateTree(data).runInContext(taskCtx); - - // Alternative for more complex states where the builder is not a simple StateTreeBuilder.To<>: - /* - const dataRef = dataTree.ref; - await state.updateTree(dataTree).runInContext(taskCtx); - const dataCell = state.select(dataRef)[0]; - */ - - const provider = params.format === 'auto' ? ctx.dataFormat.registry.auto(getFileInfo(params.file), dataStateObject) : ctx.dataFormat.registry.get(params.format) - const b = state.build().to(data.ref); - const tree = provider.getDefaultBuilder(b).getTree() - // need to await the 2nd update the so that the enclosing Task finishes after the update is done. - await state.updateTree(tree).runInContext(taskCtx); -})); - -export { DownloadDensity }; -type DownloadDensity = typeof DownloadDensity -const DownloadDensity = StateAction.build({ - from: PluginStateObject.Root, - display: { name: 'Download Density', description: 'Load a density from the provided source and create its default visual.' }, - params: (a, ctx: PluginContext) => { - const { options } = getDataFormatExtensionsOptions(ctx.dataFormat.registry) - return { - source: PD.MappedStatic('rcsb', { - 'pdbe': PD.Group({ - id: PD.Text('1tqn', { label: 'Id' }), - type: PD.Select('2fofc', [['2fofc', '2Fo-Fc'], ['fofc', 'Fo-Fc']]), - }, { isFlat: true }), - 'rcsb': PD.Group({ - id: PD.Text('1tqn', { label: 'Id' }), - type: PD.Select('2fofc', [['2fofc', '2Fo-Fc'], ['fofc', 'Fo-Fc']]), - }, { isFlat: true }), - 'url': PD.Group({ - url: PD.Text(''), - isBinary: PD.Boolean(false), - format: PD.Select('auto', options), - }, { isFlat: true }) - }, { - options: [ - ['pdbe', 'PDBe X-ray maps'], - ['rcsb', 'RCSB X-ray maps'], - ['url', 'URL'] - ] - }) - } - } -})(({ params, state }, ctx: PluginContext) => Task.create('Download Density', async taskCtx => { - const src = params.source; - let downloadParams: Transformer.Params<Download>; - let provider: DataFormatProvider<any, any> - - switch (src.name) { - case 'url': - downloadParams = src.params; - break; - case 'pdbe': - downloadParams = { - url: src.params.type === '2fofc' - ? `http://www.ebi.ac.uk/pdbe/coordinates/files/${src.params.id.toLowerCase()}.ccp4` - : `http://www.ebi.ac.uk/pdbe/coordinates/files/${src.params.id.toLowerCase()}_diff.ccp4`, - isBinary: true, - label: `PDBe X-ray map: ${src.params.id}` - }; - break; - case 'rcsb': - downloadParams = { - url: src.params.type === '2fofc' - ? `https://edmaps.rcsb.org/maps/${src.params.id.toLowerCase()}_2fofc.dsn6` - : `https://edmaps.rcsb.org/maps/${src.params.id.toLowerCase()}_fofc.dsn6`, - isBinary: true, - label: `RCSB X-ray map: ${src.params.id}` - }; - break; - default: throw new Error(`${(src as any).name} not supported.`); - } - - const data = state.build().toRoot().apply(StateTransforms.Data.Download, downloadParams); - const dataStateObject = await state.updateTree(data).runInContext(taskCtx); - - switch (src.name) { - case 'url': - downloadParams = src.params; - provider = src.params.format === 'auto' ? ctx.dataFormat.registry.auto(getFileInfo(downloadParams.url), dataStateObject) : ctx.dataFormat.registry.get(src.params.format) - break; - case 'pdbe': - provider = ctx.dataFormat.registry.get('ccp4') - break; - case 'rcsb': - provider = ctx.dataFormat.registry.get('dsn6') - break; - default: throw new Error(`${(src as any).name} not supported.`); - } - - const b = state.build().to(data.ref); - const tree = provider.getDefaultBuilder(b).getTree() - await state.updateTree(tree).runInContext(taskCtx); -})); \ No newline at end of file diff --git a/src/mol-plugin/state/actions/structure.ts b/src/mol-plugin/state/actions/structure.ts new file mode 100644 index 0000000000000000000000000000000000000000..b96e2d60fe0f0435c9079d7048d514f5f754a32b --- /dev/null +++ b/src/mol-plugin/state/actions/structure.ts @@ -0,0 +1,180 @@ +/** + * Copyright (c) 2018-2019 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 { PluginContext } from 'mol-plugin/context'; +import { StateAction, StateBuilder, StateSelection, StateTransformer } from 'mol-state'; +import { ParamDefinition as PD } from 'mol-util/param-definition'; +import { PluginStateObject } from '../objects'; +import { StateTransforms } from '../transforms'; +import { Download } from '../transforms/data'; +import { StructureRepresentation3DHelpers } from '../transforms/representation'; +import { CustomModelProperties } from '../transforms/model'; + +// TODO: "structure/volume parser provider" + +export { DownloadStructure }; +type DownloadStructure = typeof DownloadStructure +const DownloadStructure = StateAction.build({ + from: PluginStateObject.Root, + display: { name: 'Download Structure', description: 'Load a structure from the provided source and create its default Assembly and visual.' }, + params: { + source: PD.MappedStatic('bcif-static', { + 'pdbe-updated': PD.Group({ + id: PD.Text('1cbs', { label: 'Id' }), + supportProps: PD.Boolean(false) + }, { isFlat: true }), + 'rcsb': PD.Group({ + id: PD.Text('1tqn', { label: 'Id' }), + supportProps: PD.Boolean(false) + }, { isFlat: true }), + 'bcif-static': PD.Group({ + id: PD.Text('1tqn', { label: 'Id' }), + supportProps: PD.Boolean(false) + }, { isFlat: true }), + 'url': PD.Group({ + url: PD.Text(''), + format: PD.Select('cif', [['cif', 'CIF'], ['pdb', 'PDB']]), + isBinary: PD.Boolean(false), + supportProps: PD.Boolean(false) + }, { isFlat: true }) + }, { + options: [ + ['pdbe-updated', 'PDBe Updated'], + ['rcsb', 'RCSB'], + ['bcif-static', 'BinaryCIF (static PDBe Updated)'], + ['url', 'URL'] + ] + }) + } +})(({ params, state }, ctx: PluginContext) => { + const b = state.build(); + const src = params.source; + let downloadParams: StateTransformer.Params<Download>; + + switch (src.name) { + case 'url': + downloadParams = { url: src.params.url, isBinary: src.params.isBinary }; + break; + case 'pdbe-updated': + downloadParams = { url: `https://www.ebi.ac.uk/pdbe/static/entry/${src.params.id.toLowerCase()}_updated.cif`, isBinary: false, label: `PDBe: ${src.params.id}` }; + break; + case 'rcsb': + downloadParams = { url: `https://files.rcsb.org/download/${src.params.id.toUpperCase()}.cif`, isBinary: false, label: `RCSB: ${src.params.id}` }; + break; + case 'bcif-static': + downloadParams = { url: `https://webchem.ncbr.muni.cz/ModelServer/static/bcif/${src.params.id.toLowerCase()}`, isBinary: true, label: `BinaryCIF: ${src.params.id}` }; + break; + default: throw new Error(`${(src as any).name} not supported.`); + } + + const data = b.toRoot().apply(StateTransforms.Data.Download, downloadParams, { props: { isGhost: true }}); + const traj = createModelTree(data, src.name === 'url' ? src.params.format : 'cif'); + return state.updateTree(createStructureTree(ctx, traj, params.source.params.supportProps)); +}); + +export const OpenStructure = StateAction.build({ + display: { name: 'Open Structure', description: 'Load a structure from file and create its default Assembly and visual' }, + from: PluginStateObject.Root, + params: { file: PD.File({ accept: '.cif,.bcif' }) } +})(({ params, state }, ctx: PluginContext) => { + const b = state.build(); + const data = b.toRoot().apply(StateTransforms.Data.ReadFile, { file: params.file, isBinary: /\.bcif$/i.test(params.file.name) }); + const traj = createModelTree(data, 'cif'); + return state.updateTree(createStructureTree(ctx, traj, false)); +}); + +function createModelTree(b: StateBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, format: 'pdb' | 'cif' = 'cif') { + const parsed = format === 'cif' + ? b.apply(StateTransforms.Data.ParseCif, void 0, { props: { isGhost: true }}).apply(StateTransforms.Model.TrajectoryFromMmCif, void 0, { props: { isGhost: true }}) + : b.apply(StateTransforms.Model.TrajectoryFromPDB, void 0, { props: { isGhost: true }}); + + return parsed.apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 }); +} + +function createStructureTree(ctx: PluginContext, b: StateBuilder.To<PluginStateObject.Molecule.Model>, supportProps: boolean) { + let root = b; + if (supportProps) { + root = root.apply(StateTransforms.Model.CustomModelProperties); + } + const structure = root.apply(StateTransforms.Model.StructureAssemblyFromModel); + complexRepresentation(ctx, structure); + + return root; +} + +function complexRepresentation(ctx: PluginContext, root: StateBuilder.To<PluginStateObject.Molecule.Structure>) { + root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' }) + .apply(StateTransforms.Representation.StructureRepresentation3D, + StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'cartoon')); + root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' }) + .apply(StateTransforms.Representation.StructureRepresentation3D, + StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick')); + root.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' }) + .apply(StateTransforms.Representation.StructureRepresentation3D, + StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'ball-and-stick', { alpha: 0.51 })); + root.apply(StateTransforms.Model.StructureComplexElement, { type: 'spheres' }) + .apply(StateTransforms.Representation.StructureRepresentation3D, + StructureRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'spacefill')); +} + +export const CreateComplexRepresentation = StateAction.build({ + display: { name: 'Create Complex', description: 'Split the structure into Sequence/Water/Ligands/... ' }, + from: PluginStateObject.Molecule.Structure +})(({ ref, state }, ctx: PluginContext) => { + const root = state.build().to(ref); + complexRepresentation(ctx, root); + return state.updateTree(root); +}); + +export const UpdateTrajectory = StateAction.build({ + display: { name: 'Update Trajectory' }, + params: { + action: PD.Select<'advance' | 'reset'>('advance', [['advance', 'Advance'], ['reset', 'Reset']]), + by: PD.makeOptional(PD.Numeric(1, { min: -1, max: 1, step: 1 })) + } +})(({ params, state }) => { + const models = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Model) + .filter(c => c.transform.transformer === StateTransforms.Model.ModelFromTrajectory)); + + const update = state.build(); + + if (params.action === 'reset') { + for (const m of models) { + update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory, + () => ({ modelIndex: 0 })); + } + } else { + for (const m of models) { + const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, [PluginStateObject.Molecule.Trajectory]); + if (!parent || !parent.obj) continue; + const traj = parent.obj as PluginStateObject.Molecule.Trajectory; + update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory, + old => { + let modelIndex = (old.modelIndex + params.by!) % traj.data.length; + if (modelIndex < 0) modelIndex += traj.data.length; + return { modelIndex }; + }); + } + } + + return state.updateTree(update); +}); + +export const EnableModelCustomProps = StateAction.build({ + display: { name: 'Custom Properties', description: 'Enable the addition of custom properties to the model.' }, + from: PluginStateObject.Molecule.Model, + params(a, ctx: PluginContext) { + if (!a) return { properties: PD.MultiSelect([], [], { description: 'A list of property descriptor ids.' }) }; + return { properties: ctx.customModelProperties.getSelect(a.data) }; + }, + isApplicable(a, t, ctx: PluginContext) { + return t.transformer !== CustomModelProperties; + } +})(({ ref, params, state }, ctx: PluginContext) => { + const root = state.build().to(ref).insert(CustomModelProperties, params); + return state.updateTree(root); +}); diff --git a/src/mol-plugin/state/actions/volume.ts b/src/mol-plugin/state/actions/volume.ts new file mode 100644 index 0000000000000000000000000000000000000000..c8bbc9c892081968e785ebec860dd76516eae3f1 --- /dev/null +++ b/src/mol-plugin/state/actions/volume.ts @@ -0,0 +1,314 @@ +/** + * Copyright (c) 2018-2019 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 { VolumeIsoValue } from 'mol-model/volume'; +import { PluginContext } from 'mol-plugin/context'; +import { State, StateAction, StateBuilder, StateObject, StateTransformer } from 'mol-state'; +import { Task } from 'mol-task'; +import { ColorNames } from 'mol-util/color/tables'; +import { FileInfo, getFileInfo } from 'mol-util/file-info'; +import { ParamDefinition as PD } from 'mol-util/param-definition'; +import { PluginStateObject } from '../objects'; +import { StateTransforms } from '../transforms'; +import { Download } from '../transforms/data'; +import { VolumeRepresentation3DHelpers } from '../transforms/representation'; +import { VolumeStreaming } from 'mol-plugin/behavior/dynamic/volume'; + +export class DataFormatRegistry<D extends PluginStateObject.Data.Binary | PluginStateObject.Data.String, M extends StateObject> { + private _list: { name: string, provider: DataFormatProvider<D> }[] = [] + private _map = new Map<string, DataFormatProvider<D>>() + + get default() { return this._list[0]; } + get types(): [string, string][] { + return this._list.map(e => [e.name, e.provider.label] as [string, string]); + } + + constructor() { + this.add('ccp4', Ccp4Provider) + this.add('dsn6', Dsn6Provider) + this.add('dscif', DscifProvider) + }; + + add(name: string, provider: DataFormatProvider<D>) { + this._list.push({ name, provider }) + this._map.set(name, provider) + } + + remove(name: string) { + this._list.splice(this._list.findIndex(e => e.name === name), 1) + this._map.delete(name) + } + + auto(info: FileInfo, dataStateObject: D) { + for (let i = 0, il = this.list.length; i < il; ++i) { + const { provider } = this._list[i] + if (provider.isApplicable(info, dataStateObject.data)) return provider + } + throw new Error('no compatible data format provider available') + } + + get(name: string): DataFormatProvider<D> { + if (this._map.has(name)) { + return this._map.get(name)! + } else { + throw new Error(`unknown data format name '${name}'`) + } + } + + get list() { + return this._list + } +} + +interface DataFormatProvider<D extends PluginStateObject.Data.Binary | PluginStateObject.Data.String> { + label: string + description: string + fileExtensions: string[] + isApplicable(info: FileInfo, data: string | Uint8Array): boolean + getDefaultBuilder(ctx: PluginContext, data: StateBuilder.To<D>, state?: State): Task<void> +} + +const Ccp4Provider: DataFormatProvider<any> = { + label: 'CCP4/MRC/BRIX', + description: 'CCP4/MRC/BRIX', + fileExtensions: ['ccp4', 'mrc', 'map'], + isApplicable: (info: FileInfo, data: Uint8Array) => { + return info.ext === 'ccp4' || info.ext === 'mrc' || info.ext === 'map' + }, + getDefaultBuilder: (ctx: PluginContext, data: StateBuilder.To<PluginStateObject.Data.Binary>, state: State) => { + return Task.create('CCP4/MRC/BRIX default builder', async taskCtx => { + const tree = data.apply(StateTransforms.Data.ParseCcp4) + .apply(StateTransforms.Volume.VolumeFromCcp4) + .apply(StateTransforms.Representation.VolumeRepresentation3D) + await state.updateTree(tree).runInContext(taskCtx) + }) + } +} + +const Dsn6Provider: DataFormatProvider<any> = { + label: 'DSN6/BRIX', + description: 'DSN6/BRIX', + fileExtensions: ['dsn6', 'brix'], + isApplicable: (info: FileInfo, data: Uint8Array) => { + return info.ext === 'dsn6' || info.ext === 'brix' + }, + getDefaultBuilder: (ctx: PluginContext, data: StateBuilder.To<PluginStateObject.Data.Binary>, state: State) => { + return Task.create('DSN6/BRIX default builder', async taskCtx => { + const tree = data.apply(StateTransforms.Data.ParseDsn6) + .apply(StateTransforms.Volume.VolumeFromDsn6) + .apply(StateTransforms.Representation.VolumeRepresentation3D) + await state.updateTree(tree).runInContext(taskCtx) + }) + } +} + +const DscifProvider: DataFormatProvider<any> = { + label: 'DensityServer CIF', + description: 'DensityServer CIF', + fileExtensions: ['cif'], + isApplicable: (info: FileInfo, data: Uint8Array) => { + return info.ext === 'cif' + }, + getDefaultBuilder: (ctx: PluginContext, data: StateBuilder.To<PluginStateObject.Data.Binary>, state: State) => { + return Task.create('DensityServer CIF default builder', async taskCtx => { + const cifBuilder = data.apply(StateTransforms.Data.ParseCif) + const cifStateObject = await state.updateTree(cifBuilder).runInContext(taskCtx) + const b = state.build().to(cifBuilder.ref); + const blocks = cifStateObject.data.blocks.slice(1); // zero block contains query meta-data + let tree: StateBuilder.To<any> + if (blocks.length === 1) { + tree = b + .apply(StateTransforms.Volume.VolumeFromDensityServerCif, { blockHeader: blocks[0].header }) + .apply(StateTransforms.Representation.VolumeRepresentation3D, VolumeRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'isosurface', { isoValue: VolumeIsoValue.relative(1.5), alpha: 0.3 }, 'uniform', { value: ColorNames.teal })) + } else if (blocks.length === 2) { + tree = b + .apply(StateTransforms.Volume.VolumeFromDensityServerCif, { blockHeader: blocks[0].header }) + .apply(StateTransforms.Representation.VolumeRepresentation3D, VolumeRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'isosurface', { isoValue: VolumeIsoValue.relative(1.5), alpha: 0.3 }, 'uniform', { value: ColorNames.blue })) + const vol = tree.to(cifBuilder.ref) + .apply(StateTransforms.Volume.VolumeFromDensityServerCif, { blockHeader: blocks[1].header }) + const posParams = VolumeRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'isosurface', { isoValue: VolumeIsoValue.relative(3), alpha: 0.3 }, 'uniform', { value: ColorNames.green }) + tree = vol.apply(StateTransforms.Representation.VolumeRepresentation3D, posParams) + const negParams = VolumeRepresentation3DHelpers.getDefaultParamsStatic(ctx, 'isosurface', { isoValue: VolumeIsoValue.relative(-3), alpha: 0.3 }, 'uniform', { value: ColorNames.red }) + tree = tree.to(vol.ref).apply(StateTransforms.Representation.VolumeRepresentation3D, negParams) + } else { + throw new Error('unknown number of blocks') + } + + await state.updateTree(tree).runInContext(taskCtx); + }) + } +} + +// + +function getDataFormatExtensionsOptions(dataFormatRegistry: DataFormatRegistry<any, any>) { + const extensions: string[] = [] + const options: [string, string][] = [['auto', 'Automatic']] + dataFormatRegistry.list.forEach(({ name, provider }) => { + extensions.push(...provider.fileExtensions) + options.push([ name, provider.label ]) + }) + return { extensions, options } +} + +export const OpenVolume = StateAction.build({ + display: { name: 'Open Volume', description: 'Load a volume from file and create its default visual' }, + from: PluginStateObject.Root, + params: (a, ctx: PluginContext) => { + const { extensions, options } = getDataFormatExtensionsOptions(ctx.dataFormat.registry) + return { + file: PD.File({ accept: extensions.map(e => `.${e}`).join(',')}), + format: PD.Select('auto', options), + isBinary: PD.Boolean(true), // TOOD should take selected format into account + } + } +})(({ params, state }, ctx: PluginContext) => Task.create('Open Volume', async taskCtx => { + const data = state.build().toRoot().apply(StateTransforms.Data.ReadFile, { file: params.file, isBinary: params.isBinary }); + const dataStateObject = await state.updateTree(data).runInContext(taskCtx); + + // Alternative for more complex states where the builder is not a simple StateBuilder.To<>: + /* + const dataRef = dataTree.ref; + await state.updateTree(dataTree).runInContext(taskCtx); + const dataCell = state.select(dataRef)[0]; + */ + + const provider = params.format === 'auto' ? ctx.dataFormat.registry.auto(getFileInfo(params.file), dataStateObject) : ctx.dataFormat.registry.get(params.format) + const b = state.build().to(data.ref); + // need to await the 2nd update the so that the enclosing Task finishes after the update is done. + await provider.getDefaultBuilder(ctx, b, state).runInContext(taskCtx) +})); + +export { DownloadDensity }; +type DownloadDensity = typeof DownloadDensity +const DownloadDensity = StateAction.build({ + from: PluginStateObject.Root, + display: { name: 'Download Density', description: 'Load a density from the provided source and create its default visual.' }, + params: (a, ctx: PluginContext) => { + const { options } = getDataFormatExtensionsOptions(ctx.dataFormat.registry) + return { + source: PD.MappedStatic('rcsb', { + 'pdbe': PD.Group({ + id: PD.Text('1tqn', { label: 'Id' }), + type: PD.Select('2fofc', [['2fofc', '2Fo-Fc'], ['fofc', 'Fo-Fc']]), + }, { isFlat: true }), + 'pdbe-emd-ds': PD.Group({ + id: PD.Text('emd-8004', { label: 'Id' }), + detail: PD.Numeric(3, { min: 0, max: 10, step: 1 }, { label: 'Detail' }), + }, { isFlat: true }), + 'pdbe-xray-ds': PD.Group({ + id: PD.Text('1tqn', { label: 'Id' }), + detail: PD.Numeric(3, { min: 0, max: 10, step: 1 }, { label: 'Detail' }), + }, { isFlat: true }), + 'rcsb': PD.Group({ + id: PD.Text('1tqn', { label: 'Id' }), + type: PD.Select('2fofc', [['2fofc', '2Fo-Fc'], ['fofc', 'Fo-Fc']]), + }, { isFlat: true }), + 'url': PD.Group({ + url: PD.Text(''), + isBinary: PD.Boolean(false), + format: PD.Select('auto', options), + }, { isFlat: true }) + }, { + options: [ + ['pdbe', 'PDBe X-ray maps'], + ['pdbe-emd-ds', 'PDBe EMD Density Server'], + ['pdbe-xray-ds', 'PDBe X-ray Density Server'], + ['rcsb', 'RCSB X-ray maps'], + ['url', 'URL'] + ] + }) + } + } +})(({ params, state }, ctx: PluginContext) => Task.create('Download Density', async taskCtx => { + const src = params.source; + let downloadParams: StateTransformer.Params<Download>; + let provider: DataFormatProvider<any> + + switch (src.name) { + case 'url': + downloadParams = src.params; + break; + case 'pdbe': + downloadParams = { + url: src.params.type === '2fofc' + ? `http://www.ebi.ac.uk/pdbe/coordinates/files/${src.params.id.toLowerCase()}.ccp4` + : `http://www.ebi.ac.uk/pdbe/coordinates/files/${src.params.id.toLowerCase()}_diff.ccp4`, + isBinary: true, + label: `PDBe X-ray map: ${src.params.id}` + }; + break; + case 'pdbe-emd-ds': + downloadParams = { + url: `https://www.ebi.ac.uk/pdbe/densities/emd/${src.params.id.toLowerCase()}/cell?detail=${src.params.detail}`, + isBinary: true, + label: `PDBe EMD Density Server: ${src.params.id}` + }; + break; + case 'pdbe-xray-ds': + downloadParams = { + url: `https://www.ebi.ac.uk/pdbe/densities/x-ray/${src.params.id.toLowerCase()}/cell?detail=${src.params.detail}`, + isBinary: true, + label: `PDBe X-ray Density Server: ${src.params.id}` + }; + break; + case 'rcsb': + downloadParams = { + url: src.params.type === '2fofc' + ? `https://edmaps.rcsb.org/maps/${src.params.id.toLowerCase()}_2fofc.dsn6` + : `https://edmaps.rcsb.org/maps/${src.params.id.toLowerCase()}_fofc.dsn6`, + isBinary: true, + label: `RCSB X-ray map: ${src.params.id}` + }; + break; + default: throw new Error(`${(src as any).name} not supported.`); + } + + const data = state.build().toRoot().apply(StateTransforms.Data.Download, downloadParams); + const dataStateObject = await state.updateTree(data).runInContext(taskCtx); + + switch (src.name) { + case 'url': + downloadParams = src.params; + provider = src.params.format === 'auto' ? ctx.dataFormat.registry.auto(getFileInfo(downloadParams.url), dataStateObject) : ctx.dataFormat.registry.get(src.params.format) + break; + case 'pdbe': + provider = ctx.dataFormat.registry.get('ccp4') + break; + case 'pdbe-emd-ds': + case 'pdbe-xray-ds': + provider = ctx.dataFormat.registry.get('dscif') + break; + case 'rcsb': + provider = ctx.dataFormat.registry.get('dsn6') + break; + default: throw new Error(`${(src as any).name} not supported.`); + } + + const b = state.build().to(data.ref); + await provider.getDefaultBuilder(ctx, b, state).runInContext(taskCtx) +})); + +export const InitVolumeStreaming = StateAction.build({ + display: { name: 'Volume Streaming' }, + from: PluginStateObject.Molecule.Structure, + params: VolumeStreaming.Params +})(({ ref, state, params }, ctx: PluginContext) => { + // TODO: specify simpler params + // TODO: try to determine if the input is x-ray or emd (in params provider) + // TODO: for EMD, use PDBe API to determine controur level https://github.com/dsehnal/LiteMol/blob/master/src/Viewer/Extensions/DensityStreaming/Entity.ts#L168 + // TODO: custom react view for this and the VolumeStreamingBehavior transformer + + const root = state.build().to(ref) + .apply(StateTransforms.Volume.VolumeStreamingBehavior, params); + + root.apply(StateTransforms.Volume.VolumeStreamingVisual, { channel: '2FO-FC', level: '2fo-fc' }, { props: { isGhost: true } }); + root.apply(StateTransforms.Volume.VolumeStreamingVisual, { channel: 'FO-FC', level: 'fo-fc(+ve)' }, { props: { isGhost: true } }); + root.apply(StateTransforms.Volume.VolumeStreamingVisual, { channel: 'FO-FC', level: 'fo-fc(-ve)' }, { props: { isGhost: true } }); + + return state.updateTree(root); +}); \ No newline at end of file diff --git a/src/mol-plugin/state/animation/built-in.ts b/src/mol-plugin/state/animation/built-in.ts new file mode 100644 index 0000000000000000000000000000000000000000..2902a995bbf036a06390224cca243883791c5421 --- /dev/null +++ b/src/mol-plugin/state/animation/built-in.ts @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { PluginStateAnimation } from './model'; +import { PluginStateObject } from '../objects'; +import { StateTransforms } from '../transforms'; +import { StateSelection } from 'mol-state'; +import { PluginCommands } from 'mol-plugin/command'; +import { ParamDefinition as PD } from 'mol-util/param-definition'; + +export const AnimateModelIndex = PluginStateAnimation.create({ + name: 'built-in.animate-model-index', + display: { name: 'Animate Model Index' }, + params: () => ({ + mode: PD.MappedStatic('once', { + once: PD.Group({ direction: PD.Select('forward', [['forward', 'Forward'], ['backward', 'Backward']]) }, { isFlat: true }), + palindrome: PD.Group({ }), + loop: PD.Group({ }), + }, { options: [['once', 'Once'], ['palindrome', 'Palindrome'], ['loop', 'Loop']] }), + maxFPS: PD.Numeric(3, { min: 0.5, max: 30, step: 0.5 }) + }), + initialState: () => ({} as { palindromeDirections?: { [id: string]: -1 | 1 | undefined } }), + async apply(animState, t, ctx) { + // limit fps + if (t.current > 0 && t.current - t.lastApplied < 1000 / ctx.params.maxFPS) { + return { kind: 'skip' }; + } + + const state = ctx.plugin.state.dataState; + const models = state.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Model) + .filter(c => c.transform.transformer === StateTransforms.Model.ModelFromTrajectory)); + + const update = state.build(); + + const params = ctx.params; + const palindromeDirections = animState.palindromeDirections || { }; + let isEnd = false; + + for (const m of models) { + const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, [PluginStateObject.Molecule.Trajectory]); + if (!parent || !parent.obj) continue; + const traj = parent.obj as PluginStateObject.Molecule.Trajectory; + update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory, + old => { + const len = traj.data.length; + let dir: -1 | 1 = 1; + if (params.mode.name === 'once') { + dir = params.mode.params.direction === 'backward' ? -1 : 1; + // if we are at start or end already, do nothing. + if ((dir === -1 && old.modelIndex === 0) || (dir === 1 && old.modelIndex === len - 1)) { + isEnd = true; + return old; + } + } else if (params.mode.name === 'palindrome') { + if (old.modelIndex === 0) dir = 1; + else if (old.modelIndex === len - 1) dir = -1; + else dir = palindromeDirections[m.transform.ref] || 1; + } + palindromeDirections[m.transform.ref] = dir; + + let modelIndex = (old.modelIndex + dir) % len; + if (modelIndex < 0) modelIndex += len; + + isEnd = isEnd || (dir === -1 && modelIndex === 0) || (dir === 1 && modelIndex === len - 1); + + return { modelIndex }; + }); + } + + await PluginCommands.State.Update.dispatch(ctx.plugin, { state, tree: update, doNotLogTiming: true }); + + if (params.mode.name === 'once' && isEnd) return { kind: 'finished' }; + if (params.mode.name === 'palindrome') return { kind: 'next', state: { palindromeDirections } }; + return { kind: 'next', state: {} }; + } +}) \ No newline at end of file diff --git a/src/mol-plugin/state/animation/manager.ts b/src/mol-plugin/state/animation/manager.ts new file mode 100644 index 0000000000000000000000000000000000000000..fc83a7f448e6177c3c4582b7910afbbf901be3c0 --- /dev/null +++ b/src/mol-plugin/state/animation/manager.ts @@ -0,0 +1,192 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { PluginComponent } from 'mol-plugin/component'; +import { PluginContext } from 'mol-plugin/context'; +import { PluginStateAnimation } from './model'; +import { ParamDefinition as PD } from 'mol-util/param-definition'; + +export { PluginAnimationManager } + +// TODO: pause functionality (this needs to reset if the state tree changes) +// TODO: handle unregistered animations on state restore +// TODO: better API + +class PluginAnimationManager extends PluginComponent<PluginAnimationManager.State> { + private map = new Map<string, PluginStateAnimation>(); + private animations: PluginStateAnimation[] = []; + private _current: PluginAnimationManager.Current; + private _params?: PD.For<PluginAnimationManager.State['params']> = void 0; + + readonly events = { + updated: this.ev() + }; + + get isEmpty() { return this.animations.length === 0; } + get current() { return this._current!; } + + private triggerUpdate() { + this.events.updated.next(); + } + + getParams(): PD.Params { + if (!this._params) { + this._params = { + current: PD.Select(this.animations[0] && this.animations[0].name, + this.animations.map(a => [a.name, a.display.name] as [string, string]), + { label: 'Animation' }) + }; + } + return this._params as any as PD.Params; + } + + updateParams(newParams: Partial<PluginAnimationManager.State['params']>) { + this.updateState({ params: { ...this.state.params, ...newParams } }); + const anim = this.map.get(this.state.params.current)!; + const params = anim.params(this.context) as PD.Params; + this._current = { + anim, + params, + paramValues: PD.getDefaultValues(params), + state: {}, + startedTime: -1, + lastTime: 0 + } + this.triggerUpdate(); + } + + updateCurrentParams(values: any) { + this._current.paramValues = { ...this._current.paramValues, ...values }; + this.triggerUpdate(); + } + + register(animation: PluginStateAnimation) { + if (this.map.has(animation.name)) { + this.context.log.error(`Animation '${animation.name}' is already registered.`); + return; + } + this._params = void 0; + this.map.set(animation.name, animation); + this.animations.push(animation); + if (this.animations.length === 1) { + this.updateParams({ current: animation.name }); + } else { + this.triggerUpdate(); + } + } + + play<P>(animation: PluginStateAnimation<P>, params: P) { + this.stop(); + if (!this.map.has(animation.name)) { + this.register(animation); + } + this.updateParams({ current: animation.name }); + this.updateCurrentParams(params); + this.start(); + } + + start() { + this.updateState({ animationState: 'playing' }); + this.triggerUpdate(); + + this._current.lastTime = 0; + this._current.startedTime = -1; + this._current.state = this._current.anim.initialState(this._current.paramValues, this.context); + + requestAnimationFrame(this.animate); + } + + stop() { + if (typeof this._frame !== 'undefined') cancelAnimationFrame(this._frame); + this.updateState({ animationState: 'stopped' }); + this.triggerUpdate(); + } + + get isAnimating() { + return this.state.animationState === 'playing'; + } + + private _frame: number | undefined = void 0; + private animate = async (t: number) => { + this._frame = void 0; + + if (this._current.startedTime < 0) this._current.startedTime = t; + const newState = await this._current.anim.apply( + this._current.state, + { lastApplied: this._current.lastTime, current: t - this._current.startedTime }, + { params: this._current.paramValues, plugin: this.context }); + + if (newState.kind === 'finished') { + this.stop(); + } else if (newState.kind === 'next') { + this._current.state = newState.state; + this._current.lastTime = t - this._current.startedTime; + if (this.state.animationState === 'playing') this._frame = requestAnimationFrame(this.animate); + } else if (newState.kind === 'skip') { + if (this.state.animationState === 'playing') this._frame = requestAnimationFrame(this.animate); + } + } + + getSnapshot(): PluginAnimationManager.Snapshot { + if (!this.current) return { state: this.state }; + + return { + state: this.state, + current: { + paramValues: this._current.paramValues, + state: this._current.anim.stateSerialization ? this._current.anim.stateSerialization.toJSON(this._current.state) : this._current.state + } + }; + } + + setSnapshot(snapshot: PluginAnimationManager.Snapshot) { + this.updateState({ animationState: snapshot.state.animationState }); + this.updateParams(snapshot.state.params); + + if (snapshot.current) { + this.current.paramValues = snapshot.current.paramValues; + this.current.state = this._current.anim.stateSerialization + ? this._current.anim.stateSerialization.fromJSON(snapshot.current.state) + : snapshot.current.state; + this.triggerUpdate(); + if (this.state.animationState === 'playing') this.resume(); + } + } + + private resume() { + this._current.lastTime = 0; + this._current.startedTime = -1; + requestAnimationFrame(this.animate); + } + + constructor(private context: PluginContext) { + super({ params: { current: '' }, animationState: 'stopped' }); + } +} + +namespace PluginAnimationManager { + export interface Current { + anim: PluginStateAnimation + params: PD.Params, + paramValues: any, + state: any, + startedTime: number, + lastTime: number + } + + export interface State { + params: { current: string }, + animationState: 'stopped' | 'playing' + } + + export interface Snapshot { + state: State, + current?: { + paramValues: any, + state: any + } + } +} \ No newline at end of file diff --git a/src/mol-plugin/state/animation/model.ts b/src/mol-plugin/state/animation/model.ts new file mode 100644 index 0000000000000000000000000000000000000000..88d99c653879f566900b18e04a639533d8e7c378 --- /dev/null +++ b/src/mol-plugin/state/animation/model.ts @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { ParamDefinition as PD } from 'mol-util/param-definition'; +import { PluginContext } from 'mol-plugin/context'; + +export { PluginStateAnimation } + +// TODO: helpers for building animations (once more animations are added) +// for example "composite animation" + +interface PluginStateAnimation<P = any, S = any> { + name: string, + readonly display: { readonly name: string, readonly description?: string }, + params: (ctx: PluginContext) => PD.For<P>, + initialState(params: P, ctx: PluginContext): S, + + /** + * Apply the current frame and modify the state. + * @param t Current absolute time since the animation started. + */ + apply(state: S, t: PluginStateAnimation.Time, ctx: PluginStateAnimation.Context<P>): Promise<PluginStateAnimation.ApplyResult<S>>, + + /** + * The state must be serializable to JSON. If JSON.stringify is not enough, + * custom converted to an object that works with JSON.stringify can be provided. + */ + stateSerialization?: { toJSON(state: S): any, fromJSON(data: any): S } +} + +namespace PluginStateAnimation { + export interface Time { + lastApplied: number, + current: number + } + + export type ApplyResult<S> = { kind: 'finished' } | { kind: 'skip' } | { kind: 'next', state: S } + export interface Context<P> { + params: P, + plugin: PluginContext + } + + export function create<P, S>(params: PluginStateAnimation<P, S>) { + return params; + } +} \ No newline at end of file diff --git a/src/mol-plugin/state/camera.ts b/src/mol-plugin/state/camera.ts index b05a2b0692879bd8a0636b44f4ab53d362e2eae3..830dbaf8de461f343944f16c1d1e1cfc21021579 100644 --- a/src/mol-plugin/state/camera.ts +++ b/src/mol-plugin/state/camera.ts @@ -7,57 +7,53 @@ import { Camera } from 'mol-canvas3d/camera'; import { OrderedMap } from 'immutable'; import { UUID } from 'mol-util'; -import { RxEventHelper } from 'mol-util/rx-event-helper'; +import { PluginComponent } from 'mol-plugin/component'; export { CameraSnapshotManager } -class CameraSnapshotManager { - private ev = RxEventHelper.create(); - private _entries = OrderedMap<string, CameraSnapshotManager.Entry>().asMutable(); - +class CameraSnapshotManager extends PluginComponent<{ entries: OrderedMap<string, CameraSnapshotManager.Entry> }> { readonly events = { changed: this.ev() }; - get entries() { return this._entries; } - getEntry(id: string) { - return this._entries.get(id); + return this.state.entries.get(id); } remove(id: string) { - if (!this._entries.has(id)) return; - this._entries.delete(id); + if (!this.state.entries.has(id)) return; + this.updateState({ entries: this.state.entries.delete(id) }); this.events.changed.next(); } add(e: CameraSnapshotManager.Entry) { - this._entries.set(e.id, e); + this.updateState({ entries: this.state.entries.set(e.id, e) }); this.events.changed.next(); } clear() { - if (this._entries.size === 0) return; - this._entries = OrderedMap<string, CameraSnapshotManager.Entry>().asMutable(); + if (this.state.entries.size === 0) return; + this.updateState({ entries: OrderedMap<string, CameraSnapshotManager.Entry>() }); this.events.changed.next(); } getStateSnapshot(): CameraSnapshotManager.StateSnapshot { const entries: CameraSnapshotManager.Entry[] = []; - this._entries.forEach(e => entries.push(e!)); + this.state.entries.forEach(e => entries.push(e!)); return { entries }; } setStateSnapshot(state: CameraSnapshotManager.StateSnapshot ) { - this._entries = OrderedMap<string, CameraSnapshotManager.Entry>().asMutable(); + const entries = OrderedMap<string, CameraSnapshotManager.Entry>().asMutable(); for (const e of state.entries) { - this._entries.set(e.id, e); + entries.set(e.id, e); } + this.updateState({ entries: entries.asImmutable() }); this.events.changed.next(); } - dispose() { - this.ev.dispose(); + constructor() { + super({ entries: OrderedMap<string, CameraSnapshotManager.Entry>() }); } } diff --git a/src/mol-plugin/state/objects.ts b/src/mol-plugin/state/objects.ts index 519ba1e03cfb9dc57bcff0a83670ea88b1a7bb54..94b9d78a6f5aa1c096c385d09010ef7e065d6b39 100644 --- a/src/mol-plugin/state/objects.ts +++ b/src/mol-plugin/state/objects.ts @@ -12,7 +12,7 @@ import { PluginBehavior } from 'mol-plugin/behavior/behavior'; import { Representation } from 'mol-repr/representation'; import { StructureRepresentation } from 'mol-repr/structure/representation'; import { VolumeRepresentation } from 'mol-repr/volume/representation'; -import { StateObject, Transformer } from 'mol-state'; +import { StateObject, StateTransformer } from 'mol-state'; import { Ccp4File } from 'mol-io/reader/ccp4/schema'; import { Dsn6File } from 'mol-io/reader/dsn6/schema'; @@ -77,6 +77,6 @@ export namespace PluginStateObject { } export namespace PluginStateTransform { - export const CreateBuiltIn = Transformer.factory('ms-plugin'); - export const BuiltIn = Transformer.builderFactory('ms-plugin'); + export const CreateBuiltIn = StateTransformer.factory('ms-plugin'); + export const BuiltIn = StateTransformer.builderFactory('ms-plugin'); } \ No newline at end of file diff --git a/src/mol-plugin/state/snapshots.ts b/src/mol-plugin/state/snapshots.ts index 8d03ed5ae64a12902d462b635d2826350e5eae55..d9f4152d2f8ebd6a1e04916a0a498f6a0ee2d91f 100644 --- a/src/mol-plugin/state/snapshots.ts +++ b/src/mol-plugin/state/snapshots.ts @@ -6,44 +6,39 @@ import { OrderedMap } from 'immutable'; import { UUID } from 'mol-util'; -import { RxEventHelper } from 'mol-util/rx-event-helper'; import { PluginState } from '../state'; +import { PluginComponent } from 'mol-plugin/component'; export { PluginStateSnapshotManager } -class PluginStateSnapshotManager { - private ev = RxEventHelper.create(); - private _entries = OrderedMap<string, PluginStateSnapshotManager.Entry>().asMutable(); - +class PluginStateSnapshotManager extends PluginComponent<{ entries: OrderedMap<string, PluginStateSnapshotManager.Entry> }> { readonly events = { changed: this.ev() }; - get entries() { return this._entries; } - getEntry(id: string) { - return this._entries.get(id); + return this.state.entries.get(id); } remove(id: string) { - if (!this._entries.has(id)) return; - this._entries.delete(id); + if (!this.state.entries.has(id)) return; + this.updateState({ entries: this.state.entries.delete(id) }); this.events.changed.next(); } add(e: PluginStateSnapshotManager.Entry) { - this._entries.set(e.id, e); + this.updateState({ entries: this.state.entries.set(e.id, e) }); this.events.changed.next(); } clear() { - if (this._entries.size === 0) return; - this._entries = OrderedMap<string, PluginStateSnapshotManager.Entry>().asMutable(); + if (this.state.entries.size === 0) return; + this.updateState({ entries: OrderedMap<string, PluginStateSnapshotManager.Entry>() }); this.events.changed.next(); } - dispose() { - this.ev.dispose(); + constructor() { + super({ entries: OrderedMap<string, PluginStateSnapshotManager.Entry>() }); } } diff --git a/src/mol-plugin/state/transforms.ts b/src/mol-plugin/state/transforms.ts index c6914174cf71c4d92ec64916da5341ea78ff6b84..6b1c5884c181eb5cc64d4d8661e3de66780f199a 100644 --- a/src/mol-plugin/state/transforms.ts +++ b/src/mol-plugin/state/transforms.ts @@ -6,10 +6,12 @@ import * as Data from './transforms/data' import * as Model from './transforms/model' +import * as Volume from './transforms/volume' import * as Representation from './transforms/representation' export const StateTransforms = { Data, Model, + Volume, Representation } \ No newline at end of file diff --git a/src/mol-plugin/state/transforms/data.ts b/src/mol-plugin/state/transforms/data.ts index 59ed1b3a0d9e8e3eb3fb6fb615a4a3bfbb18b017..4733c0dc642176f04d659ee17f33235295c00533 100644 --- a/src/mol-plugin/state/transforms/data.ts +++ b/src/mol-plugin/state/transforms/data.ts @@ -11,7 +11,7 @@ import { Task } from 'mol-task'; import CIF from 'mol-io/reader/cif' import { PluginContext } from 'mol-plugin/context'; import { ParamDefinition as PD } from 'mol-util/param-definition'; -import { Transformer } from 'mol-state'; +import { StateTransformer } from 'mol-state'; import { readFromFile } from 'mol-util/data-source'; import * as CCP4 from 'mol-io/reader/ccp4/parser' import * as DSN6 from 'mol-io/reader/dsn6/parser' @@ -38,12 +38,12 @@ const Download = PluginStateTransform.BuiltIn({ }); }, update({ oldParams, newParams, b }) { - if (oldParams.url !== newParams.url || oldParams.isBinary !== newParams.isBinary) return Transformer.UpdateResult.Recreate; + if (oldParams.url !== newParams.url || oldParams.isBinary !== newParams.isBinary) return StateTransformer.UpdateResult.Recreate; if (oldParams.label !== newParams.label) { (b.label as string) = newParams.label || newParams.url; - return Transformer.UpdateResult.Updated; + return StateTransformer.UpdateResult.Updated; } - return Transformer.UpdateResult.Unchanged; + return StateTransformer.UpdateResult.Unchanged; } }); @@ -71,9 +71,9 @@ const ReadFile = PluginStateTransform.BuiltIn({ update({ oldParams, newParams, b }) { if (oldParams.label !== newParams.label) { (b.label as string) = newParams.label || oldParams.file.name; - return Transformer.UpdateResult.Updated; + return StateTransformer.UpdateResult.Updated; } - return Transformer.UpdateResult.Unchanged; + return StateTransformer.UpdateResult.Unchanged; }, isSerializable: () => ({ isSerializable: false, reason: 'Cannot serialize user loaded files.' }) }); diff --git a/src/mol-plugin/state/transforms/model.ts b/src/mol-plugin/state/transforms/model.ts index a28e72ada1d3fc3a30cddb12a12b8d493ca15bfd..622dc32fa44cd123ee3bbc8784bc96654b3bc5d5 100644 --- a/src/mol-plugin/state/transforms/model.ts +++ b/src/mol-plugin/state/transforms/model.ts @@ -5,27 +5,31 @@ * @author Alexander Rose <alexander.rose@weirdbyte.de> */ -import { PluginStateTransform } from '../objects'; -import { PluginStateObject as SO } from '../objects'; -import { Task, RuntimeContext } from 'mol-task'; -import { Model, Structure, ModelSymmetry, StructureSymmetry, QueryContext, StructureSelection as Sel, StructureQuery, Queries } from 'mol-model/structure'; -import { ParamDefinition as PD } from 'mol-util/param-definition'; +import { parsePDB } from 'mol-io/reader/pdb/parser'; +import { Vec3 } from 'mol-math/linear-algebra'; +import { trajectoryFromMmCIF } from 'mol-model-formats/structure/mmcif'; +import { trajectoryFromPDB } from 'mol-model-formats/structure/pdb'; +import { Model, ModelSymmetry, Queries, QueryContext, Structure, StructureQuery, StructureSelection as Sel, StructureSymmetry } from 'mol-model/structure'; +import { Assembly } from 'mol-model/structure/model/properties/symmetry'; +import { PluginContext } from 'mol-plugin/context'; +import { MolScriptBuilder } from 'mol-script/language/builder'; import Expression from 'mol-script/language/expression'; import { compile } from 'mol-script/runtime/query/compiler'; -import { MolScriptBuilder } from 'mol-script/language/builder'; import { StateObject } from 'mol-state'; -import { PluginContext } from 'mol-plugin/context'; +import { RuntimeContext, Task } from 'mol-task'; +import { ParamDefinition as PD } from 'mol-util/param-definition'; import { stringToWords } from 'mol-util/string'; -import { volumeFromCcp4 } from 'mol-model-formats/volume/ccp4'; -import { Vec3 } from 'mol-math/linear-algebra'; -import CIF from 'mol-io/reader/cif'; -import { volumeFromDsn6 } from 'mol-model-formats/volume/dsn6'; -import { volumeFromDensityServerData } from 'mol-model-formats/volume/density-server'; -import { trajectoryFromMmCIF } from 'mol-model-formats/structure/mmcif'; -import { parsePDB } from 'mol-io/reader/pdb/parser'; -import { trajectoryFromPDB } from 'mol-model-formats/structure/pdb'; +import { PluginStateObject as SO, PluginStateTransform } from '../objects'; -export { TrajectoryFromMmCif } +export { TrajectoryFromMmCif }; +export { TrajectoryFromPDB }; +export { ModelFromTrajectory }; +export { StructureFromModel }; +export { StructureAssemblyFromModel }; +export { StructureSymmetryFromModel }; +export { StructureSelection }; +export { StructureComplexElement }; +export { CustomModelProperties }; type TrajectoryFromMmCif = typeof TrajectoryFromMmCif const TrajectoryFromMmCif = PluginStateTransform.BuiltIn({ name: 'trajectory-from-mmcif', @@ -59,11 +63,10 @@ const TrajectoryFromMmCif = PluginStateTransform.BuiltIn({ }); -export { TrajectoryFromPDB } type TrajectoryFromPDB = typeof TrajectoryFromPDB const TrajectoryFromPDB = PluginStateTransform.BuiltIn({ name: 'trajectory-from-pdb', - display: { name: 'Parse PDB string and create trajectory' }, + display: { name: 'Parse PDB', description: 'Parse PDB string and create trajectory.' }, from: [SO.Data.String], to: SO.Molecule.Trajectory })({ @@ -79,12 +82,11 @@ const TrajectoryFromPDB = PluginStateTransform.BuiltIn({ }); -export { ModelFromTrajectory } const plus1 = (v: number) => v + 1, minus1 = (v: number) => v - 1; type ModelFromTrajectory = typeof ModelFromTrajectory const ModelFromTrajectory = PluginStateTransform.BuiltIn({ name: 'model-from-trajectory', - display: { name: 'Model from Trajectory', description: 'Create a molecular structure from the specified model.' }, + display: { name: 'Molecular Model', description: 'Create a molecular model from specified index in a trajectory.' }, from: SO.Molecule.Trajectory, to: SO.Molecule.Model, params: a => { @@ -98,12 +100,13 @@ const ModelFromTrajectory = PluginStateTransform.BuiltIn({ apply({ a, params }) { if (params.modelIndex < 0 || params.modelIndex >= a.data.length) throw new Error(`Invalid modelIndex ${params.modelIndex}`); const model = a.data[params.modelIndex]; - const props = { label: `Model ${model.modelNum}` }; + const props = a.data.length === 1 + ? { label: `${model.label}` } + : { label: `${model.label}:${model.modelNum}`, description: `Model ${model.modelNum} of ${a.data.length}` }; return new SO.Molecule.Model(model, props); } }); -export { StructureFromModel } type StructureFromModel = typeof StructureFromModel const StructureFromModel = PluginStateTransform.BuiltIn({ name: 'structure-from-model', @@ -122,7 +125,6 @@ function structureDesc(s: Structure) { return s.elementCount === 1 ? '1 element' : `${s.elementCount} elements`; } -export { StructureAssemblyFromModel } type StructureAssemblyFromModel = typeof StructureAssemblyFromModel const StructureAssemblyFromModel = PluginStateTransform.BuiltIn({ name: 'structure-assembly-from-model', @@ -143,17 +145,30 @@ const StructureAssemblyFromModel = PluginStateTransform.BuiltIn({ return Task.create('Build Assembly', async ctx => { const model = a.data; let id = params.id; - let asm = ModelSymmetry.findAssembly(model, id || ''); - if (!!id && id !== 'deposited' && !asm) throw new Error(`Assembly '${id}' not found`); + let asm: Assembly | undefined = void 0; + + // if no id is specified, use the 1st assembly. + if (!id && model.symmetry.assemblies.length !== 0) { + id = model.symmetry.assemblies[0].id; + } + + if (model.symmetry.assemblies.length === 0) { + if (id !== 'deposited') { + plugin.log.warn(`Model '${a.label}' has no assembly, returning deposited structure.`); + } + } else { + asm = ModelSymmetry.findAssembly(model, id || ''); + if (!asm) { + plugin.log.warn(`Model '${a.label}' has no assembly called '${id}', returning deposited structure.`); + } + } const base = Structure.ofModel(model); - if ((id && !asm) || model.symmetry.assemblies.length === 0) { - if (!!id && id !== 'deposited') plugin.log.warn(`Model '${a.label}' has no assembly, returning deposited structure.`); + if (!asm) { const label = { label: a.data.label, description: structureDesc(base) }; return new SO.Molecule.Structure(base, label); } - asm = model.symmetry.assemblies[0]; id = asm.id; const s = await StructureSymmetry.buildAssembly(base, id!).runInContext(ctx); const props = { label: `Assembly ${id}`, description: structureDesc(s) }; @@ -162,7 +177,6 @@ const StructureAssemblyFromModel = PluginStateTransform.BuiltIn({ } }); -export { StructureSymmetryFromModel } type StructureSymmetryFromModel = typeof StructureSymmetryFromModel const StructureSymmetryFromModel = PluginStateTransform.BuiltIn({ name: 'structure-symmetry-from-model', @@ -188,7 +202,6 @@ const StructureSymmetryFromModel = PluginStateTransform.BuiltIn({ } }); -export { StructureSelection } type StructureSelection = typeof StructureSelection const StructureSelection = PluginStateTransform.BuiltIn({ name: 'structure-selection', @@ -210,7 +223,6 @@ const StructureSelection = PluginStateTransform.BuiltIn({ } }); -export { StructureComplexElement } namespace StructureComplexElement { export type Types = 'atomic-sequence' | 'water' | 'atomic-het' | 'spheres' } @@ -243,7 +255,6 @@ const StructureComplexElement = PluginStateTransform.BuiltIn({ } }); -export { CustomModelProperties } type CustomModelProperties = typeof CustomModelProperties const CustomModelProperties = PluginStateTransform.BuiltIn({ name: 'custom-model-properties', @@ -267,83 +278,4 @@ async function attachProps(model: Model, ctx: PluginContext, taskCtx: RuntimeCon const p = ctx.customModelProperties.get(name); await p.attach(model).runInContext(taskCtx); } -} - -// - -export { VolumeFromCcp4 } -type VolumeFromCcp4 = typeof VolumeFromCcp4 -const VolumeFromCcp4 = PluginStateTransform.BuiltIn({ - name: 'volume-from-ccp4', - display: { name: 'Volume from CCP4/MRC/MAP', description: 'Create Volume from CCP4/MRC/MAP data' }, - from: SO.Format.Ccp4, - to: SO.Volume.Data, - params(a) { - return { - voxelSize: PD.Vec3(Vec3.create(1, 1, 1)) - }; - } -})({ - apply({ a, params }) { - return Task.create('Create volume from CCP4/MRC/MAP', async ctx => { - const volume = await volumeFromCcp4(a.data, params).runInContext(ctx) - const props = { label: 'Volume' }; - return new SO.Volume.Data(volume, props); - }); - } -}); - -export { VolumeFromDsn6 } -type VolumeFromDsn6 = typeof VolumeFromDsn6 -const VolumeFromDsn6 = PluginStateTransform.BuiltIn({ - name: 'volume-from-dsn6', - display: { name: 'Volume from DSN6/BRIX', description: 'Create Volume from DSN6/BRIX data' }, - from: SO.Format.Dsn6, - to: SO.Volume.Data, - params(a) { - return { - voxelSize: PD.Vec3(Vec3.create(1, 1, 1)) - }; - } -})({ - apply({ a, params }) { - return Task.create('Create volume from DSN6/BRIX', async ctx => { - const volume = await volumeFromDsn6(a.data, params).runInContext(ctx) - const props = { label: 'Volume' }; - return new SO.Volume.Data(volume, props); - }); - } -}); - -export { VolumeFromDensityServerCif } -type VolumeFromDensityServerCif = typeof VolumeFromDensityServerCif -const VolumeFromDensityServerCif = PluginStateTransform.BuiltIn({ - name: 'volume-from-density-server-cif', - display: { name: 'Volume from density-server CIF', description: 'Identify and create all separate models in the specified CIF data block' }, - from: SO.Format.Cif, - to: SO.Volume.Data, - params(a) { - if (!a) { - return { - blockHeader: PD.makeOptional(PD.Text(void 0, { description: 'Header of the block to parse. If none is specifed, the 1st data block in the file is used.' })) - }; - } - const blocks = a.data.blocks.slice(1); // zero block contains query meta-data - return { - blockHeader: PD.makeOptional(PD.Select(blocks[0] && blocks[0].header, blocks.map(b => [b.header, b.header] as [string, string]), { description: 'Header of the block to parse' })) - }; - } -})({ - isApplicable: a => a.data.blocks.length > 0, - apply({ a, params }) { - return Task.create('Parse density-server CIF', async ctx => { - const header = params.blockHeader || a.data.blocks[1].header; // zero block contains query meta-data - const block = a.data.blocks.find(b => b.header === header); - if (!block) throw new Error(`Data block '${[header]}' not found.`); - const densityServerCif = CIF.schema.densityServer(block) - const volume = await volumeFromDensityServerData(densityServerCif).runInContext(ctx) - const props = { label: densityServerCif.volume_data_3d_info.name.value(0), description: `${densityServerCif.volume_data_3d_info.name.value(0)}` }; - return new SO.Volume.Data(volume, props); - }); - } -}); \ No newline at end of file +} \ No newline at end of file diff --git a/src/mol-plugin/state/transforms/representation.ts b/src/mol-plugin/state/transforms/representation.ts index 31fbb134ff2df51dfc639187c3c87edefb255302..25af4e857307005d0aeae0607c18f0896e067b30 100644 --- a/src/mol-plugin/state/transforms/representation.ts +++ b/src/mol-plugin/state/transforms/representation.ts @@ -5,7 +5,7 @@ * @author Alexander Rose <alexander.rose@weirdbyte.de> */ -import { Transformer } from 'mol-state'; +import { StateTransformer } from 'mol-state'; import { Task } from 'mol-task'; import { PluginStateTransform } from '../objects'; import { PluginStateObject as SO } from '../objects'; @@ -16,12 +16,14 @@ import { BuiltInStructureRepresentationsName } from 'mol-repr/structure/registry import { Structure } from 'mol-model/structure'; import { StructureParams } from 'mol-repr/structure/representation'; import { ExplodeRepresentation3D } from 'mol-plugin/behavior/dynamic/representation'; -import { VolumeData } from 'mol-model/volume'; +import { VolumeData, VolumeIsoValue } from 'mol-model/volume'; import { BuiltInVolumeRepresentationsName } from 'mol-repr/volume/registry'; import { VolumeParams } from 'mol-repr/volume/representation'; +import { BuiltInColorThemeName, ColorTheme } from 'mol-theme/color'; +import { BuiltInSizeThemeName, SizeTheme } from 'mol-theme/size'; export namespace StructureRepresentation3DHelpers { - export function getDefaultParams(ctx: PluginContext, name: BuiltInStructureRepresentationsName, structure: Structure, structureParams?: Partial<PD.Values<StructureParams>>): Transformer.Params<StructureRepresentation3D> { + export function getDefaultParams(ctx: PluginContext, name: BuiltInStructureRepresentationsName, structure: Structure, structureParams?: Partial<PD.Values<StructureParams>>): StateTransformer.Params<StructureRepresentation3D> { const type = ctx.structureRepresentation.registry.get(name); const themeDataCtx = { structure }; @@ -35,7 +37,7 @@ export namespace StructureRepresentation3DHelpers { }) } - export function getDefaultParamsStatic(ctx: PluginContext, name: BuiltInStructureRepresentationsName, structureParams?: Partial<PD.Values<StructureParams>>): Transformer.Params<StructureRepresentation3D> { + export function getDefaultParamsStatic(ctx: PluginContext, name: BuiltInStructureRepresentationsName, structureParams?: Partial<PD.Values<StructureParams>>): StateTransformer.Params<StructureRepresentation3D> { const type = ctx.structureRepresentation.registry.get(name); const colorParams = ctx.structureRepresentation.themeCtx.colorThemeRegistry.get(type.defaultColorTheme).defaultValues; const sizeParams = ctx.structureRepresentation.themeCtx.sizeThemeRegistry.get(type.defaultSizeTheme).defaultValues @@ -112,11 +114,11 @@ const StructureRepresentation3D = PluginStateTransform.BuiltIn({ }, update({ a, b, oldParams, newParams }, plugin: PluginContext) { return Task.create('Structure Representation', async ctx => { - if (newParams.type.name !== oldParams.type.name) return Transformer.UpdateResult.Recreate; + if (newParams.type.name !== oldParams.type.name) return StateTransformer.UpdateResult.Recreate; const props = { ...b.data.props, ...newParams.type.params } b.data.setTheme(createTheme(plugin.structureRepresentation.themeCtx, { structure: a.data }, newParams)) await b.data.createOrUpdate(props, a.data).runInContext(ctx); - return Transformer.UpdateResult.Updated; + return StateTransformer.UpdateResult.Updated; }); } }); @@ -140,7 +142,7 @@ const ExplodeStructureRepresentation3D = PluginStateTransform.BuiltIn({ return Task.create('Update Explosion', async () => { const updated = await b.data.update(newParams); b.label = `Explosion T = ${newParams.t.toFixed(2)}`; - return updated ? Transformer.UpdateResult.Updated : Transformer.UpdateResult.Unchanged; + return updated ? StateTransformer.UpdateResult.Updated : StateTransformer.UpdateResult.Unchanged; }); } }); @@ -148,7 +150,7 @@ const ExplodeStructureRepresentation3D = PluginStateTransform.BuiltIn({ // export namespace VolumeRepresentation3DHelpers { - export function getDefaultParams(ctx: PluginContext, name: BuiltInVolumeRepresentationsName, volume: VolumeData, volumeParams?: Partial<PD.Values<VolumeParams>>): Transformer.Params<VolumeRepresentation3D> { + export function getDefaultParams(ctx: PluginContext, name: BuiltInVolumeRepresentationsName, volume: VolumeData, volumeParams?: Partial<PD.Values<VolumeParams>>): StateTransformer.Params<VolumeRepresentation3D> { const type = ctx.volumeRepresentation.registry.get(name); const themeDataCtx = { volume }; @@ -162,16 +164,20 @@ export namespace VolumeRepresentation3DHelpers { }) } - export function getDefaultParamsStatic(ctx: PluginContext, name: BuiltInVolumeRepresentationsName, volumeParams?: Partial<PD.Values<VolumeParams>>): Transformer.Params<VolumeRepresentation3D> { + export function getDefaultParamsStatic(ctx: PluginContext, name: BuiltInVolumeRepresentationsName, volumeParams?: Partial<PD.Values<PD.Params>>, colorName?: BuiltInColorThemeName, colorParams?: Partial<ColorTheme.Props>, sizeName?: BuiltInSizeThemeName, sizeParams?: Partial<SizeTheme.Props>): StateTransformer.Params<VolumeRepresentation3D> { const type = ctx.volumeRepresentation.registry.get(name); - const colorParams = ctx.volumeRepresentation.themeCtx.colorThemeRegistry.get(type.defaultColorTheme).defaultValues; - const sizeParams = ctx.volumeRepresentation.themeCtx.sizeThemeRegistry.get(type.defaultSizeTheme).defaultValues + const colorType = ctx.volumeRepresentation.themeCtx.colorThemeRegistry.get(colorName || type.defaultColorTheme); + const sizeType = ctx.volumeRepresentation.themeCtx.sizeThemeRegistry.get(sizeName || type.defaultSizeTheme); return ({ type: { name, params: volumeParams ? { ...type.defaultValues, ...volumeParams } : type.defaultValues }, - colorTheme: { name: type.defaultColorTheme, params: colorParams }, - sizeTheme: { name: type.defaultSizeTheme, params: sizeParams } + colorTheme: { name: type.defaultColorTheme, params: colorParams ? { ...colorType.defaultValues, ...colorParams } : colorType.defaultValues }, + sizeTheme: { name: type.defaultSizeTheme, params: sizeParams ? { ...sizeType.defaultValues, ...sizeParams } : sizeType.defaultValues } }) } + + export function getDescription(props: any) { + return props.isoValue && VolumeIsoValue.toString(props.isoValue) + } } export { VolumeRepresentation3D } type VolumeRepresentation3D = typeof VolumeRepresentation3D @@ -189,16 +195,16 @@ const VolumeRepresentation3D = PluginStateTransform.BuiltIn({ type: PD.Mapped<any>( registry.default.name, registry.types, - name => PD.Group<any>(registry.get(name).getParams(themeCtx, VolumeData.Empty ))), + name => PD.Group<any>(registry.get(name).getParams(themeCtx, VolumeData.One ))), colorTheme: PD.Mapped<any>( type.defaultColorTheme, themeCtx.colorThemeRegistry.types, - name => PD.Group<any>(themeCtx.colorThemeRegistry.get(name).getParams({ volume: VolumeData.Empty })) + name => PD.Group<any>(themeCtx.colorThemeRegistry.get(name).getParams({ volume: VolumeData.One })) ), sizeTheme: PD.Mapped<any>( type.defaultSizeTheme, themeCtx.sizeThemeRegistry.types, - name => PD.Group<any>(themeCtx.sizeThemeRegistry.get(name).getParams({ volume: VolumeData.Empty })) + name => PD.Group<any>(themeCtx.sizeThemeRegistry.get(name).getParams({ volume: VolumeData.One })) ) } } @@ -233,19 +239,18 @@ const VolumeRepresentation3D = PluginStateTransform.BuiltIn({ const repr = provider.factory({ webgl: plugin.canvas3d.webgl, ...plugin.volumeRepresentation.themeCtx }, provider.getParams) repr.setTheme(createTheme(plugin.volumeRepresentation.themeCtx, { volume: a.data }, params)) // TODO set initial state, repr.setState({}) - // TODO include isoValue in the label where available - console.log(params.type.params); await repr.createOrUpdate(props, a.data).runInContext(ctx); - return new SO.Volume.Representation3D(repr, { label: provider.label }); + return new SO.Volume.Representation3D(repr, { label: provider.label, description: VolumeRepresentation3DHelpers.getDescription(props) }); }); }, update({ a, b, oldParams, newParams }, plugin: PluginContext) { return Task.create('Volume Representation', async ctx => { - if (newParams.type.name !== oldParams.type.name) return Transformer.UpdateResult.Recreate; + if (newParams.type.name !== oldParams.type.name) return StateTransformer.UpdateResult.Recreate; const props = { ...b.data.props, ...newParams.type.params } b.data.setTheme(createTheme(plugin.volumeRepresentation.themeCtx, { volume: a.data }, newParams)) await b.data.createOrUpdate(props, a.data).runInContext(ctx); - return Transformer.UpdateResult.Updated; + b.description = VolumeRepresentation3DHelpers.getDescription(props) + return StateTransformer.UpdateResult.Updated; }); } }); \ No newline at end of file diff --git a/src/mol-plugin/state/transforms/volume.ts b/src/mol-plugin/state/transforms/volume.ts new file mode 100644 index 0000000000000000000000000000000000000000..6c682e4a13cc9df01cea36e51391ae1e832e8259 --- /dev/null +++ b/src/mol-plugin/state/transforms/volume.ts @@ -0,0 +1,195 @@ +/** + * Copyright (c) 2018-2019 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 CIF from 'mol-io/reader/cif'; +import { Vec3 } from 'mol-math/linear-algebra'; +import { volumeFromCcp4 } from 'mol-model-formats/volume/ccp4'; +import { volumeFromDensityServerData } from 'mol-model-formats/volume/density-server'; +import { volumeFromDsn6 } from 'mol-model-formats/volume/dsn6'; +import { Task } from 'mol-task'; +import { ParamDefinition as PD } from 'mol-util/param-definition'; +import { PluginStateObject as SO, PluginStateTransform } from '../objects'; +import { VolumeStreaming } from 'mol-plugin/behavior/dynamic/volume'; +import { PluginContext } from 'mol-plugin/context'; +import { StateTransformer } from 'mol-state'; +import { VolumeData, VolumeIsoValue } from 'mol-model/volume'; +import { BuiltInVolumeRepresentations } from 'mol-repr/volume/registry'; +import { createTheme } from 'mol-theme/theme'; +import { VolumeRepresentation3DHelpers } from './representation'; +import { Color } from 'mol-util/color'; + +export { VolumeFromCcp4 }; +export { VolumeFromDsn6 }; +export { VolumeFromDensityServerCif }; +type VolumeFromCcp4 = typeof VolumeFromCcp4 +const VolumeFromCcp4 = PluginStateTransform.BuiltIn({ + name: 'volume-from-ccp4', + display: { name: 'Volume from CCP4/MRC/MAP', description: 'Create Volume from CCP4/MRC/MAP data' }, + from: SO.Format.Ccp4, + to: SO.Volume.Data, + params(a) { + return { + voxelSize: PD.Vec3(Vec3.create(1, 1, 1)) + }; + } +})({ + apply({ a, params }) { + return Task.create('Create volume from CCP4/MRC/MAP', async ctx => { + const volume = await volumeFromCcp4(a.data, params).runInContext(ctx) + const props = { label: 'Volume' }; + return new SO.Volume.Data(volume, props); + }); + } +}); + +type VolumeFromDsn6 = typeof VolumeFromDsn6 +const VolumeFromDsn6 = PluginStateTransform.BuiltIn({ + name: 'volume-from-dsn6', + display: { name: 'Volume from DSN6/BRIX', description: 'Create Volume from DSN6/BRIX data' }, + from: SO.Format.Dsn6, + to: SO.Volume.Data, + params(a) { + return { + voxelSize: PD.Vec3(Vec3.create(1, 1, 1)) + }; + } +})({ + apply({ a, params }) { + return Task.create('Create volume from DSN6/BRIX', async ctx => { + const volume = await volumeFromDsn6(a.data, params).runInContext(ctx) + const props = { label: 'Volume' }; + return new SO.Volume.Data(volume, props); + }); + } +}); + +type VolumeFromDensityServerCif = typeof VolumeFromDensityServerCif +const VolumeFromDensityServerCif = PluginStateTransform.BuiltIn({ + name: 'volume-from-density-server-cif', + display: { name: 'Volume from density-server CIF', description: 'Identify and create all separate models in the specified CIF data block' }, + from: SO.Format.Cif, + to: SO.Volume.Data, + params(a) { + if (!a) { + return { + blockHeader: PD.makeOptional(PD.Text(void 0, { description: 'Header of the block to parse. If none is specifed, the 1st data block in the file is used.' })) + }; + } + const blocks = a.data.blocks.slice(1); // zero block contains query meta-data + return { + blockHeader: PD.makeOptional(PD.Select(blocks[0] && blocks[0].header, blocks.map(b => [b.header, b.header] as [string, string]), { description: 'Header of the block to parse' })) + }; + } +})({ + isApplicable: a => a.data.blocks.length > 0, + apply({ a, params }) { + return Task.create('Parse density-server CIF', async ctx => { + const header = params.blockHeader || a.data.blocks[1].header; // zero block contains query meta-data + const block = a.data.blocks.find(b => b.header === header); + if (!block) throw new Error(`Data block '${[header]}' not found.`); + const densityServerCif = CIF.schema.densityServer(block) + const volume = await volumeFromDensityServerData(densityServerCif).runInContext(ctx) + const props = { label: densityServerCif.volume_data_3d_info.name.value(0), description: `${densityServerCif.volume_data_3d_info.name.value(0)}` }; + return new SO.Volume.Data(volume, props); + }); + } +}); + +export { VolumeStreamingBehavior } +type VolumeStreamingBehavior = typeof VolumeStreamingBehavior +const VolumeStreamingBehavior = PluginStateTransform.BuiltIn({ + name: 'volume-streaming-behavior', + display: { name: 'Volume Streaming Behavior', description: 'Create Volume Streaming behavior.' }, + from: SO.Molecule.Structure, + to: VolumeStreaming.Obj, + params: VolumeStreaming.Params +})({ + canAutoUpdate: ({ oldParams, newParams }) => oldParams.serverUrl === newParams.serverUrl && oldParams.id === newParams.id, + apply: ({ a, params }, plugin: PluginContext) => Task.create('Volume Streaming', async ctx => { + const behavior = new VolumeStreaming.Behavior(plugin, params, a.data); + // get the initial data now so that the child projections dont get empty volumes. + await behavior.update(behavior.params); + return new VolumeStreaming.Obj(behavior, { label: 'Volume Streaming' }); + }), + update({ b, newParams }) { + return Task.create('Update Volume Streaming', async _ => { + await b.data.update(newParams); + return StateTransformer.UpdateResult.Updated; + }); + } +}); + +// export { VolumeStreamingData } +// type VolumeStreamingData = typeof VolumeStreamingData +// const VolumeStreamingData = PluginStateTransform.BuiltIn({ +// name: 'volume-streaming-data', +// display: { name: 'Volume Streaming Data' }, +// from: VolumeStreaming.Obj, +// to: SO.Volume.Data, +// params: { +// channel: PD.Select<keyof VolumeStreaming.ChannelData>('EM', [['EM', 'EM'], ['FO-FC', 'Fo-Fc'], ['2FO-FC', '2Fo-Fc']], { isHidden: true }), +// level: PD.Text<VolumeStreaming.LevelType>('em') +// } +// })({ +// apply({ a, params }, plugin: PluginContext) { +// const data = a.data.currentData[params.channel] || VolumeData.Empty; +// console.log({ data }); +// return new SO.Volume.Data(a.data.currentData[params.channel] || VolumeData.Empty, { label: params.level }); +// } +// }); + +export { VolumeStreamingVisual } +type VolumeStreamingVisual = typeof VolumeStreamingVisual +const VolumeStreamingVisual = PluginStateTransform.BuiltIn({ + name: 'volume-streaming-visual', + display: { name: 'Volume Streaming Visual' }, + from: VolumeStreaming.Obj, + to: SO.Volume.Representation3D, + params: { + channel: PD.Select<keyof VolumeStreaming.ChannelData>('EM', [['EM', 'EM'], ['FO-FC', 'Fo-Fc'], ['2FO-FC', '2Fo-Fc']], { isHidden: true }), + level: PD.Text<VolumeStreaming.LevelType>('em') + } +})({ + apply: ({ a, params: srcParams }, plugin: PluginContext) => Task.create('Volume Representation', async ctx => { + const { data, params } = createVolumeProps(a.data, srcParams.channel, srcParams.level); + + const provider = BuiltInVolumeRepresentations.isosurface; + const props = params.type.params || {} + const repr = provider.factory({ webgl: plugin.canvas3d.webgl, ...plugin.volumeRepresentation.themeCtx }, provider.getParams) + repr.setTheme(createTheme(plugin.volumeRepresentation.themeCtx, { volume: data }, params)) + await repr.createOrUpdate(props, data).runInContext(ctx); + return new SO.Volume.Representation3D(repr, { label: srcParams.level, description: VolumeRepresentation3DHelpers.getDescription(props) }); + }), + update: ({ a, b, oldParams, newParams }, plugin: PluginContext) => Task.create('Volume Representation', async ctx => { + // TODO : check if params/underlying data/etc have changed; maybe will need to export "data" or some other "tag" in the Representation for this to work + const { data, params } = createVolumeProps(a.data, newParams.channel, newParams.level); + const props = { ...b.data.props, ...params.type.params }; + b.data.setTheme(createTheme(plugin.volumeRepresentation.themeCtx, { volume: data }, params)) + await b.data.createOrUpdate(props, data).runInContext(ctx); + return StateTransformer.UpdateResult.Updated; + }) +}); + +function createVolumeProps(streaming: VolumeStreaming.Behavior, channel: keyof VolumeStreaming.ChannelData, level: VolumeStreaming.LevelType) { + const data = streaming.currentData[channel] || VolumeData.One; + // TODO: createTheme fails when VolumeData.Empty is used for some reason. + + let isoValue: VolumeIsoValue, color: Color; + + if (level === 'em' && streaming.params.levels.name === 'em') { + isoValue = streaming.params.levels.params.isoValue; + color = streaming.params.levels.params.color; + } else if (level !== 'em' && streaming.params.levels.name === 'x-ray') { + isoValue = streaming.params.levels.params[level].isoValue; + color = streaming.params.levels.params[level].color; + } else { + throw new Error(`Unsupported iso level ${level}.`); + } + + const params = VolumeRepresentation3DHelpers.getDefaultParamsStatic(streaming.ctx, 'isosurface', { isoValue, alpha: 0.3 }, 'uniform', { value: color }); + return { data, params }; +} \ No newline at end of file diff --git a/src/mol-plugin/ui/base.tsx b/src/mol-plugin/ui/base.tsx index db43db77315ab37271bdd5b436115d5176272961..d4ae0171d84882a5f0c9fb6e2152b4fbfd5e2d83 100644 --- a/src/mol-plugin/ui/base.tsx +++ b/src/mol-plugin/ui/base.tsx @@ -10,7 +10,7 @@ import { PluginContext } from '../context'; export const PluginReactContext = React.createContext(void 0 as any as PluginContext); -export abstract class PluginComponent<P = {}, S = {}, SS = {}> extends React.Component<P, S, SS> { +export abstract class PluginUIComponent<P = {}, S = {}, SS = {}> extends React.Component<P, S, SS> { static contextType = PluginReactContext; readonly plugin: PluginContext; @@ -35,7 +35,7 @@ export abstract class PluginComponent<P = {}, S = {}, SS = {}> extends React.Com } } -export abstract class PurePluginComponent<P = {}, S = {}, SS = {}> extends React.PureComponent<P, S, SS> { +export abstract class PurePluginUIComponent<P = {}, S = {}, SS = {}> extends React.PureComponent<P, S, SS> { static contextType = PluginReactContext; readonly plugin: PluginContext; diff --git a/src/mol-plugin/ui/camera.tsx b/src/mol-plugin/ui/camera.tsx index ddcf5995d5fff6c62de49ea2e04d0f150da79a69..796bc5d869d85681a4af84d8f6711ec2cc3033bc 100644 --- a/src/mol-plugin/ui/camera.tsx +++ b/src/mol-plugin/ui/camera.tsx @@ -6,11 +6,11 @@ import { PluginCommands } from 'mol-plugin/command'; import * as React from 'react'; -import { PluginComponent } from './base'; +import { PluginUIComponent } from './base'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import { ParameterControls } from './controls/parameters'; -export class CameraSnapshots extends PluginComponent<{ }, { }> { +export class CameraSnapshots extends PluginUIComponent<{ }, { }> { render() { return <div> <div className='msp-section-header'>Camera Snapshots</div> @@ -20,7 +20,7 @@ export class CameraSnapshots extends PluginComponent<{ }, { }> { } } -class CameraSnapshotControls extends PluginComponent<{ }, { name: string, description: string }> { +class CameraSnapshotControls extends PluginUIComponent<{ }, { name: string, description: string }> { static Params = { name: PD.Text(), description: PD.Text() @@ -48,7 +48,7 @@ class CameraSnapshotControls extends PluginComponent<{ }, { name: string, descri } } -class CameraSnapshotList extends PluginComponent<{ }, { }> { +class CameraSnapshotList extends PluginUIComponent<{ }, { }> { componentDidMount() { this.subscribe(this.plugin.events.state.cameraSnapshots.changed, () => this.forceUpdate()); } @@ -65,7 +65,7 @@ class CameraSnapshotList extends PluginComponent<{ }, { }> { render() { return <ul style={{ listStyle: 'none' }} className='msp-state-list'> - {this.plugin.state.cameraSnapshots.entries.valueSeq().map(e =><li key={e!.id}> + {this.plugin.state.cameraSnapshots.state.entries.valueSeq().map(e =><li key={e!.id}> <button className='msp-btn msp-btn-block msp-form-control' onClick={this.apply(e!.id)}>{e!.name || e!.timestamp} <small>{e!.description}</small></button> <button onClick={this.remove(e!.id)} className='msp-btn msp-btn-link msp-state-list-remove-button'> <span className='msp-icon msp-icon-remove' /> diff --git a/src/mol-plugin/ui/controls.tsx b/src/mol-plugin/ui/controls.tsx index 2a29958116c3495a7dc844ea0dd2e10f4643761a..c45e9fa015170f3646ea1590ea11db59976df29f 100644 --- a/src/mol-plugin/ui/controls.tsx +++ b/src/mol-plugin/ui/controls.tsx @@ -6,11 +6,11 @@ import * as React from 'react'; import { PluginCommands } from 'mol-plugin/command'; -import { UpdateTrajectory } from 'mol-plugin/state/actions/basic'; -import { PluginComponent } from './base'; +import { UpdateTrajectory } from 'mol-plugin/state/actions/structure'; +import { PluginUIComponent } from './base'; import { LociLabelEntry } from 'mol-plugin/util/loci-label-manager'; -export class Controls extends PluginComponent<{ }, { }> { +export class Controls extends PluginUIComponent<{ }, { }> { render() { return <> @@ -18,7 +18,7 @@ export class Controls extends PluginComponent<{ }, { }> { } } -export class TrajectoryControls extends PluginComponent { +export class TrajectoryControls extends PluginUIComponent { render() { return <div> <button className='msp-btn msp-btn-link' onClick={() => PluginCommands.State.ApplyAction.dispatch(this.plugin, { @@ -37,7 +37,7 @@ export class TrajectoryControls extends PluginComponent { } } -export class LociLabelControl extends PluginComponent<{}, { entries: ReadonlyArray<LociLabelEntry> }> { +export class LociLabelControl extends PluginUIComponent<{}, { entries: ReadonlyArray<LociLabelEntry> }> { state = { entries: [] } componentDidMount() { diff --git a/src/mol-plugin/ui/controls/common.tsx b/src/mol-plugin/ui/controls/common.tsx index 5c33082538ebac60e3a6d2f2bdb37f0b7bc61a4b..3412a9df0350e98e5355314b8b3aac8a66c25171 100644 --- a/src/mol-plugin/ui/controls/common.tsx +++ b/src/mol-plugin/ui/controls/common.tsx @@ -27,6 +27,61 @@ export class ControlGroup extends React.Component<{ header: string, initialExpan } } +export class NumericInput extends React.PureComponent<{ + value: number, + onChange: (v: number) => void, + onEnter?: () => void, + onBlur?: () => void, + blurOnEnter?: boolean, + isDisabled?: boolean, + placeholder?: string +}, { value: string }> { + state = { value: '0' }; + input = React.createRef<HTMLInputElement>(); + + onChange = (e: React.ChangeEvent<HTMLInputElement>) => { + const value = +e.target.value; + this.setState({ value: e.target.value }, () => { + if (!Number.isNaN(value) && value !== this.props.value) { + this.props.onChange(value); + } + }); + } + + onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => { + if ((e.keyCode === 13 || e.charCode === 13)) { + if (this.props.blurOnEnter && this.input.current) { + this.input.current.blur(); + } + if (this.props.onEnter) this.props.onEnter(); + } + } + + onBlur = () => { + this.setState({ value: '' + this.props.value }); + if (this.props.onBlur) this.props.onBlur(); + } + + static getDerivedStateFromProps(props: { value: number }, state: { value: string }) { + const value = +state.value; + if (Number.isNaN(value) || value === props.value) return null; + return { value: '' + props.value }; + } + + render() { + return <input type='text' + ref={this.input} + onBlur={this.onBlur} + value={this.state.value} + placeholder={this.props.placeholder} + onChange={this.onChange} + onKeyPress={this.props.onEnter || this.props.blurOnEnter ? this.onKeyPress : void 0} + disabled={!!this.props.isDisabled} + /> + } +} + + // export const ToggleButton = (props: { // onChange: (v: boolean) => void, // value: boolean, diff --git a/src/mol-plugin/ui/controls/parameters.tsx b/src/mol-plugin/ui/controls/parameters.tsx index caa63c87a96e8ed267212070ff1285fd947c3161..b2d5e3bab971401f264ec0e2843059a1807e2eb6 100644 --- a/src/mol-plugin/ui/controls/parameters.tsx +++ b/src/mol-plugin/ui/controls/parameters.tsx @@ -15,6 +15,7 @@ import { camelCaseToWords } from 'mol-util/string'; import * as React from 'react'; import LineGraphComponent from './line-graph/line-graph-component'; import { Slider, Slider2 } from './slider'; +import { NumericInput } from './common'; export interface ParameterControlsProps<P extends PD.Params = PD.Params> { params: P, @@ -28,15 +29,17 @@ export class ParameterControls<P extends PD.Params> extends React.PureComponent< render() { const params = this.props.params; const values = this.props.values; - return <div style={{ width: '100%' }}> - {Object.keys(params).map(key => { + const keys = Object.keys(params); + if (keys.length === 0) return null; + return <> + {keys.map(key => { const param = params[key]; if (param.isHidden) return null; const Control = controlFor(param); if (!Control) return null; return <Control param={param} key={key} onChange={this.props.onChange} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} name={key} value={values[key]} /> })} - </div>; + </>; } } @@ -152,53 +155,22 @@ export class LineGraphControl extends React.PureComponent<ParamProps<PD.LineGrap } } -export class NumberInputControl extends React.PureComponent<ParamProps<PD.Numeric>, { value: string }> { +export class NumberInputControl extends React.PureComponent<ParamProps<PD.Numeric>> { state = { value: '0' }; - protected update(value: any) { + update = (value: number) => { this.props.onChange({ param: this.props.param, name: this.props.name, value }); } - onChange = (e: React.ChangeEvent<HTMLInputElement>) => { - const value = +e.target.value; - this.setState({ value: e.target.value }, () => { - if (!Number.isNaN(value) && value !== this.props.value) { - this.update(value); - } - }); - } - - onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => { - if (!this.props.onEnter) return; - if ((e.keyCode === 13 || e.charCode === 13)) { - this.props.onEnter(); - } - } - - onBlur = () => { - this.setState({ value: '' + this.props.value }); - } - - static getDerivedStateFromProps(props: { value: number }, state: { value: string }) { - const value = +state.value; - if (Number.isNaN(value) || value === props.value) return null; - return { value: '' + props.value }; - } - render() { const placeholder = this.props.param.label || camelCaseToWords(this.props.name); const label = this.props.param.label || camelCaseToWords(this.props.name); return <div className='msp-control-row'> <span title={this.props.param.description}>{label}</span> <div> - <input type='text' - onBlur={this.onBlur} - value={this.state.value} - placeholder={placeholder} - onChange={this.onChange} - onKeyPress={this.props.onEnter ? this.onKeyPress : void 0} - disabled={this.props.isDisabled} - /> + <NumericInput + value={this.props.value} onEnter={this.props.onEnter} placeholder={placeholder} + isDisabled={this.props.isDisabled} onChange={this.update} /> </div> </div>; } @@ -208,7 +180,7 @@ export class NumberRangeControl extends SimpleParam<PD.Numeric> { onChange = (v: number) => { this.update(v); } renderControl() { return <Slider value={this.props.value} min={this.props.param.min!} max={this.props.param.max!} - step={this.props.param.step} onChange={this.onChange} disabled={this.props.isDisabled} /> + step={this.props.param.step} onChange={this.onChange} disabled={this.props.isDisabled} onEnter={this.props.onEnter} /> } } @@ -267,7 +239,7 @@ export class BoundedIntervalControl extends SimpleParam<PD.Interval> { onChange = (v: [number, number]) => { this.update(v); } renderControl() { return <Slider2 value={this.props.value} min={this.props.param.min!} max={this.props.param.max!} - step={this.props.param.step} onChange={this.onChange} disabled={this.props.isDisabled} />; + step={this.props.param.step} onChange={this.onChange} disabled={this.props.isDisabled} onEnter={this.props.onEnter} />; } } @@ -464,6 +436,10 @@ export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>, render() { const params = this.props.param.params; + + // Do not show if there are no params. + if (Object.keys(params).length === 0) return null; + const label = this.props.param.label || camelCaseToWords(this.props.name); const controls = <ParameterControls params={params} onChange={this.onChangeParam} values={this.props.value} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />; diff --git a/src/mol-plugin/ui/controls/slider.tsx b/src/mol-plugin/ui/controls/slider.tsx index f930a20fe558d0496ba4eeec0689a97ad4bb506d..b57f3efc8d806a20518ad4b636596e0b2e88e353 100644 --- a/src/mol-plugin/ui/controls/slider.tsx +++ b/src/mol-plugin/ui/controls/slider.tsx @@ -5,6 +5,8 @@ */ import * as React from 'react' +import { NumericInput } from './common'; +import { noop } from 'mol-util'; export class Slider extends React.Component<{ min: number, @@ -12,7 +14,8 @@ export class Slider extends React.Component<{ value: number, step?: number, onChange: (v: number) => void, - disabled?: boolean + disabled?: boolean, + onEnter?: () => void }, { isChanging: boolean, current: number }> { state = { isChanging: false, current: 0 } @@ -35,18 +38,35 @@ export class Slider extends React.Component<{ this.setState({ current }); } + updateManually = (v: number) => { + this.setState({ isChanging: true }); + + let n = v; + if (this.props.step === 1) n = Math.round(n); + if (n < this.props.min) n = this.props.min; + if (n > this.props.max) n = this.props.max; + + this.setState({ current: n, isChanging: true }); + } + + onManualBlur = () => { + this.setState({ isChanging: false }); + this.props.onChange(this.state.current); + } + render() { let step = this.props.step; if (step === void 0) step = 1; return <div className='msp-slider'> <div> - <div> - <SliderBase min={this.props.min} max={this.props.max} step={step} value={this.state.current} disabled={this.props.disabled} - onBeforeChange={this.begin} - onChange={this.updateCurrent as any} onAfterChange={this.end as any} /> - </div></div> + <SliderBase min={this.props.min} max={this.props.max} step={step} value={this.state.current} disabled={this.props.disabled} + onBeforeChange={this.begin} + onChange={this.updateCurrent as any} onAfterChange={this.end as any} /> + </div> <div> - {`${Math.round(100 * this.state.current) / 100}`} + <NumericInput + value={this.state.current} blurOnEnter={true} onBlur={this.onManualBlur} + isDisabled={this.props.disabled} onChange={this.updateManually} /> </div> </div>; } @@ -58,7 +78,8 @@ export class Slider2 extends React.Component<{ value: [number, number], step?: number, onChange: (v: [number, number]) => void, - disabled?: boolean + disabled?: boolean, + onEnter?: () => void }, { isChanging: boolean, current: [number, number] }> { state = { isChanging: false, current: [0, 1] as [number, number] } @@ -81,20 +102,41 @@ export class Slider2 extends React.Component<{ this.setState({ current }); } + updateMax = (v: number) => { + let n = v; + if (this.props.step === 1) n = Math.round(n); + if (n < this.state.current[0]) n = this.state.current[0] + else if (n < this.props.min) n = this.props.min; + if (n > this.props.max) n = this.props.max; + this.props.onChange([this.state.current[0], n]); + } + + updateMin = (v: number) => { + let n = v; + if (this.props.step === 1) n = Math.round(n); + if (n < this.props.min) n = this.props.min; + if (n > this.state.current[1]) n = this.state.current[1]; + else if (n > this.props.max) n = this.props.max; + this.props.onChange([n, this.state.current[1]]); + } + render() { let step = this.props.step; if (step === void 0) step = 1; return <div className='msp-slider2'> <div> - {`${Math.round(100 * this.state.current[0]) / 100}`} + <NumericInput + value={this.state.current[0]} onEnter={this.props.onEnter} blurOnEnter={true} + isDisabled={this.props.disabled} onChange={this.updateMin} /> </div> <div> - <div> - <SliderBase min={this.props.min} max={this.props.max} step={step} value={this.state.current} disabled={this.props.disabled} - onBeforeChange={this.begin} onChange={this.updateCurrent as any} onAfterChange={this.end as any} range={true} pushable={true} /> - </div></div> + <SliderBase min={this.props.min} max={this.props.max} step={step} value={this.state.current} disabled={this.props.disabled} + onBeforeChange={this.begin} onChange={this.updateCurrent as any} onAfterChange={this.end as any} range={true} pushable={true} /> + </div> <div> - {`${Math.round(100 * this.state.current[1]) / 100}`} + <NumericInput + value={this.state.current[1]} onEnter={this.props.onEnter} blurOnEnter={true} + isDisabled={this.props.disabled} onChange={this.updateMax} /> </div> </div>; } @@ -102,10 +144,10 @@ export class Slider2 extends React.Component<{ /** * The following code was adapted from react-components/slider library. - * + * * The MIT License (MIT) * Copyright (c) 2015-present Alipay.com, https://www.alipay.com/ - * + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights @@ -116,12 +158,12 @@ export class Slider2 extends React.Component<{ * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS - * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ @@ -151,9 +193,6 @@ function classNames(_classes: { [name: string]: boolean | number }) { return classes.join(' '); } -function noop() { -} - function isNotTouchEvent(e: TouchEvent) { return e.touches.length > 1 || (e.type.toLowerCase() === 'touchend' && e.touches.length > 0); } @@ -540,7 +579,7 @@ export class SliderBase extends React.Component<SliderBaseProps, SliderBaseState } return false; - // return this.state.bounds.some((x, i) => e.target + // return this.state.bounds.some((x, i) => e.target // ( // //this.handleElements[i] && e.target === ReactDOM.findDOMNode(this.handleElements[i]) @@ -702,7 +741,7 @@ export class SliderBase extends React.Component<SliderBaseProps, SliderBaseState dragging: handle === i, index: i, key: i, - ref: (h: any) => this.handleElements.push(h) //`handle-${i}`, + ref: (h: any) => this.handleElements.push(h) // `handle-${i}`, })); if (!range) { handles.shift(); } diff --git a/src/mol-plugin/ui/plugin.tsx b/src/mol-plugin/ui/plugin.tsx index 3118cc358799d9a90efd78eae81f6d9c2c492ec1..12b4855985287c87f1a55501fe9c15370234b296 100644 --- a/src/mol-plugin/ui/plugin.tsx +++ b/src/mol-plugin/ui/plugin.tsx @@ -4,22 +4,22 @@ * @author David Sehnal <david.sehnal@gmail.com> */ +import { List } from 'immutable'; +import { PluginState } from 'mol-plugin/state'; +import { formatTime } from 'mol-util'; +import { LogEntry } from 'mol-util/log-entry'; import * as React from 'react'; import { PluginContext } from '../context'; -import { StateTree } from './state-tree'; -import { Viewport, ViewportControls } from './viewport'; -import { Controls, TrajectoryControls, LociLabelControl } from './controls'; -import { PluginComponent, PluginReactContext } from './base'; +import { PluginReactContext, PluginUIComponent } from './base'; import { CameraSnapshots } from './camera'; +import { Controls, LociLabelControl, TrajectoryControls } from './controls'; import { StateSnapshots } from './state'; -import { List } from 'immutable'; -import { LogEntry } from 'mol-util/log-entry'; -import { formatTime } from 'mol-util'; +import { StateObjectActions } from './state/actions'; +import { AnimationControls } from './state/animation'; +import { StateTree } from './state/tree'; import { BackgroundTaskProgress } from './task'; -import { ApplyActionContol } from './state/apply-action'; -import { PluginState } from 'mol-plugin/state'; -import { UpdateTransformContol } from './state/update-transform'; -import { StateObjectCell } from 'mol-state'; +import { Viewport, ViewportControls } from './viewport'; +import { StateTransform } from 'mol-state'; export class Plugin extends React.Component<{ plugin: PluginContext }, {}> { @@ -38,9 +38,9 @@ export class Plugin extends React.Component<{ plugin: PluginContext }, {}> { } } -class Layout extends PluginComponent { +class Layout extends PluginUIComponent { componentDidMount() { - this.subscribe(this.plugin.layout.updated, () => this.forceUpdate()); + this.subscribe(this.plugin.layout.events.updated, () => this.forceUpdate()); } region(kind: 'left' | 'right' | 'bottom' | 'main', element: JSX.Element) { @@ -52,7 +52,7 @@ class Layout extends PluginComponent { } render() { - const layout = this.plugin.layout.latestState; + const layout = this.plugin.layout.state; return <div className='msp-plugin'> <div className={`msp-plugin-content ${layout.isExpanded ? 'msp-layout-expanded' : 'msp-layout-standard msp-layout-standard-outside'}`}> <div className={layout.showControls ? 'msp-layout-hide-top' : 'msp-layout-hide-top msp-layout-hide-right msp-layout-hide-bottom msp-layout-hide-left'}> @@ -61,6 +61,7 @@ class Layout extends PluginComponent { {layout.showControls && this.region('right', <div className='msp-scrollable-container msp-right-controls'> <CurrentObject /> <Controls /> + <AnimationControls /> <CameraSnapshots /> <StateSnapshots /> </div>)} @@ -71,7 +72,7 @@ class Layout extends PluginComponent { } } -export class ViewportWrapper extends PluginComponent { +export class ViewportWrapper extends PluginUIComponent { render() { return <> <Viewport /> @@ -89,7 +90,7 @@ export class ViewportWrapper extends PluginComponent { } } -export class State extends PluginComponent { +export class State extends PluginUIComponent { componentDidMount() { this.subscribe(this.plugin.state.behavior.kind, () => this.forceUpdate()); } @@ -103,26 +104,26 @@ export class State extends PluginComponent { const kind = this.plugin.state.behavior.kind.value; return <div className='msp-scrollable-container'> <div className='msp-btn-row-group msp-data-beh'> - <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.set('data')} style={{ fontWeight: kind === 'data' ? 'bold' : 'normal'}}>Data</button> - <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.set('behavior')} style={{ fontWeight: kind === 'behavior' ? 'bold' : 'normal'}}>Behavior</button> + <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.set('data')} style={{ fontWeight: kind === 'data' ? 'bold' : 'normal' }}>Data</button> + <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.set('behavior')} style={{ fontWeight: kind === 'behavior' ? 'bold' : 'normal' }}>Behavior</button> </div> <StateTree state={kind === 'data' ? this.plugin.state.dataState : this.plugin.state.behaviorState} /> </div> } } -export class Log extends PluginComponent<{}, { entries: List<LogEntry> }> { +export class Log extends PluginUIComponent<{}, { entries: List<LogEntry> }> { private wrapper = React.createRef<HTMLDivElement>(); componentDidMount() { - this.subscribe(this.plugin.events.log, () => this.setState({ entries: this.plugin.log.entries.takeLast(100).toList() })); + this.subscribe(this.plugin.events.log, () => this.setState({ entries: this.plugin.log.entries })); } componentDidUpdate() { this.scrollToBottom(); } - state = { entries: this.plugin.log.entries.takeLast(100).toList() }; + state = { entries: this.plugin.log.entries }; private scrollToBottom() { const log = this.wrapper.current; @@ -130,19 +131,26 @@ export class Log extends PluginComponent<{}, { entries: List<LogEntry> }> { } render() { + // TODO: ability to show full log + // showing more entries dramatically slows animations. + const maxEntries = 10; + const xs = this.state.entries, l = xs.size; + const entries: JSX.Element[] = []; + for (let i = Math.max(0, l - maxEntries), o = 0; i < l; i++) { + const e = xs.get(i); + entries.push(<li key={o++}> + <div className={'msp-log-entry-badge msp-log-entry-' + e!.type} /> + <div className='msp-log-timestamp'>{formatTime(e!.timestamp)}</div> + <div className='msp-log-entry'>{e!.message}</div> + </li>); + } return <div ref={this.wrapper} className='msp-log' style={{ position: 'absolute', top: '0', right: '0', bottom: '0', left: '0', overflowY: 'auto' }}> - <ul className='msp-list-unstyled'> - {this.state.entries.map((e, i) => <li key={i}> - <div className={'msp-log-entry-badge msp-log-entry-' + e!.type} /> - <div className='msp-log-timestamp'>{formatTime(e!.timestamp)}</div> - <div className='msp-log-entry'>{e!.message}</div> - </li>)} - </ul> + <ul className='msp-list-unstyled'>{entries}</ul> </div>; } } -export class CurrentObject extends PluginComponent { +export class CurrentObject extends PluginUIComponent { get current() { return this.plugin.state.behavior.currentObject.value; } @@ -163,21 +171,21 @@ export class CurrentObject extends PluginComponent { const current = this.current; const ref = current.ref; const cell = current.state.cells.get(ref)!; - const parent: StateObjectCell | undefined = (cell.sourceRef && current.state.cells.get(cell.sourceRef)!) || void 0; - - const type = cell && cell.obj ? cell.obj.type : void 0; const transform = cell.transform; const def = transform.transformer.definition; + const display = cell.obj ? cell.obj.label : (def.display && def.display.name) || def.name; - const actions = type ? current.state.actions.fromType(type) : []; - return <> - <div className='msp-section-header'> - {cell.obj ? cell.obj.label : (def.display && def.display.name) || def.name} - </div> - { (parent && parent.status === 'ok') && <UpdateTransformContol state={current.state} transform={transform} /> } - {cell.status === 'ok' && - actions.map((act, i) => <ApplyActionContol plugin={this.plugin} key={`${act.id}`} state={current.state} action={act} nodeRef={ref} />) - } + let showActions = true; + if (ref === StateTransform.RootRef) { + const children = current.state.tree.children.get(ref); + showActions = children.size !== 0; + } + + if (!showActions) return null; + + return cell.status === 'ok' && <> + <div className='msp-section-header'>{`Actions (${display})`}</div> + <StateObjectActions state={current.state} nodeRef={ref} /> </>; } } \ No newline at end of file diff --git a/src/mol-plugin/ui/state.tsx b/src/mol-plugin/ui/state.tsx index b9a16ad129c0e3dd4fc737170b9675fe757ba850..6e13eb42e85f0cd17e167e54713ea618840bee76 100644 --- a/src/mol-plugin/ui/state.tsx +++ b/src/mol-plugin/ui/state.tsx @@ -6,14 +6,14 @@ import { PluginCommands } from 'mol-plugin/command'; import * as React from 'react'; -import { PluginComponent } from './base'; +import { PluginUIComponent } from './base'; import { shallowEqual } from 'mol-util'; import { List } from 'immutable'; import { ParameterControls } from './controls/parameters'; import { ParamDefinition as PD} from 'mol-util/param-definition'; import { Subject } from 'rxjs'; -export class StateSnapshots extends PluginComponent<{ }, { serverUrl: string }> { +export class StateSnapshots extends PluginUIComponent<{ }, { serverUrl: string }> { state = { serverUrl: 'https://webchem.ncbr.muni.cz/molstar-state' } updateServerUrl = (serverUrl: string) => { this.setState({ serverUrl }) }; @@ -31,7 +31,7 @@ export class StateSnapshots extends PluginComponent<{ }, { serverUrl: string }> // TODO: this is not nice: device some custom event system. const UploadedEvent = new Subject(); -class StateSnapshotControls extends PluginComponent<{ serverUrl: string, serverChanged: (url: string) => void }, { name: string, description: string, serverUrl: string, isUploading: boolean }> { +class StateSnapshotControls extends PluginUIComponent<{ serverUrl: string, serverChanged: (url: string) => void }, { name: string, description: string, serverUrl: string, isUploading: boolean }> { state = { name: '', description: '', serverUrl: this.props.serverUrl, isUploading: false }; static Params = { @@ -93,7 +93,7 @@ class StateSnapshotControls extends PluginComponent<{ serverUrl: string, serverC } } -class LocalStateSnapshotList extends PluginComponent<{ }, { }> { +class LocalStateSnapshotList extends PluginUIComponent<{ }, { }> { componentDidMount() { this.subscribe(this.plugin.events.state.snapshots.changed, () => this.forceUpdate()); } @@ -110,7 +110,7 @@ class LocalStateSnapshotList extends PluginComponent<{ }, { }> { render() { return <ul style={{ listStyle: 'none' }} className='msp-state-list'> - {this.plugin.state.snapshots.entries.valueSeq().map(e =><li key={e!.id}> + {this.plugin.state.snapshots.state.entries.valueSeq().map(e =><li key={e!.id}> <button className='msp-btn msp-btn-block msp-form-control' onClick={this.apply(e!.id)}>{e!.name || e!.timestamp} <small>{e!.description}</small></button> <button onClick={this.remove(e!.id)} className='msp-btn msp-btn-link msp-state-list-remove-button'> <span className='msp-icon msp-icon-remove' /> @@ -121,7 +121,7 @@ class LocalStateSnapshotList extends PluginComponent<{ }, { }> { } type RemoteEntry = { url: string, removeUrl: string, timestamp: number, id: string, name: string, description: string } -class RemoteStateSnapshotList extends PluginComponent<{ serverUrl: string }, { entries: List<RemoteEntry>, isFetching: boolean }> { +class RemoteStateSnapshotList extends PluginUIComponent<{ serverUrl: string }, { entries: List<RemoteEntry>, isFetching: boolean }> { state = { entries: List<RemoteEntry>(), isFetching: false }; componentDidMount() { diff --git a/src/mol-plugin/ui/state/actions.tsx b/src/mol-plugin/ui/state/actions.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2d660af3b85ca7220cbd1ccc5976f5f1c5119c55 --- /dev/null +++ b/src/mol-plugin/ui/state/actions.tsx @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2018 - 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import * as React from 'react'; +import { PluginUIComponent } from '../base'; +import { ApplyActionContol } from './apply-action'; +import { State } from 'mol-state'; + +export class StateObjectActions extends PluginUIComponent<{ state: State, nodeRef: string }> { + get current() { + return this.plugin.state.behavior.currentObject.value; + } + + componentDidMount() { + this.subscribe(this.plugin.state.behavior.currentObject, o => { + this.forceUpdate(); + }); + + this.subscribe(this.plugin.events.state.object.updated, ({ ref, state }) => { + const current = this.current; + if (current.ref !== ref || current.state !== state) return; + this.forceUpdate(); + }); + } + + render() { + const { state, nodeRef: ref } = this.props; + const cell = state.cells.get(ref)!; + const actions = state.actions.fromCell(cell, this.plugin); + return actions.map((act, i) => <ApplyActionContol plugin={this.plugin} key={`${act.id}`} state={state} action={act} nodeRef={ref} />); + } +} \ No newline at end of file diff --git a/src/mol-plugin/ui/state/animation.tsx b/src/mol-plugin/ui/state/animation.tsx new file mode 100644 index 0000000000000000000000000000000000000000..64ebcc6c01f8474456d893097042057ca951cc17 --- /dev/null +++ b/src/mol-plugin/ui/state/animation.tsx @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import * as React from 'react'; +import { PluginUIComponent } from '../base'; +import { ParameterControls, ParamOnChange } from '../controls/parameters'; + +export class AnimationControls extends PluginUIComponent<{ }> { + componentDidMount() { + this.subscribe(this.plugin.state.animation.events.updated, () => this.forceUpdate()); + } + + updateParams: ParamOnChange = p => { + this.plugin.state.animation.updateParams({ [p.name]: p.value }); + } + + updateCurrentParams: ParamOnChange = p => { + this.plugin.state.animation.updateCurrentParams({ [p.name]: p.value }); + } + + startOrStop = () => { + const anim = this.plugin.state.animation; + if (anim.state.animationState === 'playing') anim.stop(); + else anim.start(); + } + + render() { + const anim = this.plugin.state.animation; + if (anim.isEmpty) return null; + + const isDisabled = anim.state.animationState === 'playing'; + + return <div className='msp-animation-section'> + <div className='msp-section-header'>Animations</div> + + <ParameterControls params={anim.getParams()} values={anim.state.params} onChange={this.updateParams} isDisabled={isDisabled} /> + <ParameterControls params={anim.current.params} values={anim.current.paramValues} onChange={this.updateCurrentParams} isDisabled={isDisabled} /> + + <div className='msp-btn-row-group'> + <button className='msp-btn msp-btn-block msp-form-control' onClick={this.startOrStop}> + {anim.state.animationState === 'playing' ? 'Stop' : 'Start'} + </button> + </div> + </div> + } +} \ No newline at end of file diff --git a/src/mol-plugin/ui/state/apply-action.tsx b/src/mol-plugin/ui/state/apply-action.tsx index c82faeee8d7ce946b1dff0ddb087fb05382520f8..702a5cfc7025694fd89cde0304ab9c1b186cc1fe 100644 --- a/src/mol-plugin/ui/state/apply-action.tsx +++ b/src/mol-plugin/ui/state/apply-action.tsx @@ -6,8 +6,7 @@ import { PluginCommands } from 'mol-plugin/command'; import { PluginContext } from 'mol-plugin/context'; -import { State, Transform } from 'mol-state'; -import { StateAction } from 'mol-state/action'; +import { State, StateTransform, StateAction } from 'mol-state'; import { memoizeLatest } from 'mol-util/memoize'; import { StateTransformParameters, TransformContolBase } from './common'; import { ParamDefinition as PD } from 'mol-util/param-definition'; @@ -17,13 +16,13 @@ export { ApplyActionContol }; namespace ApplyActionContol { export interface Props { plugin: PluginContext, - nodeRef: Transform.Ref, + nodeRef: StateTransform.Ref, state: State, action: StateAction } export interface ComponentState { - ref: Transform.Ref, + ref: StateTransform.Ref, version: string, params: any, error?: string, @@ -48,7 +47,7 @@ class ApplyActionContol extends TransformContolBase<ApplyActionContol.Props, App applyText() { return 'Apply'; } isUpdate() { return false; } - private _getInfo = memoizeLatest((t: Transform.Ref, v: string) => StateTransformParameters.infoFromAction(this.plugin, this.props.state, this.props.action, this.props.nodeRef)); + private _getInfo = memoizeLatest((t: StateTransform.Ref, v: string) => StateTransformParameters.infoFromAction(this.plugin, this.props.state, this.props.action, this.props.nodeRef)); state = { ref: this.props.nodeRef, version: this.props.state.transforms.get(this.props.nodeRef).version, error: void 0, isInitial: true, params: this.getInfo().initialValues, busy: false }; diff --git a/src/mol-plugin/ui/state/common.tsx b/src/mol-plugin/ui/state/common.tsx index daf5f2568aeca146e4cbf638d614a67ddc2906cf..57659f7aa9d237043a57242526ea1b1dc3dafc06 100644 --- a/src/mol-plugin/ui/state/common.tsx +++ b/src/mol-plugin/ui/state/common.tsx @@ -4,18 +4,17 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { State, Transform, Transformer } from 'mol-state'; +import { State, StateTransform, StateTransformer, StateAction } from 'mol-state'; import * as React from 'react'; -import { PurePluginComponent } from '../base'; +import { PurePluginUIComponent } from '../base'; import { ParameterControls, ParamOnChange } from '../controls/parameters'; -import { StateAction } from 'mol-state/action'; import { PluginContext } from 'mol-plugin/context'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import { Subject } from 'rxjs'; export { StateTransformParameters, TransformContolBase }; -class StateTransformParameters extends PurePluginComponent<StateTransformParameters.Props> { +class StateTransformParameters extends PurePluginUIComponent<StateTransformParameters.Props> { validate(params: any) { // TODO return void 0; @@ -61,7 +60,7 @@ namespace StateTransformParameters { return true; } - export function infoFromAction(plugin: PluginContext, state: State, action: StateAction, nodeRef: Transform.Ref): Props['info'] { + export function infoFromAction(plugin: PluginContext, state: State, action: StateAction, nodeRef: StateTransform.Ref): Props['info'] { const source = state.cells.get(nodeRef)!.obj!; const params = action.definition.params ? action.definition.params(source, plugin) : { }; const initialValues = PD.getDefaultValues(params); @@ -72,7 +71,7 @@ namespace StateTransformParameters { }; } - export function infoFromTransform(plugin: PluginContext, state: State, transform: Transform): Props['info'] { + export function infoFromTransform(plugin: PluginContext, state: State, transform: StateTransform): Props['info'] { const cell = state.cells.get(transform.ref)!; // const source: StateObjectCell | undefined = (cell.sourceRef && state.cells.get(cell.sourceRef)!) || void 0; // const create = transform.transformer.definition.params; @@ -88,7 +87,7 @@ namespace StateTransformParameters { } namespace TransformContolBase { - export interface ControlState { + export interface ComponentState { params: any, error?: string, busy: boolean, @@ -97,10 +96,10 @@ namespace TransformContolBase { } } -abstract class TransformContolBase<P, S extends TransformContolBase.ControlState> extends PurePluginComponent<P, S> { +abstract class TransformContolBase<P, S extends TransformContolBase.ComponentState> extends PurePluginUIComponent<P, S> { abstract applyAction(): Promise<void>; abstract getInfo(): StateTransformParameters.Props['info']; - abstract getHeader(): Transformer.Definition['display']; + abstract getHeader(): StateTransformer.Definition['display']; abstract canApply(): boolean; abstract getTransformerId(): string; abstract canAutoApply(newParams: any): boolean; @@ -167,7 +166,7 @@ abstract class TransformContolBase<P, S extends TransformContolBase.ControlState render() { const info = this.getInfo(); - if (info.isEmpty && this.isUpdate()) return null; + const isEmpty = info.isEmpty && this.isUpdate(); const display = this.getHeader(); @@ -176,17 +175,26 @@ abstract class TransformContolBase<P, S extends TransformContolBase.ControlState ? this.plugin.customParamEditors.get(tId)! : StateTransformParameters; - return <div className='msp-transform-wrapper'> + const wrapClass = this.isUpdate() + ? !isEmpty && !this.state.isCollapsed + ? 'msp-transform-update-wrapper' + : 'msp-transform-update-wrapper-collapsed' + : 'msp-transform-wrapper'; + + return <div className={wrapClass}> <div className='msp-transform-header'> - <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded}>{display.name}</button> - {!this.state.isCollapsed && <button className='msp-btn msp-btn-link msp-transform-default-params' onClick={this.setDefault} disabled={this.state.busy} style={{ float: 'right'}} title='Set default params'>↻</button>} + <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded} title={display.description}> + {display.name} + {!isEmpty && this.state.isCollapsed && this.isUpdate() && <small>Click to Edit</small>} + </button> </div> - {!this.state.isCollapsed && <> + {!isEmpty && !this.state.isCollapsed && <> <ParamEditor info={info} events={this.events} params={this.state.params} isDisabled={this.state.busy} /> <div className='msp-transform-apply-wrap'> + <button className='msp-btn msp-btn-block msp-transform-default-params' onClick={this.setDefault} disabled={this.state.busy} title='Set default params'>↻</button> <button className='msp-btn msp-btn-block msp-transform-refresh msp-form-control' title='Refresh params' onClick={this.refresh} disabled={this.state.busy || this.state.isInitial}> - ↶ Reset + ↶ Back </button> <div className='msp-transform-apply'> <button className={`msp-btn msp-btn-block msp-btn-commit msp-btn-commit-${this.canApply() ? 'on' : 'off'}`} onClick={this.apply} disabled={!this.canApply()}> diff --git a/src/mol-plugin/ui/state-tree.tsx b/src/mol-plugin/ui/state/tree.tsx similarity index 55% rename from src/mol-plugin/ui/state-tree.tsx rename to src/mol-plugin/ui/state/tree.tsx index 87123f35abcb6f93a966933e488c5815939f8328..d2fc8b245bb5ab0979760db376853fd10cfcc04a 100644 --- a/src/mol-plugin/ui/state-tree.tsx +++ b/src/mol-plugin/ui/state/tree.tsx @@ -1,23 +1,49 @@ /** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018 - 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> */ import * as React from 'react'; import { PluginStateObject } from 'mol-plugin/state/objects'; -import { State, StateObject } from 'mol-state' +import { State, StateObject, StateObjectCell, StateTransform } from 'mol-state' import { PluginCommands } from 'mol-plugin/command'; -import { PluginComponent } from './base'; +import { PluginUIComponent } from '../base'; +import { UpdateTransformContol } from './update-transform'; +import { StateObjectActions } from './actions'; +import { Observable, Subject } from 'rxjs'; + +export class StateTree extends PluginUIComponent<{ state: State }, { showActions: boolean }> { + state = { showActions: true }; + + componentDidMount() { + this.subscribe(this.plugin.events.state.cell.created, e => { + if (e.cell.transform.parent === StateTransform.RootRef) this.forceUpdate(); + }); + + this.subscribe(this.plugin.events.state.cell.removed, e => { + if (e.parent === StateTransform.RootRef) this.forceUpdate(); + }); + } + + static getDerivedStateFromProps(props: { state: State }, state: { showActions: boolean }) { + const n = props.state.tree.root.ref; + const children = props.state.tree.children.get(n); + const showActions = children.size === 0; + if (state.showActions === showActions) return null; + return { showActions }; + } -export class StateTree extends PluginComponent<{ state: State }> { render() { - const n = this.props.state.tree.root.ref; - return <StateTreeNode state={this.props.state} nodeRef={n} />; + const ref = this.props.state.tree.root.ref; + if (this.state.showActions) { + return <StateObjectActions state={this.props.state} nodeRef={ref} /> + } + return <StateTreeNode state={this.props.state} nodeRef={ref} depth={0} />; } } -class StateTreeNode extends PluginComponent<{ nodeRef: string, state: State }, { state: State, isCollapsed: boolean }> { +class StateTreeNode extends PluginUIComponent<{ nodeRef: string, state: State, depth: number }, { state: State, isCollapsed: boolean }> { is(e: State.ObjectEvent) { return e.ref === this.props.nodeRef && e.state === this.props.state; } @@ -60,24 +86,37 @@ class StateTreeNode extends PluginComponent<{ nodeRef: string, state: State }, { } render() { - if (this.props.state.cells.get(this.props.nodeRef)!.obj === StateObject.Null) return null; + const cell = this.props.state.cells.get(this.props.nodeRef)!; + if (cell.obj === StateObject.Null) return null; const cellState = this.cellState; - + const showLabel = cell.status !== 'ok' || !cell.transform.props || !cell.transform.props.isGhost; const children = this.props.state.tree.children.get(this.props.nodeRef); - return <div> - <StateTreeNodeLabel nodeRef={this.props.nodeRef} state={this.props.state} /> + const newDepth = showLabel ? this.props.depth + 1 : this.props.depth; + + if (!showLabel) { + if (children.size === 0) return null; + return <div style={{ display: cellState.isCollapsed ? 'none' : 'block' }}> + {children.map(c => <StateTreeNode state={this.props.state} nodeRef={c!} key={c} depth={newDepth} />)} + </div>; + } + + return <> + <StateTreeNodeLabel nodeRef={this.props.nodeRef} state={this.props.state} depth={this.props.depth} /> {children.size === 0 ? void 0 - : <div className='msp-tree-children' style={{ display: cellState.isCollapsed ? 'none' : 'block' }}> - {children.map(c => <StateTreeNode state={this.props.state} nodeRef={c!} key={c} />)} + : <div style={{ display: cellState.isCollapsed ? 'none' : 'block' }}> + {children.map(c => <StateTreeNode state={this.props.state} nodeRef={c!} key={c} depth={newDepth} />)} </div> } - </div>; + </>; } } -class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State }, { state: State, isCurrent: boolean, isCollapsed: boolean }> { +class StateTreeNodeLabel extends PluginUIComponent< + { nodeRef: string, state: State, depth: number }, + { state: State, isCurrent: boolean, isCollapsed: boolean, updaterCollapsed: boolean }> { + is(e: State.ObjectEvent) { return e.ref === this.props.nodeRef && e.state === this.props.state; } @@ -107,7 +146,8 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State state = { isCurrent: this.props.state.current === this.props.nodeRef, isCollapsed: this.props.state.cellStates.get(this.props.nodeRef).isCollapsed, - state: this.props.state + state: this.props.state, + updaterCollapsed: true } static getDerivedStateFromProps(props: { nodeRef: string, state: State }, state: { state: State, isCurrent: boolean, isCollapsed: boolean }) { @@ -115,18 +155,20 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State return { isCurrent: props.state.current === props.nodeRef, isCollapsed: props.state.cellStates.get(props.nodeRef).isCollapsed, - state: props.state + state: props.state, + updaterCollapsed: true }; } setCurrent = (e: React.MouseEvent<HTMLElement>) => { e.preventDefault(); + e.currentTarget.blur(); PluginCommands.State.SetCurrentObject.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef }); } remove = (e: React.MouseEvent<HTMLElement>) => { e.preventDefault(); - PluginCommands.State.RemoveObject.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef }); + PluginCommands.State.RemoveObject.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef, removeParentGhosts: true }); } toggleVisible = (e: React.MouseEvent<HTMLElement>) => { @@ -153,6 +195,13 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State e.currentTarget.blur(); } + private toggleUpdaterObs = new Subject(); + toggleUpdater = (e: React.MouseEvent<HTMLAnchorElement>) => { + e.preventDefault(); + e.currentTarget.blur(); + this.toggleUpdaterObs.next(); + } + render() { const n = this.props.state.transforms.get(this.props.nodeRef)!; const cell = this.props.state.cells.get(this.props.nodeRef)!; @@ -171,26 +220,63 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State } else { const obj = cell.obj as PluginStateObject.Any; const title = `${obj.label} ${obj.description ? obj.description : ''}` - label = <><a title={title} href='#' onClick={this.setCurrent}>{obj.label}</a> {obj.description ? <small>{obj.description}</small> : void 0}</>; + if (this.state.isCurrent) { + label = <><a title={title} href='#' onClick={this.toggleUpdater}><b>{obj.label}</b> {obj.description ? <small>{obj.description}</small> : void 0}</a></>; + } else { + label = <><a title={title} href='#' onClick={this.setCurrent}>{obj.label} {obj.description ? <small>{obj.description}</small> : void 0}</a></>; + } } const children = this.props.state.tree.children.get(this.props.nodeRef); const cellState = this.props.state.cellStates.get(this.props.nodeRef); - const remove = <button onClick={this.remove} className='msp-btn msp-btn-link msp-tree-remove-button'> - <span className='msp-icon msp-icon-remove' /> - </button>; - const visibility = <button onClick={this.toggleVisible} className={`msp-btn msp-btn-link msp-tree-visibility${cellState.isHidden ? ' msp-tree-visibility-hidden' : ''}`}> <span className='msp-icon msp-icon-visual-visibility' /> </button>; - return <div className={`msp-tree-row${isCurrent ? ' msp-tree-row-current' : ''}`} onMouseEnter={this.highlight} onMouseLeave={this.clearHighlight}> - {isCurrent ? <b>{label}</b> : label} + const style: React.HTMLAttributes<HTMLDivElement>['style'] = { + marginLeft: this.state.isCurrent ? void 0 : `${this.props.depth * 10}px`, + // paddingLeft: !this.state.isCurrent ? void 0 : `${this.props.depth * 10}px`, + borderLeft: isCurrent || this.props.depth === 0 ? 'none' : void 0 + } + + const row = <div className={`msp-tree-row${isCurrent ? ' msp-tree-row-current' : ''}`} onMouseEnter={this.highlight} onMouseLeave={this.clearHighlight} style={style}> + {label} {children.size > 0 && <button onClick={this.toggleExpanded} className='msp-btn msp-btn-link msp-tree-toggle-exp-button'> <span className={`msp-icon msp-icon-${cellState.isCollapsed ? 'expand' : 'collapse'}`} /> </button>} - {remove}{visibility} - </div> + {!cell.transform.props.isLocked && <button onClick={this.remove} className='msp-btn msp-btn-link msp-tree-remove-button'> + <span className='msp-icon msp-icon-remove' /> + </button>}{visibility} + </div>; + + if (this.state.isCurrent) { + return <> + {row} + <StateTreeNodeTransform {...this.props} toggleCollapsed={this.toggleUpdaterObs} /> + </> + } + + return row; + } +} + +class StateTreeNodeTransform extends PluginUIComponent<{ nodeRef: string, state: State, depth: number, toggleCollapsed?: Observable<any> }> { + componentDidMount() { + // this.subscribe(this.plugin.events.state.object.updated, ({ ref, state }) => { + // if (this.props.nodeRef !== ref || this.props.state !== state) return; + // this.forceUpdate(); + // }); + } + + render() { + const ref = this.props.nodeRef; + const cell = this.props.state.cells.get(ref)!; + const parent: StateObjectCell | undefined = (cell.sourceRef && this.props.state.cells.get(cell.sourceRef)!) || void 0; + + if (!parent || parent.status !== 'ok') return null; + + const transform = cell.transform; + return <UpdateTransformContol state={this.props.state} transform={transform} initiallyCollapsed={true} toggleCollapsed={this.props.toggleCollapsed} />; } } \ No newline at end of file diff --git a/src/mol-plugin/ui/state/update-transform.tsx b/src/mol-plugin/ui/state/update-transform.tsx index ceff41e16f33a7debcd7edb87bb8693c79a58ff6..e6c0bc198bfdb2d7abf330d1aaab0b07a223a13d 100644 --- a/src/mol-plugin/ui/state/update-transform.tsx +++ b/src/mol-plugin/ui/state/update-transform.tsx @@ -4,24 +4,23 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { State, Transform } from 'mol-state'; +import { State, StateTransform } from 'mol-state'; import { memoizeLatest } from 'mol-util/memoize'; import { StateTransformParameters, TransformContolBase } from './common'; +import { Observable } from 'rxjs'; export { UpdateTransformContol }; namespace UpdateTransformContol { export interface Props { - transform: Transform, - state: State + transform: StateTransform, + state: State, + toggleCollapsed?: Observable<any>, + initiallyCollapsed?: boolean } - export interface ComponentState { - transform: Transform, - params: any, - error?: string, - busy: boolean, - isInitial: boolean + export interface ComponentState extends TransformContolBase.ComponentState { + transform: StateTransform } } @@ -46,9 +45,23 @@ class UpdateTransformContol extends TransformContolBase<UpdateTransformContol.Pr return autoUpdate({ a: cell.obj!, b: parentCell.obj!, oldParams: this.getInfo().initialValues, newParams }, this.plugin); } - private _getInfo = memoizeLatest((t: Transform) => StateTransformParameters.infoFromTransform(this.plugin, this.props.state, this.props.transform)); + componentDidMount() { + if (super.componentDidMount) super.componentDidMount(); - state: UpdateTransformContol.ComponentState = { transform: this.props.transform, error: void 0, isInitial: true, params: this.getInfo().initialValues, busy: false }; + if (this.props.toggleCollapsed) this.subscribe(this.props.toggleCollapsed, () => this.setState({ isCollapsed: !this.state.isCollapsed })); + + this.subscribe(this.plugin.events.state.object.updated, ({ ref, state }) => { + if (this.props.transform.ref !== ref || this.props.state !== state) return; + if (this.state.params !== this.props.transform.params) { + this._getInfo = memoizeLatest((t: StateTransform) => StateTransformParameters.infoFromTransform(this.plugin, this.props.state, t)); + this.setState({ params: this.props.transform.params, isInitial: true }) + } + }); + } + + private _getInfo = memoizeLatest((t: StateTransform) => StateTransformParameters.infoFromTransform(this.plugin, this.props.state, t)); + + state: UpdateTransformContol.ComponentState = { transform: this.props.transform, error: void 0, isInitial: true, params: this.getInfo().initialValues, busy: false, isCollapsed: this.props.initiallyCollapsed }; static getDerivedStateFromProps(props: UpdateTransformContol.Props, state: UpdateTransformContol.ComponentState) { if (props.transform === state.transform) return null; diff --git a/src/mol-plugin/ui/task.tsx b/src/mol-plugin/ui/task.tsx index c45b783ea05824070df202b5e24712b37cf61c09..20b1c7f14a072a0276ef709d4893c9f0785e8f8f 100644 --- a/src/mol-plugin/ui/task.tsx +++ b/src/mol-plugin/ui/task.tsx @@ -5,13 +5,13 @@ */ import * as React from 'react'; -import { PluginComponent } from './base'; +import { PluginUIComponent } from './base'; import { OrderedMap } from 'immutable'; import { TaskManager } from 'mol-plugin/util/task-manager'; import { filter } from 'rxjs/operators'; import { Progress } from 'mol-task'; -export class BackgroundTaskProgress extends PluginComponent<{ }, { tracked: OrderedMap<number, TaskManager.ProgressEvent> }> { +export class BackgroundTaskProgress extends PluginUIComponent<{ }, { tracked: OrderedMap<number, TaskManager.ProgressEvent> }> { componentDidMount() { this.subscribe(this.plugin.events.task.progress.pipe(filter(e => e.level !== 'none')), e => { this.setState({ tracked: this.state.tracked.set(e.id, e) }) @@ -30,7 +30,7 @@ export class BackgroundTaskProgress extends PluginComponent<{ }, { tracked: Orde } } -class ProgressEntry extends PluginComponent<{ event: TaskManager.ProgressEvent }> { +class ProgressEntry extends PluginUIComponent<{ event: TaskManager.ProgressEvent }> { render() { const root = this.props.event.progress.root; const subtaskCount = countSubtasks(this.props.event.progress.root) - 1; diff --git a/src/mol-plugin/ui/viewport.tsx b/src/mol-plugin/ui/viewport.tsx index b23bf8977b3a8aa7e33481ba23ae61b17863d00b..ce917a04dbbaa269c33d1c9115bdec8f818f5baf 100644 --- a/src/mol-plugin/ui/viewport.tsx +++ b/src/mol-plugin/ui/viewport.tsx @@ -8,7 +8,7 @@ import * as React from 'react'; import { ButtonsType } from 'mol-util/input/input-observer'; import { Canvas3dIdentifyHelper } from 'mol-plugin/util/canvas3d-identify'; -import { PluginComponent } from './base'; +import { PluginUIComponent } from './base'; import { PluginCommands } from 'mol-plugin/command'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import { ParameterControls } from './controls/parameters'; @@ -20,7 +20,7 @@ interface ViewportState { noWebGl: boolean } -export class ViewportControls extends PluginComponent { +export class ViewportControls extends PluginUIComponent { state = { isSettingsExpanded: false } @@ -35,11 +35,11 @@ export class ViewportControls extends PluginComponent { } toggleControls = () => { - PluginCommands.Layout.Update.dispatch(this.plugin, { state: { showControls: !this.plugin.layout.latestState.showControls } }); + PluginCommands.Layout.Update.dispatch(this.plugin, { state: { showControls: !this.plugin.layout.state.showControls } }); } toggleExpanded = () => { - PluginCommands.Layout.Update.dispatch(this.plugin, { state: { isExpanded: !this.plugin.layout.latestState.isExpanded } }); + PluginCommands.Layout.Update.dispatch(this.plugin, { state: { isExpanded: !this.plugin.layout.state.isExpanded } }); } setSettings = (p: { param: PD.Base<any>, name: string, value: any }) => { @@ -55,7 +55,7 @@ export class ViewportControls extends PluginComponent { this.forceUpdate(); }); - this.subscribe(this.plugin.layout.updated, () => { + this.subscribe(this.plugin.layout.events.updated, () => { this.forceUpdate(); }); } @@ -73,15 +73,15 @@ export class ViewportControls extends PluginComponent { // TODO: show some icons dimmed etc.. return <div className={'msp-viewport-controls'}> <div className='msp-viewport-controls-buttons'> - {this.icon('tools', this.toggleControls, 'Toggle Controls', this.plugin.layout.latestState.showControls)} - {this.icon('expand-layout', this.toggleExpanded, 'Toggle Expanded', this.plugin.layout.latestState.isExpanded)} + {this.icon('tools', this.toggleControls, 'Toggle Controls', this.plugin.layout.state.showControls)} + {this.icon('expand-layout', this.toggleExpanded, 'Toggle Expanded', this.plugin.layout.state.isExpanded)} {this.icon('settings', this.toggleSettingsExpanded, 'Settings', this.state.isSettingsExpanded)} {this.icon('reset-scene', this.resetCamera, 'Reset Camera')} </div> {this.state.isSettingsExpanded && <div className='msp-viewport-controls-scene-options'> <ControlGroup header='Layout' initialExpanded={true}> - <ParameterControls params={PluginLayoutStateParams} values={this.plugin.layout.latestState} onChange={this.setLayout} /> + <ParameterControls params={PluginLayoutStateParams} values={this.plugin.layout.state} onChange={this.setLayout} /> </ControlGroup> <ControlGroup header='Viewport' initialExpanded={true}> <ParameterControls params={Canvas3DParams} values={this.plugin.canvas3d.props} onChange={this.setSettings} /> @@ -91,7 +91,7 @@ export class ViewportControls extends PluginComponent { } } -export class Viewport extends PluginComponent<{ }, ViewportState> { +export class Viewport extends PluginUIComponent<{ }, ViewportState> { private container = React.createRef<HTMLDivElement>(); private canvas = React.createRef<HTMLCanvasElement>(); @@ -128,7 +128,7 @@ export class Viewport extends PluginComponent<{ }, ViewportState> { idHelper.select(x, y); }); - this.subscribe(this.plugin.layout.updated, () => { + this.subscribe(this.plugin.layout.events.updated, () => { setTimeout(this.handleResize, 50); }); } diff --git a/src/mol-plugin/util/canvas3d-identify.ts b/src/mol-plugin/util/canvas3d-identify.ts index 74562d92b5c968166cd0f2346af1d2ef92c0ef55..0a54889f02385057cb7fdc581b5e7f39e48bfdeb 100644 --- a/src/mol-plugin/util/canvas3d-identify.ts +++ b/src/mol-plugin/util/canvas3d-identify.ts @@ -52,7 +52,7 @@ export class Canvas3dIdentifyHelper { } private animate: (t: number) => void = t => { - if (this.inside && t - this.prevT > 1000 / this.maxFps) { + if (!this.ctx.state.animation.isAnimating && this.inside && t - this.prevT > 1000 / this.maxFps) { this.prevT = t; this.currentIdentifyT = t; this.identify(false, t); diff --git a/src/mol-repr/representation.ts b/src/mol-repr/representation.ts index 5032bef59feeebf06e2b78c8a41b4ddad4644495..8900e9ee121c7e06df5b36397f81d5c1cb3dc38d 100644 --- a/src/mol-repr/representation.ts +++ b/src/mol-repr/representation.ts @@ -188,11 +188,7 @@ namespace Representation { } return renderObjects }, - get props() { - const props = {} - reprList.forEach(r => Object.assign(props, r.props)) - return props as P - }, + get props() { return currentProps }, get params() { return currentParams }, createOrUpdate: (props: Partial<P> = {}, data?: D) => { if (data && data !== currentData) { diff --git a/src/mol-repr/volume/isosurface.ts b/src/mol-repr/volume/isosurface.ts index 4504d8766e2e580261163279c377abaaf4f7b3a6..7ebfe7df4ba6f453bcd590045134b82967325380 100644 --- a/src/mol-repr/volume/isosurface.ts +++ b/src/mol-repr/volume/isosurface.ts @@ -19,23 +19,27 @@ import { VisualContext } from 'mol-repr/visual'; import { NullLocation } from 'mol-model/location'; import { Lines } from 'mol-geo/geometry/lines/lines'; -const IsoValueParam = PD.Conditioned( - VolumeIsoValue.relative(VolumeData.Empty.dataStats, 2), - { - 'absolute': PD.Converted( - (v: VolumeIsoValue) => VolumeIsoValue.toAbsolute(v).absoluteValue, - (v: number) => VolumeIsoValue.absolute(VolumeData.Empty.dataStats, v), - PD.Numeric(0.5, { min: -1, max: 1, step: 0.01 }) - ), - 'relative': PD.Converted( - (v: VolumeIsoValue) => VolumeIsoValue.toRelative(v).relativeValue, - (v: number) => VolumeIsoValue.relative(VolumeData.Empty.dataStats, v), - PD.Numeric(2, { min: -10, max: 10, step: 0.01 }) - ) - }, - (v: VolumeIsoValue) => v.kind === 'absolute' ? 'absolute' : 'relative', - (v: VolumeIsoValue, c: 'absolute' | 'relative') => c === 'absolute' ? VolumeIsoValue.toAbsolute(v) : VolumeIsoValue.toRelative(v) -) +export function createIsoValueParam(defaultValue: VolumeIsoValue) { + return PD.Conditioned( + defaultValue, + { + 'absolute': PD.Converted( + (v: VolumeIsoValue) => VolumeIsoValue.toAbsolute(v, VolumeData.One.dataStats).absoluteValue, + (v: number) => VolumeIsoValue.absolute(v), + PD.Numeric(0.5, { min: -1, max: 1, step: 0.01 }) + ), + 'relative': PD.Converted( + (v: VolumeIsoValue) => VolumeIsoValue.toRelative(v, VolumeData.One.dataStats).relativeValue, + (v: number) => VolumeIsoValue.relative(v), + PD.Numeric(2, { min: -10, max: 10, step: 0.01 }) + ) + }, + (v: VolumeIsoValue) => v.kind === 'absolute' ? 'absolute' : 'relative', + (v: VolumeIsoValue, c: 'absolute' | 'relative') => c === 'absolute' ? VolumeIsoValue.toAbsolute(v, VolumeData.One.dataStats) : VolumeIsoValue.toRelative(v, VolumeData.One.dataStats) + ) +} + +export const IsoValueParam = createIsoValueParam(VolumeIsoValue.relative(2)); type IsoValueParam = typeof IsoValueParam export const VolumeIsosurfaceParams = { @@ -50,7 +54,7 @@ export async function createVolumeIsosurfaceMesh(ctx: VisualContext, volume: Vol ctx.runtime.update({ message: 'Marching cubes...' }); const surface = await computeMarchingCubesMesh({ - isoLevel: VolumeIsoValue.toAbsolute(props.isoValue).absoluteValue, + isoLevel: VolumeIsoValue.toAbsolute(props.isoValue, volume.dataStats).absoluteValue, scalarField: volume.data }, mesh).runAsChild(ctx.runtime); @@ -88,7 +92,7 @@ export async function createVolumeIsosurfaceWireframe(ctx: VisualContext, volume ctx.runtime.update({ message: 'Marching cubes...' }); const wireframe = await computeMarchingCubesLines({ - isoLevel: VolumeIsoValue.toAbsolute(props.isoValue).absoluteValue, + isoLevel: VolumeIsoValue.toAbsolute(props.isoValue, volume.dataStats).absoluteValue, scalarField: volume.data }, lines).runAsChild(ctx.runtime) @@ -135,24 +139,26 @@ export const IsosurfaceParams = { export type IsosurfaceParams = typeof IsosurfaceParams export function getIsosurfaceParams(ctx: ThemeRegistryContext, volume: VolumeData) { const p = PD.clone(IsosurfaceParams) - const { min, max, mean, sigma } = volume.dataStats + const stats = volume.dataStats + const { min, max, mean, sigma } = stats p.isoValue = PD.Conditioned( - VolumeIsoValue.relative(volume.dataStats, 2), + VolumeIsoValue.relative(2), { 'absolute': PD.Converted( - (v: VolumeIsoValue) => VolumeIsoValue.toAbsolute(v).absoluteValue, - (v: number) => VolumeIsoValue.absolute(volume.dataStats, v), + (v: VolumeIsoValue) => VolumeIsoValue.toAbsolute(v, stats).absoluteValue, + (v: number) => VolumeIsoValue.absolute(v), PD.Numeric(mean, { min, max, step: sigma / 100 }) ), 'relative': PD.Converted( - (v: VolumeIsoValue) => VolumeIsoValue.toRelative(v).relativeValue, - (v: number) => VolumeIsoValue.relative(volume.dataStats, v), - PD.Numeric(2, { min: -10, max: 10, step: 0.001 }) + (v: VolumeIsoValue) => VolumeIsoValue.toRelative(v, stats).relativeValue, + (v: number) => VolumeIsoValue.relative(v), + PD.Numeric(2, { min: Math.floor((min - mean) / sigma), max: Math.ceil((max - mean) / sigma), step: Math.ceil((max - min) / sigma) / 100 }) ) }, (v: VolumeIsoValue) => v.kind === 'absolute' ? 'absolute' : 'relative', - (v: VolumeIsoValue, c: 'absolute' | 'relative') => c === 'absolute' ? VolumeIsoValue.toAbsolute(v) : VolumeIsoValue.toRelative(v) + (v: VolumeIsoValue, c: 'absolute' | 'relative') => c === 'absolute' ? VolumeIsoValue.toAbsolute(v, stats) : VolumeIsoValue.toRelative(v, stats) ) + return p } diff --git a/src/mol-state/action.ts b/src/mol-state/action.ts index 53b41b53b66b1727944f135ebdb4baf9176350be..31b597fc2fd2e1f09193ee9375f6ecc580b65d63 100644 --- a/src/mol-state/action.ts +++ b/src/mol-state/action.ts @@ -9,7 +9,8 @@ import { UUID } from 'mol-util'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import { StateObject, StateObjectCell } from './object'; import { State } from './state'; -import { Transformer } from './transformer'; +import { StateTransformer } from './transformer'; +import { StateTransform } from './transform'; export { StateAction }; @@ -45,7 +46,7 @@ namespace StateAction { run(params: ApplyParams<A, P>, globalCtx: unknown): T | Task<T>, /** Test if the transform can be applied to a given node */ - isApplicable?(a: A, globalCtx: unknown): boolean + isApplicable?(a: A, aTransform: StateTransform<any, A, any>, globalCtx: unknown): boolean } export interface Definition<A extends StateObject = StateObject, T = any, P extends {} = {}> extends DefinitionBase<A, T, P> { @@ -63,12 +64,15 @@ namespace StateAction { return action; } - export function fromTransformer<T extends Transformer>(transformer: T) { + export function fromTransformer<T extends StateTransformer>(transformer: T) { const def = transformer.definition; - return create<Transformer.From<T>, void, Transformer.Params<T>>({ + return create<StateTransformer.From<T>, void, StateTransformer.Params<T>>({ from: def.from, display: def.display, - params: def.params as Transformer.Definition<Transformer.From<T>, any, Transformer.Params<T>>['params'], + params: def.params as StateTransformer.Definition<StateTransformer.From<T>, any, StateTransformer.Params<T>>['params'], + isApplicable: transformer.definition.isApplicable + ? (a, t, ctx) => transformer.definition.isApplicable!(a, ctx) + : void 0, run({ cell, state, params }) { const tree = state.build().to(cell.transform.ref).apply(transformer, params); return state.updateTree(tree) as Task<void>; @@ -80,7 +84,8 @@ namespace StateAction { export interface Type<A extends StateObject.Ctor, P extends { }> { from?: A | A[], params?: PD.For<P> | ((a: StateObject.From<A>, globalCtx: any) => PD.For<P>), - display?: string | { name: string, description?: string } + display?: string | { name: string, description?: string }, + isApplicable?: DefinitionBase<StateObject.From<A>, any, P>['isApplicable'] } export interface Root { @@ -106,6 +111,7 @@ namespace StateAction { : !!info.params ? info.params as any : void 0, + isApplicable: info.isApplicable, ...(typeof def === 'function' ? { run: def } : def) diff --git a/src/mol-state/action/manager.ts b/src/mol-state/action/manager.ts index 4e489df48ccf19d884881f97fd7152a730062276..5a2f87b79cba66bc166831cb4d0a6bb805333759 100644 --- a/src/mol-state/action/manager.ts +++ b/src/mol-state/action/manager.ts @@ -4,9 +4,9 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { StateAction } from 'mol-state/action'; -import { StateObject } from '../object'; -import { Transformer } from 'mol-state/transformer'; +import { StateAction } from '../action'; +import { StateObject, StateObjectCell } from '../object'; +import { StateTransformer } from '../transformer'; export { StateActionManager } @@ -14,8 +14,8 @@ class StateActionManager { private actions: Map<StateAction['id'], StateAction> = new Map(); private fromTypeIndex = new Map<StateObject.Type, StateAction[]>(); - add(actionOrTransformer: StateAction | Transformer) { - const action = Transformer.is(actionOrTransformer) ? actionOrTransformer.toAction() : actionOrTransformer; + add(actionOrTransformer: StateAction | StateTransformer) { + const action = StateTransformer.is(actionOrTransformer) ? actionOrTransformer.toAction() : actionOrTransformer; if (this.actions.has(action.id)) return this; @@ -32,7 +32,31 @@ class StateActionManager { return this; } - fromType(type: StateObject.Type): ReadonlyArray<StateAction> { - return this.fromTypeIndex.get(type) || []; + fromCell(cell: StateObjectCell, ctx: unknown): ReadonlyArray<StateAction> { + const obj = cell.obj; + if (!obj) return []; + + const actions = this.fromTypeIndex.get(obj.type); + if (!actions) return []; + let hasTest = false; + for (const a of actions) { + if (a.definition.isApplicable) { + hasTest = true; + break; + } + } + if (!hasTest) return actions; + + const ret: StateAction[] = []; + for (const a of actions) { + if (a.definition.isApplicable) { + if (a.definition.isApplicable(obj, cell.transform, ctx)) { + ret.push(a); + } + } else { + ret.push(a); + } + } + return ret; } } \ No newline at end of file diff --git a/src/mol-state/index.ts b/src/mol-state/index.ts index 8ef37d2fd8463422cf850a1f5aafb69b448ac16c..5d8d630f6180271f2aa218fb35b5d4f391fdc3b9 100644 --- a/src/mol-state/index.ts +++ b/src/mol-state/index.ts @@ -5,7 +5,10 @@ */ export * from './object' +export * from './tree' export * from './state' +export * from './state/builder' +export * from './state/selection' export * from './transformer' -export * from './tree' -export * from './transform' \ No newline at end of file +export * from './transform' +export * from './action' \ No newline at end of file diff --git a/src/mol-state/object.ts b/src/mol-state/object.ts index 3e2df3343df0b95b85698bbbbdabcfbda6dbbbd6..93177ad2ed4639fe005826d8647353d7eda58a10 100644 --- a/src/mol-state/object.ts +++ b/src/mol-state/object.ts @@ -5,10 +5,10 @@ */ import { UUID } from 'mol-util'; -import { Transform } from './transform'; +import { StateTransform } from './transform'; import { ParamDefinition } from 'mol-util/param-definition'; import { State } from './state'; -import { StateSelection } from './state/selection'; +import { StateSelection } from 'mol-state'; export { StateObject, StateObjectCell } @@ -53,13 +53,12 @@ namespace StateObject { }; } -interface StateObjectCell { - transform: Transform, +interface StateObjectCell<T = StateObject> { + transform: StateTransform, // Which object was used as a parent to create data in this cell - sourceRef: Transform.Ref | undefined, + sourceRef: StateTransform.Ref | undefined, - version: string status: StateObjectCell.Status, params: { @@ -68,7 +67,7 @@ interface StateObjectCell { } | undefined; errorText?: string, - obj?: StateObject + obj?: T } namespace StateObjectCell { diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts index 42030da32278c8e08da5aefae600c17bf1d455ea..81b56a729fc1dac6954a907dbe5e90ee4617f040 100644 --- a/src/mol-state/state.ts +++ b/src/mol-state/state.ts @@ -6,13 +6,12 @@ import { StateObject, StateObjectCell } from './object'; import { StateTree } from './tree'; -import { Transform } from './transform'; -import { Transformer } from './transformer'; -import { UUID } from 'mol-util'; +import { StateTransform } from './transform'; +import { StateTransformer } from './transformer'; import { RuntimeContext, Task } from 'mol-task'; import { StateSelection } from './state/selection'; import { RxEventHelper } from 'mol-util/rx-event-helper'; -import { StateTreeBuilder } from './tree/builder'; +import { StateBuilder } from './state/builder'; import { StateAction } from './action'; import { StateActionManager } from './action/manager'; import { TransientTree } from './tree/transient'; @@ -23,10 +22,10 @@ import { ParamDefinition } from 'mol-util/param-definition'; export { State } class State { - private _tree: TransientTree = StateTree.createEmpty().asTransient(); + private _tree: TransientTree; protected errorFree = true; - private transformCache = new Map<Transform.Ref, unknown>(); + private transformCache = new Map<StateTransform.Ref, unknown>(); private ev = RxEventHelper.create(); @@ -35,7 +34,7 @@ class State { cell: { stateUpdated: this.ev<State.ObjectEvent & { cellState: StateObjectCell.State }>(), created: this.ev<State.ObjectEvent & { cell: StateObjectCell }>(), - removed: this.ev<State.ObjectEvent & { parent: Transform.Ref }>(), + removed: this.ev<State.ObjectEvent & { parent: StateTransform.Ref }>(), }, object: { updated: this.ev<State.ObjectEvent & { action: 'in-place' | 'recreate', obj: StateObject, oldObj?: StateObject }>(), @@ -47,7 +46,7 @@ class State { }; readonly behaviors = { - currentObject: this.ev.behavior<State.ObjectEvent>({ state: this, ref: Transform.RootRef }) + currentObject: this.ev.behavior<State.ObjectEvent>({ state: this, ref: StateTransform.RootRef }) }; readonly actions = new StateActionManager(); @@ -57,7 +56,7 @@ class State { get cellStates() { return (this._tree as StateTree).cellStates; } get current() { return this.behaviors.currentObject.value.ref; } - build() { return this._tree.build(); } + build() { return new StateBuilder.Root(this._tree); } readonly cells: State.Cells = new Map(); @@ -70,11 +69,11 @@ class State { return this.updateTree(tree); } - setCurrent(ref: Transform.Ref) { + setCurrent(ref: StateTransform.Ref) { this.behaviors.currentObject.next({ state: this, ref }); } - updateCellState(ref: Transform.Ref, stateOrProvider: ((old: StateObjectCell.State) => Partial<StateObjectCell.State>) | Partial<StateObjectCell.State>) { + updateCellState(ref: StateTransform.Ref, stateOrProvider: ((old: StateObjectCell.State) => Partial<StateObjectCell.State>) | Partial<StateObjectCell.State>) { const update = typeof stateOrProvider === 'function' ? stateOrProvider(this.tree.cellStates.get(ref)) : stateOrProvider; @@ -110,7 +109,7 @@ class State { * Creates a Task that applies the specified StateAction (i.e. must use run* on the result) * If no ref is specified, apply to root. */ - applyAction<A extends StateAction>(action: A, params: StateAction.Params<A>, ref: Transform.Ref = Transform.RootRef): Task<void> { + applyAction<A extends StateAction>(action: A, params: StateAction.Params<A>, ref: StateTransform.Ref = StateTransform.RootRef): Task<void> { return Task.create('Apply Action', ctx => { const cell = this.cells.get(ref); if (!cell) throw new Error(`'${ref}' does not exist.`); @@ -123,19 +122,19 @@ class State { /** * Reconcialites the existing state tree with the new version. * - * If the tree is StateTreeBuilder.To<T>, the corresponding StateObject is returned by the task. + * If the tree is StateBuilder.To<T>, the corresponding StateObject is returned by the task. * @param tree Tree instance or a tree builder instance * @param doNotReportTiming Indicates whether to log timing of the individual transforms */ - updateTree<T extends StateObject>(tree: StateTree | StateTreeBuilder | StateTreeBuilder.To<T>, doNotLogTiming?: boolean): Task<T> - updateTree(tree: StateTree | StateTreeBuilder, doNotLogTiming?: boolean): Task<void> - updateTree(tree: StateTree | StateTreeBuilder, doNotLogTiming: boolean = false): Task<any> { + updateTree<T extends StateObject>(tree: StateTree | StateBuilder | StateBuilder.To<T>, doNotLogTiming?: boolean): Task<T> + updateTree(tree: StateTree | StateBuilder, doNotLogTiming?: boolean): Task<void> + updateTree(tree: StateTree | StateBuilder, doNotLogTiming: boolean = false): Task<any> { return Task.create('Update Tree', async taskCtx => { let updated = false; try { const ctx = this.updateTreeAndCreateCtx(tree, taskCtx, doNotLogTiming); updated = await update(ctx); - if (StateTreeBuilder.isTo(tree)) { + if (StateBuilder.isTo(tree)) { const cell = this.select(tree.ref)[0]; return cell && cell.obj; } @@ -145,20 +144,20 @@ class State { }); } - private updateTreeAndCreateCtx(tree: StateTree | StateTreeBuilder, taskCtx: RuntimeContext, doNotLogTiming: boolean) { - const _tree = (StateTreeBuilder.is(tree) ? tree.getTree() : tree).asTransient(); + private updateTreeAndCreateCtx(tree: StateTree | StateBuilder, taskCtx: RuntimeContext, doNotLogTiming: boolean) { + const _tree = (StateBuilder.is(tree) ? tree.getTree() : tree).asTransient(); const oldTree = this._tree; this._tree = _tree; const ctx: UpdateContext = { parent: this, - editInfo: StateTreeBuilder.is(tree) ? tree.editInfo : void 0, + editInfo: StateBuilder.is(tree) ? tree.editInfo : void 0, errorFree: this.errorFree, taskCtx, oldTree, tree: _tree, - cells: this.cells as Map<Transform.Ref, StateObjectCell>, + cells: this.cells as Map<StateTransform.Ref, StateObjectCell>, transformCache: this.transformCache, results: [], @@ -175,16 +174,16 @@ class State { return ctx; } - constructor(rootObject: StateObject, params?: { globalContext?: unknown }) { + constructor(rootObject: StateObject, params?: { globalContext?: unknown, rootProps?: StateTransform.Props }) { + this._tree = StateTree.createEmpty(StateTransform.createRoot(params && params.rootProps)).asTransient(); const tree = this._tree; const root = tree.root; - (this.cells as Map<Transform.Ref, StateObjectCell>).set(root.ref, { + (this.cells as Map<StateTransform.Ref, StateObjectCell>).set(root.ref, { transform: root, sourceRef: void 0, obj: rootObject, status: 'ok', - version: root.version, errorText: void 0, params: { definition: {}, @@ -197,10 +196,10 @@ class State { } namespace State { - export type Cells = ReadonlyMap<Transform.Ref, StateObjectCell> + export type Cells = ReadonlyMap<StateTransform.Ref, StateObjectCell> export type Tree = StateTree - export type Builder = StateTreeBuilder + export type Builder = StateBuilder export interface ObjectEvent { state: State, @@ -211,22 +210,22 @@ namespace State { readonly tree: StateTree.Serialized } - export function create(rootObject: StateObject, params?: { globalContext?: unknown, defaultObjectProps?: unknown }) { + export function create(rootObject: StateObject, params?: { globalContext?: unknown, rootProps?: StateTransform.Props }) { return new State(rootObject, params); } } -type Ref = Transform.Ref +type Ref = StateTransform.Ref interface UpdateContext { parent: State, - editInfo: StateTreeBuilder.EditInfo | undefined + editInfo: StateBuilder.EditInfo | undefined errorFree: boolean, taskCtx: RuntimeContext, oldTree: StateTree, tree: TransientTree, - cells: Map<Transform.Ref, StateObjectCell>, + cells: Map<StateTransform.Ref, StateObjectCell>, transformCache: Map<Ref, unknown>, results: UpdateNodeResult[], @@ -243,7 +242,7 @@ async function update(ctx: UpdateContext) { // if only a single node was added/updated, we can skip potentially expensive diffing const fastTrack = !!(ctx.errorFree && ctx.editInfo && ctx.editInfo.count === 1 && ctx.editInfo.lastUpdate && ctx.editInfo.sourceTree === ctx.oldTree); - let deletes: Transform.Ref[], deletedObjects: (StateObject | undefined)[] = [], roots: Transform.Ref[]; + let deletes: StateTransform.Ref[], deletedObjects: (StateObject | undefined)[] = [], roots: StateTransform.Ref[]; if (fastTrack) { deletes = []; @@ -309,7 +308,7 @@ async function update(ctx: UpdateContext) { await updateSubtree(ctx, root); } - let newCurrent: Transform.Ref | undefined = ctx.newCurrent; + let newCurrent: StateTransform.Ref | undefined = ctx.newCurrent; // Raise object updated events for (const update of ctx.results) { if (update.action === 'created') { @@ -342,15 +341,15 @@ async function update(ctx: UpdateContext) { return deletes.length > 0 || roots.length > 0 || ctx.changed; } -function findUpdateRoots(cells: Map<Transform.Ref, StateObjectCell>, tree: StateTree) { +function findUpdateRoots(cells: Map<StateTransform.Ref, StateObjectCell>, tree: StateTree) { const findState = { roots: [] as Ref[], cells }; StateTree.doPreOrder(tree, tree.root, findState, findUpdateRootsVisitor); return findState.roots; } -function findUpdateRootsVisitor(n: Transform, _: any, s: { roots: Ref[], cells: Map<Ref, StateObjectCell> }) { +function findUpdateRootsVisitor(n: StateTransform, _: any, s: { roots: Ref[], cells: Map<Ref, StateObjectCell> }) { const cell = s.cells.get(n.ref); - if (!cell || cell.version !== n.version || cell.status === 'error') { + if (!cell || cell.transform.version !== n.version || cell.status === 'error') { s.roots.push(n.ref); return false; } @@ -360,7 +359,7 @@ function findUpdateRootsVisitor(n: Transform, _: any, s: { roots: Ref[], cells: } type FindDeletesCtx = { newTree: StateTree, cells: State.Cells, deletes: Ref[] } -function checkDeleteVisitor(n: Transform, _: any, ctx: FindDeletesCtx) { +function checkDeleteVisitor(n: StateTransform, _: any, ctx: FindDeletesCtx) { if (!ctx.newTree.transforms.has(n.ref) && ctx.cells.has(n.ref)) ctx.deletes.push(n.ref); } function findDeletes(ctx: UpdateContext): Ref[] { @@ -369,7 +368,7 @@ function findDeletes(ctx: UpdateContext): Ref[] { return deleteCtx.deletes; } -function syncStatesVisitor(n: Transform, tree: StateTree, oldState: StateTree.CellStates) { +function syncStatesVisitor(n: StateTransform, tree: StateTree, oldState: StateTree.CellStates) { if (!oldState.has(n.ref)) return; (tree as TransientTree).updateCellState(n.ref, oldState.get(n.ref)); } @@ -385,7 +384,7 @@ function setCellStatus(ctx: UpdateContext, ref: Ref, status: StateObjectCell.Sta if (changed) ctx.parent.events.cell.stateUpdated.next({ state: ctx.parent, ref, cellState: ctx.tree.cellStates.get(ref) }); } -function initCellStatusVisitor(t: Transform, _: any, ctx: UpdateContext) { +function initCellStatusVisitor(t: StateTransform, _: any, ctx: UpdateContext) { ctx.cells.get(t.ref)!.transform = t; setCellStatus(ctx, t.ref, 'pending'); } @@ -397,7 +396,7 @@ function initCellStatus(ctx: UpdateContext, roots: Ref[]) { } type InitCellsCtx = { ctx: UpdateContext, added: StateObjectCell[] } -function initCellsVisitor(transform: Transform, _: any, { ctx, added }: InitCellsCtx) { +function initCellsVisitor(transform: StateTransform, _: any, { ctx, added }: InitCellsCtx) { if (ctx.cells.has(transform.ref)) { return; } @@ -406,7 +405,6 @@ function initCellsVisitor(transform: Transform, _: any, { ctx, added }: InitCell transform, sourceRef: void 0, status: 'pending', - version: UUID.create22(), errorText: void 0, params: void 0 }; @@ -428,7 +426,7 @@ function findNewCurrent(tree: StateTree, start: Ref, deletes: Ref[], cells: Map< } function _findNewCurrent(tree: StateTree, ref: Ref, deletes: Set<Ref>, cells: Map<Ref, StateObjectCell>): Ref { - if (ref === Transform.RootRef) return ref; + if (ref === StateTransform.RootRef) return ref; const node = tree.transforms.get(ref)!; const siblings = tree.children.get(node.parent)!.values(); @@ -542,7 +540,7 @@ async function updateSubtree(ctx: UpdateContext, root: Ref) { } } -function resolveParams(ctx: UpdateContext, transform: Transform, src: StateObject) { +function resolveParams(ctx: UpdateContext, transform: StateTransform, src: StateObject) { const prms = transform.transformer.definition.params; const definition = prms ? prms(src, ctx.parent.globalContext) : {}; const values = transform.params ? transform.params : ParamDefinition.getDefaultValues(definition); @@ -555,8 +553,7 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNo const transform = current.transform; // special case for Root - if (current.transform.ref === Transform.RootRef) { - current.version = transform.version; + if (current.transform.ref === StateTransform.RootRef) { return { action: 'none' }; } @@ -574,7 +571,6 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNo current.params = params; const obj = await createObject(ctx, currentRef, transform.transformer, parent, params.values); current.obj = obj; - current.version = transform.version; return { ref: currentRef, action: 'created', obj }; } else { @@ -584,21 +580,18 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNo const updateKind = !!current.obj && current.obj !== StateObject.Null ? await updateObject(ctx, currentRef, transform.transformer, parent, current.obj!, oldParams, newParams) - : Transformer.UpdateResult.Recreate; + : StateTransformer.UpdateResult.Recreate; switch (updateKind) { - case Transformer.UpdateResult.Recreate: { + case StateTransformer.UpdateResult.Recreate: { const oldObj = current.obj; const newObj = await createObject(ctx, currentRef, transform.transformer, parent, newParams); current.obj = newObj; - current.version = transform.version; return { ref: currentRef, action: 'replaced', oldObj, obj: newObj }; } - case Transformer.UpdateResult.Updated: - current.version = transform.version; + case StateTransformer.UpdateResult.Updated: return { ref: currentRef, action: 'updated', obj: current.obj! }; default: - current.version = transform.version; return { action: 'none' }; } } @@ -609,15 +602,15 @@ function runTask<T>(t: T | Task<T>, ctx: RuntimeContext) { return t as T; } -function createObject(ctx: UpdateContext, ref: Ref, transformer: Transformer, a: StateObject, params: any) { +function createObject(ctx: UpdateContext, ref: Ref, transformer: StateTransformer, a: StateObject, params: any) { const cache = Object.create(null); ctx.transformCache.set(ref, cache); return runTask(transformer.definition.apply({ a, params, cache }, ctx.parent.globalContext), ctx.taskCtx); } -async function updateObject(ctx: UpdateContext, ref: Ref, transformer: Transformer, a: StateObject, b: StateObject, oldParams: any, newParams: any) { +async function updateObject(ctx: UpdateContext, ref: Ref, transformer: StateTransformer, a: StateObject, b: StateObject, oldParams: any, newParams: any) { if (!transformer.definition.update) { - return Transformer.UpdateResult.Recreate; + return StateTransformer.UpdateResult.Recreate; } let cache = ctx.transformCache.get(ref); if (!cache) { diff --git a/src/mol-state/state/builder.ts b/src/mol-state/state/builder.ts new file mode 100644 index 0000000000000000000000000000000000000000..ec0bab0c3c2e4c245a4c614b39d4fd912adc736a --- /dev/null +++ b/src/mol-state/state/builder.ts @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { StateTree } from '../tree/immutable'; +import { TransientTree } from '../tree/transient'; +import { StateObject, StateObjectCell } from '../object'; +import { StateTransform } from '../transform'; +import { StateTransformer } from '../transformer'; + +export { StateBuilder } + +interface StateBuilder { + readonly editInfo: StateBuilder.EditInfo, + getTree(): StateTree +} + +namespace StateBuilder { + export interface EditInfo { + sourceTree: StateTree, + count: number, + lastUpdate?: StateTransform.Ref + } + + interface State { + tree: TransientTree, + editInfo: EditInfo + } + + export function is(obj: any): obj is StateBuilder { + return !!obj && typeof (obj as StateBuilder).getTree === 'function'; + } + + export function isTo(obj: any): obj is StateBuilder.To<any> { + return !!obj && typeof (obj as StateBuilder).getTree === 'function' && typeof (obj as StateBuilder.To<any>).ref === 'string'; + } + + export class Root implements StateBuilder { + private state: State; + get editInfo() { return this.state.editInfo; } + + to<A extends StateObject>(ref: StateTransform.Ref) { return new To<A>(this.state, ref, this); } + toRoot<A extends StateObject>() { return new To<A>(this.state, this.state.tree.root.ref, this); } + delete(ref: StateTransform.Ref) { + this.editInfo.count++; + this.state.tree.remove(ref); + return this; + } + getTree(): StateTree { return this.state.tree.asImmutable(); } + constructor(tree: StateTree) { this.state = { tree: tree.asTransient(), editInfo: { sourceTree: tree, count: 0, lastUpdate: void 0 } } } + } + + export class To<A extends StateObject> implements StateBuilder { + get editInfo() { return this.state.editInfo; } + + readonly ref: StateTransform.Ref; + + /** + * Apply the transformed to the parent node + * If no params are specified (params <- undefined), default params are lazily resolved. + */ + apply<T extends StateTransformer<A, any, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>, initialCellState?: Partial<StateObjectCell.State>): To<StateTransformer.To<T>> { + const t = tr.apply(this.ref, params, options); + this.state.tree.add(t, initialCellState); + this.editInfo.count++; + this.editInfo.lastUpdate = t.ref; + return new To(this.state, t.ref, this.root); + } + + /** + * Inserts a new transform that does not change the object type and move the original children to it. + */ + insert<T extends StateTransformer<A, A, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>, initialCellState?: Partial<StateObjectCell.State>): To<StateTransformer.To<T>> { + // cache the children + const children = this.state.tree.children.get(this.ref).toArray(); + + // add the new node + const t = tr.apply(this.ref, params, options); + this.state.tree.add(t, initialCellState); + + // move the original children to the new node + for (const c of children) { + this.state.tree.changeParent(c, t.ref); + } + + this.editInfo.count++; + this.editInfo.lastUpdate = t.ref; + return new To(this.state, t.ref, this.root); + } + + update<T extends StateTransformer<any, A, any>>(transformer: T, params: (old: StateTransformer.Params<T>) => StateTransformer.Params<T>): Root + update(params: any): Root + update<T extends StateTransformer<any, A, any>>(paramsOrTransformer: T, provider?: (old: StateTransformer.Params<T>) => StateTransformer.Params<T>) { + let params: any; + if (provider) { + const old = this.state.tree.transforms.get(this.ref)!; + params = provider(old.params as any); + } else { + params = paramsOrTransformer; + } + + if (this.state.tree.setParams(this.ref, params)) { + this.editInfo.count++; + this.editInfo.lastUpdate = this.ref; + } + + return this.root; + } + + to<A extends StateObject>(ref: StateTransform.Ref) { return this.root.to<A>(ref); } + toRoot<A extends StateObject>() { return this.root.toRoot<A>(); } + delete(ref: StateTransform.Ref) { return this.root.delete(ref); } + + getTree(): StateTree { return this.state.tree.asImmutable(); } + + constructor(private state: State, ref: StateTransform.Ref, private root: Root) { + this.ref = ref; + if (!this.state.tree.transforms.has(ref)) { + throw new Error(`Could not find node '${ref}'.`); + } + } + } +} \ No newline at end of file diff --git a/src/mol-state/state/selection.ts b/src/mol-state/state/selection.ts index 5bb6010f5805c52ee41a79c8d31eac078229d648..b70b4c8d19f817601030faa5d23ec96bd5c0fd10 100644 --- a/src/mol-state/state/selection.ts +++ b/src/mol-state/state/selection.ts @@ -7,7 +7,7 @@ import { StateObject, StateObjectCell } from '../object'; import { State } from '../state'; import { StateTree } from '../tree'; -import { Transform } from '../transform'; +import { StateTransform } from '../transform'; namespace StateSelection { export type Selector = Query | Builder | string | StateObjectCell; @@ -29,7 +29,7 @@ namespace StateSelection { } function isObj(arg: any): arg is StateObjectCell { - return (arg as StateObjectCell).version !== void 0; + return (arg as StateObjectCell).transform !== void 0 && (arg as StateObjectCell).status !== void 0; } function isBuilder(arg: any): arg is Builder { @@ -75,7 +75,7 @@ namespace StateSelection { export namespace Generators { export const root = build(() => (state: State) => [state.cells.get(state.tree.root.ref)!]); - export function byRef(...refs: Transform.Ref[]) { + export function byRef(...refs: StateTransform.Ref[]) { return build(() => (state: State) => { const ret: StateObjectCell[] = []; for (const ref of refs) { @@ -97,7 +97,7 @@ namespace StateSelection { }); } - function _findRootsOfType(n: Transform, _: any, s: { type: StateObject.Type, roots: StateObjectCell[], cells: State.Cells }) { + function _findRootsOfType(n: StateTransform, _: any, s: { type: StateObject.Type, roots: StateObjectCell[], cells: State.Cells }) { const cell = s.cells.get(n.ref); if (cell && cell.obj && cell.obj.type === s.type) { s.roots.push(cell); @@ -196,7 +196,7 @@ namespace StateSelection { registerModifier('parent', parent); export function parent(b: Selector) { return unique(mapEntity(b, (n, s) => s.cells.get(s.tree.transforms.get(n.transform.ref)!.parent))); } - export function findAncestorOfType(tree: StateTree, cells: State.Cells, root: Transform.Ref, types: StateObject.Ctor[]): StateObjectCell | undefined { + export function findAncestorOfType(tree: StateTree, cells: State.Cells, root: StateTransform.Ref, types: StateObject.Ctor[]): StateObjectCell | undefined { let current = tree.transforms.get(root)!, len = types.length; while (true) { current = tree.transforms.get(current.parent)!; @@ -206,13 +206,13 @@ namespace StateSelection { for (let i = 0; i < len; i++) { if (obj.type === types[i].type) return cells.get(current.ref); } - if (current.ref === Transform.RootRef) { + if (current.ref === StateTransform.RootRef) { return void 0; } } } - export function findRootOfType(tree: StateTree, cells: State.Cells, root: Transform.Ref, types: StateObject.Ctor[]): StateObjectCell | undefined { + export function findRootOfType(tree: StateTree, cells: State.Cells, root: StateTransform.Ref, types: StateObject.Ctor[]): StateObjectCell | undefined { let parent: StateObjectCell | undefined, _root = root; while (true) { const _parent = StateSelection.findAncestorOfType(tree, cells, _root, types); diff --git a/src/mol-state/transform.ts b/src/mol-state/transform.ts index 86edad5a9e06d2fd28e0ed74c76b1a4085c046a6..12de75d1c48126ae3f4577e894c014512a007410 100644 --- a/src/mol-state/transform.ts +++ b/src/mol-state/transform.ts @@ -5,19 +5,21 @@ */ import { StateObject } from './object'; -import { Transformer } from './transformer'; +import { StateTransformer } from './transformer'; import { UUID } from 'mol-util'; -export interface Transform<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = {}> { +export { Transform as StateTransform } + +interface Transform<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = {}> { readonly parent: Transform.Ref, - readonly transformer: Transformer<A, B, P>, + readonly transformer: StateTransformer<A, B, P>, readonly props: Transform.Props, readonly ref: Transform.Ref, readonly params?: P, readonly version: string } -export namespace Transform { +namespace Transform { export type Ref = string export const RootRef = '-=root=-' as Ref; @@ -25,7 +27,8 @@ export namespace Transform { export interface Props { tag?: string isGhost?: boolean, - isBinding?: boolean + // determine if the corresponding cell can be deleted by the user. + isLocked?: boolean } export interface Options { @@ -33,7 +36,7 @@ export namespace Transform { props?: Props } - export function create<A extends StateObject, B extends StateObject, P extends {} = {}>(parent: Ref, transformer: Transformer<A, B, P>, params?: P, options?: Options): Transform<A, B, P> { + export function create<A extends StateObject, B extends StateObject, P extends {} = {}>(parent: Ref, transformer: StateTransformer<A, B, P>, params?: P, options?: Options): Transform<A, B, P> { const ref = options && options.ref ? options.ref : UUID.create22() as string as Ref; return { parent, @@ -45,12 +48,20 @@ export namespace Transform { } } - export function withParams<T>(t: Transform, params: any): Transform { + export function withParams(t: Transform, params: any): Transform { return { ...t, params, version: UUID.create22() }; } - export function createRoot(): Transform { - return create(RootRef, Transformer.ROOT, {}, { ref: RootRef }); + export function withParent(t: Transform, parent: Ref): Transform { + return { ...t, parent, version: UUID.create22() }; + } + + export function withNewVersion(t: Transform): Transform { + return { ...t, version: UUID.create22() }; + } + + export function createRoot(props?: Props): Transform { + return create(RootRef, StateTransformer.ROOT, {}, { ref: RootRef, props }); } export interface Serialized { @@ -78,7 +89,7 @@ export namespace Transform { } export function fromJSON(t: Serialized): Transform { - const transformer = Transformer.get(t.transformer); + const transformer = StateTransformer.get(t.transformer); const pFromJson = transformer.definition.customSerialization ? transformer.definition.customSerialization.toJSON : _id; diff --git a/src/mol-state/transformer.ts b/src/mol-state/transformer.ts index b10778f9f0741a0fbf4e80af14759489638f936a..b6eaa8c914016bb7ee900a552b8f38778666be46 100644 --- a/src/mol-state/transformer.ts +++ b/src/mol-state/transformer.ts @@ -5,25 +5,28 @@ */ import { Task } from 'mol-task'; -import { StateObject } from './object'; -import { Transform } from './transform'; +import { StateObject, StateObjectCell } from './object'; +import { StateTransform } from './transform'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import { StateAction } from './action'; import { capitalize } from 'mol-util/string'; -export interface Transformer<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = {}> { - apply(parent: Transform.Ref, params?: P, props?: Partial<Transform.Options>): Transform<A, B, P>, +export { Transformer as StateTransformer } + +interface Transformer<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = {}> { + apply(parent: StateTransform.Ref, params?: P, props?: Partial<StateTransform.Options>): StateTransform<A, B, P>, toAction(): StateAction<A, void, P>, readonly namespace: string, readonly id: Transformer.Id, readonly definition: Transformer.Definition<A, B, P> } -export namespace Transformer { +namespace Transformer { export type Id = string & { '@type': 'transformer-id' } export type Params<T extends Transformer<any, any, any>> = T extends Transformer<any, any, infer P> ? P : unknown; export type From<T extends Transformer<any, any, any>> = T extends Transformer<infer A, any, any> ? A : unknown; export type To<T extends Transformer<any, any, any>> = T extends Transformer<any, infer B, any> ? B : unknown; + export type Cell<T extends Transformer<any, any, any>> = T extends Transformer<any, infer B, any> ? StateObjectCell<B> : unknown; export function is(obj: any): obj is Transformer { return !!obj && typeof (obj as Transformer).toAction === 'function' && typeof (obj as Transformer).apply === 'function'; @@ -130,7 +133,7 @@ export namespace Transformer { } const t: Transformer<A, B, P> = { - apply(parent, params, props) { return Transform.create<A, B, P>(parent, t, params, props); }, + apply(parent, params, props) { return StateTransform.create<A, B, P>(parent, t, params, props); }, toAction() { return StateAction.fromTransformer(t); }, namespace, id, diff --git a/src/mol-state/tree/builder.ts b/src/mol-state/tree/builder.ts deleted file mode 100644 index 0cd0faa0b36088c79de318bd233bdba057a07c59..0000000000000000000000000000000000000000 --- a/src/mol-state/tree/builder.ts +++ /dev/null @@ -1,104 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author David Sehnal <david.sehnal@gmail.com> - */ - -import { StateTree } from './immutable'; -import { TransientTree } from './transient'; -import { StateObject, StateObjectCell } from '../object'; -import { Transform } from '../transform'; -import { Transformer } from '../transformer'; - -export { StateTreeBuilder } - -interface StateTreeBuilder { - readonly editInfo: StateTreeBuilder.EditInfo, - getTree(): StateTree -} - -namespace StateTreeBuilder { - export interface EditInfo { - sourceTree: StateTree, - count: number, - lastUpdate?: Transform.Ref - } - - interface State { - tree: TransientTree, - editInfo: EditInfo - } - - export function is(obj: any): obj is StateTreeBuilder { - return !!obj && typeof (obj as StateTreeBuilder).getTree === 'function'; - } - - export function isTo(obj: any): obj is StateTreeBuilder.To<any> { - return !!obj && typeof (obj as StateTreeBuilder).getTree === 'function' && typeof (obj as StateTreeBuilder.To<any>).ref === 'string'; - } - - export class Root implements StateTreeBuilder { - private state: State; - get editInfo() { return this.state.editInfo; } - - to<A extends StateObject>(ref: Transform.Ref) { return new To<A>(this.state, ref, this); } - toRoot<A extends StateObject>() { return new To<A>(this.state, this.state.tree.root.ref, this); } - delete(ref: Transform.Ref) { - this.editInfo.count++; - this.state.tree.remove(ref); - return this; - } - getTree(): StateTree { return this.state.tree.asImmutable(); } - constructor(tree: StateTree) { this.state = { tree: tree.asTransient(), editInfo: { sourceTree: tree, count: 0, lastUpdate: void 0 } } } - } - - export class To<A extends StateObject> implements StateTreeBuilder { - get editInfo() { return this.state.editInfo; } - - readonly ref: Transform.Ref; - - /** - * Apply the transformed to the parent node - * If no params are specified (params <- undefined), default params are lazily resolved. - */ - apply<T extends Transformer<A, any, any>>(tr: T, params?: Transformer.Params<T>, options?: Partial<Transform.Options>, initialCellState?: Partial<StateObjectCell.State>): To<Transformer.To<T>> { - const t = tr.apply(this.ref, params, options); - this.state.tree.add(t, initialCellState); - this.editInfo.count++; - this.editInfo.lastUpdate = t.ref; - return new To(this.state, t.ref, this.root); - } - - update<T extends Transformer<any, A, any>>(transformer: T, params: (old: Transformer.Params<T>) => Transformer.Params<T>): Root - update(params: any): Root - update<T extends Transformer<any, A, any>>(paramsOrTransformer: T, provider?: (old: Transformer.Params<T>) => Transformer.Params<T>) { - let params: any; - if (provider) { - const old = this.state.tree.transforms.get(this.ref)!; - params = provider(old.params as any); - } else { - params = paramsOrTransformer; - } - - if (this.state.tree.setParams(this.ref, params)) { - this.editInfo.count++; - this.editInfo.lastUpdate = this.ref; - } - - return this.root; - } - - to<A extends StateObject>(ref: Transform.Ref) { return this.root.to<A>(ref); } - toRoot<A extends StateObject>() { return this.root.toRoot<A>(); } - delete(ref: Transform.Ref) { return this.root.delete(ref); } - - getTree(): StateTree { return this.state.tree.asImmutable(); } - - constructor(private state: State, ref: Transform.Ref, private root: Root) { - this.ref = ref; - if (!this.state.tree.transforms.has(ref)) { - throw new Error(`Could not find node '${ref}'.`); - } - } - } -} \ No newline at end of file diff --git a/src/mol-state/tree/immutable.ts b/src/mol-state/tree/immutable.ts index 8d77221c278c662fd785604d64a8e3093084d3a6..ab3d8f60301414dc4deb67d41348a393eea6e2f5 100644 --- a/src/mol-state/tree/immutable.ts +++ b/src/mol-state/tree/immutable.ts @@ -5,9 +5,8 @@ */ import { Map as ImmutableMap, OrderedSet } from 'immutable'; -import { Transform } from '../transform'; +import { StateTransform } from '../transform'; import { TransientTree } from './transient'; -import { StateTreeBuilder } from './builder'; import { StateObjectCell } from 'mol-state/object'; export { StateTree } @@ -17,17 +16,16 @@ export { StateTree } * Represented as an immutable map. */ interface StateTree { - readonly root: Transform, + readonly root: StateTransform, readonly transforms: StateTree.Transforms, readonly children: StateTree.Children, readonly cellStates: StateTree.CellStates, - asTransient(): TransientTree, - build(): StateTreeBuilder.Root + asTransient(): TransientTree } namespace StateTree { - type Ref = Transform.Ref + type Ref = StateTransform.Ref export interface ChildSet { readonly size: number, @@ -43,21 +41,17 @@ namespace StateTree { get(ref: Ref): T } - export interface Transforms extends _Map<Transform> {} + export interface Transforms extends _Map<StateTransform> {} export interface Children extends _Map<ChildSet> { } export interface CellStates extends _Map<StateObjectCell.State> { } class Impl implements StateTree { - get root() { return this.transforms.get(Transform.RootRef)! } + get root() { return this.transforms.get(StateTransform.RootRef)! } asTransient(): TransientTree { return new TransientTree(this); } - build(): StateTreeBuilder.Root { - return new StateTreeBuilder.Root(this); - } - constructor(public transforms: StateTree.Transforms, public children: Children, public cellStates: CellStates) { } } @@ -65,8 +59,8 @@ namespace StateTree { /** * Create an instance of an immutable tree. */ - export function createEmpty(): StateTree { - const root = Transform.createRoot(); + export function createEmpty(customRoot?: StateTransform): StateTree { + const root = customRoot || StateTransform.createRoot(); return create(ImmutableMap([[root.ref, root]]), ImmutableMap([[root.ref, OrderedSet()]]), ImmutableMap([[root.ref, StateObjectCell.DefaultState]])); } @@ -74,10 +68,10 @@ namespace StateTree { return new Impl(nodes, children, cellStates); } - type VisitorCtx = { tree: StateTree, state: any, f: (node: Transform, tree: StateTree, state: any) => boolean | undefined | void }; + type VisitorCtx = { tree: StateTree, state: any, f: (node: StateTransform, tree: StateTree, state: any) => boolean | undefined | void }; function _postOrderFunc(this: VisitorCtx, c: Ref | undefined) { _doPostOrder(this, this.tree.transforms.get(c!)!); } - function _doPostOrder(ctx: VisitorCtx, root: Transform) { + function _doPostOrder(ctx: VisitorCtx, root: StateTransform) { const children = ctx.tree.children.get(root.ref); if (children && children.size) { children.forEach(_postOrderFunc, ctx); @@ -88,14 +82,14 @@ namespace StateTree { /** * Visit all nodes in a subtree in "post order", meaning leafs get visited first. */ - export function doPostOrder<S>(tree: StateTree, root: Transform, state: S, f: (node: Transform, tree: StateTree, state: S) => boolean | undefined | void): S { + export function doPostOrder<S>(tree: StateTree, root: StateTransform, state: S, f: (node: StateTransform, tree: StateTree, state: S) => boolean | undefined | void): S { const ctx: VisitorCtx = { tree, state, f }; _doPostOrder(ctx, root); return ctx.state; } function _preOrderFunc(this: VisitorCtx, c: Ref | undefined) { _doPreOrder(this, this.tree.transforms.get(c!)!); } - function _doPreOrder(ctx: VisitorCtx, root: Transform) { + function _doPreOrder(ctx: VisitorCtx, root: StateTransform) { const ret = ctx.f(root, ctx.tree, ctx.state); if (typeof ret === 'boolean' && !ret) return; const children = ctx.tree.children.get(root.ref); @@ -108,44 +102,44 @@ namespace StateTree { * Visit all nodes in a subtree in "pre order", meaning leafs get visited last. * If the visitor function returns false, the visiting for that branch is interrupted. */ - export function doPreOrder<S>(tree: StateTree, root: Transform, state: S, f: (node: Transform, tree: StateTree, state: S) => boolean | undefined | void): S { + export function doPreOrder<S>(tree: StateTree, root: StateTransform, state: S, f: (node: StateTransform, tree: StateTree, state: S) => boolean | undefined | void): S { const ctx: VisitorCtx = { tree, state, f }; _doPreOrder(ctx, root); return ctx.state; } - function _subtree(n: Transform, _: any, subtree: Transform[]) { subtree.push(n); } + function _subtree(n: StateTransform, _: any, subtree: StateTransform[]) { subtree.push(n); } /** * Get all nodes in a subtree, leafs come first. */ - export function subtreePostOrder<T>(tree: StateTree, root: Transform) { - return doPostOrder<Transform[]>(tree, root, [], _subtree); + export function subtreePostOrder(tree: StateTree, root: StateTransform) { + return doPostOrder<StateTransform[]>(tree, root, [], _subtree); } - function _visitNodeToJson(node: Transform, tree: StateTree, ctx: [Transform.Serialized, StateObjectCell.State][]) { + function _visitNodeToJson(node: StateTransform, tree: StateTree, ctx: [StateTransform.Serialized, StateObjectCell.State][]) { // const children: Ref[] = []; // tree.children.get(node.ref).forEach(_visitChildToJson as any, children); - ctx.push([Transform.toJSON(node), tree.cellStates.get(node.ref)]); + ctx.push([StateTransform.toJSON(node), tree.cellStates.get(node.ref)]); } export interface Serialized { /** Transforms serialized in pre-order */ - transforms: [Transform.Serialized, StateObjectCell.State][] + transforms: [StateTransform.Serialized, StateObjectCell.State][] } - export function toJSON<T>(tree: StateTree): Serialized { - const transforms: [Transform.Serialized, StateObjectCell.State][] = []; + export function toJSON(tree: StateTree): Serialized { + const transforms: [StateTransform.Serialized, StateObjectCell.State][] = []; doPreOrder(tree, tree.root, transforms, _visitNodeToJson); return { transforms }; } - export function fromJSON<T>(data: Serialized): StateTree { - const nodes = ImmutableMap<Ref, Transform>().asMutable(); + export function fromJSON(data: Serialized): StateTree { + const nodes = ImmutableMap<Ref, StateTransform>().asMutable(); const children = ImmutableMap<Ref, OrderedSet<Ref>>().asMutable(); const cellStates = ImmutableMap<Ref, StateObjectCell.State>().asMutable(); for (const t of data.transforms) { - const transform = Transform.fromJSON(t[0]); + const transform = StateTransform.fromJSON(t[0]); nodes.set(transform.ref, transform); cellStates.set(transform.ref, t[1]); diff --git a/src/mol-state/tree/transient.ts b/src/mol-state/tree/transient.ts index 9646dab8f430b2322fc5e2c83138c976019b835c..571d914c3faeb4a9f049d736ede11e492f317fce 100644 --- a/src/mol-state/tree/transient.ts +++ b/src/mol-state/tree/transient.ts @@ -5,24 +5,23 @@ */ import { Map as ImmutableMap, OrderedSet } from 'immutable'; -import { Transform } from '../transform'; +import { StateTransform } from '../transform'; import { StateTree } from './immutable'; -import { StateTreeBuilder } from './builder'; import { StateObjectCell } from 'mol-state/object'; import { shallowEqual } from 'mol-util/object'; export { TransientTree } class TransientTree implements StateTree { - transforms = this.tree.transforms as ImmutableMap<Transform.Ref, Transform>; - children = this.tree.children as ImmutableMap<Transform.Ref, OrderedSet<Transform.Ref>>; - cellStates = this.tree.cellStates as ImmutableMap<Transform.Ref, StateObjectCell.State>; + transforms = this.tree.transforms as ImmutableMap<StateTransform.Ref, StateTransform>; + children = this.tree.children as ImmutableMap<StateTransform.Ref, OrderedSet<StateTransform.Ref>>; + cellStates = this.tree.cellStates as ImmutableMap<StateTransform.Ref, StateObjectCell.State>; private changedNodes = false; private changedChildren = false; private changedStates = false; - private _childMutations: Map<Transform.Ref, OrderedSet<Transform.Ref>> | undefined = void 0; + private _childMutations: Map<StateTransform.Ref, OrderedSet<StateTransform.Ref>> | undefined = void 0; private get childMutations() { if (this._childMutations) return this._childMutations; @@ -48,36 +47,32 @@ class TransientTree implements StateTree { this.children = this.children.asMutable(); } - get root() { return this.transforms.get(Transform.RootRef)! } - - build(): StateTreeBuilder.Root { - return new StateTreeBuilder.Root(this); - } + get root() { return this.transforms.get(StateTransform.RootRef)! } asTransient() { return this.asImmutable().asTransient(); } - private addChild(parent: Transform.Ref, child: Transform.Ref) { + private addChild(parent: StateTransform.Ref, child: StateTransform.Ref) { this.changeChildren(); if (this.childMutations.has(parent)) { this.childMutations.get(parent)!.add(child); } else { - const set = (this.children.get(parent) as OrderedSet<Transform.Ref>).asMutable(); + const set = (this.children.get(parent) as OrderedSet<StateTransform.Ref>).asMutable(); set.add(child); this.children.set(parent, set); this.childMutations.set(parent, set); } } - private removeChild(parent: Transform.Ref, child: Transform.Ref) { + private removeChild(parent: StateTransform.Ref, child: StateTransform.Ref) { this.changeChildren(); if (this.childMutations.has(parent)) { this.childMutations.get(parent)!.remove(child); } else { - const set = (this.children.get(parent) as OrderedSet<Transform.Ref>).asMutable(); + const set = (this.children.get(parent) as OrderedSet<StateTransform.Ref>).asMutable(); set.remove(child); this.children.set(parent, set); this.childMutations.set(parent, set); @@ -85,17 +80,35 @@ class TransientTree implements StateTree { } private clearRoot() { - const parent = Transform.RootRef; + const parent = StateTransform.RootRef; if (this.children.get(parent).size === 0) return; this.changeChildren(); - const set = OrderedSet<Transform.Ref>(); + const set = OrderedSet<StateTransform.Ref>(); this.children.set(parent, set); this.childMutations.set(parent, set); } - add(transform: Transform, initialState?: Partial<StateObjectCell.State>) { + changeParent(ref: StateTransform.Ref, newParent: StateTransform.Ref) { + ensurePresent(this.transforms, ref); + + const old = this.transforms.get(ref); + this.removeChild(old.parent, ref); + this.addChild(newParent, ref); + this.changeNodes(); + this.transforms.set(ref, StateTransform.withParent(old, newParent)); + } + + updateVersion(ref: StateTransform.Ref) { + ensurePresent(this.transforms, ref); + + const t = this.transforms.get(ref); + this.changeNodes(); + this.transforms.set(ref, StateTransform.withNewVersion(t)); + } + + add(transform: StateTransform, initialState?: Partial<StateObjectCell.State>) { const ref = transform.ref; if (this.transforms.has(transform.ref)) { @@ -134,7 +147,7 @@ class TransientTree implements StateTree { } /** Calls Transform.definition.params.areEqual if available, otherwise uses shallowEqual to check if the params changed */ - setParams(ref: Transform.Ref, params: any) { + setParams(ref: StateTransform.Ref, params: any) { ensurePresent(this.transforms, ref); const transform = this.transforms.get(ref)!; @@ -148,11 +161,11 @@ class TransientTree implements StateTree { this.transforms = this.transforms.asMutable(); } - this.transforms.set(transform.ref, Transform.withParams(transform, params)); + this.transforms.set(transform.ref, StateTransform.withParams(transform, params)); return true; } - updateCellState(ref: Transform.Ref, state: Partial<StateObjectCell.State>) { + updateCellState(ref: StateTransform.Ref, state: Partial<StateObjectCell.State>) { ensurePresent(this.transforms, ref); const old = this.cellStates.get(ref); @@ -164,12 +177,12 @@ class TransientTree implements StateTree { return true; } - remove(ref: Transform.Ref): Transform[] { + remove(ref: StateTransform.Ref): StateTransform[] { const node = this.transforms.get(ref); if (!node) return []; const st = StateTree.subtreePostOrder(this, node); - if (ref === Transform.RootRef) { + if (ref === StateTransform.RootRef) { st.pop(); if (st.length === 0) return st; this.clearRoot(); @@ -206,17 +219,17 @@ class TransientTree implements StateTree { } } -function fixChildMutations(this: ImmutableMap<Transform.Ref, OrderedSet<Transform.Ref>>, m: OrderedSet<Transform.Ref>, k: Transform.Ref) { this.set(k, m.asImmutable()); } +function fixChildMutations(this: ImmutableMap<StateTransform.Ref, OrderedSet<StateTransform.Ref>>, m: OrderedSet<StateTransform.Ref>, k: StateTransform.Ref) { this.set(k, m.asImmutable()); } -function alreadyPresent(ref: Transform.Ref) { +function alreadyPresent(ref: StateTransform.Ref) { throw new Error(`Transform '${ref}' is already present in the tree.`); } -function parentNotPresent(ref: Transform.Ref) { +function parentNotPresent(ref: StateTransform.Ref) { throw new Error(`Parent '${ref}' must be present in the tree.`); } -function ensurePresent(nodes: StateTree.Transforms, ref: Transform.Ref) { +function ensurePresent(nodes: StateTree.Transforms, ref: StateTransform.Ref) { if (!nodes.has(ref)) { throw new Error(`Node '${ref}' is not present in the tree.`); } diff --git a/src/mol-theme/color.ts b/src/mol-theme/color.ts index ba17219d3f3ed1d39ed855a0e5188ef2ba811311..33f4c9e3c93fc5544682676f33d55a30f9f8c863 100644 --- a/src/mol-theme/color.ts +++ b/src/mol-theme/color.ts @@ -29,8 +29,6 @@ import { TableLegend } from 'mol-util/color/tables'; export type LocationColor = (location: Location, isSecondary: boolean) => Color -export type ColorThemeProps = { [k: string]: any } - export { ColorTheme } interface ColorTheme<P extends PD.Params> { readonly factory: ColorTheme.Factory<P> @@ -75,4 +73,5 @@ export const BuiltInColorThemes = { 'shape-group': ShapeGroupColorThemeProvider, 'unit-index': UnitIndexColorThemeProvider, 'uniform': UniformColorThemeProvider, -} \ No newline at end of file +} +export type BuiltInColorThemeName = keyof typeof BuiltInColorThemes \ No newline at end of file diff --git a/src/mol-theme/size.ts b/src/mol-theme/size.ts index 52469b7a1dff63f6b1b740e50e6abd96347827fb..c0dbbe3e39e03b5ec59e7296ce5b21eb3d4c00c1 100644 --- a/src/mol-theme/size.ts +++ b/src/mol-theme/size.ts @@ -43,4 +43,5 @@ export const BuiltInSizeThemes = { 'physical': PhysicalSizeThemeProvider, 'shape-group': ShapeGroupSizeThemeProvider, 'uniform': UniformSizeThemeProvider -} \ No newline at end of file +} +export type BuiltInSizeThemeName = keyof typeof BuiltInSizeThemes \ No newline at end of file diff --git a/src/mol-util/index.ts b/src/mol-util/index.ts index 9c1aa24bfe624cbcd813ffdbf42bcbbcf3cfc6ef..73d5c415b5d46f3f69de80f8e9eec0a05651f8be 100644 --- a/src/mol-util/index.ts +++ b/src/mol-util/index.ts @@ -14,6 +14,8 @@ import { Progress } from 'mol-task'; export * from './value-cell' export { BitFlags, StringBuilder, UUID, Mask } +export const noop = function () { }; + export function round(n: number, d: number) { let f = Math.pow(10, d) return Math.round(f * n) / f diff --git a/src/mol-util/lru-cache.ts b/src/mol-util/lru-cache.ts new file mode 100644 index 0000000000000000000000000000000000000000..38d950981e8cf5d7b5bfebda70c8ac0c52d48445 --- /dev/null +++ b/src/mol-util/lru-cache.ts @@ -0,0 +1,53 @@ + +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * Adapted from LiteMol. + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { LinkedList } from 'mol-data/generic'; + +export { LRUCache } + +interface LRUCache<T> { + entries: LinkedList<LRUCache.Entry<T>>, + capacity: number +} + +namespace LRUCache { + export interface Entry<T> { + key: string, + data: T + } + + function entry<T>(key: string, data: T): Entry<T> { + return { key, data }; + } + + export function create<T>(capacity: number): LRUCache<T> { + return { + entries: LinkedList<Entry<T>>(), + capacity: Math.max(1, capacity) + }; + } + + export function get<T>(cache: LRUCache<T>, key: string) { + for (let e = cache.entries.first; e; e = e.next) { + if (e.value.key === key) { + cache.entries.remove(e); + cache.entries.addLast(e.value); + return e.value.data; + } + } + return void 0; + } + + export function set<T>(cache: LRUCache<T>, key: string, data: T): T { + if (cache.entries.count >= cache.capacity) { + cache.entries.remove(cache.entries.first!); + } + cache.entries.addLast(entry(key, data)); + return data; + } +} \ No newline at end of file diff --git a/src/servers/volume/common/binary-schema.ts b/src/servers/volume/common/binary-schema.ts index 8ce1b47007f50177248f128924d75f617a8daf22..3bd0b10cc276d19f6fee996b42e398d0bb473913 100644 --- a/src/servers/volume/common/binary-schema.ts +++ b/src/servers/volume/common/binary-schema.ts @@ -7,6 +7,7 @@ */ import * as UTF8 from 'mol-io/common/utf8' +import { SimpleBuffer } from 'mol-io/common/simple-buffer'; export type Bool = { kind: 'bool' } export type Int = { kind: 'int' } @@ -105,7 +106,7 @@ export function encode(element: Element, src: any): Buffer { return write(element, src); } -function decodeElement(e: Element, buffer: Buffer, offset: number, target: { value: any }) { +function decodeElement(e: Element, buffer: SimpleBuffer, offset: number, target: { value: any }) { switch (e.kind) { case 'bool': target.value = !!buffer.readInt8(offset); offset += 1; break; case 'int': target.value = buffer.readInt32LE(offset); offset += 4; break; @@ -147,7 +148,7 @@ function decodeElement(e: Element, buffer: Buffer, offset: number, target: { val return offset; } -export function decode<T>(element: Element, buffer: Buffer, offset?: number) { +export function decode<T>(element: Element, buffer: SimpleBuffer, offset?: number) { const target = { value: void 0 as any }; decodeElement(element, buffer, offset! | 0, target); return target.value as T; diff --git a/src/servers/volume/common/data-format.ts b/src/servers/volume/common/data-format.ts index f1fdeda83277dd145831667fdad3dd4f476bd7af..2f0525475388dd923732969a3f110181ba32d926 100644 --- a/src/servers/volume/common/data-format.ts +++ b/src/servers/volume/common/data-format.ts @@ -6,18 +6,9 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import * as File from './file' import * as Schema from './binary-schema' - -export type ValueType = 'float32' | 'int8' | 'int16' - -export namespace ValueType { - export const Float32: ValueType = 'float32'; - export const Int8: ValueType = 'int8'; - export const Int16: ValueType = 'int16'; -} - -export type ValueArray = Float32Array | Int8Array | Int16Array +import { FileHandle } from 'mol-io/common/file-handle'; +import { TypedArrayValueType } from 'mol-io/common/typed-array'; export interface Spacegroup { number: number, @@ -62,7 +53,7 @@ export interface Header { channels: string[], /** Determines the data type of the values */ - valueType: ValueType, + valueType: TypedArrayValueType, /** The value are stored in blockSize^3 cubes */ blockSize: number, @@ -102,31 +93,16 @@ namespace _schema { const headerSchema = _schema.schema; -export function getValueByteSize(type: ValueType) { - if (type === ValueType.Float32) return 4; - if (type === ValueType.Int16) return 2; - return 1; -} - -export function createValueArray(type: ValueType, size: number) { - switch (type) { - case ValueType.Float32: return new Float32Array(new ArrayBuffer(4 * size)); - case ValueType.Int8: return new Int8Array(new ArrayBuffer(1 * size)); - case ValueType.Int16: return new Int16Array(new ArrayBuffer(2 * size)); - } - throw Error(`${type} is not a supported value format.`); -} - export function encodeHeader(header: Header) { return Schema.encode(headerSchema, header); } -export async function readHeader(file: number): Promise<{ header: Header, dataOffset: number }> { - let { buffer } = await File.readBuffer(file, 0, 4 * 4096); +export async function readHeader(file: FileHandle): Promise<{ header: Header, dataOffset: number }> { + let { buffer } = await file.readBuffer(0, 4 * 4096); const headerSize = buffer.readInt32LE(0); if (headerSize > buffer.byteLength - 4) { - buffer = (await File.readBuffer(file, 0, headerSize + 4)).buffer; + buffer = (await file.readBuffer(0, headerSize + 4)).buffer; } const header = Schema.decode<Header>(headerSchema, buffer, 4); diff --git a/src/servers/volume/common/file.ts b/src/servers/volume/common/file.ts index 6b83f00cef7e225de8dde078e257698fc17eb162..c01670aa19473a7ff5f4ffe5981ad2e61e006907 100644 --- a/src/servers/volume/common/file.ts +++ b/src/servers/volume/common/file.ts @@ -8,9 +8,8 @@ import * as fs from 'fs' import * as path from 'path' -import * as DataFormat from './data-format' - -export const IsNativeEndianLittle = new Uint16Array(new Uint8Array([0x12, 0x34]).buffer)[0] === 0x3412; +import { FileHandle } from 'mol-io/common/file-handle'; +import { SimpleBuffer } from 'mol-io/common/simple-buffer'; export async function openRead(filename: string) { return new Promise<number>((res, rej) => { @@ -29,43 +28,6 @@ export async function openRead(filename: string) { }); } -export function readBuffer(file: number, position: number, sizeOrBuffer: Buffer | number, size?: number, byteOffset?: number): Promise<{ bytesRead: number, buffer: Buffer }> { - return new Promise((res, rej) => { - if (typeof sizeOrBuffer === 'number') { - let buff = new Buffer(new ArrayBuffer(sizeOrBuffer)); - fs.read(file, buff, 0, sizeOrBuffer, position, (err, bytesRead, buffer) => { - if (err) { - rej(err); - return; - } - res({ bytesRead, buffer }); - }); - } else { - if (size === void 0) { - rej('readBuffer: Specify size.'); - return; - } - - fs.read(file, sizeOrBuffer, byteOffset ? +byteOffset : 0, size, position, (err, bytesRead, buffer) => { - if (err) { - rej(err); - return; - } - res({ bytesRead, buffer }); - }); - } - }) -} - -export function writeBuffer(file: number, position: number, buffer: Buffer, size?: number): Promise<number> { - return new Promise<number>((res, rej) => { - fs.write(file, buffer, 0, size !== void 0 ? size : buffer.length, position, (err, written) => { - if (err) rej(err); - else res(written); - }) - }) -} - function makeDir(path: string, root?: string): boolean { let dirs = path.split(/\/|\\/g), dir = dirs.shift(); @@ -95,77 +57,8 @@ export function createFile(filename: string) { }); } -const __emptyFunc = function () { }; -export function close(file: number | undefined) { - try { - if (file !== void 0) fs.close(file, __emptyFunc); - } catch (e) { - - } -} - -const smallBuffer = new Buffer(8); -export async function writeInt(file: number, value: number, position: number) { +const smallBuffer = SimpleBuffer.fromBuffer(new Buffer(8)); +export async function writeInt(file: FileHandle, value: number, position: number) { smallBuffer.writeInt32LE(value, 0); - await writeBuffer(file, position, smallBuffer, 4); -} - -export interface TypedArrayBufferContext { - type: DataFormat.ValueType, - elementByteSize: number, - readBuffer: Buffer, - valuesBuffer: Uint8Array, - values: DataFormat.ValueArray -} - -function getElementByteSize(type: DataFormat.ValueType) { - if (type === DataFormat.ValueType.Float32) return 4; - if (type === DataFormat.ValueType.Int16) return 2; - return 1; -} - -function makeTypedArray(type: DataFormat.ValueType, buffer: ArrayBuffer): DataFormat.ValueArray { - if (type === DataFormat.ValueType.Float32) return new Float32Array(buffer); - if (type === DataFormat.ValueType.Int16) return new Int16Array(buffer); - return new Int8Array(buffer); -} - -export function createTypedArrayBufferContext(size: number, type: DataFormat.ValueType): TypedArrayBufferContext { - let elementByteSize = getElementByteSize(type); - let arrayBuffer = new ArrayBuffer(elementByteSize * size); - let readBuffer = new Buffer(arrayBuffer); - let valuesBuffer = IsNativeEndianLittle ? arrayBuffer : new ArrayBuffer(elementByteSize * size); - return { - type, - elementByteSize, - readBuffer, - valuesBuffer: new Uint8Array(valuesBuffer), - values: makeTypedArray(type, valuesBuffer) - }; -} - -function flipByteOrder(source: Buffer, target: Uint8Array, byteCount: number, elementByteSize: number, offset: number) { - for (let i = 0, n = byteCount; i < n; i += elementByteSize) { - for (let j = 0; j < elementByteSize; j++) { - target[offset + i + elementByteSize - j - 1] = source[offset + i + j]; - } - } -} - -export async function readTypedArray(ctx: TypedArrayBufferContext, file: number, position: number, count: number, valueOffset: number, littleEndian?: boolean) { - let byteCount = ctx.elementByteSize * count; - let byteOffset = ctx.elementByteSize * valueOffset; - - await readBuffer(file, position, ctx.readBuffer, byteCount, byteOffset); - if (ctx.elementByteSize > 1 && ((littleEndian !== void 0 && littleEndian !== IsNativeEndianLittle) || !IsNativeEndianLittle)) { - // fix the endian - flipByteOrder(ctx.readBuffer, ctx.valuesBuffer, byteCount, ctx.elementByteSize, byteOffset); - } - return ctx.values; -} - -export function ensureLittleEndian(source: Buffer, target: Buffer, byteCount: number, elementByteSize: number, offset: number) { - if (IsNativeEndianLittle) return; - if (!byteCount || elementByteSize <= 1) return; - flipByteOrder(source, target, byteCount, elementByteSize, offset); + await file.writeBuffer(position, smallBuffer, 4); } \ No newline at end of file diff --git a/src/servers/volume/pack.ts b/src/servers/volume/pack.ts index 8be8f9afaa777466561df4ed1b27b509d951ca4f..ebf43fb45e103a4cfaceb59c0fb24271353f275b 100644 --- a/src/servers/volume/pack.ts +++ b/src/servers/volume/pack.ts @@ -9,13 +9,30 @@ import pack from './pack/main' import VERSION from './pack/version' -let config = { - input: <{ name: string, filename: string }[]>[], +interface Config { + input: { name: string, filename: string }[], + format: 'ccp4' | 'dsn6', + isPeriodic: boolean, + outputFilename: string, + blockSizeInMB: number +} + +let config: Config = { + input: [], + format: 'ccp4', isPeriodic: false, outputFilename: '', - blockSize: 96 + blockSizeInMB: 96 }; +function getFormat(format: string): Config['format'] { + switch (format.toLowerCase()) { + case 'ccp4': return 'ccp4' + case 'dsn6': return 'dsn6' + } + throw new Error(`unsupported format '${format}'`) +} + function printHelp() { let help = [ `VolumeServer Packer ${VERSION}, (c) 2016 - now, David Sehnal`, @@ -50,7 +67,10 @@ function parseInput() { for (let i = 2; i < process.argv.length; i++) { switch (process.argv[i].toLowerCase()) { case '-blocksize': - config.blockSize = +process.argv[++i]; + config.blockSizeInMB = +process.argv[++i]; + break; + case '-format': + config.format = getFormat(process.argv[++i]); break; case '-xray': input = true; @@ -82,5 +102,5 @@ function parseInput() { } if (parseInput()) { - pack(config.input, config.blockSize, config.isPeriodic, config.outputFilename); + pack(config.input, config.blockSizeInMB, config.isPeriodic, config.outputFilename, config.format); } \ No newline at end of file diff --git a/src/servers/volume/pack/ccp4.ts b/src/servers/volume/pack/ccp4.ts deleted file mode 100644 index 46ac835bc1d1f6dd33d749d2a0458e8071e41657..0000000000000000000000000000000000000000 --- a/src/servers/volume/pack/ccp4.ts +++ /dev/null @@ -1,163 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * Taken/adapted from DensityServer (https://github.com/dsehnal/DensityServer) - * - * @author David Sehnal <david.sehnal@gmail.com> - */ - -import * as File from '../common/file' -import * as DataFormat from '../common/data-format' - -export const enum Mode { Int8 = 0, Int16 = 1, Float32 = 2 } - -export interface Header { - name: string, - mode: Mode, - grid: number[], // grid is converted to the axis order!! - axisOrder: number[], - extent: number[], - origin: number[], - spacegroupNumber: number, - cellSize: number[], - cellAngles: number[], - littleEndian: boolean, - dataOffset: number -} - -/** Represents a circular buffer for 2 * blockSize layers */ -export interface SliceBuffer { - buffer: File.TypedArrayBufferContext, - sliceCapacity: number, - slicesRead: number, - - values: DataFormat.ValueArray, - sliceCount: number, - - /** Have all the input slice been read? */ - isFinished: boolean -} - -export interface Data { - header: Header, - file: number, - slices: SliceBuffer -} - -export function getValueType(header: Header) { - if (header.mode === Mode.Float32) return DataFormat.ValueType.Float32; - if (header.mode === Mode.Int16) return DataFormat.ValueType.Int16; - return DataFormat.ValueType.Int8; -} - -export function assignSliceBuffer(data: Data, blockSize: number) { - const { extent } = data.header; - const valueType = getValueType(data.header); - const sliceSize = extent[0] * extent[1] * DataFormat.getValueByteSize(valueType); - const sliceCapacity = Math.max(1, Math.floor(Math.min(64 * 1024 * 1024, sliceSize * extent[2]) / sliceSize)); - const buffer = File.createTypedArrayBufferContext(sliceCapacity * extent[0] * extent[1], valueType); - data.slices = { - buffer, - sliceCapacity, - slicesRead: 0, - values: buffer.values, - sliceCount: 0, - isFinished: false - }; -} - -function compareProp(a: any, b: any) { - if (a instanceof Array && b instanceof Array) { - if (a.length !== b.length) return false; - for (let i = 0; i < a.length; i++) { - if (a[i] !== b[i]) return false; - } - return true; - } - return a === b; -} - -export function compareHeaders(a: Header, b: Header) { - for (const p of ['grid', 'axisOrder', 'extent', 'origin', 'spacegroupNumber', 'cellSize', 'cellAngles', 'mode']) { - if (!compareProp((a as any)[p], (b as any)[p])) return false; - } - return true; -} - -function getArray(r: (offset: number) => number, offset: number, count: number) { - const ret: number[] = []; - for (let i = 0; i < count; i++) { - ret[i] = r(offset + i); - } - return ret; -} - -async function readHeader(name: string, file: number) { - const headerSize = 1024; - const { buffer: data } = await File.readBuffer(file, 0, headerSize); - - let littleEndian = true; - - let mode = data.readInt32LE(3 * 4); - if (mode < 0 || mode > 2) { - littleEndian = false; - mode = data.readInt32BE(3 * 4, true); - if (mode < 0 || mode > 2) { - throw Error('Only CCP4 modes 0, 1, and 2 are supported.'); - } - } - - const readInt = littleEndian ? (o: number) => data.readInt32LE(o * 4) : (o: number) => data.readInt32BE(o * 4); - const readFloat = littleEndian ? (o: number) => data.readFloatLE(o * 4) : (o: number) => data.readFloatBE(o * 4); - - const origin2k = getArray(readFloat, 49, 3); - const nxyzStart = getArray(readInt, 4, 3); - const header: Header = { - name, - mode, - grid: getArray(readInt, 7, 3), - axisOrder: getArray(readInt, 16, 3).map(i => i - 1), - extent: getArray(readInt, 0, 3), - origin: origin2k[0] === 0.0 && origin2k[1] === 0.0 && origin2k[2] === 0.0 ? nxyzStart : origin2k, - spacegroupNumber: readInt(22), - cellSize: getArray(readFloat, 10, 3), - cellAngles: getArray(readFloat, 13, 3), - // mean: readFloat(21), - littleEndian, - dataOffset: headerSize + readInt(23) /* symBytes */ - }; - // "normalize" the grid axis order - header.grid = [header.grid[header.axisOrder[0]], header.grid[header.axisOrder[1]], header.grid[header.axisOrder[2]]]; - return header; -} - -export async function readSlices(data: Data) { - const { slices, header } = data; - if (slices.isFinished) { - return; - } - - const { extent } = header; - const sliceSize = extent[0] * extent[1]; - const sliceByteOffset = slices.buffer.elementByteSize * sliceSize * slices.slicesRead; - const sliceCount = Math.min(slices.sliceCapacity, extent[2] - slices.slicesRead); - const sliceByteCount = sliceCount * sliceSize; - - await File.readTypedArray(slices.buffer, data.file, header.dataOffset + sliceByteOffset, sliceByteCount, 0, header.littleEndian); - slices.slicesRead += sliceCount; - slices.sliceCount = sliceCount; - - if (slices.slicesRead >= extent[2]) { - slices.isFinished = true; - } -} - -export async function open(name: string, filename: string): Promise<Data> { - const file = await File.openRead(filename); - const header = await readHeader(name, file); - return { - header, - file, - slices: void 0 as any - }; -} \ No newline at end of file diff --git a/src/servers/volume/pack/data-model.ts b/src/servers/volume/pack/data-model.ts index 268fdeb5cd9dbb43f7d194f2f4a0d08e069085b8..9e80f0f80be0cb6a1a4ed76546aed775e4b56dd0 100644 --- a/src/servers/volume/pack/data-model.ts +++ b/src/servers/volume/pack/data-model.ts @@ -5,8 +5,11 @@ * * @author David Sehnal <david.sehnal@gmail.com> */ -import * as CCP4 from './ccp4' +import * as Format from './format' import * as DataFormat from '../common/data-format' +import { FileHandle } from 'mol-io/common/file-handle'; +import { SimpleBuffer } from 'mol-io/common/simple-buffer'; +import { TypedArrayValueArray, TypedArrayValueType } from 'mol-io/common/typed-array'; const FORMAT_VERSION = '1.0.0'; @@ -23,16 +26,16 @@ export interface ValuesInfo { } export interface BlockBuffer { - values: DataFormat.ValueArray[], - buffers: Buffer[], + values: TypedArrayValueArray[], + buffers: SimpleBuffer[], slicesWritten: number } export interface DownsamplingBuffer { /** dimensions (sampleCount[1], sampleCount[0] / 2, 1), axis order (K, H, L) */ - downsampleH: DataFormat.ValueArray, + downsampleH: TypedArrayValueArray, /** "Cyclic" (in the 1st dimensions) buffer with dimensions (5, sampleCount[0] / 2, sampleCount[1] / 2), axis order (L, H, K), */ - downsampleHK: DataFormat.ValueArray, + downsampleHK: TypedArrayValueArray, slicesWritten: number, startSliceIndex: number @@ -68,18 +71,18 @@ export interface Kernel { export interface Context { /** Output file handle */ - file: number, + file: FileHandle, /** Periodic are x-ray density files that cover the entire grid and have [0,0,0] origin */ isPeriodic: boolean, - channels: CCP4.Data[], - valueType: DataFormat.ValueType, + channels: Format.Context[], + valueType: TypedArrayValueType, blockSize: number, /** Able to store channels.length * blockSize^3 values. */ - cubeBuffer: Buffer, + cubeBuffer: SimpleBuffer, /** All values are stored in little endian format which might not be the native endian of the system */ - litteEndianCubeBuffer: Buffer, + litteEndianCubeBuffer: SimpleBuffer, kernel: Kernel, sampling: Sampling[], @@ -90,7 +93,7 @@ export interface Context { } export function createHeader(ctx: Context): DataFormat.Header { - const header = ctx.channels[0].header; + const header = ctx.channels[0].data.header; const grid = header.grid; function normalize(data: number[]) { @@ -99,13 +102,13 @@ export function createHeader(ctx: Context): DataFormat.Header { return { formatVersion: FORMAT_VERSION, - valueType: CCP4.getValueType(header), + valueType: header.valueType, blockSize: ctx.blockSize, axisOrder: header.axisOrder, origin: normalize(header.origin), dimensions: normalize(header.extent), spacegroup: { number: header.spacegroupNumber, size: header.cellSize, angles: header.cellAngles, isPeriodic: ctx.isPeriodic }, - channels: ctx.channels.map(c => c.header.name), + channels: ctx.channels.map(c => c.data.header.name), sampling: ctx.sampling.map(s => { const N = s.sampleCount[0] * s.sampleCount[1] * s.sampleCount[2]; const valuesInfo = []; diff --git a/src/servers/volume/pack/downsampling.ts b/src/servers/volume/pack/downsampling.ts index 5541b8a9a120f2f26a975cfa00a6915e95a32a48..25f67d795e5676a6f6ebe026d5f73fc0383db6f6 100644 --- a/src/servers/volume/pack/downsampling.ts +++ b/src/servers/volume/pack/downsampling.ts @@ -7,10 +7,10 @@ */ import * as Data from './data-model' -import * as DataFormat from '../common/data-format' +import { TypedArrayValueArray } from 'mol-io/common/typed-array'; -/** - * Downsamples each slice of input data and checks if there is enough data to perform +/** + * Downsamples each slice of input data and checks if there is enough data to perform * higher rate downsampling. */ export function downsampleLayer(ctx: Data.Context) { @@ -25,8 +25,8 @@ export function downsampleLayer(ctx: Data.Context) { } } -/** - * When the "native" (rate = 1) sampling is finished, there might still +/** + * When the "native" (rate = 1) sampling is finished, there might still * be some data left to be processed for the higher rate samplings. */ export function finalize(ctx: Data.Context) { @@ -46,7 +46,7 @@ export function finalize(ctx: Data.Context) { /** * The functions downsampleH and downsampleHK both essentially do the * same thing: downsample along H (1st axis in axis order) and K (2nd axis in axis order) axes respectively. - * + * * The reason there are two copies of almost the same code is performance: * Both functions use a different memory layout to improve cache coherency * - downsampleU uses the H axis as the fastest moving one @@ -54,7 +54,7 @@ export function finalize(ctx: Data.Context) { */ -function conv(w: number, c: number[], src: DataFormat.ValueArray, b: number, i0: number, i1: number, i2: number, i3: number, i4: number) { +function conv(w: number, c: number[], src: TypedArrayValueArray, b: number, i0: number, i1: number, i2: number, i3: number, i4: number) { return w * (c[0] * src[b + i0] + c[1] * src[b + i1] + c[2] * src[b + i2] + c[3] * src[b + i3] + c[4] * src[b + i4]); } @@ -63,7 +63,7 @@ function conv(w: number, c: number[], src: DataFormat.ValueArray, b: number, i0: * flipping the 1st and 2nd axis in the process to optimize cache coherency for downsampleUV call * (i.e. use (K, H, L) axis order). */ -function downsampleH(kernel: Data.Kernel, srcDims: number[], src: DataFormat.ValueArray, srcLOffset: number, buffer: Data.DownsamplingBuffer) { +function downsampleH(kernel: Data.Kernel, srcDims: number[], src: TypedArrayValueArray, srcLOffset: number, buffer: Data.DownsamplingBuffer) { const target = buffer.downsampleH; const sizeH = srcDims[0], sizeK = srcDims[1], srcBaseOffset = srcLOffset * sizeH * sizeK; const targetH = Math.floor((sizeH + 1) / 2); @@ -87,9 +87,9 @@ function downsampleH(kernel: Data.Kernel, srcDims: number[], src: DataFormat.Val } } -/** - * Downsample first axis in the slice present in buffer.downsampleH - * The result is written into the "cyclical" downsampleHk buffer +/** + * Downsample first axis in the slice present in buffer.downsampleH + * The result is written into the "cyclical" downsampleHk buffer * in the (L, H, K) axis order. */ function downsampleHK(kernel: Data.Kernel, dimsX: number[], buffer: Data.DownsamplingBuffer) { diff --git a/src/servers/volume/pack/format.ts b/src/servers/volume/pack/format.ts new file mode 100644 index 0000000000000000000000000000000000000000..313d083e2033e8ea3e5ad96429bca5385b153fb0 --- /dev/null +++ b/src/servers/volume/pack/format.ts @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2019 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 * as File from '../common/file' +import { FileHandle } from 'mol-io/common/file-handle'; +import { Ccp4Provider } from './format/ccp4'; +import { TypedArrayBufferContext, TypedArrayValueArray, TypedArrayValueType, getElementByteSize, createTypedArrayBufferContext } from 'mol-io/common/typed-array'; +import { Dsn6Provider } from './format/dsn6'; + +export interface Header { + name: string, + valueType: TypedArrayValueType, + grid: number[], // grid is converted to the axis order!! + axisOrder: number[], + extent: number[], + origin: number[], + spacegroupNumber: number, + cellSize: number[], + cellAngles: number[], + littleEndian: boolean, + dataOffset: number + originalHeader: unknown // TODO +} + +/** Represents a circular buffer for 2 * blockSize layers */ +export interface SliceBuffer { + buffer: TypedArrayBufferContext, + maxBlockBytes: number + sliceCapacity: number, + slicesRead: number, + + values: TypedArrayValueArray, + sliceCount: number, + + /** Have all the input slice been read? */ + isFinished: boolean +} + +export interface Data { + header: Header, + file: FileHandle, + slices: SliceBuffer +} + +export interface Provider { + readHeader: (name: string, file: FileHandle) => Promise<Header>, + readSlices: (data: Data) => Promise<void> +} + +export interface Context { + data: Data, + provider: Provider +} + +export function assignSliceBuffer(data: Data, blockSizeInMB: number) { + const { extent, valueType } = data.header; + const maxBlockBytes = blockSizeInMB * 1024 * 1024 + const sliceSize = extent[0] * extent[1] * getElementByteSize(valueType); + const sliceCapacity = Math.max(1, Math.floor(Math.min(maxBlockBytes, sliceSize * extent[2]) / sliceSize)); + const buffer = createTypedArrayBufferContext(sliceCapacity * extent[0] * extent[1], valueType); + data.slices = { + buffer, + maxBlockBytes, + sliceCapacity, + slicesRead: 0, + values: buffer.values, + sliceCount: 0, + isFinished: false + }; +} + +function compareProp(a: any, b: any) { + if (a instanceof Array && b instanceof Array) { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false; + } + return true; + } + return a === b; +} + +export function compareHeaders(a: Header, b: Header) { + for (const p of ['grid', 'axisOrder', 'extent', 'origin', 'spacegroupNumber', 'cellSize', 'cellAngles', 'mode']) { + if (!compareProp((a as any)[p], (b as any)[p])) return false; + } + return true; +} + +export type Type = 'ccp4' | 'dsn6' + +export function getProviderFromType(type: Type): Provider { + switch (type) { + case 'ccp4': return Ccp4Provider + case 'dsn6': return Dsn6Provider + } +} + +export async function open(name: string, filename: string, type: Type): Promise<Context> { + const provider = getProviderFromType(type) + const descriptor = await File.openRead(filename); + const file = FileHandle.fromDescriptor(descriptor) + const header = await provider.readHeader(name, file); + const data = { header, file, slices: void 0 as any } + return { data, provider }; +} \ No newline at end of file diff --git a/src/servers/volume/pack/format/ccp4.ts b/src/servers/volume/pack/format/ccp4.ts new file mode 100644 index 0000000000000000000000000000000000000000..6269dff6571fc517cb9f881a6cd10889eba4a385 --- /dev/null +++ b/src/servers/volume/pack/format/ccp4.ts @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2019 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 { FileHandle } from 'mol-io/common/file-handle'; +import { readCcp4Header, readCcp4Slices, getCcp4DataOffset, getCcp4ValueType } from 'mol-io/reader/ccp4/parser'; +import { Header, Provider, Data } from '../format'; +import { getCcp4Origin } from 'mol-model-formats/volume/ccp4'; +import { Ccp4Header } from 'mol-io/reader/ccp4/schema'; + +async function readHeader(name: string, file: FileHandle) { + const { header: ccp4Header, littleEndian } = await readCcp4Header(file) + + const header: Header = { + name, + valueType: getCcp4ValueType(ccp4Header), + grid: [ccp4Header.NX, ccp4Header.NY, ccp4Header.NZ], + axisOrder: [ccp4Header.MAPC, ccp4Header.MAPR, ccp4Header.MAPS].map(i => i - 1), + extent: [ccp4Header.NC, ccp4Header.NR, ccp4Header.NS], + origin: getCcp4Origin(ccp4Header), + spacegroupNumber: ccp4Header.ISPG, + cellSize: [ccp4Header.xLength, ccp4Header.yLength, ccp4Header.zLength], + cellAngles: [ccp4Header.alpha, ccp4Header.beta, ccp4Header.gamma], + littleEndian, + dataOffset: getCcp4DataOffset(ccp4Header), + originalHeader: ccp4Header + }; + // "normalize" the grid axis order + header.grid = [header.grid[header.axisOrder[0]], header.grid[header.axisOrder[1]], header.grid[header.axisOrder[2]]]; + return header; +} + +export async function readSlices(data: Data) { + const { slices, header } = data; + if (slices.isFinished) { + return; + } + + const { extent, originalHeader } = header; + const sliceSize = extent[0] * extent[1]; + const sliceByteOffset = slices.buffer.elementByteSize * sliceSize * slices.slicesRead; + const sliceCount = Math.min(slices.sliceCapacity, extent[2] - slices.slicesRead); + const sliceByteCount = slices.buffer.elementByteSize * sliceCount * sliceSize; + + await readCcp4Slices(originalHeader as Ccp4Header, slices.buffer, data.file, header.dataOffset + sliceByteOffset, sliceByteCount, header.littleEndian); + slices.slicesRead += sliceCount; + slices.sliceCount = sliceCount; + + if (slices.slicesRead >= extent[2]) { + slices.isFinished = true; + } +} + +export const Ccp4Provider: Provider = { readHeader, readSlices } \ No newline at end of file diff --git a/src/servers/volume/pack/format/dsn6.ts b/src/servers/volume/pack/format/dsn6.ts new file mode 100644 index 0000000000000000000000000000000000000000..088a7bac5567e148e7358a20a849346d19aa6c04 --- /dev/null +++ b/src/servers/volume/pack/format/dsn6.ts @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { FileHandle } from 'mol-io/common/file-handle'; +import { Header, Provider, Data } from '../format'; +import { readDsn6Header, dsn6HeaderSize, parseDsn6Values, getDsn6Counts } from 'mol-io/reader/dsn6/parser'; +import { TypedArrayValueType } from 'mol-io/common/typed-array'; +import { Dsn6Header } from 'mol-io/reader/dsn6/schema'; + +async function readHeader(name: string, file: FileHandle) { + const { header: dsn6Header, littleEndian } = await readDsn6Header(file) + + const header: Header = { + name, + valueType: TypedArrayValueType.Float32, + grid: [dsn6Header.xRate, dsn6Header.yRate, dsn6Header.zRate].reverse(), + axisOrder: [0, 1, 2].reverse(), + extent: [dsn6Header.xExtent, dsn6Header.yExtent, dsn6Header.zExtent].reverse(), + origin: [dsn6Header.xStart, dsn6Header.yStart, dsn6Header.zStart].reverse(), + spacegroupNumber: 1, // set as P 1, since it is not available in DSN6 files + cellSize: [dsn6Header.xlen, dsn6Header.ylen, dsn6Header.zlen], + cellAngles: [dsn6Header.alpha, dsn6Header.beta, dsn6Header.gamma], + littleEndian, + dataOffset: dsn6HeaderSize, + originalHeader: dsn6Header + }; + return header; +} + +export async function readSlices(data: Data) { + // TODO due to the dsn6 data layout we the read file into one big buffer + // to avoid this, either change the sampling algoritm to work with this layout or + // read the data into a collection of buffers that can be access like one big buffer + // => for now not worth putting time in, for big files better use another file format + + const { slices, header, file } = data; + if (slices.isFinished) { + return; + } + + const { extent, dataOffset, originalHeader } = header; + const sliceCount = extent[2] + + const { byteCount } = getDsn6Counts(originalHeader as Dsn6Header) + if (byteCount > slices.maxBlockBytes) { + throw new Error(`dsn6 file to large, can't read ${byteCount} bytes at once, increase block size or use another file format`) + } + + const { buffer } = await file.readBuffer(dataOffset, byteCount) + if (!(slices.values instanceof Float32Array)) { + throw new Error(`dsn6 reader only supports Float32Array for output values`) + } + await parseDsn6Values(originalHeader as Dsn6Header, buffer, slices.values, header.littleEndian) + + slices.slicesRead += sliceCount; + slices.sliceCount = sliceCount; + + if (slices.slicesRead >= extent[2]) { + slices.isFinished = true; + } +} + +export const Dsn6Provider: Provider = { readHeader, readSlices } \ No newline at end of file diff --git a/src/servers/volume/pack/main.ts b/src/servers/volume/pack/main.ts index 116be4bb1f030f8546d0f5a8b48aa3566fb0d5a4..a43bddae2d571cfb11f13b0953e01175d681591f 100644 --- a/src/servers/volume/pack/main.ts +++ b/src/servers/volume/pack/main.ts @@ -6,16 +6,16 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import * as CCP4 from './ccp4' +import * as Format from './format' import * as File from '../common/file' import * as Data from './data-model' import * as Sampling from './sampling' import * as DataFormat from '../common/data-format' -import * as fs from 'fs' +import { FileHandle } from 'mol-io/common/file-handle'; -export default async function pack(input: { name: string, filename: string }[], blockSize: number, isPeriodic: boolean, outputFilename: string) { +export default async function pack(input: { name: string, filename: string }[], blockSizeInMB: number, isPeriodic: boolean, outputFilename: string, format: Format.Type) { try { - await create(outputFilename, input, blockSize, isPeriodic); + await create(outputFilename, input, blockSizeInMB, isPeriodic, format); } catch (e) { console.error('[Error] ' + e); } @@ -36,7 +36,7 @@ function updateAllocationProgress(progress: Data.Progress, progressDone: number) } /** - * Pre allocate the disk space to be able to do "random" writes into the entire file. + * Pre allocate the disk space to be able to do "random" writes into the entire file. */ async function allocateFile(ctx: Data.Context) { const { totalByteSize, file } = ctx; @@ -44,20 +44,20 @@ async function allocateFile(ctx: Data.Context) { const progress: Data.Progress = { current: 0, max: Math.ceil(totalByteSize / buffer.byteLength) }; let written = 0; while (written < totalByteSize) { - written += fs.writeSync(file, buffer, 0, Math.min(totalByteSize - written, buffer.byteLength)); + written += file.writeBufferSync(written, buffer, Math.min(totalByteSize - written, buffer.byteLength)); updateAllocationProgress(progress, 1); } } -function determineBlockSize(data: CCP4.Data, blockSize: number) { +function determineBlockSize(data: Format.Data, blockSizeInMB: number) { const { extent } = data.header; const maxLayerSize = 1024 * 1024 * 1024; const valueCount = extent[0] * extent[1]; - if (valueCount * blockSize <= maxLayerSize) return blockSize; + if (valueCount * blockSizeInMB <= maxLayerSize) return blockSizeInMB; - while (blockSize > 0) { - blockSize -= 4; - if (valueCount * blockSize <= maxLayerSize) return blockSize; + while (blockSizeInMB > 0) { + blockSizeInMB -= 4; + if (valueCount * blockSizeInMB <= maxLayerSize) return blockSizeInMB; } throw new Error('Could not determine a valid block size.'); @@ -66,13 +66,13 @@ function determineBlockSize(data: CCP4.Data, blockSize: number) { async function writeHeader(ctx: Data.Context) { const header = DataFormat.encodeHeader(Data.createHeader(ctx)); await File.writeInt(ctx.file, header.byteLength, 0); - await File.writeBuffer(ctx.file, 4, header); + await ctx.file.writeBuffer(4, header); } -async function create(filename: string, sourceDensities: { name: string, filename: string }[], sourceBlockSize: number, isPeriodic: boolean) { +async function create(filename: string, sourceDensities: { name: string, filename: string }[], sourceBlockSizeInMB: number, isPeriodic: boolean, format: Format.Type) { const startedTime = getTime(); - if (sourceBlockSize % 4 !== 0 || sourceBlockSize < 4) { + if (sourceBlockSizeInMB % 4 !== 0 || sourceBlockSizeInMB < 4) { throw Error('Block size must be a positive number divisible by 4.'); } @@ -80,40 +80,42 @@ async function create(filename: string, sourceDensities: { name: string, filenam throw Error('Specify at least one source density.'); } - process.stdout.write('Initializing... '); - const files: number[] = []; + process.stdout.write(`Initializing using ${format} format...`); + const files: FileHandle[] = []; try { - // Step 1a: Read the CCP4 headers - const channels: CCP4.Data[] = []; - for (const s of sourceDensities) channels.push(await CCP4.open(s.name, s.filename)); - // Step 1b: Check if the CCP4 headers are compatible. - const isOk = channels.reduce((ok, s) => ok && CCP4.compareHeaders(channels[0].header, s.header), true); + // Step 1a: Read the Format headers + const channels: Format.Context[] = []; + for (const s of sourceDensities) { + channels.push(await Format.open(s.name, s.filename, format)); + } + // Step 1b: Check if the Format headers are compatible. + const isOk = channels.reduce((ok, s) => ok && Format.compareHeaders(channels[0].data.header, s.data.header), true); if (!isOk) { throw new Error('Input file headers are not compatible (different grid, etc.).'); } - const blockSize = determineBlockSize(channels[0], sourceBlockSize); - for (const ch of channels) CCP4.assignSliceBuffer(ch, blockSize); + const blockSizeInMB = determineBlockSize(channels[0].data, sourceBlockSizeInMB); + for (const ch of channels) Format.assignSliceBuffer(ch.data, blockSizeInMB); // Step 1c: Create data context. - const context = await Sampling.createContext(filename, channels, blockSize, isPeriodic); - for (const s of channels) files.push(s.file); + const context = await Sampling.createContext(filename, channels, blockSizeInMB, isPeriodic); + for (const s of channels) files.push(s.data.file); files.push(context.file); process.stdout.write(' done.\n'); - console.log(`Block size: ${blockSize}`); + console.log(`Block size: ${blockSizeInMB}`); - // Step 2: Allocate disk space. + // Step 2: Allocate disk space. process.stdout.write('Allocating... 0%'); await allocateFile(context); process.stdout.write('\rAllocating... done.\n'); - // Step 3: Process and write the data + // Step 3: Process and write the data process.stdout.write('Writing data... 0%'); await Sampling.processData(context); process.stdout.write('\rWriting data... done.\n'); // Step 4: Write the header at the start of the file. - // The header is written last because the sigma/min/max values are computed + // The header is written last because the sigma/min/max values are computed // during step 3. process.stdout.write('Writing header... '); await writeHeader(context); @@ -123,11 +125,11 @@ async function create(filename: string, sourceDensities: { name: string, filenam const time = getTime() - startedTime; console.log(`[Done] ${time.toFixed(0)}ms.`); } finally { - for (let f of files) File.close(f); + for (let f of files) f.close(); // const ff = await File.openRead(filename); // const hh = await DataFormat.readHeader(ff); // File.close(ff); // console.log(hh.header); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/servers/volume/pack/sampling.ts b/src/servers/volume/pack/sampling.ts index e193fb218bba78b243e4ebbcf4d837ec705bd552..97d77961d06c77960d7b0450831e762e6e374284 100644 --- a/src/servers/volume/pack/sampling.ts +++ b/src/servers/volume/pack/sampling.ts @@ -6,30 +6,33 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import * as CCP4 from './ccp4' +import * as Format from './format' import * as Data from './data-model' import * as File from '../common/file' import * as Downsampling from './downsampling' import * as Writer from './writer' import * as DataFormat from '../common/data-format' +import { FileHandle } from 'mol-io/common/file-handle'; +import { getElementByteSize, createTypedArray, TypedArrayValueType } from 'mol-io/common/typed-array'; +import { SimpleBuffer } from 'mol-io/common/simple-buffer'; -export async function createContext(filename: string, channels: CCP4.Data[], blockSize: number, isPeriodic: boolean): Promise<Data.Context> { - const header = channels[0].header; - const samplingCounts = getSamplingCounts(channels[0].header.extent, blockSize); - const valueType = CCP4.getValueType(header); - const cubeBuffer = new Buffer(new ArrayBuffer(channels.length * blockSize * blockSize * blockSize * DataFormat.getValueByteSize(valueType))); +export async function createContext(filename: string, channels: Format.Context[], blockSize: number, isPeriodic: boolean): Promise<Data.Context> { + const { extent, valueType, grid, origin } = channels[0].data.header; - const litteEndianCubeBuffer = File.IsNativeEndianLittle + const samplingCounts = getSamplingCounts(extent, blockSize); + const cubeBuffer = new Buffer(new ArrayBuffer(channels.length * blockSize * blockSize * blockSize * getElementByteSize(valueType))); + + const litteEndianCubeBuffer = SimpleBuffer.IsNativeEndianLittle ? cubeBuffer - : new Buffer(new ArrayBuffer(channels.length * blockSize * blockSize * blockSize * DataFormat.getValueByteSize(valueType))); + : new Buffer(new ArrayBuffer(channels.length * blockSize * blockSize * blockSize * getElementByteSize(valueType))); // The data can be periodic iff the extent is the same as the grid and origin is 0. - if (header.grid.some((v, i) => v !== header.extent[i]) || header.origin.some(v => v !== 0)) { + if (grid.some((v, i) => v !== extent[i]) || origin.some(v => v !== 0)) { isPeriodic = false; } const ctx: Data.Context = { - file: await File.createFile(filename), + file: FileHandle.fromDescriptor(await File.createFile(filename)), isPeriodic, channels, valueType, @@ -60,9 +63,9 @@ export async function createContext(filename: string, channels: CCP4.Data[], blo export async function processData(ctx: Data.Context) { const channel = ctx.channels[0]; - while (!channel.slices.isFinished) { + while (!channel.data.slices.isFinished) { for (const src of ctx.channels) { - await CCP4.readSlices(src); + await src.provider.readSlices(src.data); } await processSlices(ctx); } @@ -92,9 +95,9 @@ function getSamplingCounts(baseSampleCount: number[], blockSize: number) { } } -function createBlockBuffer(sampleCount: number[], blockSize: number, valueType: DataFormat.ValueType, numChannels: number): Data.BlockBuffer { +function createBlockBuffer(sampleCount: number[], blockSize: number, valueType: TypedArrayValueType, numChannels: number): Data.BlockBuffer { const values = []; - for (let i = 0; i < numChannels; i++) values[i] = DataFormat.createValueArray(valueType, sampleCount[0] * sampleCount[1] * blockSize); + for (let i = 0; i < numChannels; i++) values[i] = createTypedArray(valueType, sampleCount[0] * sampleCount[1] * blockSize); return { values, buffers: values.map(xs => new Buffer(xs.buffer)), @@ -102,12 +105,12 @@ function createBlockBuffer(sampleCount: number[], blockSize: number, valueType: }; } -function createDownsamplingBuffer(valueType: DataFormat.ValueType, sourceSampleCount: number[], targetSampleCount: number[], numChannels: number): Data.DownsamplingBuffer[] { +function createDownsamplingBuffer(valueType: TypedArrayValueType, sourceSampleCount: number[], targetSampleCount: number[], numChannels: number): Data.DownsamplingBuffer[] { const ret = []; for (let i = 0; i < numChannels; i++) { ret[ret.length] = { - downsampleH: DataFormat.createValueArray(valueType, sourceSampleCount[1] * targetSampleCount[0]), - downsampleHK: DataFormat.createValueArray(valueType, 5 * targetSampleCount[0] * targetSampleCount[1]), + downsampleH: createTypedArray(valueType, sourceSampleCount[1] * targetSampleCount[0]), + downsampleHK: createTypedArray(valueType, 5 * targetSampleCount[0] * targetSampleCount[1]), slicesWritten: 0, startSliceIndex: 0 } @@ -115,7 +118,7 @@ function createDownsamplingBuffer(valueType: DataFormat.ValueType, sourceSampleC return ret; } -function createSampling(index: number, valueType: DataFormat.ValueType, numChannels: number, sampleCounts: number[][], blockSize: number): Data.Sampling { +function createSampling(index: number, valueType: TypedArrayValueType, numChannels: number, sampleCounts: number[][], blockSize: number): Data.Sampling { const sampleCount = sampleCounts[index]; const valuesInfo: Data.ValuesInfo[] = []; for (let i = 0; i < numChannels; i++) { @@ -134,7 +137,7 @@ function createSampling(index: number, valueType: DataFormat.ValueType, numChann downsampling: index < sampleCounts.length - 1 ? createDownsamplingBuffer(valueType, sampleCount, sampleCounts[index + 1], numChannels) : void 0, byteOffset: 0, - byteSize: numChannels * sampleCount[0] * sampleCount[1] * sampleCount[2] * DataFormat.getValueByteSize(valueType), + byteSize: numChannels * sampleCount[0] * sampleCount[1] * sampleCount[2] * getElementByteSize(valueType), writeByteOffset: 0 } } @@ -148,7 +151,7 @@ function copyLayer(ctx: Data.Context, sliceIndex: number) { const targetOffset = blocks.slicesWritten * size; for (let channelIndex = 0; channelIndex < channels.length; channelIndex++) { - const src = channels[channelIndex].slices.values; + const src = channels[channelIndex].data.slices.values; const target = blocks.values[channelIndex]; for (let i = 0; i < size; i++) { const v = src[srcOffset + i]; @@ -197,14 +200,14 @@ async function writeBlocks(ctx: Data.Context, isDataFinished: boolean) { async function processSlices(ctx: Data.Context) { const channel = ctx.channels[0]; - const sliceCount = channel.slices.sliceCount; + const sliceCount = channel.data.slices.sliceCount; for (let i = 0; i < sliceCount; i++) { copyLayer(ctx, i); Downsampling.downsampleLayer(ctx); await writeBlocks(ctx, false); - const isDataFinished = i === sliceCount - 1 && channel.slices.isFinished; + const isDataFinished = i === sliceCount - 1 && channel.data.slices.isFinished; if (isDataFinished) { Downsampling.finalize(ctx); await writeBlocks(ctx, true); diff --git a/src/servers/volume/pack/writer.ts b/src/servers/volume/pack/writer.ts index 6cf357df36f23a3a36e0724d8709c97a511e4c6a..287feae38b140ded5127563e7b5868d59e283ab5 100644 --- a/src/servers/volume/pack/writer.ts +++ b/src/servers/volume/pack/writer.ts @@ -7,8 +7,8 @@ */ import * as Data from './data-model' -import * as File from '../common/file' -import * as DataFormat from '../common/data-format' +import { getElementByteSize } from 'mol-io/common/typed-array'; +import { SimpleBuffer } from 'mol-io/common/simple-buffer'; /** Converts a layer to blocks and writes them to the output file. */ export async function writeBlockLayer(ctx: Data.Context, sampling: Data.Sampling) { @@ -19,7 +19,7 @@ export async function writeBlockLayer(ctx: Data.Context, sampling: Data.Sampling for (let v = 0; v < nV; v++) { for (let u = 0; u < nU; u++) { const size = fillCubeBuffer(ctx, sampling, u, v); - await File.writeBuffer(ctx.file, startOffset + sampling.writeByteOffset, ctx.litteEndianCubeBuffer, size); + await ctx.file.writeBuffer(startOffset + sampling.writeByteOffset, ctx.litteEndianCubeBuffer, size); sampling.writeByteOffset += size; updateProgress(ctx.progress, 1); } @@ -32,7 +32,7 @@ function fillCubeBuffer(ctx: Data.Context, sampling: Data.Sampling, u: number, v const { blockSize, cubeBuffer } = ctx; const { sampleCount } = sampling; const { buffers, slicesWritten } = sampling.blocks; - const elementSize = DataFormat.getValueByteSize(ctx.valueType); + const elementSize = getElementByteSize(ctx.valueType); const sizeH = sampleCount[0], sizeHK = sampleCount[0] * sampleCount[1]; const offsetH = u * blockSize, offsetK = v * blockSize; @@ -52,7 +52,7 @@ function fillCubeBuffer(ctx: Data.Context, sampling: Data.Sampling, u: number, v } } // flip the byte order if needed. - File.ensureLittleEndian(ctx.cubeBuffer, ctx.litteEndianCubeBuffer, writeOffset, elementSize, 0); + SimpleBuffer.ensureLittleEndian(ctx.cubeBuffer, ctx.litteEndianCubeBuffer, writeOffset, elementSize, 0); return writeOffset; } diff --git a/src/servers/volume/server/api.ts b/src/servers/volume/server/api.ts index c1046a4875498e357e62f37450109b264161a797..42f028cf0f6890cc38cc70d5626e33c7955eab38 100644 --- a/src/servers/volume/server/api.ts +++ b/src/servers/volume/server/api.ts @@ -12,6 +12,7 @@ import * as Data from './query/data-model' import { ConsoleLogger } from 'mol-util/console-logger' import * as DataFormat from '../common/data-format' import ServerConfig from '../server-config' +import { FileHandle } from 'mol-io/common/file-handle'; export function getOutputFilename(source: string, id: string, { asBinary, box, detail, forcedSamplingLevel }: Data.QueryParams) { function n(s: string) { return (s || '').replace(/[ \n\t]/g, '').toLowerCase() } @@ -57,16 +58,16 @@ export async function queryBox(params: Data.QueryParams, outputProvider: () => D } async function readHeader(filename: string | undefined, sourceId: string) { - let file: number | undefined = void 0; + let file: FileHandle | undefined; try { if (!filename) return void 0; - file = await File.openRead(filename); + file = FileHandle.fromDescriptor(await File.openRead(filename)); const header = await DataFormat.readHeader(file); return header.header; } catch (e) { ConsoleLogger.error(`Info ${sourceId}`, e); return void 0; } finally { - File.close(file); + if (file) file.close(); } } \ No newline at end of file diff --git a/src/servers/volume/server/query/compose.ts b/src/servers/volume/server/query/compose.ts index af6f8ce8255d63f6d393eb96131a636683daa934..29f546c11439fae5931cbbf2fa4e61682c16080c 100644 --- a/src/servers/volume/server/query/compose.ts +++ b/src/servers/volume/server/query/compose.ts @@ -6,11 +6,10 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import * as DataFormat from '../../common/data-format' import * as Data from './data-model' import * as Box from '../algebra/box' import * as Coords from '../algebra/coordinate' -import * as File from '../../common/file' +import { createTypedArrayBufferContext, getElementByteSize, readTypedArray } from 'mol-io/common/typed-array'; export default async function compose(query: Data.QueryContext.Data) { for (const block of query.samplingInfo.blocks) { @@ -19,19 +18,21 @@ export default async function compose(query: Data.QueryContext.Data) { } async function readBlock(query: Data.QueryContext.Data, coord: Coords.Grid<'Block'>, blockBox: Box.Fractional): Promise<Data.BlockData> { + const { valueType, blockSize } = query.data.header; + const elementByteSize = getElementByteSize(valueType) const numChannels = query.data.header.channels.length; const blockSampleCount = Box.dimensions(Box.fractionalToGrid(blockBox, query.samplingInfo.sampling.dataDomain)); const size = numChannels * blockSampleCount[0] * blockSampleCount[1] * blockSampleCount[2]; - const { valueType, blockSize } = query.data.header; + const byteSize = elementByteSize * size const dataSampleCount = query.data.header.sampling[query.samplingInfo.sampling.index].sampleCount; - const buffer = File.createTypedArrayBufferContext(size, valueType); + const buffer = createTypedArrayBufferContext(size, valueType); const byteOffset = query.samplingInfo.sampling.byteOffset - + DataFormat.getValueByteSize(valueType) * numChannels * blockSize + + elementByteSize * numChannels * blockSize * (blockSampleCount[1] * blockSampleCount[2] * coord[0] + dataSampleCount[0] * blockSampleCount[2] * coord[1] + dataSampleCount[0] * dataSampleCount[1] * coord[2]); - const values = await File.readTypedArray(buffer, query.data.file, byteOffset, size, 0); + const values = await readTypedArray(buffer, query.data.file, byteOffset, byteSize, 0); return { sampleCount: blockSampleCount, values diff --git a/src/servers/volume/server/query/data-model.ts b/src/servers/volume/server/query/data-model.ts index d20c192ce4b1c319fac66eedc57858ab319c9e7d..1536a797931ae8bf0afa47f32f29bf72c86eace9 100644 --- a/src/servers/volume/server/query/data-model.ts +++ b/src/servers/volume/server/query/data-model.ts @@ -11,6 +11,8 @@ import * as Coords from '../algebra/coordinate' import * as Box from '../algebra/box' import Writer from 'mol-io/writer/writer' import { SpacegroupCell } from 'mol-math/geometry'; +import { FileHandle } from 'mol-io/common/file-handle'; +import { TypedArrayValueArray } from 'mol-io/common/typed-array'; ////////////////////////////////////// // DATA @@ -25,7 +27,7 @@ export interface Sampling { } export interface DataContext { - file: number, + file: FileHandle, header: DataFormat.Header, spacegroup: SpacegroupCell, dataBox: Box.Fractional, @@ -34,7 +36,7 @@ export interface DataContext { export interface BlockData { sampleCount: number[], - values: DataFormat.ValueArray + values: TypedArrayValueArray } ////////////////////////////////////// @@ -74,5 +76,5 @@ export namespace QueryContext { type Base = { guid: string, params: QueryParams } export type Error = { kind: 'Error', message: string } & Base export type Empty = { kind: 'Empty', data: DataContext } & Base - export type Data = { kind: 'Data', data: DataContext, samplingInfo: QuerySamplingInfo, values: DataFormat.ValueArray[] } & Base + export type Data = { kind: 'Data', data: DataContext, samplingInfo: QuerySamplingInfo, values: TypedArrayValueArray[] } & Base } \ No newline at end of file diff --git a/src/servers/volume/server/query/encode.ts b/src/servers/volume/server/query/encode.ts index f456c9edf3951c3c11c2e6d14497cecf1892cf09..008159dfdfc8995b3a1c330ead00176a96e9b524 100644 --- a/src/servers/volume/server/query/encode.ts +++ b/src/servers/volume/server/query/encode.ts @@ -13,6 +13,7 @@ import VERSION from '../version' import * as DataFormat from '../../common/data-format' import { Column } from 'mol-data/db'; import { ArrayEncoding, ArrayEncoder } from 'mol-io/common/binary-cif'; +import { TypedArrayValueType, TypedArrayValueArray } from 'mol-io/common/typed-array'; export default function encode(query: Data.QueryContext, output: Data.QueryOutputStream) { let w = CifWriter.createEncoder({ binary: query.params.asBinary, encoderName: `VolumeServer ${VERSION}` }); @@ -106,7 +107,7 @@ const _volume_data_3d_info: CifWriter.Category<ResultContext> = { } }; -function _volume_data_3d_number(i: number, ctx: DataFormat.ValueArray): number { +function _volume_data_3d_number(i: number, ctx: TypedArrayValueArray): number { return ctx[i]; } @@ -118,7 +119,7 @@ const _volume_data_3d: CifWriter.Category<ResultContext> = { const E = ArrayEncoding; let encoder: ArrayEncoder; let typedArray: any; - if (ctx.query.data.header.valueType === DataFormat.ValueType.Float32 || ctx.query.data.header.valueType === DataFormat.ValueType.Int16) { + if (ctx.query.data.header.valueType === TypedArrayValueType.Float32 || ctx.query.data.header.valueType === TypedArrayValueType.Int16) { let min: number, max: number; min = data[0], max = data[0]; for (let i = 0, n = data.length; i < n; i++) { diff --git a/src/servers/volume/server/query/execute.ts b/src/servers/volume/server/query/execute.ts index c5d4272dfdbb87975ec80759d5abab86e4cdf6cf..0e76f5afdbbb25d4a5c4fbce3296894644a731f1 100644 --- a/src/servers/volume/server/query/execute.ts +++ b/src/servers/volume/server/query/execute.ts @@ -21,6 +21,8 @@ import encode from './encode' import { SpacegroupCell } from 'mol-math/geometry'; import { Vec3 } from 'mol-math/linear-algebra'; import { UUID } from 'mol-util'; +import { FileHandle } from 'mol-io/common/file-handle'; +import { createTypedArray, TypedArrayValueType } from 'mol-io/common/typed-array'; export default async function execute(params: Data.QueryParams, outputProvider: () => Data.QueryOutputStream) { const start = getTime(); @@ -30,16 +32,16 @@ export default async function execute(params: Data.QueryParams, outputProvider: params.detail = Math.min(Math.max(0, params.detail | 0), ServerConfig.limits.maxOutputSizeInVoxelCountByPrecisionLevel.length - 1); ConsoleLogger.logId(guid, 'Info', `id=${params.sourceId},encoding=${params.asBinary ? 'binary' : 'text'},detail=${params.detail},${queryBoxToString(params.box)}`); - let sourceFile: number | undefined = void 0; + let sourceFile: FileHandle | undefined; try { - sourceFile = await File.openRead(params.sourceFilename); + sourceFile = FileHandle.fromDescriptor(await File.openRead(params.sourceFilename)); await _execute(sourceFile, params, guid, outputProvider); return true; } catch (e) { ConsoleLogger.errorId(guid, e); return false; } finally { - File.close(sourceFile); + if (sourceFile) sourceFile.close(); ConsoleLogger.logId(guid, 'Time', `${Math.round(getTime() - start)}ms`); State.pendingQueries--; } @@ -80,7 +82,7 @@ function createSampling(header: DataFormat.Header, index: number, dataOffset: nu } } -async function createDataContext(file: number): Promise<Data.DataContext> { +async function createDataContext(file: FileHandle): Promise<Data.DataContext> { const { header, dataOffset } = await DataFormat.readHeader(file); const origin = Coords.fractional(header.origin[0], header.origin[1], header.origin[2]); @@ -141,10 +143,10 @@ function getQueryBox(data: Data.DataContext, queryBox: Data.QueryParamsBox) { } } -function allocateValues(domain: Coords.GridDomain<'Query'>, numChannels: number, valueType: DataFormat.ValueType) { +function allocateValues(domain: Coords.GridDomain<'Query'>, numChannels: number, valueType: TypedArrayValueType) { const values = []; for (let i = 0; i < numChannels; i++) { - values[values.length] = DataFormat.createValueArray(valueType, domain.sampleVolume); + values[values.length] = createTypedArray(valueType, domain.sampleVolume); } return values; } @@ -185,7 +187,7 @@ function createQueryContext(data: Data.DataContext, params: Data.QueryParams, gu } -async function _execute(file: number, params: Data.QueryParams, guid: string, outputProvider: () => Data.QueryOutputStream) { +async function _execute(file: FileHandle, params: Data.QueryParams, guid: string, outputProvider: () => Data.QueryOutputStream) { let output: any = void 0; try { // Step 1a: Create data context diff --git a/webpack.config.js b/webpack.config.js index 756a94fc275d4ebdb70689f34c154c7ca24bd4a2..8271634c5e16fae7baeffaece602852313099714 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -49,6 +49,8 @@ const sharedConfig = { }), new webpack.DefinePlugin({ __PLUGIN_VERSION_TIMESTAMP__: webpack.DefinePlugin.runtimeValue(() => `${new Date().valueOf()}`, true), + // include this for production version of React + // 'process.env.NODE_ENV': JSON.stringify('production') }), new MiniCssExtractPlugin({ filename: 'app.css' }) ], @@ -62,6 +64,7 @@ const sharedConfig = { function createEntryPoint(name, dir, out) { return { + node: { fs: 'empty' }, // TODO find better solution? Currently used in file-handle.ts entry: path.resolve(__dirname, `build/src/${dir}/${name}.js`), output: { filename: `${name}.js`, path: path.resolve(__dirname, `build/${out}`) }, ...sharedConfig