diff --git a/README.md b/README.md index a03c9462eb3ba0a884e85a3f5facb8de1830d000..839436bb0b9a1c8d69ce66bba6ba4bc39aec0fbb 100644 --- a/README.md +++ b/README.md @@ -55,19 +55,9 @@ This project builds on experience from previous solutions: ### Build automatically on file save: npm run watch - npm run watch-extra -### Build/watch mol-viewer -**Build** - - npm run build - npm run build-viewer - -**Watch** - - npm run watch - npm run watch-extra - npm run watch-viewer +### With debug mode enabled: + DEBUG=molstar npm run watch **Run** diff --git a/package-lock.json b/package-lock.json index 8f86e5adeac89d096105360c76b4de9fcdd01ed1..f4e425fdcc74f07d5a4ffd8e8134939c45e58860 100644 Binary files a/package-lock.json and b/package-lock.json differ diff --git a/package.json b/package.json index 430c3c900dc84aab9210303940f250a3f5431885..848f310b9d4b42ad1c37a3d5fafdf1ebbb1f5745 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "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 production", - "watch-webpack": "webpack -w --mode development", + "watch-webpack": "webpack -w --mode development --display minimal", "model-server": "node build/src/servers/model/server.js", "model-server-watch": "nodemon --watch build/src build/src/servers/model/server.js" }, @@ -79,52 +79,52 @@ "@types/benchmark": "^1.0.31", "@types/compression": "0.0.36", "@types/express": "^4.16.1", - "@types/jest": "^24.0.9", - "@types/node": "^11.10.4", - "@types/node-fetch": "^2.1.6", - "@types/react": "^16.8.6", - "@types/react-dom": "^16.8.2", + "@types/jest": "^24.0.11", + "@types/node": "^11.13.4", + "@types/node-fetch": "^2.3.2", + "@types/react": "^16.8.13", + "@types/react-dom": "^16.8.4", "@types/webgl2": "0.0.4", "@types/swagger-ui-dist": "3.0.0", "benchmark": "^2.1.4", "circular-dependency-plugin": "^5.0.2", "concurrently": "^4.1.0", "cpx": "^1.5.0", - "css-loader": "^2.1.0", + "css-loader": "^2.1.1", "extra-watch-webpack-plugin": "^1.0.3", "file-loader": "^3.0.1", "glslify": "^7.0.0", "glslify-import": "^3.1.0", "glslify-loader": "^2.0.0", - "graphql-code-generator": "^0.18.0", - "graphql-codegen-time": "^0.18.0", - "graphql-codegen-typescript-template": "^0.18.0", - "jest": "^24.1.0", + "graphql-code-generator": "^0.18.1", + "graphql-codegen-time": "^0.18.1", + "graphql-codegen-typescript-template": "^0.18.1", + "jest": "^24.7.1", "jest-raw-loader": "^1.0.1", - "mini-css-extract-plugin": "^0.5.0", + "mini-css-extract-plugin": "^0.6.0", "node-sass": "^4.11.0", - "raw-loader": "^1.0.0", - "resolve-url-loader": "^3.0.1", + "raw-loader": "^2.0.0", + "resolve-url-loader": "^3.1.0", "sass-loader": "^7.1.0", "style-loader": "^0.23.1", - "ts-jest": "^24.0.0", - "tslint": "^5.13.1", - "typescript": "^3.3.3", - "uglify-js": "^3.4.9", + "ts-jest": "^24.0.2", + "tslint": "^5.15.0", + "typescript": "^3.4.3", + "uglify-js": "^3.5.4", "util.promisify": "^1.0.0", - "webpack": "^4.29.6", - "webpack-cli": "^3.2.3" + "webpack": "^4.30.0", + "webpack-cli": "^3.3.0" }, "dependencies": { "argparse": "^1.0.10", - "compression": "^1.7.3", + "compression": "^1.7.4", "express": "^4.16.4", - "graphql": "^14.1.1", + "graphql": "^14.2.1", "immutable": "^3.8.2", "node-fetch": "^2.3.0", - "react": "^16.8.4", - "react-dom": "^16.8.4", + "react": "^16.8.6", + "react-dom": "^16.8.6", "rxjs": "^6.4.0", - "swagger-ui-dist": "^3.21.0" + "swagger-ui-dist": "^3.22.1" } } diff --git a/src/apps/basic-wrapper/index.ts b/src/apps/basic-wrapper/index.ts index aaa87f3d9a2788661fd03c5371bb4af8124c4634..9ce98ddac0cc0b27808a39f163dc91f8df875cca 100644 --- a/src/apps/basic-wrapper/index.ts +++ b/src/apps/basic-wrapper/index.ts @@ -50,7 +50,7 @@ class BasicWrapper { return parsed .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 }) - .apply(StateTransforms.Model.CustomModelProperties, { properties: [StripedResidues.Descriptor.name] }, { ref: 'props', props: { isGhost: false } }) + .apply(StateTransforms.Model.CustomModelProperties, { properties: [StripedResidues.Descriptor.name] }, { ref: 'props', state: { isGhost: false } }) .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: 'asm' }); } @@ -98,7 +98,8 @@ class BasicWrapper { } setBackground(color: number) { - PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { backgroundColor: Color(color) } }); + const renderer = this.plugin.canvas3d.props.renderer; + PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { renderer: { ...renderer, backgroundColor: Color(color) } } }); } toggleSpin() { diff --git a/src/apps/model-server-query/index.tsx b/src/apps/model-server-query/index.tsx index a34f8617c8c3d70dfaf792023a9bd3a0f3a3b11d..0c143ceba2f121b03f34d0dd8151fe83e5f3ff0e 100644 --- a/src/apps/model-server-query/index.tsx +++ b/src/apps/model-server-query/index.tsx @@ -105,7 +105,7 @@ const state: State = { query: new Rx.BehaviorSubject(QueryList[1].definition), id: new Rx.BehaviorSubject('1cbs'), params: new Rx.BehaviorSubject({ }), - isBinary: new Rx.BehaviorSubject(false), + isBinary: new Rx.BehaviorSubject<boolean>(false), models: new Rx.BehaviorSubject<number[]>([]), url: new Rx.Subject() } diff --git a/src/apps/viewer/extensions/jolecule.ts b/src/apps/viewer/extensions/jolecule.ts index cdf0026b39a4201f802fe01f18bc736c2ca0c71e..1ce9c969a9a05c522b3f14044ce81b56be030abf 100644 --- a/src/apps/viewer/extensions/jolecule.ts +++ b/src/apps/viewer/extensions/jolecule.ts @@ -57,7 +57,7 @@ interface JoleculeSnapshot { function createTemplate(plugin: PluginContext, state: State, id: string) { const b = new StateBuilder.Root(state.tree); - const data = b.toRoot().apply(StateTransforms.Data.Download, { url: `https://www.ebi.ac.uk/pdbe/static/entry/${id}_updated.cif` }, { props: { isGhost: true }}); + const data = b.toRoot().apply(StateTransforms.Data.Download, { url: `https://www.ebi.ac.uk/pdbe/static/entry/${id}_updated.cif` }, { state: { isGhost: true }}); const model = createModelTree(data, 'cif'); const structure = model.apply(StateTransforms.Model.StructureFromModel, {}); complexRepresentation(plugin, structure, { hideWater: true }); diff --git a/src/examples/proteopedia-wrapper/changelog.md b/src/examples/proteopedia-wrapper/changelog.md index 041ecacd3454949a8be9e03ad2a6284c5b9f8381..9680f701e7da996b4d28c9627f87833d9571539d 100644 --- a/src/examples/proteopedia-wrapper/changelog.md +++ b/src/examples/proteopedia-wrapper/changelog.md @@ -1,3 +1,11 @@ +== v3.0 == + +* Fixed initial camera zoom. +* Custom chain coloring. +* Customize visualizations. +* Show ligand list. +* Show 3D-SNFG. + == v2.0 == * Changed how state saving works. diff --git a/src/examples/proteopedia-wrapper/coloring.ts b/src/examples/proteopedia-wrapper/coloring.ts new file mode 100644 index 0000000000000000000000000000000000000000..a2199b6edbc72f94989016b1e97de943d467e6aa --- /dev/null +++ b/src/examples/proteopedia-wrapper/coloring.ts @@ -0,0 +1,106 @@ +/** + * 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 { Unit, StructureProperties, StructureElement, Link } from 'mol-model/structure'; + +import { Color } from 'mol-util/color'; +import { Location } from 'mol-model/location'; +import { ColorTheme, LocationColor } from 'mol-theme/color'; +import { ParamDefinition as PD } from 'mol-util/param-definition' +import { ThemeDataContext } from 'mol-theme/theme'; +import { Column } from 'mol-data/db'; + +const Description = 'Gives every chain a color from a list based on its `asym_id` value.' + +export function createProteopediaCustomTheme(colors: number[]) { + const ProteopediaCustomColorThemeParams = { + colors: PD.ObjectList({ color: PD.Color(Color(0xffffff)) }, ({ color }) => Color.toHexString(color), + { defaultValue: colors.map(c => ({ color: Color(c) })) }) + } + type ProteopediaCustomColorThemeParams = typeof ProteopediaCustomColorThemeParams + function getChainIdColorThemeParams(ctx: ThemeDataContext) { + return ProteopediaCustomColorThemeParams // TODO return copy + } + + function getAsymId(unit: Unit): StructureElement.Property<string> { + switch (unit.kind) { + case Unit.Kind.Atomic: + return StructureProperties.chain.label_asym_id + case Unit.Kind.Spheres: + case Unit.Kind.Gaussians: + return StructureProperties.coarse.asym_id + } + } + + function addAsymIds(map: Map<string, number>, data: Column<string>) { + let j = map.size + for (let o = 0, ol = data.rowCount; o < ol; ++o) { + const k = data.value(o) + if (!map.has(k)) { + map.set(k, j) + j += 1 + } + } + } + + function ProteopediaCustomColorTheme(ctx: ThemeDataContext, props: PD.Values<ProteopediaCustomColorThemeParams>): ColorTheme<ProteopediaCustomColorThemeParams> { + let color: LocationColor + + const colors = props.colors, colorCount = colors.length, defaultColor = colors[0].color; + + if (ctx.structure) { + const l = StructureElement.create() + const { models } = ctx.structure + const asymIdSerialMap = new Map<string, number>() + for (let i = 0, il = models.length; i < il; ++i) { + const m = models[i] + addAsymIds(asymIdSerialMap, m.atomicHierarchy.chains.label_asym_id) + if (m.coarseHierarchy.isDefined) { + addAsymIds(asymIdSerialMap, m.coarseHierarchy.spheres.asym_id) + addAsymIds(asymIdSerialMap, m.coarseHierarchy.gaussians.asym_id) + } + } + + color = (location: Location): Color => { + if (StructureElement.isLocation(location)) { + const asym_id = getAsymId(location.unit); + const o = asymIdSerialMap.get(asym_id(location)) || 0; + return colors[o % colorCount].color; + } else if (Link.isLocation(location)) { + const asym_id = getAsymId(location.aUnit) + l.unit = location.aUnit + l.element = location.aUnit.elements[location.aIndex] + const o = asymIdSerialMap.get(asym_id(l)) || 0; + return colors[o % colorCount].color; + } + return defaultColor + } + } else { + color = () => defaultColor + } + + return { + factory: ProteopediaCustomColorTheme, + granularity: 'group', + color, + props, + description: Description, + legend: undefined + } + } + + const ProteopediaCustomColorThemeProvider: ColorTheme.Provider<ProteopediaCustomColorThemeParams> = { + label: 'Proteopedia Custom', + factory: ProteopediaCustomColorTheme, + getParams: getChainIdColorThemeParams, + defaultValues: PD.getDefaultValues(ProteopediaCustomColorThemeParams), + isApplicable: (ctx: ThemeDataContext) => !!ctx.structure + } + + return ProteopediaCustomColorThemeProvider; +} \ No newline at end of file diff --git a/src/examples/proteopedia-wrapper/helpers.ts b/src/examples/proteopedia-wrapper/helpers.ts index 427fb057e0b423573a13bb1ea934a977e75a8845..17082c93c22762304fcf3c7357e623bd6e3524c3 100644 --- a/src/examples/proteopedia-wrapper/helpers.ts +++ b/src/examples/proteopedia-wrapper/helpers.ts @@ -92,9 +92,26 @@ export interface LoadParams { export interface RepresentationStyle { sequence?: RepresentationStyle.Entry, hetGroups?: RepresentationStyle.Entry, + snfg3d?: { hide?: boolean }, water?: RepresentationStyle.Entry } export namespace RepresentationStyle { - export type Entry = { kind?: BuiltInStructureRepresentationsName, coloring?: BuiltInColorThemeName } + export type Entry = { hide?: boolean, kind?: BuiltInStructureRepresentationsName, coloring?: BuiltInColorThemeName } +} + +export enum StateElements { + Model = 'model', + ModelProps = 'model-props', + Assembly = 'assembly', + + Sequence = 'sequence', + SequenceVisual = 'sequence-visual', + Het = 'het', + HetVisual = 'het-visual', + Het3DSNFG = 'het-3dsnfg', + Water = 'water', + WaterVisual = 'water-visual', + + HetGroupFocus = 'het-group-focus' } \ No newline at end of file diff --git a/src/examples/proteopedia-wrapper/index.html b/src/examples/proteopedia-wrapper/index.html index 009eb5bf8193a534a6a2c21e342e1c47cfb29e6d..288db037a5d4f2406c2a3b486fd2417dde62e592 100644 --- a/src/examples/proteopedia-wrapper/index.html +++ b/src/examples/proteopedia-wrapper/index.html @@ -55,11 +55,14 @@ </select> </div> <div id="app"></div> - <script> + <script> + // it might be a good idea to define these colors in a separate script file + var CustomColors = [0x00ff00, 0x0000ff]; + // create an instance of the plugin var PluginWrapper = new MolStarProteopediaWrapper(); - console.log('Wrapper version', MolStarProteopediaWrapper.VERSION_MAJOR); + console.log('Wrapper version', MolStarProteopediaWrapper.VERSION_MAJOR, MolStarProteopediaWrapper.VERSION_MINOR); function $(id) { return document.getElementById(id); } @@ -78,13 +81,23 @@ // var format = 'pdb'; // var assemblyId = 'deposited'; - PluginWrapper.init('app' /** or document.getElementById('app') */); + var representationStyle = { + sequence: { coloring: 'proteopedia-custom' }, // or just { } + hetGroups: { kind: 'ball-and-stick' }, // or 'spacefill + water: { hide: true }, + snfg3d: { hide: false } + }; + + PluginWrapper.init('app' /** or document.getElementById('app') */, { + customColorList: CustomColors + }); PluginWrapper.setBackground(0xffffff); - PluginWrapper.load({ url: url, format: format, assemblyId: assemblyId }); + PluginWrapper.load({ url: url, format: format, assemblyId: assemblyId, representationStyle: representationStyle }); PluginWrapper.toggleSpin(); PluginWrapper.events.modelInfo.subscribe(function (info) { console.log('Model Info', info); + listHetGroups(info); }); addControl('Load Asym Unit', () => PluginWrapper.load({ url: url, format: format })); @@ -92,6 +105,22 @@ addSeparator(); + addHeader('Representation'); + + addControl('Custom Chain Colors', () => PluginWrapper.updateStyle({ sequence: { coloring: 'proteopedia-custom' } }, true)); + addControl('Default Chain Colors', () => PluginWrapper.updateStyle({ sequence: { } }, true)); + + addControl('HET Spacefill', () => PluginWrapper.updateStyle({ hetGroups: { kind: 'spacefill' } }, true)); + addControl('HET Ball-and-stick', () => PluginWrapper.updateStyle({ hetGroups: { kind: 'ball-and-stick' } }, true)); + + addControl('Hide 3DSNFG', () => PluginWrapper.updateStyle({ snfg3d: { hide: true } }, true)); + addControl('Show 3DSNFG', () => PluginWrapper.updateStyle({ snfg3d: { hide: false } }, true)); + + addControl('Hide Water', () => PluginWrapper.updateStyle({ water: { hide: true } }, true)); + addControl('Show Water', () => PluginWrapper.updateStyle({ water: { hide: false } }, true)); + + addSeparator(); + addHeader('Camera'); addControl('Toggle Spin', () => PluginWrapper.toggleSpin()); @@ -115,6 +144,12 @@ addControl('Apply Evo Cons', () => PluginWrapper.coloring.evolutionaryConservation()); addControl('Default Visuals', () => PluginWrapper.updateStyle()); + addSeparator(); + addHeader('HET Groups'); + + addControl('Reset', () => PluginWrapper.hetGroups.reset()); + addHetGroupsContainer(); + addSeparator(); addHeader('State'); @@ -133,6 +168,12 @@ //////////////////////////////////////////////////////// + function addHetGroupsContainer() { + var div = document.createElement('div'); + div.id = 'het-groups'; + $('controls').appendChild(div); + } + function addControl(label, action) { var btn = document.createElement('button'); btn.onclick = action; @@ -150,6 +191,19 @@ h.innerText = header; $('controls').appendChild(h); } + + function listHetGroups(info) { + var div = $('het-groups'); + div.innerHTML = ''; + info.hetResidues.forEach(function (r) { + var l = document.createElement('button'); + l.innerText = r.name; + l.onclick = function () { + PluginWrapper.hetGroups.focusFirst(r.name); + }; + div.appendChild(l); + }); + } </script> </body> </html> \ No newline at end of file diff --git a/src/examples/proteopedia-wrapper/index.ts b/src/examples/proteopedia-wrapper/index.ts index 91677335b5ee870e06931e007c0ca3b7857db417..c72a1adbe4107474d221a2d29694de44a00b7fe6 100644 --- a/src/examples/proteopedia-wrapper/index.ts +++ b/src/examples/proteopedia-wrapper/index.ts @@ -15,15 +15,25 @@ import { PluginStateObject as PSO, PluginStateObject } from 'mol-plugin/state/ob import { AnimateModelIndex } from 'mol-plugin/state/animation/built-in'; import { StateBuilder, StateObject } from 'mol-state'; import { EvolutionaryConservation } from './annotation'; -import { LoadParams, SupportedFormats, RepresentationStyle, ModelInfo } from './helpers'; +import { LoadParams, SupportedFormats, RepresentationStyle, ModelInfo, StateElements } from './helpers'; import { RxEventHelper } from 'mol-util/rx-event-helper'; import { ControlsWrapper } from './ui/controls'; import { PluginState } from 'mol-plugin/state'; +import { Scheduler } from 'mol-task'; +import { createProteopediaCustomTheme } from './coloring'; +import { MolScriptBuilder as MS } from 'mol-script/language/builder'; +import { BuiltInStructureRepresentations } from 'mol-repr/structure/registry'; +import { BuiltInColorThemes } from 'mol-theme/color'; +import { BuiltInSizeThemes } from 'mol-theme/size'; +import { ColorNames } from 'mol-util/color/tables'; +// import { Vec3 } from 'mol-math/linear-algebra'; +// import { ParamDefinition } from 'mol-util/param-definition'; +// import { Text } from 'mol-geo/geometry/text/text'; require('mol-plugin/skin/light.scss') class MolStarProteopediaWrapper { - static VERSION_MAJOR = 2; - static VERSION_MINOR = 0; + static VERSION_MAJOR = 3; + static VERSION_MINOR = 1; private _ev = RxEventHelper.create(); @@ -33,9 +43,14 @@ class MolStarProteopediaWrapper { plugin: PluginContext; - init(target: string | HTMLElement) { + init(target: string | HTMLElement, options?: { + customColorList?: number[] + }) { this.plugin = createPlugin(typeof target === 'string' ? document.getElementById(target)! : target, { ...DefaultPluginSpec, + animations: [ + AnimateModelIndex + ], layout: { initial: { isExpanded: false, @@ -47,6 +62,9 @@ class MolStarProteopediaWrapper { } }); + const customColoring = createProteopediaCustomTheme((options && options.customColorList) || []); + + this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.add('proteopedia-custom', customColoring); this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.add(EvolutionaryConservation.Descriptor.name, EvolutionaryConservation.colorTheme!); this.plugin.lociLabels.addProvider(EvolutionaryConservation.labelProvider); this.plugin.customModelProperties.register(EvolutionaryConservation.propertyProvider); @@ -66,43 +84,86 @@ class MolStarProteopediaWrapper { : b.apply(StateTransforms.Model.TrajectoryFromPDB); return parsed - .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 }, { ref: 'model' }); + .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 }, { ref: StateElements.Model }); } private structure(assemblyId: string) { - const model = this.state.build().to('model'); + const model = this.state.build().to(StateElements.Model); + + const s = model + .apply(StateTransforms.Model.CustomModelProperties, { properties: [EvolutionaryConservation.Descriptor.name] }, { ref: StateElements.ModelProps, state: { isGhost: false } }) + .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: StateElements.Assembly }); - return model - .apply(StateTransforms.Model.CustomModelProperties, { properties: [EvolutionaryConservation.Descriptor.name] }, { ref: 'props', props: { isGhost: false } }) - .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: 'asm' }); + s.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' }, { ref: StateElements.Sequence }); + s.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' }, { ref: StateElements.Het }); + s.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' }, { ref: StateElements.Water }); + + return s; } - private visual(ref: string, style?: RepresentationStyle) { - const structure = this.getObj<PluginStateObject.Molecule.Structure>(ref); + private visual(_style?: RepresentationStyle, partial?: boolean) { + const structure = this.getObj<PluginStateObject.Molecule.Structure>(StateElements.Assembly); if (!structure) return; - const root = this.state.build().to(ref); - - root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' }, { ref: 'sequence' }) - .apply(StateTransforms.Representation.StructureRepresentation3D, - StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin, - (style && style.sequence && style.sequence.kind) || 'cartoon', - (style && style.sequence && style.sequence.coloring) || 'unit-index', structure), - { ref: 'sequence-visual' }); - root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' }, { ref: 'het' }) - .apply(StateTransforms.Representation.StructureRepresentation3D, - StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin, - (style && style.hetGroups && style.hetGroups.kind) || 'ball-and-stick', - (style && style.hetGroups && style.hetGroups.coloring), structure), - { ref: 'het-visual' }); - root.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' }, { ref: 'water' }) - .apply(StateTransforms.Representation.StructureRepresentation3D, - StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin, - (style && style.water && style.water.kind) || 'ball-and-stick', - (style && style.water && style.water.coloring), structure, { alpha: 0.51 }), - { ref: 'water-visual' }); - - return root; + const style = _style || { }; + + const update = this.state.build(); + + if (!partial || (partial && style.sequence)) { + const root = update.to(StateElements.Sequence); + if (style.sequence && style.sequence.hide) { + root.delete(StateElements.SequenceVisual); + } else { + root.applyOrUpdate(StateElements.SequenceVisual, StateTransforms.Representation.StructureRepresentation3D, + StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin, + (style.sequence && style.sequence.kind) || 'cartoon', + (style.sequence && style.sequence.coloring) || 'unit-index', structure)); + } + } + + if (!partial || (partial && style.hetGroups)) { + const root = update.to(StateElements.Het); + if (style.hetGroups && style.hetGroups.hide) { + root.delete(StateElements.HetVisual); + } else { + if (style.hetGroups && style.hetGroups.hide) { + root.delete(StateElements.HetVisual); + } else { + root.applyOrUpdate(StateElements.HetVisual, StateTransforms.Representation.StructureRepresentation3D, + StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin, + (style.hetGroups && style.hetGroups.kind) || 'ball-and-stick', + (style.hetGroups && style.hetGroups.coloring), structure)); + } + } + } + + if (!partial || (partial && style.snfg3d)) { + const root = update.to(StateElements.Het); + if (style.hetGroups && style.hetGroups.hide) { + root.delete(StateElements.HetVisual); + } else { + if (style.snfg3d && style.snfg3d.hide) { + root.delete(StateElements.Het3DSNFG); + } else { + root.applyOrUpdate(StateElements.Het3DSNFG, StateTransforms.Representation.StructureRepresentation3D, + StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin, 'carbohydrate', void 0, structure)); + } + } + } + + if (!partial || (partial && style.water)) { + const root = update.to(StateElements.Water); + if (style.water && style.water.hide) { + root.delete(StateElements.WaterVisual); + } else { + root.applyOrUpdate(StateElements.WaterVisual, StateTransforms.Representation.StructureRepresentation3D, + StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin, + (style.water && style.water.kind) || 'ball-and-stick', + (style.water && style.water.coloring), structure, { alpha: 0.51 })); + } + } + + return update; } private getObj<T extends StateObject>(ref: string): T['data'] { @@ -134,7 +195,7 @@ class MolStarProteopediaWrapper { if (this.loadedParams.url !== url || this.loadedParams.format !== format) { loadType = 'full'; } else if (this.loadedParams.url === url) { - if (state.select('asm').length > 0) loadType = 'update'; + if (state.select(StateElements.Assembly).length > 0) loadType = 'update'; } if (loadType === 'full') { @@ -146,24 +207,25 @@ class MolStarProteopediaWrapper { await this.applyState(structureTree); } else { const tree = state.build(); - tree.to('asm').update(StateTransforms.Model.StructureAssemblyFromModel, p => ({ ...p, id: assemblyId || 'deposited' })); + tree.to(StateElements.Assembly).update(StateTransforms.Model.StructureAssemblyFromModel, p => ({ ...p, id: assemblyId || 'deposited' })); await this.applyState(tree); } await this.updateStyle(representationStyle); this.loadedParams = { url, format, assemblyId }; - PluginCommands.Camera.Reset.dispatch(this.plugin, { }); + Scheduler.setImmediate(() => PluginCommands.Camera.Reset.dispatch(this.plugin, { })); } - async updateStyle(style?: RepresentationStyle) { - const tree = this.visual('asm', style); + async updateStyle(style?: RepresentationStyle, partial?: boolean) { + const tree = this.visual(style, partial); if (!tree) return; await PluginCommands.State.Update.dispatch(this.plugin, { state: this.plugin.state.dataState, tree }); } setBackground(color: number) { - PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { backgroundColor: Color(color) } }); + const renderer = this.plugin.canvas3d.props.renderer; + PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { renderer: { ...renderer, backgroundColor: Color(color) } } }); } toggleSpin() { @@ -186,7 +248,7 @@ class MolStarProteopediaWrapper { coloring = { evolutionaryConservation: async () => { - await this.updateStyle({ sequence: { kind: 'spacefill' } }); + await this.updateStyle({ sequence: { kind: 'spacefill' } }, true); const state = this.state; @@ -194,7 +256,7 @@ class MolStarProteopediaWrapper { const tree = state.build(); const colorTheme = { name: EvolutionaryConservation.Descriptor.name, params: this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.get(EvolutionaryConservation.Descriptor.name).defaultValues }; - tree.to('sequence-visual').update(StateTransforms.Representation.StructureRepresentation3D, old => ({ ...old, colorTheme })); + tree.to(StateElements.SequenceVisual).update(StateTransforms.Representation.StructureRepresentation3D, old => ({ ...old, colorTheme })); // for (const v of visuals) { // } @@ -202,6 +264,69 @@ class MolStarProteopediaWrapper { } } + hetGroups = { + reset: () => { + const update = this.state.build().delete(StateElements.HetGroupFocus); + PluginCommands.State.Update.dispatch(this.plugin, { state: this.state, tree: update }); + PluginCommands.Camera.Reset.dispatch(this.plugin, { }); + }, + focusFirst: async (resn: string) => { + if (!this.state.transforms.has(StateElements.Assembly)) return; + + // const asm = (this.state.select(StateElements.Assembly)[0].obj as PluginStateObject.Molecule.Structure).data; + + const update = this.state.build(); + + update.delete(StateElements.HetGroupFocus); + + const surroundings = MS.struct.modifier.includeSurroundings({ + 0: MS.struct.filter.first([ + MS.struct.generator.atomGroups({ + 'residue-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.label_comp_id(), resn]), + 'group-by': MS.struct.atomProperty.macromolecular.residueKey() + }) + ]), + radius: 5, + 'as-whole-residues': true + }); + + const sel = update.to(StateElements.Assembly) + .apply(StateTransforms.Model.StructureSelection, { label: resn, query: surroundings }, { ref: StateElements.HetGroupFocus }); + + sel.apply(StateTransforms.Representation.StructureRepresentation3D, this.createSurVisualParams()); + // sel.apply(StateTransforms.Representation.StructureLabels3D, { + // target: { name: 'residues', params: { } }, + // options: { + // ...ParamDefinition.getDefaultValues(Text.Params), + // background: true, + // backgroundMargin: 0.2, + // backgroundColor: ColorNames.snow, + // backgroundOpacity: 0.9, + // } + // }); + + await PluginCommands.State.Update.dispatch(this.plugin, { state: this.state, tree: update }); + + const focus = (this.state.select(StateElements.HetGroupFocus)[0].obj as PluginStateObject.Molecule.Structure).data; + const sphere = focus.boundary.sphere; + // const asmCenter = asm.boundary.sphere.center; + // const position = Vec3.sub(Vec3.zero(), sphere.center, asmCenter); + // Vec3.normalize(position, position); + // Vec3.scaleAndAdd(position, sphere.center, position, sphere.radius); + const snapshot = this.plugin.canvas3d.camera.getFocus(sphere.center, 0.75 * sphere.radius); + PluginCommands.Camera.SetSnapshot.dispatch(this.plugin, { snapshot, durationMs: 250 }); + } + } + + private createSurVisualParams() { + const asm = this.state.select(StateElements.Assembly)[0].obj as PluginStateObject.Molecule.Structure; + return StructureRepresentation3DHelpers.createParams(this.plugin, asm.data, { + repr: BuiltInStructureRepresentations['ball-and-stick'], + color: [BuiltInColorThemes.uniform, () => ({ value: ColorNames.gray })], + size: [BuiltInSizeThemes.uniform, () => ({ value: 0.33 } )] + }); + } + snapshot = { get: () => { return this.plugin.state.getSnapshot(); diff --git a/src/mol-canvas3d/camera.ts b/src/mol-canvas3d/camera.ts index d61da319e3d6452fddfd17ed06c439af45324766..731dc74e67c6ab1674e39d86924465ea154a5f8d 100644 --- a/src/mol-canvas3d/camera.ts +++ b/src/mol-canvas3d/camera.ts @@ -84,7 +84,7 @@ class Camera implements Object3D { return ret; } - focus(target: Vec3, radius: number) { + getFocus(target: Vec3, radius: number): Partial<Camera.Snapshot> { const fov = this.state.fov const { width, height } = this.viewport const aspect = width / height @@ -98,7 +98,11 @@ class Camera implements Object3D { if (currentDistance < targetDistance) Vec3.negate(this.deltaDirection, this.deltaDirection) Vec3.add(this.newPosition, this.state.position, this.deltaDirection) - this.setState({ target, position: this.newPosition }) + return { target, position: Vec3.clone(this.newPosition) }; + } + + focus(target: Vec3, radius: number) { + this.setState(this.getFocus(target, radius)); } // lookAt(target: Vec3) { diff --git a/src/mol-canvas3d/camera/util.ts b/src/mol-canvas3d/camera/util.ts index 52c431833d5e93b2633d2eb08be71f6ab19735e9..488999370a4d42ab2ad196bafdbb0f9b1f11f12f 100644 --- a/src/mol-canvas3d/camera/util.ts +++ b/src/mol-canvas3d/camera/util.ts @@ -1,19 +1,28 @@ /** - * 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> */ import { Mat4, Vec3, Vec4, EPSILON } from 'mol-math/linear-algebra' -export type Viewport = { +export { Viewport } + +type Viewport = { x: number y: number width: number height: number } -export namespace Viewport { +function Viewport() { + return Viewport.zero() +} + +namespace Viewport { + export function zero(): Viewport { + return { x: 0, y: 0, width: 0, height: 0 } + } export function create(x: number, y: number, width: number, height: number): Viewport { return { x, y, width, height } } @@ -38,9 +47,15 @@ export namespace Viewport { v4[3] = viewport.height return v4 } + + export function equals(a: Viewport, b: Viewport) { + return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height + } } -const tmpVec3 = Vec3.zero() +// + +const tmpVec3 = Vec3() /** Modifies the direction & up vectors in place, both are normalized */ export function cameraLookAt(position: Vec3, up: Vec3, direction: Vec3, target: Vec3) { @@ -68,7 +83,7 @@ export function cameraLookAt(position: Vec3, up: Vec3, direction: Vec3, target: const NEAR_RANGE = 0 const FAR_RANGE = 1 -const tmpVec4 = Vec4.zero() +const tmpVec4 = Vec4() /** Transform point into 2D window coordinates. */ export function cameraProject (out: Vec4, point: Vec3, viewport: Viewport, projectionView: Mat4) { diff --git a/src/mol-canvas3d/canvas3d.ts b/src/mol-canvas3d/canvas3d.ts index e4491ed0f88c6e563d3637a4bcdeb38108b25c2a..358c5d3908dbd662aebdd9c4b1379d1d4d86aca6 100644 --- a/src/mol-canvas3d/canvas3d.ts +++ b/src/mol-canvas3d/canvas3d.ts @@ -9,7 +9,7 @@ import { now } from 'mol-util/now'; import { Vec3 } from 'mol-math/linear-algebra' import InputObserver, { ModifiersKeys, ButtonsType } from 'mol-util/input/input-observer' -import Renderer, { RendererStats } from 'mol-gl/renderer' +import Renderer, { RendererStats, RendererParams } from 'mol-gl/renderer' import { GraphicsRenderObject } from 'mol-gl/render-object' import { TrackballControls, TrackballControlsParams } from './controls/trackball' @@ -19,11 +19,10 @@ import { createContext, getGLContext, WebGLContext } from 'mol-gl/webgl/context' import { Representation } from 'mol-repr/representation'; import { createRenderTarget } from 'mol-gl/webgl/render-target'; import Scene from 'mol-gl/scene'; -import { RenderVariant } from 'mol-gl/webgl/render-item'; +import { GraphicsRenderVariant } from 'mol-gl/webgl/render-item'; import { PickingId } from 'mol-geo/geometry/picking'; import { MarkerAction } from 'mol-geo/geometry/marker-data'; import { Loci, EmptyLoci, isEmptyLoci } from 'mol-model/loci'; -import { Color } from 'mol-util/color'; import { Camera } from './camera'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import { BoundingSphereHelper, DebugHelperParams } from './helper/bounding-sphere-helper'; @@ -35,11 +34,10 @@ export const Canvas3DParams = { // TODO: FPS cap? // maxFps: PD.Numeric(30), cameraMode: PD.Select('perspective', [['perspective', 'Perspective'], ['orthographic', 'Orthographic']]), - backgroundColor: PD.Color(Color(0x000000)), cameraClipDistance: PD.Numeric(0, { min: 0.0, max: 50.0, step: 0.1 }, { description: 'The distance between camera and scene at which to clip regardless of near clipping plane.' }), clip: PD.Interval([1, 100], { min: 1, max: 100, step: 1 }), fog: PD.Interval([50, 100], { min: 1, max: 100, step: 1 }), - pickingAlphaThreshold: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }, { description: 'The minimum opacity value needed for an object to be pickable.' }), + renderer: PD.Group(RendererParams), trackball: PD.Group(TrackballControlsParams), debug: PD.Group(DebugHelperParams) } @@ -70,11 +68,11 @@ interface Canvas3D { resetCamera: () => void readonly camera: Camera downloadScreenshot: () => void - getImageData: (variant: RenderVariant) => ImageData + getImageData: (variant: GraphicsRenderVariant) => ImageData setProps: (props: Partial<Canvas3DProps>) => void /** Returns a copy of the current Canvas3D instance props */ - readonly props: Canvas3DProps + readonly props: Readonly<Canvas3DProps> readonly input: InputObserver readonly stats: RendererStats readonly interaction: Canvas3dInteractionHelper['events'] @@ -117,7 +115,7 @@ namespace Canvas3D { const scene = Scene.create(webgl) const controls = TrackballControls.create(input, camera, p.trackball) - const renderer = Renderer.create(webgl, camera, { clearColor: p.backgroundColor }) + const renderer = Renderer.create(webgl, camera, p.renderer) let pickScale = 0.25 / webgl.pixelRatio let pickWidth = Math.round(canvas.width * pickScale) @@ -210,19 +208,15 @@ namespace Canvas3D { case 'pick': renderer.setViewport(0, 0, pickWidth, pickHeight); objectPickTarget.bind(); - renderer.clear() renderer.render(scene, 'pickObject'); instancePickTarget.bind(); - renderer.clear() renderer.render(scene, 'pickInstance'); groupPickTarget.bind(); - renderer.clear() renderer.render(scene, 'pickGroup'); break; case 'draw': webgl.unbindFramebuffer(); renderer.setViewport(0, 0, canvas.width, canvas.height); - renderer.clear() renderer.render(scene, variant); if (debugHelper.isEnabled) { debugHelper.syncVisibility() @@ -388,7 +382,7 @@ namespace Canvas3D { downloadScreenshot: () => { // TODO }, - getImageData: (variant: RenderVariant) => { + getImageData: (variant: GraphicsRenderVariant) => { switch (variant) { case 'draw': return renderer.getImageData() case 'pickObject': return objectPickTarget.getImageData() @@ -401,17 +395,11 @@ namespace Canvas3D { if (props.cameraMode !== undefined && props.cameraMode !== camera.state.mode) { camera.setState({ mode: props.cameraMode }) } - if (props.backgroundColor !== undefined && props.backgroundColor !== renderer.props.clearColor) { - renderer.setClearColor(props.backgroundColor) - } - if (props.cameraClipDistance !== undefined) p.cameraClipDistance = props.cameraClipDistance if (props.clip !== undefined) p.clip = [props.clip[0], props.clip[1]] if (props.fog !== undefined) p.fog = [props.fog[0], props.fog[1]] - if (props.pickingAlphaThreshold !== undefined && props.pickingAlphaThreshold !== renderer.props.pickingAlphaThreshold) { - renderer.setPickingAlphaThreshold(props.pickingAlphaThreshold) - } + if (props.renderer) renderer.setProps(props.renderer) if (props.trackball) controls.setProps(props.trackball) if (props.debug) debugHelper.setProps(props.debug) requestDraw(true) @@ -420,11 +408,10 @@ namespace Canvas3D { get props() { return { cameraMode: camera.state.mode, - backgroundColor: renderer.props.clearColor, cameraClipDistance: p.cameraClipDistance, clip: p.clip, fog: p.fog, - pickingAlphaThreshold: renderer.props.pickingAlphaThreshold, + renderer: { ...renderer.props }, trackball: { ...controls.props }, debug: { ...debugHelper.props } } diff --git a/src/mol-canvas3d/helper/bounding-sphere-helper.ts b/src/mol-canvas3d/helper/bounding-sphere-helper.ts index c3272d840f4a26240a0ef4ae42cc8ba8821ddd75..8f5a9634784f0812431f232bf71a145724796116 100644 --- a/src/mol-canvas3d/helper/bounding-sphere-helper.ts +++ b/src/mol-canvas3d/helper/bounding-sphere-helper.ts @@ -4,7 +4,7 @@ * @author Alexander Rose <alexander.rose@weirdbyte.de> */ -import { createRenderObject, RenderObject } from 'mol-gl/render-object' +import { createRenderObject, GraphicsRenderObject, getNextMaterialId } from 'mol-gl/render-object' import { MeshBuilder } from 'mol-geo/geometry/mesh/mesh-builder'; import { addSphere } from 'mol-geo/geometry/mesh/builder/sphere'; import { Mesh } from 'mol-geo/geometry/mesh/mesh'; @@ -27,15 +27,15 @@ export const DebugHelperParams = { export type DebugHelperParams = typeof DebugHelperParams export type DebugHelperProps = PD.Values<DebugHelperParams> -type BoundingSphereData = { boundingSphere: Sphere3D, renderObject: RenderObject, mesh: Mesh } +type BoundingSphereData = { boundingSphere: Sphere3D, renderObject: GraphicsRenderObject, mesh: Mesh } export class BoundingSphereHelper { readonly scene: Scene private readonly parent: Scene private _props: DebugHelperProps - private objectsData = new Map<RenderObject, BoundingSphereData>() - private instancesData = new Map<RenderObject, BoundingSphereData>() + private objectsData = new Map<GraphicsRenderObject, BoundingSphereData>() + private instancesData = new Map<GraphicsRenderObject, BoundingSphereData>() private sceneData: BoundingSphereData | undefined constructor(ctx: WebGLContext, parent: Scene, props: Partial<DebugHelperProps>) { @@ -136,7 +136,8 @@ function createBoundingSphereMesh(boundingSphere: Sphere3D, mesh?: Mesh) { return MeshBuilder.getMesh(builderState) } +const boundingSphereHelberMaterialId = getNextMaterialId() function createBoundingSphereRenderObject(mesh: Mesh, color: Color, transform?: TransformData) { const values = Mesh.Utils.createValuesSimple(mesh, { alpha: 0.1, doubleSided: false }, color, 1, transform) - return createRenderObject('mesh', values, { visible: true, alphaFactor: 1, pickable: false, opaque: false }) + return createRenderObject('mesh', values, { visible: true, alphaFactor: 1, pickable: false, opaque: false }, boundingSphereHelberMaterialId) } \ No newline at end of file diff --git a/src/mol-geo/geometry/base.ts b/src/mol-geo/geometry/base.ts index eabbbc2454694f7f9e6696f68401f1d23f3ad782..c168c5a03b3f019e950e25b6c184976dfa0b39a3 100644 --- a/src/mol-geo/geometry/base.ts +++ b/src/mol-geo/geometry/base.ts @@ -64,9 +64,10 @@ export namespace BaseGeometry { uAlpha: ValueCell.create(props.alpha), uHighlightColor: ValueCell.create(Color.toArrayNormalized(props.highlightColor, Vec3.zero(), 0)), uSelectColor: ValueCell.create(Color.toArrayNormalized(props.selectColor, Vec3.zero(), 0)), + dUseFog: ValueCell.create(props.useFog), + uGroupCount: ValueCell.create(counts.groupCount), drawCount: ValueCell.create(counts.drawCount), - dUseFog: ValueCell.create(props.useFog), } } diff --git a/src/mol-geo/geometry/color-data.ts b/src/mol-geo/geometry/color-data.ts index 19acb4e3396212cbafad013d967fd5ac5eeb1a2c..f4991df31f0e78e79baeed21abf7b5c8c1d6b370 100644 --- a/src/mol-geo/geometry/color-data.ts +++ b/src/mol-geo/geometry/color-data.ts @@ -33,14 +33,14 @@ export function createColors(locationIt: LocationIterator, colorTheme: ColorThem export function createValueColor(value: Color, colorData?: ColorData): ColorData { if (colorData) { - ValueCell.update(colorData.uColor, Color.toRgbNormalized(value) as Vec3) + ValueCell.update(colorData.uColor, Color.toVec3Normalized(colorData.uColor.ref.value, value)) if (colorData.dColorType.ref.value !== 'uniform') { ValueCell.update(colorData.dColorType, 'uniform') } return colorData } else { return { - uColor: ValueCell.create(Color.toRgbNormalized(value) as Vec3), + uColor: ValueCell.create(Color.toVec3Normalized(Vec3(), value)), tColor: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }), uColorTexDim: ValueCell.create(Vec2.create(1, 1)), dColorType: ValueCell.create('uniform'), @@ -74,7 +74,7 @@ export function createTextureColor(colors: TextureImage<Uint8Array>, type: Color /** Creates color texture with color for each instance/unit */ export function createInstanceColor(locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): ColorData { const { instanceCount } = locationIt - const colors = createTextureImage(Math.max(1, instanceCount), 3, colorData && colorData.tColor.ref.value.array) + const colors = createTextureImage(Math.max(1, instanceCount), 3, Uint8Array, colorData && colorData.tColor.ref.value.array) locationIt.reset() while (locationIt.hasNext) { const { location, isSecondary, instanceIndex } = locationIt.move() @@ -87,7 +87,7 @@ export function createInstanceColor(locationIt: LocationIterator, color: Locatio /** Creates color texture with color for each group (i.e. shared across instances/units) */ export function createGroupColor(locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): ColorData { const { groupCount } = locationIt - const colors = createTextureImage(Math.max(1, groupCount), 3, colorData && colorData.tColor.ref.value.array) + const colors = createTextureImage(Math.max(1, groupCount), 3, Uint8Array, colorData && colorData.tColor.ref.value.array) locationIt.reset() while (locationIt.hasNext && !locationIt.isNextNewInstance) { const { location, isSecondary, groupIndex } = locationIt.move() @@ -100,7 +100,7 @@ export function createGroupColor(locationIt: LocationIterator, color: LocationCo export function createGroupInstanceColor(locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): ColorData { const { groupCount, instanceCount } = locationIt const count = instanceCount * groupCount - const colors = createTextureImage(Math.max(1, count), 3, colorData && colorData.tColor.ref.value.array) + const colors = createTextureImage(Math.max(1, count), 3, Uint8Array, colorData && colorData.tColor.ref.value.array) locationIt.reset() while (locationIt.hasNext) { const { location, isSecondary, index } = locationIt.move() diff --git a/src/mol-geo/geometry/direct-volume/direct-volume.ts b/src/mol-geo/geometry/direct-volume/direct-volume.ts index ea2c9694c19dd979d2245baba0cde0a8050eb37d..32eb6957b6d7a9bcf261962e08a33bcd106a414d 100644 --- a/src/mol-geo/geometry/direct-volume/direct-volume.ts +++ b/src/mol-geo/geometry/direct-volume/direct-volume.ts @@ -25,6 +25,7 @@ import { ColorListOptions, ColorListName } from 'mol-util/color/scale'; import { Color } from 'mol-util/color'; import { BaseGeometry } from '../base'; import { createEmptyOverpaint } from '../overpaint-data'; +import { createEmptyTransparency } from '../transparency-data'; const VolumeBox = Box() const RenderModeOptions = [['isosurface', 'Isosurface'], ['volume', 'Volume']] as [string, string][] @@ -103,6 +104,7 @@ export namespace DirectVolume { const color = createColors(locationIt, theme.color) const marker = createMarkers(instanceCount * groupCount) const overpaint = createEmptyOverpaint() + const transparency = createEmptyTransparency() const counts = { drawCount: VolumeBox.indices.length, groupCount, instanceCount } @@ -117,6 +119,7 @@ export namespace DirectVolume { ...color, ...marker, ...overpaint, + ...transparency, ...transform, ...BaseGeometry.createValues(props, counts), diff --git a/src/mol-geo/geometry/geometry.ts b/src/mol-geo/geometry/geometry.ts index 668e118df8cb0377f01069cf1c9ffadcd0a36d51..36678a3feda45c1772dcfe01d26a271b27a6dbf3 100644 --- a/src/mol-geo/geometry/geometry.ts +++ b/src/mol-geo/geometry/geometry.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> */ @@ -21,6 +21,7 @@ import { TransformData } from './transform-data'; import { Theme } from 'mol-theme/theme'; import { RenderObjectValuesType } from 'mol-gl/render-object'; import { ValueOf } from 'mol-util/type-helpers'; +import { TextureMesh } from './texture-mesh/texture-mesh'; export type GeometryKindType = { 'mesh': Mesh, @@ -29,6 +30,7 @@ export type GeometryKindType = { 'text': Text, 'lines': Lines, 'direct-volume': DirectVolume, + 'texture-mesh': TextureMesh, } export type GeometryKindParams = { 'mesh': Mesh.Params, @@ -37,6 +39,7 @@ export type GeometryKindParams = { 'text': Text.Params, 'lines': Lines.Params, 'direct-volume': DirectVolume.Params, + 'texture-mesh': TextureMesh.Params, } export type GeometryKind = keyof GeometryKindType export type Geometry = ValueOf<GeometryKindType> @@ -63,6 +66,7 @@ export namespace Geometry { case 'text': return geometry.charCount * 2 * 3 case 'lines': return geometry.lineCount * 2 * 3 case 'direct-volume': return 12 * 3 + case 'texture-mesh': return geometry.vertexCount.ref.value } } @@ -76,6 +80,8 @@ export namespace Geometry { return getDrawCount(geometry) === 0 ? 0 : (arrayMax(geometry.groupBuffer.ref.value) + 1) case 'direct-volume': return 1 + case 'texture-mesh': + return geometry.groupCount.ref.value } } @@ -88,6 +94,7 @@ export namespace Geometry { case 'text': return Text.Utils as any case 'lines': return Lines.Utils as any case 'direct-volume': return DirectVolume.Utils as any + case 'texture-mesh': return TextureMesh.Utils as any } throw new Error('unknown geometry kind') } diff --git a/src/mol-geo/geometry/lines/lines.ts b/src/mol-geo/geometry/lines/lines.ts index a2795a9c8646390fd2fc0455ba9a779429ac9870..13dea32130f80e3d0c400373eca9feb74005d054 100644 --- a/src/mol-geo/geometry/lines/lines.ts +++ b/src/mol-geo/geometry/lines/lines.ts @@ -23,6 +23,7 @@ import { Theme } from 'mol-theme/theme'; import { Color } from 'mol-util/color'; import { BaseGeometry } from '../base'; import { createEmptyOverpaint } from '../overpaint-data'; +import { createEmptyTransparency } from '../transparency-data'; /** Wide line */ export interface Lines { @@ -119,6 +120,7 @@ export namespace Lines { const size = createSizes(locationIt, theme.size) const marker = createMarkers(instanceCount * groupCount) const overpaint = createEmptyOverpaint() + const transparency = createEmptyTransparency() const counts = { drawCount: lines.lineCount * 2 * 3, groupCount, instanceCount } @@ -137,6 +139,7 @@ export namespace Lines { ...size, ...marker, ...overpaint, + ...transparency, ...transform, ...BaseGeometry.createValues(props, counts), @@ -177,7 +180,7 @@ function getBoundingSphere(lineStart: Float32Array, lineEnd: Float32Array, lineC const start = calculateBoundingSphere(lineStart, lineCount * 4, transform, transformCount) const end = calculateBoundingSphere(lineEnd, lineCount * 4, transform, transformCount) return { - boundingSphere: Sphere3D.addSphere(start.boundingSphere, end.boundingSphere), - invariantBoundingSphere: Sphere3D.addSphere(start.invariantBoundingSphere, end.invariantBoundingSphere) + boundingSphere: Sphere3D.expandBySphere(start.boundingSphere, end.boundingSphere), + invariantBoundingSphere: Sphere3D.expandBySphere(start.invariantBoundingSphere, end.invariantBoundingSphere) } } \ No newline at end of file diff --git a/src/mol-geo/geometry/marker-data.ts b/src/mol-geo/geometry/marker-data.ts index 2c49bd1b233b05d234831506791289b26de0ecd6..65d8e59816a2c018f250227b051f72e44c95fa18 100644 --- a/src/mol-geo/geometry/marker-data.ts +++ b/src/mol-geo/geometry/marker-data.ts @@ -65,7 +65,7 @@ export function applyMarkerAction(array: Uint8Array, start: number, end: number, } export function createMarkers(count: number, markerData?: MarkerData): MarkerData { - const markers = createTextureImage(Math.max(1, count), 1, markerData && markerData.tMarker.ref.value.array) + const markers = createTextureImage(Math.max(1, count), 1, Uint8Array, markerData && markerData.tMarker.ref.value.array) if (markerData) { ValueCell.update(markerData.tMarker, markers) ValueCell.update(markerData.uMarkerTexDim, Vec2.create(markers.width, markers.height)) diff --git a/src/mol-geo/geometry/mesh/mesh-builder.ts b/src/mol-geo/geometry/mesh/mesh-builder.ts index 2bcaf085159f4597b6ee0c0d915b754c6aeac46c..cfe57234db1d2b4fd8ce2648158cf4d4369e5656 100644 --- a/src/mol-geo/geometry/mesh/mesh-builder.ts +++ b/src/mol-geo/geometry/mesh/mesh-builder.ts @@ -45,7 +45,7 @@ export namespace MeshBuilder { export function addTriangle(state: State, a: Vec3, b: Vec3, c: Vec3) { const { vertices, normals, indices, groups, currentGroup } = state const offset = vertices.elementCount - + // positions ChunkedArray.add3(vertices, a[0], a[1], a[2]); ChunkedArray.add3(vertices, b[0], b[1], b[2]); diff --git a/src/mol-geo/geometry/mesh/mesh.ts b/src/mol-geo/geometry/mesh/mesh.ts index fd594d952c6ad0aa3f886828537e0f833651b53c..1095cd65484827f2a2b72916adea018f47bef388 100644 --- a/src/mol-geo/geometry/mesh/mesh.ts +++ b/src/mol-geo/geometry/mesh/mesh.ts @@ -22,6 +22,7 @@ import { MeshValues } from 'mol-gl/renderable/mesh'; import { Color } from 'mol-util/color'; import { BaseGeometry } from '../base'; import { createEmptyOverpaint } from '../overpaint-data'; +import { createEmptyTransparency } from '../transparency-data'; export interface Mesh { readonly kind: 'mesh', @@ -383,6 +384,7 @@ export namespace Mesh { const color = createColors(locationIt, theme.color) const marker = createMarkers(instanceCount * groupCount) const overpaint = createEmptyOverpaint() + const transparency = createEmptyTransparency() const counts = { drawCount: mesh.triangleCount * 3, groupCount, instanceCount } @@ -401,6 +403,7 @@ export namespace Mesh { ...color, ...marker, ...overpaint, + ...transparency, ...transform, ...BaseGeometry.createValues(props, counts), diff --git a/src/mol-geo/geometry/overpaint-data.ts b/src/mol-geo/geometry/overpaint-data.ts index 875bef81b528dd5989f394dbfd4e0cfdf53870bb..a1c74f1273bb7f2b854357062a0bb1390ca45802 100644 --- a/src/mol-geo/geometry/overpaint-data.ts +++ b/src/mol-geo/geometry/overpaint-data.ts @@ -28,7 +28,7 @@ export function clearOverpaint(array: Uint8Array, start: number, end: number) { } export function createOverpaint(count: number, overpaintData?: OverpaintData): OverpaintData { - const overpaint = createTextureImage(Math.max(1, count), 4, overpaintData && overpaintData.tOverpaint.ref.value.array) + const overpaint = createTextureImage(Math.max(1, count), 4, Uint8Array, overpaintData && overpaintData.tOverpaint.ref.value.array) if (overpaintData) { ValueCell.update(overpaintData.tOverpaint, overpaint) ValueCell.update(overpaintData.uOverpaintTexDim, Vec2.create(overpaint.width, overpaint.height)) diff --git a/src/mol-geo/geometry/points/points.ts b/src/mol-geo/geometry/points/points.ts index 7cda2e36ef2d8241561cdbcb1a341cfbf99d1446..cb5f9c648f38006387d7d9604bcc3704f5153ccf 100644 --- a/src/mol-geo/geometry/points/points.ts +++ b/src/mol-geo/geometry/points/points.ts @@ -22,6 +22,7 @@ import { RenderableState } from 'mol-gl/renderable'; import { Color } from 'mol-util/color'; import { BaseGeometry } from '../base'; import { createEmptyOverpaint } from '../overpaint-data'; +import { createEmptyTransparency } from '../transparency-data'; /** Point cloud */ export interface Points { @@ -84,6 +85,7 @@ export namespace Points { const size = createSizes(locationIt, theme.size) const marker = createMarkers(instanceCount * groupCount) const overpaint = createEmptyOverpaint() + const transparency = createEmptyTransparency() const counts = { drawCount: points.pointCount, groupCount, instanceCount } @@ -101,6 +103,7 @@ export namespace Points { ...size, ...marker, ...overpaint, + ...transparency, ...transform, ...BaseGeometry.createValues(props, counts), diff --git a/src/mol-geo/geometry/size-data.ts b/src/mol-geo/geometry/size-data.ts index fb4b94b851027ba336703e18d8502f2b34c6338b..9fdc122f25e83585c983a3c0552bb5af6012d111 100644 --- a/src/mol-geo/geometry/size-data.ts +++ b/src/mol-geo/geometry/size-data.ts @@ -101,7 +101,7 @@ export function createTextureSize(sizes: TextureImage<Uint8Array>, type: SizeTyp /** Creates size texture with size for each instance/unit */ export function createInstanceSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData { const { instanceCount} = locationIt - const sizes = createTextureImage(Math.max(1, instanceCount), 1, sizeData && sizeData.tSize.ref.value.array) + const sizes = createTextureImage(Math.max(1, instanceCount), 1, Uint8Array, sizeData && sizeData.tSize.ref.value.array) locationIt.reset() while (locationIt.hasNext && !locationIt.isNextNewInstance) { const v = locationIt.move() @@ -114,7 +114,7 @@ export function createInstanceSize(locationIt: LocationIterator, sizeFn: Locatio /** Creates size texture with size for each group (i.e. shared across instances/units) */ export function createGroupSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData { const { groupCount } = locationIt - const sizes = createTextureImage(Math.max(1, groupCount), 1, sizeData && sizeData.tSize.ref.value.array) + const sizes = createTextureImage(Math.max(1, groupCount), 1, Uint8Array, sizeData && sizeData.tSize.ref.value.array) locationIt.reset() while (locationIt.hasNext && !locationIt.isNextNewInstance) { const v = locationIt.move() @@ -127,7 +127,7 @@ export function createGroupSize(locationIt: LocationIterator, sizeFn: LocationSi export function createGroupInstanceSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData { const { groupCount, instanceCount } = locationIt const count = instanceCount * groupCount - const sizes = createTextureImage(Math.max(1, count), 1, sizeData && sizeData.tSize.ref.value.array) + const sizes = createTextureImage(Math.max(1, count), 1, Uint8Array, sizeData && sizeData.tSize.ref.value.array) locationIt.reset() while (locationIt.hasNext && !locationIt.isNextNewInstance) { const v = locationIt.move() diff --git a/src/mol-geo/geometry/spheres/spheres.ts b/src/mol-geo/geometry/spheres/spheres.ts index eb22f55a918043f512f28824d857b3fb61d00db1..9d38ea9a2bfcb28cd64b5119e62f6caccad21303 100644 --- a/src/mol-geo/geometry/spheres/spheres.ts +++ b/src/mol-geo/geometry/spheres/spheres.ts @@ -19,6 +19,7 @@ import { createSizes, getMaxSize } from '../size-data'; import { Color } from 'mol-util/color'; import { BaseGeometry } from '../base'; import { createEmptyOverpaint } from '../overpaint-data'; +import { createEmptyTransparency } from '../transparency-data'; /** Spheres */ export interface Spheres { @@ -81,6 +82,7 @@ export namespace Spheres { const size = createSizes(locationIt, theme.size) const marker = createMarkers(instanceCount * groupCount) const overpaint = createEmptyOverpaint() + const transparency = createEmptyTransparency() const counts = { drawCount: spheres.sphereCount * 2 * 3, groupCount, instanceCount } @@ -101,6 +103,7 @@ export namespace Spheres { ...size, ...marker, ...overpaint, + ...transparency, ...transform, padding: ValueCell.create(padding), diff --git a/src/mol-geo/geometry/text/font-atlas.ts b/src/mol-geo/geometry/text/font-atlas.ts index 0ad781f2bd77edd0ee34462da0aa2199c9546ec4..46f11f633197e941f93173e9aba398bb5a100562 100644 --- a/src/mol-geo/geometry/text/font-atlas.ts +++ b/src/mol-geo/geometry/text/font-atlas.ts @@ -81,7 +81,7 @@ export class FontAtlas { this.maxWidth = Math.round(this.lineHeight * 0.75) // create texture (for ~350 characters) - this.texture = createTextureImage(350 * this.lineHeight * this.maxWidth, 1) + this.texture = createTextureImage(350 * this.lineHeight * this.maxWidth, 1, Uint8Array) // prepare scratch canvas this.scratchCanvas = document.createElement('canvas') diff --git a/src/mol-geo/geometry/text/text.ts b/src/mol-geo/geometry/text/text.ts index 4238f2432cc9b24f9cb21e2d3dbf2841c70179af..0323d397ded64e55b7fb0c7ffd4880f93252bd4d 100644 --- a/src/mol-geo/geometry/text/text.ts +++ b/src/mol-geo/geometry/text/text.ts @@ -25,6 +25,7 @@ import { clamp } from 'mol-math/interpolate'; import { createRenderObject as _createRenderObject } from 'mol-gl/render-object'; import { BaseGeometry } from '../base'; import { createEmptyOverpaint } from '../overpaint-data'; +import { createEmptyTransparency } from '../transparency-data'; type TextAttachment = ( 'bottom-left' | 'bottom-center' | 'bottom-right' | @@ -57,7 +58,7 @@ export interface Text { export namespace Text { export function createEmpty(text?: Text): Text { - const ft = text ? text.fontTexture.ref.value : createTextureImage(0, 1) + const ft = text ? text.fontTexture.ref.value : createTextureImage(0, 1, Uint8Array) const cb = text ? text.centerBuffer.ref.value : new Float32Array(0) const mb = text ? text.mappingBuffer.ref.value : new Float32Array(0) const db = text ? text.depthBuffer.ref.value : new Float32Array(0) @@ -124,6 +125,7 @@ export namespace Text { const size = createSizes(locationIt, theme.size) const marker = createMarkers(instanceCount * groupCount) const overpaint = createEmptyOverpaint() + const transparency = createEmptyTransparency() const counts = { drawCount: text.charCount * 2 * 3, groupCount, instanceCount } @@ -145,6 +147,7 @@ export namespace Text { ...size, ...marker, ...overpaint, + ...transparency, ...transform, aTexCoord: text.tcoordBuffer, diff --git a/src/mol-geo/geometry/texture-mesh/texture-mesh.ts b/src/mol-geo/geometry/texture-mesh/texture-mesh.ts new file mode 100644 index 0000000000000000000000000000000000000000..9e235f03ca3bbe469448c6f9188a1a8bee9e5266 --- /dev/null +++ b/src/mol-geo/geometry/texture-mesh/texture-mesh.ts @@ -0,0 +1,160 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { ValueCell } from 'mol-util' +import { Sphere3D } from 'mol-math/geometry' +import { ParamDefinition as PD } from 'mol-util/param-definition'; +import { LocationIterator } from 'mol-geo/util/location-iterator'; +import { TransformData } from '../transform-data'; +import { createColors } from '../color-data'; +import { createMarkers } from '../marker-data'; +import { GeometryUtils } from '../geometry'; +import { Theme } from 'mol-theme/theme'; +import { Color } from 'mol-util/color'; +import { BaseGeometry } from '../base'; +import { createEmptyOverpaint } from '../overpaint-data'; +import { createEmptyTransparency } from '../transparency-data'; +import { TextureMeshValues } from 'mol-gl/renderable/texture-mesh'; +import { calculateTransformBoundingSphere } from 'mol-gl/renderable/util'; +import { Texture } from 'mol-gl/webgl/texture'; +import { Vec2 } from 'mol-math/linear-algebra'; +import { fillSerial } from 'mol-util/array'; + +export interface TextureMesh { + readonly kind: 'texture-mesh', + + /** Number of vertices in the texture-mesh */ + readonly vertexCount: ValueCell<number>, + /** Number of groups in the texture-mesh */ + readonly groupCount: ValueCell<number>, + + readonly geoTextureDim: ValueCell<Vec2>, + /** texture has vertex positions in XYZ and group id in W */ + readonly vertexGroupTexture: ValueCell<Texture>, + readonly normalTexture: ValueCell<Texture>, + + readonly boundingSphere: ValueCell<Sphere3D>, +} + +export namespace TextureMesh { + export function create(vertexCount: number, groupCount: number, vertexGroupTexture: Texture, normalTexture: Texture, boundingSphere: Sphere3D, textureMesh?: TextureMesh): TextureMesh { + const { width, height } = vertexGroupTexture + if (textureMesh) { + ValueCell.update(textureMesh.vertexCount, vertexCount) + ValueCell.update(textureMesh.groupCount, groupCount) + ValueCell.update(textureMesh.geoTextureDim, Vec2.set(textureMesh.geoTextureDim.ref.value, width, height)) + ValueCell.update(textureMesh.vertexGroupTexture, vertexGroupTexture) + ValueCell.update(textureMesh.normalTexture, normalTexture) + ValueCell.update(textureMesh.boundingSphere, boundingSphere) + return textureMesh + } else { + return { + kind: 'texture-mesh', + vertexCount: ValueCell.create(vertexCount), + groupCount: ValueCell.create(groupCount), + geoTextureDim: ValueCell.create(Vec2.create(width, height)), + vertexGroupTexture: ValueCell.create(vertexGroupTexture), + normalTexture: ValueCell.create(normalTexture), + boundingSphere: ValueCell.create(boundingSphere), + } + } + } + + export function createEmpty(textureMesh?: TextureMesh): TextureMesh { + return {} as TextureMesh // TODO + } + + export const Params = { + ...BaseGeometry.Params, + doubleSided: PD.Boolean(false), + flipSided: PD.Boolean(false), + flatShaded: PD.Boolean(false), + } + export type Params = typeof Params + + export const Utils: GeometryUtils<TextureMesh, Params> = { + Params, + createEmpty, + createValues, + createValuesSimple, + updateValues, + updateBoundingSphere, + createRenderableState: BaseGeometry.createRenderableState, + updateRenderableState: BaseGeometry.updateRenderableState + } + + function createValues(textureMesh: TextureMesh, transform: TransformData, locationIt: LocationIterator, theme: Theme, props: PD.Values<Params>): TextureMeshValues { + const { instanceCount, groupCount } = locationIt + const color = createColors(locationIt, theme.color) + const marker = createMarkers(instanceCount * groupCount) + const overpaint = createEmptyOverpaint() + const transparency = createEmptyTransparency() + + const counts = { drawCount: textureMesh.vertexCount.ref.value, groupCount, instanceCount } + + const transformBoundingSphere = calculateTransformBoundingSphere(textureMesh.boundingSphere.ref.value, transform.aTransform.ref.value, transform.instanceCount.ref.value) + + return { + uGeoTexDim: textureMesh.geoTextureDim, + tPositionGroup: textureMesh.vertexGroupTexture, + tNormal: textureMesh.normalTexture, + + // aGroup is used as a vertex index here and the group id is retirieved from tPositionGroup + aGroup: ValueCell.create(fillSerial(new Float32Array(textureMesh.vertexCount.ref.value))), + boundingSphere: ValueCell.create(transformBoundingSphere), + invariantBoundingSphere: textureMesh.boundingSphere, + + ...color, + ...marker, + ...overpaint, + ...transparency, + ...transform, + + ...BaseGeometry.createValues(props, counts), + dDoubleSided: ValueCell.create(props.doubleSided), + dFlatShaded: ValueCell.create(props.flatShaded), + dFlipSided: ValueCell.create(props.flipSided), + dGeoTexture: ValueCell.create(true), + } + } + + function createValuesSimple(textureMesh: TextureMesh, props: Partial<PD.Values<Params>>, colorValue: Color, sizeValue: number, transform?: TransformData) { + const s = BaseGeometry.createSimple(colorValue, sizeValue, transform) + const p = { ...PD.getDefaultValues(Params), ...props } + return createValues(textureMesh, s.transform, s.locationIterator, s.theme, p) + } + + function updateValues(values: TextureMeshValues, props: PD.Values<Params>) { + if (Color.fromNormalizedArray(values.uHighlightColor.ref.value, 0) !== props.highlightColor) { + ValueCell.update(values.uHighlightColor, Color.toArrayNormalized(props.highlightColor, values.uHighlightColor.ref.value, 0)) + } + if (Color.fromNormalizedArray(values.uSelectColor.ref.value, 0) !== props.selectColor) { + ValueCell.update(values.uSelectColor, Color.toArrayNormalized(props.selectColor, values.uSelectColor.ref.value, 0)) + } + ValueCell.updateIfChanged(values.alpha, props.alpha) // `uAlpha` is set in renderable.render + ValueCell.updateIfChanged(values.dUseFog, props.useFog) + + ValueCell.updateIfChanged(values.dDoubleSided, props.doubleSided) + ValueCell.updateIfChanged(values.dFlatShaded, props.flatShaded) + ValueCell.updateIfChanged(values.dFlipSided, props.flipSided) + + if (values.drawCount.ref.value > values.aGroup.ref.value.length) { + // console.log('updating vertex ids in aGroup to handle larger drawCount') + ValueCell.update(values.aGroup, fillSerial(new Float32Array(values.drawCount.ref.value))) + } + } + + function updateBoundingSphere(values: TextureMeshValues, textureMesh: TextureMesh) { + const invariantBoundingSphere = textureMesh.boundingSphere.ref.value + const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, values.aTransform.ref.value, values.instanceCount.ref.value) + if (!Sphere3D.equals(boundingSphere, values.boundingSphere.ref.value)) { + ValueCell.update(values.boundingSphere, boundingSphere) + } + if (!Sphere3D.equals(invariantBoundingSphere, values.invariantBoundingSphere.ref.value)) { + ValueCell.update(values.invariantBoundingSphere, invariantBoundingSphere) + } + } +} \ No newline at end of file diff --git a/src/mol-geo/geometry/transparency-data.ts b/src/mol-geo/geometry/transparency-data.ts new file mode 100644 index 0000000000000000000000000000000000000000..3b19b6130f9b6dff272c9dd7617c5ae8cb3ad98b --- /dev/null +++ b/src/mol-geo/geometry/transparency-data.ts @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { ValueCell } from 'mol-util/value-cell' +import { Vec2 } from 'mol-math/linear-algebra' +import { TextureImage, createTextureImage } from 'mol-gl/renderable/util'; +import { Transparency } from 'mol-theme/transparency'; + +export type TransparencyData = { + tTransparency: ValueCell<TextureImage<Uint8Array>> + uTransparencyTexDim: ValueCell<Vec2> + dTransparency: ValueCell<boolean>, + dTransparencyVariant: ValueCell<string>, +} + +export function applyTransparencyValue(array: Uint8Array, start: number, end: number, value: number) { + for (let i = start; i < end; ++i) { + array[i] = value * 255 + } + return true +} + +export function clearTransparency(array: Uint8Array, start: number, end: number) { + array.fill(0, start, end) +} + +export function createTransparency(count: number, variant: Transparency.Variant, transparencyData?: TransparencyData): TransparencyData { + const transparency = createTextureImage(Math.max(1, count), 1, Uint8Array, transparencyData && transparencyData.tTransparency.ref.value.array) + if (transparencyData) { + ValueCell.update(transparencyData.tTransparency, transparency) + ValueCell.update(transparencyData.uTransparencyTexDim, Vec2.create(transparency.width, transparency.height)) + ValueCell.update(transparencyData.dTransparency, count > 0) + ValueCell.update(transparencyData.dTransparencyVariant, variant) + return transparencyData + } else { + return { + tTransparency: ValueCell.create(transparency), + uTransparencyTexDim: ValueCell.create(Vec2.create(transparency.width, transparency.height)), + dTransparency: ValueCell.create(count > 0), + dTransparencyVariant: ValueCell.create(variant), + } + } +} + +const emptyTransparencyTexture = { array: new Uint8Array(1), width: 1, height: 1 } +export function createEmptyTransparency(transparencyData?: TransparencyData): TransparencyData { + if (transparencyData) { + ValueCell.update(transparencyData.tTransparency, emptyTransparencyTexture) + ValueCell.update(transparencyData.uTransparencyTexDim, Vec2.create(1, 1)) + return transparencyData + } else { + return { + tTransparency: ValueCell.create(emptyTransparencyTexture), + uTransparencyTexDim: ValueCell.create(Vec2.create(1, 1)), + dTransparency: ValueCell.create(false), + dTransparencyVariant: ValueCell.create('single'), + } + } +} \ No newline at end of file diff --git a/src/mol-geo/util/marching-cubes/algorithm.ts b/src/mol-geo/util/marching-cubes/algorithm.ts index e9d9e4ad46ee87691bc4c9d34b01dfe8a476b071..e446ad2576f1d9a3ffc2b8afda4c0ba2f87b8c2a 100644 --- a/src/mol-geo/util/marching-cubes/algorithm.ts +++ b/src/mol-geo/util/marching-cubes/algorithm.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 David Sehnal <david.sehnal@gmail.com> * @author Alexander Rose <alexander.rose@weirdbyte.de> @@ -12,7 +12,6 @@ import { Index, EdgeIdInfo, CubeEdges, EdgeTable, TriTable } from './tables' import { defaults } from 'mol-util' import { MarchinCubesBuilder, MarchinCubesMeshBuilder, MarchinCubesLinesBuilder } from './builder'; import { Lines } from '../../geometry/lines/lines'; -// import { Lines } from '../../geometry/lines/lines'; /** * The parameters required by the algorithm. @@ -156,17 +155,40 @@ class MarchingCubesState { const ret = this.verticesOnEdges[edgeId]; if (ret > 0) return ret - 1; + const sf = this.scalarField + const sfg = this.scalarFieldGet + const edge = CubeEdges[edgeNum]; const a = edge.a, b = edge.b; const li = a.i + this.i, lj = a.j + this.j, lk = a.k + this.k; const hi = b.i + this.i, hj = b.j + this.j, hk = b.k + this.k; - const v0 = this.scalarFieldGet(this.scalarField, li, lj, lk); - const v1 = this.scalarFieldGet(this.scalarField, hi, hj, hk); + const v0 = sfg(sf, li, lj, lk); + const v1 = sfg(sf, hi, hj, hk); const t = (this.isoLevel - v0) / (v0 - v1); - const id = this.builder.addVertex(li + t * (li - hi), lj + t * (lj - hj), lk + t * (lk - hk)); + const id = this.builder.addVertex( + li + t * (li - hi), + lj + t * (lj - hj), + lk + t * (lk - hk) + ); this.verticesOnEdges[edgeId] = id + 1; + // TODO cache scalarField differences for slices + // TODO make calculation optional + const n0x = sfg(sf, Math.max(0, li - 1), lj, lk) - sfg(sf, Math.min(this.nX - 1, li + 1), lj, lk) + const n0y = sfg(sf, li, Math.max(0, lj - 1), lk) - sfg(sf, li, Math.min(this.nY - 1, lj + 1), lk) + const n0z = sfg(sf, li, lj, Math.max(0, lk - 1)) - sfg(sf, li, lj, Math.min(this.nZ, lk + 1)) + + const n1x = sfg(sf, Math.max(0, hi - 1), hj, hk) - sfg(sf, Math.min(this.nX - 1, hi + 1), hj, hk) + const n1y = sfg(sf, hi, Math.max(0, hj - 1), hk) - sfg(sf, hi, Math.min(this.nY - 1, hj + 1), hk) + const n1z = sfg(sf, hi, hj, Math.max(0, hk - 1)) - sfg(sf, hi, hj, Math.min(this.nZ - 1, hk + 1)) + + this.builder.addNormal( + n0x + t * (n0x - n1x), + n0y + t * (n0y - n1y), + n0z + t * (n0z - n1z) + ) + if (this.idField) { const u = this.idFieldGet!(this.idField, li, lj, lk); const v = this.idFieldGet!(this.idField, hi, hj, hk) diff --git a/src/mol-geo/util/marching-cubes/builder.ts b/src/mol-geo/util/marching-cubes/builder.ts index be7abebb915efd13d1d86f11e3ce866633b09263..8d78d9f32614947573d734bc390fee08b05e9b9d 100644 --- a/src/mol-geo/util/marching-cubes/builder.ts +++ b/src/mol-geo/util/marching-cubes/builder.ts @@ -7,7 +7,7 @@ */ import { ChunkedArray } from '../../../mol-data/util'; -import { ValueCell } from 'mol-util'; +import { ValueCell, noop } from 'mol-util'; import { Mesh } from '../../geometry/mesh/mesh'; import { AllowedContours } from './tables'; import { LinesBuilder } from '../../geometry/lines/lines-builder'; @@ -15,6 +15,7 @@ import { Lines } from '../../geometry/lines/lines'; export interface MarchinCubesBuilder<T> { addVertex(x: number, y: number, z: number): number + addNormal(x: number, y: number, z: number): void addGroup(group: number): void addTriangle(vertList: number[], a: number, b: number, c: number, edgeFilter: number): void get(): T @@ -24,6 +25,7 @@ export function MarchinCubesMeshBuilder(vertexChunkSize: number, mesh?: Mesh): M const triangleChunkSize = Math.min(1 << 16, vertexChunkSize * 4) const vertices = ChunkedArray.create(Float32Array, 3, vertexChunkSize, mesh && mesh.vertexBuffer.ref.value); + const normals = ChunkedArray.create(Float32Array, 3, vertexChunkSize, mesh && mesh.normalBuffer.ref.value); const groups = ChunkedArray.create(Float32Array, 1, vertexChunkSize, mesh && mesh.groupBuffer.ref.value); const indices = ChunkedArray.create(Uint32Array, 3, triangleChunkSize, mesh && mesh.indexBuffer.ref.value); @@ -35,6 +37,9 @@ export function MarchinCubesMeshBuilder(vertexChunkSize: number, mesh?: Mesh): M ++vertexCount return ChunkedArray.add3(vertices, x, y, z ); }, + addNormal: (x: number, y: number, z: number) => { + ChunkedArray.add3(normals, x, y, z ); + }, addGroup: (group: number) => { ChunkedArray.add(groups, group); }, @@ -44,6 +49,7 @@ export function MarchinCubesMeshBuilder(vertexChunkSize: number, mesh?: Mesh): M }, get: () => { const vb = ChunkedArray.compact(vertices, true) as Float32Array; + const nb = ChunkedArray.compact(normals, true) as Float32Array; const ib = ChunkedArray.compact(indices, true) as Uint32Array; const gb = ChunkedArray.compact(groups, true) as Float32Array; @@ -54,8 +60,8 @@ export function MarchinCubesMeshBuilder(vertexChunkSize: number, mesh?: Mesh): M vertexBuffer: mesh ? ValueCell.update(mesh.vertexBuffer, vb) : ValueCell.create(vb), groupBuffer: mesh ? ValueCell.update(mesh.groupBuffer, gb) : ValueCell.create(gb), indexBuffer: mesh ? ValueCell.update(mesh.indexBuffer, ib) : ValueCell.create(ib), - normalBuffer: mesh ? mesh.normalBuffer : ValueCell.create(new Float32Array(0)), - normalsComputed: false + normalBuffer: mesh ? ValueCell.update(mesh.normalBuffer, nb) : ValueCell.create(nb), + normalsComputed: true } } } @@ -72,6 +78,7 @@ export function MarchinCubesLinesBuilder(vertexChunkSize: number, lines?: Lines) addVertex: (x: number, y: number, z: number) => { return ChunkedArray.add3(vertices, x, y, z); }, + addNormal: () => noop, addGroup: (group: number) => { ChunkedArray.add(groups, group); }, diff --git a/src/mol-gl/_spec/gl.shim.ts b/src/mol-gl/_spec/gl.shim.ts index d4242f5a1628383c9dcb93083eb7608ad56df978..b593beef409268de777313028b3ecdfc9c069de1 100644 --- a/src/mol-gl/_spec/gl.shim.ts +++ b/src/mol-gl/_spec/gl.shim.ts @@ -1,5 +1,3 @@ - - /** * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. * @@ -679,14 +677,14 @@ export function createGl(width: number, height: number, contextAttributes: WebGL return { size: 1, type: gl.INT_VEC3, - name: `activeUniform${index}`, + name: `__activeUniform${index}`, }; }, getActiveAttrib: function (program: WebGLProgram, index: number) { return { size: 1, type: gl.FLOAT, - name: `activeAttrib${index}` + name: `__activeAttrib${index}` }; }, clear: function () { }, diff --git a/src/mol-gl/_spec/renderer.spec.ts b/src/mol-gl/_spec/renderer.spec.ts index 314d75f0bd3dc31c67d860fd296c56e969a8c4b1..62cbb35e491404bd1e029c06fa1017eb4e8d24f2 100644 --- a/src/mol-gl/_spec/renderer.spec.ts +++ b/src/mol-gl/_spec/renderer.spec.ts @@ -23,6 +23,7 @@ import { fillSerial } from 'mol-util/array'; import { Color } from 'mol-util/color'; import { Sphere3D } from 'mol-math/geometry'; import { createEmptyOverpaint } from 'mol-geo/geometry/overpaint-data'; +import { createEmptyTransparency } from 'mol-geo/geometry/transparency-data'; // function writeImage(gl: WebGLRenderingContext, width: number, height: number) { // const pixels = new Uint8Array(width * height * 4) @@ -54,6 +55,7 @@ function createPoints() { const size = createValueSize(1) const marker = createEmptyMarkers() const overpaint = createEmptyOverpaint() + const transparency = createEmptyTransparency() const aTransform = ValueCell.create(new Float32Array(16)) const m4 = Mat4.identity() @@ -73,6 +75,7 @@ function createPoints() { ...marker, ...size, ...overpaint, + ...transparency, uAlpha: ValueCell.create(1.0), uHighlightColor: ValueCell.create(Vec3.create(1.0, 0.4, 0.6)), @@ -102,7 +105,7 @@ function createPoints() { opaque: true } - return createRenderObject('points', values, state) + return createRenderObject('points', values, state, -1) } describe('renderer', () => { @@ -114,9 +117,9 @@ describe('renderer', () => { expect(ctx.gl.canvas.width).toBe(32) expect(ctx.gl.canvas.height).toBe(32) - expect(ctx.bufferCount).toBe(0); - expect(ctx.textureCount).toBe(0); - expect(ctx.vaoCount).toBe(0); + expect(ctx.stats.bufferCount).toBe(0); + expect(ctx.stats.textureCount).toBe(0); + expect(ctx.stats.vaoCount).toBe(0); expect(ctx.programCache.count).toBe(0); expect(ctx.shaderCache.count).toBe(0); @@ -134,16 +137,16 @@ describe('renderer', () => { const points = createPoints() scene.add(points) - expect(ctx.bufferCount).toBe(4); - expect(ctx.textureCount).toBe(4); - expect(ctx.vaoCount).toBe(4); + expect(ctx.stats.bufferCount).toBe(4); + expect(ctx.stats.textureCount).toBe(5); + expect(ctx.stats.vaoCount).toBe(4); expect(ctx.programCache.count).toBe(4); expect(ctx.shaderCache.count).toBe(8); scene.remove(points) - expect(ctx.bufferCount).toBe(0); - expect(ctx.textureCount).toBe(0); - expect(ctx.vaoCount).toBe(0); + expect(ctx.stats.bufferCount).toBe(0); + expect(ctx.stats.textureCount).toBe(0); + expect(ctx.stats.vaoCount).toBe(0); expect(ctx.programCache.count).toBe(4); expect(ctx.shaderCache.count).toBe(8); diff --git a/src/mol-gl/compute/histogram-pyramid/reduction.ts b/src/mol-gl/compute/histogram-pyramid/reduction.ts new file mode 100644 index 0000000000000000000000000000000000000000..9fda346fea9ced4abab09d230e824ef0d57be62e --- /dev/null +++ b/src/mol-gl/compute/histogram-pyramid/reduction.ts @@ -0,0 +1,162 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { createComputeRenderable, ComputeRenderable } from '../../renderable' +import { WebGLContext } from '../../webgl/context'; +import { createComputeRenderItem } from '../../webgl/render-item'; +import { Values, TextureSpec, UniformSpec } from '../../renderable/schema'; +import { Texture, createTexture } from 'mol-gl/webgl/texture'; +import { ShaderCode } from 'mol-gl/shader-code'; +import { ValueCell } from 'mol-util'; +import { QuadSchema, QuadValues } from '../util'; +import { Vec2 } from 'mol-math/linear-algebra'; +import { getHistopyramidSum } from './sum'; +import { Framebuffer, createFramebuffer } from 'mol-gl/webgl/framebuffer'; +import { isPowerOfTwo } from 'mol-math/misc'; + +const HistopyramidReductionSchema = { + ...QuadSchema, + tPreviousLevel: TextureSpec('texture', 'rgba', 'float', 'nearest'), + uSize: UniformSpec('f'), + uTexSize: UniformSpec('f'), +} + +let HistopyramidReductionRenderable: ComputeRenderable<Values<typeof HistopyramidReductionSchema>> +function getHistopyramidReductionRenderable(ctx: WebGLContext, initialTexture: Texture) { + if (HistopyramidReductionRenderable) { + ValueCell.update(HistopyramidReductionRenderable.values.tPreviousLevel, initialTexture) + HistopyramidReductionRenderable.update() + return HistopyramidReductionRenderable + } else { + const values: Values<typeof HistopyramidReductionSchema> = { + ...QuadValues, + tPreviousLevel: ValueCell.create(initialTexture), + uSize: ValueCell.create(0), + uTexSize: ValueCell.create(0), + } + + const schema = { ...HistopyramidReductionSchema } + const shaderCode = ShaderCode( + require('mol-gl/shader/quad.vert').default, + require('mol-gl/shader/histogram-pyramid/reduction.frag').default + ) + const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values) + + HistopyramidReductionRenderable = createComputeRenderable(renderItem, values); + return HistopyramidReductionRenderable + } +} + +type TextureFramebuffer = { texture: Texture, framebuffer: Framebuffer } +const LevelTexturesFramebuffers: TextureFramebuffer[] = [] +function getLevelTextureFramebuffer(ctx: WebGLContext, level: number) { + let textureFramebuffer = LevelTexturesFramebuffers[level] + const size = Math.pow(2, level) + if (textureFramebuffer === undefined) { + const texture = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest') + const framebuffer = createFramebuffer(ctx.gl, ctx.stats) + texture.attachFramebuffer(framebuffer, 0) + textureFramebuffer = { texture, framebuffer } + textureFramebuffer.texture.define(size, size) + LevelTexturesFramebuffers[level] = textureFramebuffer + } + return textureFramebuffer +} + +function setRenderingDefaults(ctx: WebGLContext) { + const { gl, state } = ctx + state.disable(gl.CULL_FACE) + state.disable(gl.BLEND) + state.disable(gl.DEPTH_TEST) + state.disable(gl.SCISSOR_TEST) + state.depthMask(false) + state.colorMask(true, true, true, true) + state.clearColor(0, 0, 0, 0) +} + +export interface HistogramPyramid { + pyramidTex: Texture + count: number + height: number + levels: number + scale: Vec2 +} + +export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture, scale: Vec2): HistogramPyramid { + const { gl, framebufferCache } = ctx + + // printTexture(ctx, inputTexture, 2) + if (inputTexture.width !== inputTexture.height || !isPowerOfTwo(inputTexture.width)) { + throw new Error('inputTexture must be of square power-of-two size') + } + + // This part set the levels + const levels = Math.ceil(Math.log(inputTexture.width) / Math.log(2)) + const maxSize = Math.pow(2, levels) + // console.log('levels', levels, 'maxSize', maxSize) + + const pyramidTexture = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest') + pyramidTexture.define(maxSize, maxSize) + + const framebuffer = framebufferCache.get('reduction').value + pyramidTexture.attachFramebuffer(framebuffer, 0) + gl.clear(gl.COLOR_BUFFER_BIT) + + const levelTexturesFramebuffers: TextureFramebuffer[] = [] + for (let i = 0; i < levels; ++i) levelTexturesFramebuffers.push(getLevelTextureFramebuffer(ctx, i)) + + const renderable = getHistopyramidReductionRenderable(ctx, inputTexture) + ctx.state.currentRenderItemId = -1 + setRenderingDefaults(ctx) + + let offset = 0; + for (let i = 0; i < levels; i++) { + const currLevel = levels - 1 - i + const tf = levelTexturesFramebuffers[currLevel] + tf.framebuffer.bind() + // levelTextures[currLevel].attachFramebuffer(framebuffer, 0) + + const size = Math.pow(2, currLevel) + // console.log('size', size, 'draw-level', currLevel, 'read-level', levels - i) + gl.clear(gl.COLOR_BUFFER_BIT) + gl.viewport(0, 0, size, size) + + ValueCell.update(renderable.values.uSize, Math.pow(2, i + 1) / maxSize) + ValueCell.update(renderable.values.uTexSize, size) + if (i > 0) { + ValueCell.update(renderable.values.tPreviousLevel, levelTexturesFramebuffers[levels - i].texture) + renderable.update() + } + ctx.state.currentRenderItemId = -1 + renderable.render() + + pyramidTexture.bind(0) + gl.copyTexSubImage2D(gl.TEXTURE_2D, 0, offset, 0, 0, 0, size, size); + pyramidTexture.unbind(0) + + offset += size; + } + + gl.finish() + + // printTexture(ctx, pyramidTexture, 2) + + // + + const finalCount = getHistopyramidSum(ctx, levelTexturesFramebuffers[0].texture) + const height = Math.ceil(finalCount / Math.pow(2, levels)) + // const scale = Vec2.create(maxSize / inputTexture.width, maxSize / inputTexture.height) + // console.log('height', height, 'finalCount', finalCount, 'scale', scale) + + + return { + pyramidTex: pyramidTexture, + count: finalCount, + height, + levels, + scale + } +} \ No newline at end of file diff --git a/src/mol-gl/compute/histogram-pyramid/sum.ts b/src/mol-gl/compute/histogram-pyramid/sum.ts new file mode 100644 index 0000000000000000000000000000000000000000..cc360e859463268776a95bde4d95e128f5024099 --- /dev/null +++ b/src/mol-gl/compute/histogram-pyramid/sum.ts @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { createComputeRenderable, ComputeRenderable } from '../../renderable' +import { WebGLContext } from '../../webgl/context'; +import { createComputeRenderItem } from '../../webgl/render-item'; +import { Values, TextureSpec } from '../../renderable/schema'; +import { Texture, createTexture } from 'mol-gl/webgl/texture'; +import { ShaderCode } from 'mol-gl/shader-code'; +import { ValueCell } from 'mol-util'; +import { decodeFloatRGB } from 'mol-util/float-packing'; +import { QuadSchema, QuadValues } from '../util'; + +const HistopyramidSumSchema = { + ...QuadSchema, + tTexture: TextureSpec('texture', 'rgba', 'float', 'nearest'), +} + +let HistopyramidSumRenderable: ComputeRenderable<Values<typeof HistopyramidSumSchema>> +function getHistopyramidSumRenderable(ctx: WebGLContext, texture: Texture) { + if (HistopyramidSumRenderable) { + ValueCell.update(HistopyramidSumRenderable.values.tTexture, texture) + HistopyramidSumRenderable.update() + return HistopyramidSumRenderable + } else { + const values: Values<typeof HistopyramidSumSchema> = { + ...QuadValues, + tTexture: ValueCell.create(texture), + } + + const schema = { ...HistopyramidSumSchema } + const shaderCode = ShaderCode( + require('mol-gl/shader/quad.vert').default, + require('mol-gl/shader/histogram-pyramid/sum.frag').default + ) + const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values) + + HistopyramidSumRenderable = createComputeRenderable(renderItem, values) + return HistopyramidSumRenderable + } +} + +let SumTexture: Texture +function getSumTexture(ctx: WebGLContext) { + if (SumTexture) return SumTexture + SumTexture = createTexture(ctx, 'image-uint8', 'rgba', 'ubyte', 'nearest') + SumTexture.define(1, 1) + return SumTexture +} + +/** name for shared framebuffer used for histogram-pyramid operations */ +const FramebufferName = 'histogram-pyramid-sum' + +function setRenderingDefaults(ctx: WebGLContext) { + const { gl, state } = ctx + state.disable(gl.CULL_FACE) + state.disable(gl.BLEND) + state.disable(gl.DEPTH_TEST) + state.disable(gl.SCISSOR_TEST) + state.depthMask(false) + state.colorMask(true, true, true, true) + state.clearColor(0, 0, 0, 0) +} + +const sumArray = new Uint8Array(4) +export function getHistopyramidSum(ctx: WebGLContext, pyramidTopTexture: Texture) { + const { gl, framebufferCache } = ctx + + const renderable = getHistopyramidSumRenderable(ctx, pyramidTopTexture) + ctx.state.currentRenderItemId = -1 + + const framebuffer = framebufferCache.get(FramebufferName).value + const sumTexture = getSumTexture(ctx) + sumTexture.attachFramebuffer(framebuffer, 0) + + setRenderingDefaults(ctx) + + gl.viewport(0, 0, 1, 1) + renderable.render() + gl.finish() + ctx.readPixels(0, 0, 1, 1, sumArray) + ctx.unbindFramebuffer() + + return decodeFloatRGB(sumArray[0], sumArray[1], sumArray[2]) +} \ No newline at end of file diff --git a/src/mol-gl/compute/marching-cubes/active-voxels.ts b/src/mol-gl/compute/marching-cubes/active-voxels.ts new file mode 100644 index 0000000000000000000000000000000000000000..04a9f3f8770382b51b566882125dd716df65b3c8 --- /dev/null +++ b/src/mol-gl/compute/marching-cubes/active-voxels.ts @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { createComputeRenderable } from '../../renderable' +import { WebGLContext } from '../../webgl/context'; +import { createComputeRenderItem } from '../../webgl/render-item'; +import { Values, TextureSpec, UniformSpec } from '../../renderable/schema'; +import { Texture, createTexture } from 'mol-gl/webgl/texture'; +import { ShaderCode } from 'mol-gl/shader-code'; +import { ValueCell } from 'mol-util'; +import { Vec3, Vec2 } from 'mol-math/linear-algebra'; +import { QuadSchema, QuadValues } from '../util'; +import { getTriCount } from './tables'; + +/** name for shared framebuffer used for gpu marching cubes operations */ +const FramebufferName = 'marching-cubes-active-voxels' + +const ActiveVoxelsSchema = { + ...QuadSchema, + + tTriCount: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'), + tVolumeData: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'), + uIsoValue: UniformSpec('f'), + + uGridDim: UniformSpec('v3'), + uGridTexDim: UniformSpec('v3'), + + uScale: UniformSpec('v2'), +} + +function getActiveVoxelsRenderable(ctx: WebGLContext, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, isoValue: number, scale: Vec2) { + const values: Values<typeof ActiveVoxelsSchema> = { + ...QuadValues, + uQuadScale: ValueCell.create(scale), + + tTriCount: ValueCell.create(getTriCount()), + tVolumeData: ValueCell.create(volumeData), + uIsoValue: ValueCell.create(isoValue), + + uGridDim: ValueCell.create(gridDim), + uGridTexDim: ValueCell.create(gridTexDim), + + uScale: ValueCell.create(scale), + } + + const schema = { ...ActiveVoxelsSchema } + const shaderCode = ShaderCode( + require('mol-gl/shader/quad.vert').default, + require('mol-gl/shader/marching-cubes/active-voxels.frag').default + ) + const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values) + + return createComputeRenderable(renderItem, values); +} + +function setRenderingDefaults(ctx: WebGLContext) { + const { gl, state } = ctx + state.disable(gl.CULL_FACE) + state.disable(gl.BLEND) + state.disable(gl.DEPTH_TEST) + state.disable(gl.SCISSOR_TEST) + state.depthMask(false) + state.colorMask(true, true, true, true) + state.clearColor(0, 0, 0, 0) +} + +export function calcActiveVoxels(ctx: WebGLContext, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, isoValue: number, gridScale: Vec2) { + const { gl, framebufferCache } = ctx + const { width, height } = volumeData + + const framebuffer = framebufferCache.get(FramebufferName).value + framebuffer.bind() + + const activeVoxelsTex = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest') + activeVoxelsTex.define(width, height) + + const renderable = getActiveVoxelsRenderable(ctx, volumeData, gridDim, gridTexDim, isoValue, gridScale) + ctx.state.currentRenderItemId = -1 + + activeVoxelsTex.attachFramebuffer(framebuffer, 0) + setRenderingDefaults(ctx) + gl.viewport(0, 0, width, height) + renderable.render() + + // console.log('gridScale', gridScale, 'gridTexDim', gridTexDim, 'gridDim', gridDim) + // console.log('volumeData', volumeData) + // console.log('at', readTexture(ctx, activeVoxelsTex)) + + gl.finish() + + return activeVoxelsTex +} \ No newline at end of file diff --git a/src/mol-gl/compute/marching-cubes/isosurface.ts b/src/mol-gl/compute/marching-cubes/isosurface.ts new file mode 100644 index 0000000000000000000000000000000000000000..053e5d0a382d841b4dc151ce5b41abb0852fce7f --- /dev/null +++ b/src/mol-gl/compute/marching-cubes/isosurface.ts @@ -0,0 +1,202 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { createComputeRenderable } from '../../renderable' +import { WebGLContext } from '../../webgl/context'; +import { createComputeRenderItem } from '../../webgl/render-item'; +import { Values, TextureSpec, UniformSpec } from '../../renderable/schema'; +import { Texture, createTexture } from 'mol-gl/webgl/texture'; +import { ShaderCode } from 'mol-gl/shader-code'; +import { ValueCell } from 'mol-util'; +import { Vec3, Vec2, Mat4 } from 'mol-math/linear-algebra'; +import { QuadSchema, QuadValues } from '../util'; +import { HistogramPyramid } from '../histogram-pyramid/reduction'; +import { getTriIndices } from './tables'; + +/** name for shared framebuffer used for gpu marching cubes operations */ +const FramebufferName = 'marching-cubes-isosurface' + +const IsosurfaceSchema = { + ...QuadSchema, + + tTriIndices: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'), + tActiveVoxelsPyramid: TextureSpec('texture', 'rgba', 'float', 'nearest'), + tActiveVoxelsBase: TextureSpec('texture', 'rgba', 'float', 'nearest'), + tVolumeData: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'), + uIsoValue: UniformSpec('f'), + + uSize: UniformSpec('f'), + uLevels: UniformSpec('f'), + uCount: UniformSpec('f'), + + uGridDim: UniformSpec('v3'), + uGridTexDim: UniformSpec('v3'), + uGridTransform: UniformSpec('m4'), + + uScale: UniformSpec('v2'), +} + +function getIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Texture, activeVoxelsBase: Texture, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, transform: Mat4, isoValue: number, levels: number, scale: Vec2, count: number, height: number) { + // console.log('uSize', Math.pow(2, levels)) + const values: Values<typeof IsosurfaceSchema> = { + ...QuadValues, + uQuadScale: ValueCell.create(Vec2.create(1, height / Math.pow(2, levels))), + + tTriIndices: ValueCell.create(getTriIndices()), + tActiveVoxelsPyramid: ValueCell.create(activeVoxelsPyramid), + tActiveVoxelsBase: ValueCell.create(activeVoxelsBase), + tVolumeData: ValueCell.create(volumeData), + uIsoValue: ValueCell.create(isoValue), + + uSize: ValueCell.create(Math.pow(2, levels)), + uLevels: ValueCell.create(levels), + uCount: ValueCell.create(count), + + uGridDim: ValueCell.create(gridDim), + uGridTexDim: ValueCell.create(gridTexDim), + uGridTransform: ValueCell.create(transform), + + uScale: ValueCell.create(scale), + } + + const schema = { ...IsosurfaceSchema } + const shaderCode = ShaderCode( + require('mol-gl/shader/quad.vert').default, + require('mol-gl/shader/marching-cubes/isosurface.frag').default, + { drawBuffers: true } + ) + const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values) + + return createComputeRenderable(renderItem, values); +} + +function setRenderingDefaults(ctx: WebGLContext) { + const { gl, state } = ctx + state.disable(gl.CULL_FACE) + state.disable(gl.BLEND) + state.disable(gl.DEPTH_TEST) + state.disable(gl.SCISSOR_TEST) + state.depthMask(false) + state.colorMask(true, true, true, true) + state.clearColor(0, 0, 0, 0) +} + +export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Texture, volumeData: Texture, histogramPyramid: HistogramPyramid, gridDim: Vec3, gridTexDim: Vec3, transform: Mat4, isoValue: number, vertexGroupTexture?: Texture, normalTexture?: Texture) { + const { gl, framebufferCache } = ctx + const { pyramidTex, height, levels, scale, count } = histogramPyramid + + // console.log('iso', 'gridDim', gridDim, 'scale', scale, 'gridTexDim', gridTexDim) + // console.log('iso volumeData', volumeData) + + const framebuffer = framebufferCache.get(FramebufferName).value + + let needsClear = false + + if (!vertexGroupTexture) { + vertexGroupTexture = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest') + vertexGroupTexture.define(pyramidTex.width, pyramidTex.height) + } else if (vertexGroupTexture.width !== pyramidTex.width || vertexGroupTexture.height !== pyramidTex.height) { + vertexGroupTexture.define(pyramidTex.width, pyramidTex.height) + } else { + needsClear = true + } + + if (!normalTexture) { + normalTexture = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest') + normalTexture.define(pyramidTex.width, pyramidTex.height) + } else if (normalTexture.width !== pyramidTex.width || normalTexture.height !== pyramidTex.height) { + normalTexture.define(pyramidTex.width, pyramidTex.height) + } else { + needsClear = true + } + + // const infoTex = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest') + // infoTex.define(pyramidTex.width, pyramidTex.height) + + // const pointTexA = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest') + // pointTexA.define(pyramidTex.width, pyramidTex.height) + + // const pointTexB = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest') + // pointTexB.define(pyramidTex.width, pyramidTex.height) + + // const coordTex = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest') + // coordTex.define(pyramidTex.width, pyramidTex.height) + + // const indexTex = createTexture(ctx, 'image-float32', 'rgba', 'float', 'nearest') + // indexTex.define(pyramidTex.width, pyramidTex.height) + + const renderable = getIsosurfaceRenderable(ctx, pyramidTex, activeVoxelsBase, volumeData, gridDim, gridTexDim, transform, isoValue, levels, scale, count, height) + ctx.state.currentRenderItemId = -1 + + vertexGroupTexture.attachFramebuffer(framebuffer, 0) + normalTexture.attachFramebuffer(framebuffer, 1) + // infoTex.attachFramebuffer(framebuffer, 1) + // pointTexA.attachFramebuffer(framebuffer, 2) + // pointTexB.attachFramebuffer(framebuffer, 3) + // coordTex.attachFramebuffer(framebuffer, 4) + // indexTex.attachFramebuffer(framebuffer, 5) + + const { drawBuffers } = ctx.extensions + if (!drawBuffers) throw new Error('need WebGL draw buffers') + + drawBuffers.drawBuffers([ + drawBuffers.COLOR_ATTACHMENT0, + drawBuffers.COLOR_ATTACHMENT1, + // drawBuffers.COLOR_ATTACHMENT2, + // drawBuffers.COLOR_ATTACHMENT3, + // drawBuffers.COLOR_ATTACHMENT4, + // drawBuffers.COLOR_ATTACHMENT5 + ]) + + setRenderingDefaults(ctx) + gl.viewport(0, 0, pyramidTex.width, pyramidTex.height) + if (needsClear) gl.clear(gl.COLOR_BUFFER_BIT) + renderable.render() + + gl.finish() + + // const vgt = readTexture(ctx, vertexGroupTexture, pyramidTex.width, height) + // console.log('vertexGroupTexture', vgt.array.subarray(0, 4 * count)) + + // const vt = readTexture(ctx, verticesTex, pyramidTex.width, height) + // console.log('vt', vt) + // const vertices = new Float32Array(3 * compacted.count) + // for (let i = 0; i < compacted.count; ++i) { + // vertices[i * 3] = vt.array[i * 4] + // vertices[i * 3 + 1] = vt.array[i * 4 + 1] + // vertices[i * 3 + 2] = vt.array[i * 4 + 2] + // } + // console.log('vertices', vertices) + + // const it = readTexture(ctx, infoTex, pyramidTex.width, height) + // console.log('info', it.array.subarray(0, 4 * compacted.count)) + + // const pat = readTexture(ctx, pointTexA, pyramidTex.width, height) + // console.log('point a', pat.array.subarray(0, 4 * compacted.count)) + + // const pbt = readTexture(ctx, pointTexB, pyramidTex.width, height) + // console.log('point b', pbt.array.subarray(0, 4 * compacted.count)) + + // const ct = readTexture(ctx, coordTex, pyramidTex.width, height) + // console.log('coord', ct.array.subarray(0, 4 * compacted.count)) + + // const idxt = readTexture(ctx, indexTex, pyramidTex.width, height) + // console.log('index', idxt.array.subarray(0, 4 * compacted.count)) + + // const { field, idField } = await fieldFromTexture2d(ctx, volumeData, gridDimensions) + // console.log({ field, idField }) + + // const valuesA = new Float32Array(compacted.count) + // const valuesB = new Float32Array(compacted.count) + // for (let i = 0; i < compacted.count; ++i) { + // valuesA[i] = field.space.get(field.data, pat.array[i * 4], pat.array[i * 4 + 1], pat.array[i * 4 + 2]) + // valuesB[i] = field.space.get(field.data, pbt.array[i * 4], pbt.array[i * 4 + 1], pbt.array[i * 4 + 2]) + // } + // console.log('valuesA', valuesA) + // console.log('valuesB', valuesB) + + return { vertexGroupTexture, normalTexture, vertexCount: count } +} \ No newline at end of file diff --git a/src/mol-gl/compute/marching-cubes/tables.ts b/src/mol-gl/compute/marching-cubes/tables.ts new file mode 100644 index 0000000000000000000000000000000000000000..3fafe4de31e5b1d5c7d6120b468b19ba483210e6 --- /dev/null +++ b/src/mol-gl/compute/marching-cubes/tables.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { TriTable, } from 'mol-geo/util/marching-cubes/tables'; +import { TextureImage, createTextureImage } from 'mol-gl/renderable/util'; + +let TriCount: TextureImage<Uint8Array> | undefined +export function getTriCount(): TextureImage<Uint8Array> { + if (TriCount !== undefined) return TriCount + TriCount = createTextureImage(16 * 16, 1, Uint8Array) + const { array } = TriCount + for (let i = 0, il = TriTable.length; i < il; ++i) { + array[i] = TriTable[i].length / 3 + } + return TriCount +} + +let TriIndices: TextureImage<Uint8Array> | undefined +export function getTriIndices(): TextureImage<Uint8Array> { + if (TriIndices !== undefined) return TriIndices + TriIndices = createTextureImage(64 * 64, 1, Uint8Array) + const { array } = TriIndices + for (let i = 0, il = TriTable.length; i < il; ++i) { + for (let j = 0; j < 16; ++j) { + if (j < TriTable[i].length) { + array[i * 16 + j] = TriTable[i][j] + } else { + array[i * 16 + j] = 255 + } + } + } + return TriIndices +} \ No newline at end of file diff --git a/src/mol-gl/compute/util.ts b/src/mol-gl/compute/util.ts new file mode 100644 index 0000000000000000000000000000000000000000..fb7f9947e7f493b1d268c72fd9af38605c333135 --- /dev/null +++ b/src/mol-gl/compute/util.ts @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { WebGLContext } from 'mol-gl/webgl/context'; +import { Texture } from 'mol-gl/webgl/texture'; +import { printTextureImage } from 'mol-gl/renderable/util'; +import { defaults, ValueCell } from 'mol-util'; +import { ValueSpec, AttributeSpec, UniformSpec, Values } from 'mol-gl/renderable/schema'; +import { Vec2 } from 'mol-math/linear-algebra'; + +export const QuadPositions = new Float32Array([ + 1.0, 1.0, -1.0, 1.0, -1.0, -1.0, // First triangle + -1.0, -1.0, 1.0, -1.0, 1.0, 1.0 // Second triangle +]) + +export const QuadSchema = { + drawCount: ValueSpec('number'), + instanceCount: ValueSpec('number'), + aPosition: AttributeSpec('float32', 2, 0), + uQuadScale: UniformSpec('v2'), +} + +export const QuadValues: Values<typeof QuadSchema> = { + drawCount: ValueCell.create(6), + instanceCount: ValueCell.create(1), + aPosition: ValueCell.create(QuadPositions), + uQuadScale: ValueCell.create(Vec2.create(1, 1)), +} + +// + +export function readTexture(ctx: WebGLContext, texture: Texture, width?: number, height?: number) { + const { gl, framebufferCache } = ctx + width = defaults(width, texture.width) + height = defaults(height, texture.height) + const framebuffer = framebufferCache.get('read-texture').value + const array = texture.type === gl.UNSIGNED_BYTE ? new Uint8Array(width * height * 4) : new Float32Array(width * height * 4) + framebuffer.bind() + texture.attachFramebuffer(framebuffer, 0) + ctx.readPixels(0, 0, width, height, array) + + return { array, width, height } +} + +export function printTexture(ctx: WebGLContext, texture: Texture, scale: number) { + printTextureImage(readTexture(ctx, texture), scale) +} \ No newline at end of file diff --git a/src/mol-gl/render-object.ts b/src/mol-gl/render-object.ts index 57ad487070e0ec547dc07fe982808eecab3690ae..ec7536ff335d4b2405125dd1088a109cc7d889ca 100644 --- a/src/mol-gl/render-object.ts +++ b/src/mol-gl/render-object.ts @@ -8,33 +8,30 @@ import { RenderableState, Renderable } from './renderable' import { RenderableValues } from './renderable/schema'; import { idFactory } from 'mol-util/id-factory'; import { WebGLContext } from './webgl/context'; -import { GaussianDensityValues, GaussianDensityRenderable } from './renderable/gaussian-density'; import { DirectVolumeValues, DirectVolumeRenderable } from './renderable/direct-volume'; import { MeshValues, MeshRenderable } from './renderable/mesh'; import { PointsValues, PointsRenderable } from './renderable/points'; import { LinesValues, LinesRenderable } from './renderable/lines'; import { SpheresValues, SpheresRenderable } from './renderable/spheres'; import { TextValues, TextRenderable } from './renderable/text'; +import { TextureMeshValues, TextureMeshRenderable } from './renderable/texture-mesh'; const getNextId = idFactory(0, 0x7FFFFFFF) -export interface BaseRenderObject { id: number, type: string, values: RenderableValues, state: RenderableState } +export const getNextMaterialId = idFactory(0, 0x7FFFFFFF) + +export interface BaseRenderObject { id: number, type: string, values: RenderableValues, state: RenderableState, materialId: number } export interface MeshRenderObject extends BaseRenderObject { type: 'mesh', values: MeshValues } export interface PointsRenderObject extends BaseRenderObject { type: 'points', values: PointsValues } export interface SpheresRenderObject extends BaseRenderObject { type: 'spheres', values: SpheresValues } export interface TextRenderObject extends BaseRenderObject { type: 'text', values: TextValues } export interface LinesRenderObject extends BaseRenderObject { type: 'lines', values: LinesValues } export interface DirectVolumeRenderObject extends BaseRenderObject { type: 'direct-volume', values: DirectVolumeValues } - -export interface GaussianDensityRenderObject extends BaseRenderObject { type: 'gaussian-density', values: GaussianDensityValues } +export interface TextureMeshRenderObject extends BaseRenderObject { type: 'texture-mesh', values: TextureMeshValues } // -export type GraphicsRenderObject = MeshRenderObject | PointsRenderObject | SpheresRenderObject | TextRenderObject | LinesRenderObject | DirectVolumeRenderObject - -export type ComputeRenderObject = GaussianDensityRenderObject - -export type RenderObject = GraphicsRenderObject | ComputeRenderObject +export type GraphicsRenderObject = MeshRenderObject | PointsRenderObject | SpheresRenderObject | TextRenderObject | LinesRenderObject | DirectVolumeRenderObject | TextureMeshRenderObject export type RenderObjectKindType = { 'mesh': MeshRenderObject @@ -43,8 +40,7 @@ export type RenderObjectKindType = { 'text': TextRenderObject 'lines': LinesRenderObject 'direct-volume': DirectVolumeRenderObject - - 'gaussian-density': GaussianDensityRenderObject + 'texture-mesh': TextureMeshRenderObject } export type RenderObjectValuesType = { 'mesh': MeshValues @@ -53,26 +49,24 @@ export type RenderObjectValuesType = { 'text': TextValues 'lines': LinesValues 'direct-volume': DirectVolumeValues - - 'gaussian-density': GaussianDensityValues + 'texture-mesh': TextureMeshValues } export type RenderObjectType = keyof RenderObjectKindType // -export function createRenderObject<T extends RenderObjectType>(type: T, values: RenderObjectValuesType[T], state: RenderableState): RenderObjectKindType[T] { - return { id: getNextId(), type, values, state } as RenderObjectKindType[T] +export function createRenderObject<T extends RenderObjectType>(type: T, values: RenderObjectValuesType[T], state: RenderableState, materialId: number): RenderObjectKindType[T] { + return { id: getNextId(), type, values, state, materialId } as RenderObjectKindType[T] } -export function createRenderable(ctx: WebGLContext, o: RenderObject): Renderable<any> { +export function createRenderable(ctx: WebGLContext, o: GraphicsRenderObject): Renderable<any> { switch (o.type) { - case 'mesh': return MeshRenderable(ctx, o.id, o.values, o.state) - case 'points': return PointsRenderable(ctx, o.id, o.values, o.state) - case 'spheres': return SpheresRenderable(ctx, o.id, o.values, o.state) - case 'text': return TextRenderable(ctx, o.id, o.values, o.state) - case 'lines': return LinesRenderable(ctx, o.id, o.values, o.state) - case 'direct-volume': return DirectVolumeRenderable(ctx, o.id, o.values, o.state) - - case 'gaussian-density': return GaussianDensityRenderable(ctx, o.id, o.values, o.state) + case 'mesh': return MeshRenderable(ctx, o.id, o.values, o.state, o.materialId) + case 'points': return PointsRenderable(ctx, o.id, o.values, o.state, o.materialId) + case 'spheres': return SpheresRenderable(ctx, o.id, o.values, o.state, o.materialId) + case 'text': return TextRenderable(ctx, o.id, o.values, o.state, o.materialId) + case 'lines': return LinesRenderable(ctx, o.id, o.values, o.state, o.materialId) + case 'direct-volume': return DirectVolumeRenderable(ctx, o.id, o.values, o.state, o.materialId) + case 'texture-mesh': return TextureMeshRenderable(ctx, o.id, o.values, o.state, o.materialId) } } \ No newline at end of file diff --git a/src/mol-gl/renderable.ts b/src/mol-gl/renderable.ts index 34f0eee4159c8750f94c7eabc9669119e7b4a6b8..19bb43892275aae908decac6710fa30a9b1ba7ce 100644 --- a/src/mol-gl/renderable.ts +++ b/src/mol-gl/renderable.ts @@ -6,7 +6,7 @@ import { Program } from './webgl/program'; import { RenderableValues, Values, RenderableSchema } from './renderable/schema'; -import { RenderVariant, RenderItem } from './webgl/render-item'; +import { GraphicsRenderItem, ComputeRenderItem, GraphicsRenderVariant } from './webgl/render-item'; import { ValueCell } from 'mol-util'; import { idFactory } from 'mol-util/id-factory'; import { clamp } from 'mol-math/interpolate'; @@ -22,22 +22,24 @@ export type RenderableState = { export interface Renderable<T extends RenderableValues> { readonly id: number + readonly materialId: number readonly values: T readonly state: RenderableState - render: (variant: RenderVariant) => void - getProgram: (variant: RenderVariant) => Program + render: (variant: GraphicsRenderVariant) => void + getProgram: (variant: GraphicsRenderVariant) => Program update: () => void dispose: () => void } -export function createRenderable<T extends Values<RenderableSchema>>(renderItem: RenderItem, values: T, state: RenderableState): Renderable<T> { +export function createRenderable<T extends Values<RenderableSchema>>(renderItem: GraphicsRenderItem, values: T, state: RenderableState): Renderable<T> { return { id: getNextRenderableId(), + materialId: renderItem.materialId, values, state, - render: (variant: RenderVariant) => { + render: (variant: GraphicsRenderVariant) => { if (values.uAlpha && values.alpha) { ValueCell.updateIfChanged(values.uAlpha, clamp(values.alpha.ref.value * state.alphaFactor, 0, 1)) } @@ -46,7 +48,29 @@ export function createRenderable<T extends Values<RenderableSchema>>(renderItem: } renderItem.render(variant) }, - getProgram: (variant: RenderVariant) => renderItem.getProgram(variant), + getProgram: (variant: GraphicsRenderVariant) => renderItem.getProgram(variant), + update: () => renderItem.update(), + dispose: () => renderItem.destroy() + } +} + +// + +export interface ComputeRenderable<T extends RenderableValues> { + readonly id: number + readonly values: T + + render: () => void + update: () => void + dispose: () => void +} + +export function createComputeRenderable<T extends Values<RenderableSchema>>(renderItem: ComputeRenderItem, values: T): ComputeRenderable<T> { + return { + id: getNextRenderableId(), + values, + + render: () => renderItem.render('compute'), update: () => renderItem.update(), dispose: () => renderItem.destroy() } diff --git a/src/mol-gl/renderable/direct-volume.ts b/src/mol-gl/renderable/direct-volume.ts index eb94c44996943bdd529c67c93898f25375b9009c..8b8d2dfe47ab5c0366156b739bbfcba32debbb2c 100644 --- a/src/mol-gl/renderable/direct-volume.ts +++ b/src/mol-gl/renderable/direct-volume.ts @@ -6,7 +6,7 @@ import { Renderable, RenderableState, createRenderable } from '../renderable' import { WebGLContext } from '../webgl/context'; -import { createRenderItem } from '../webgl/render-item'; +import { createGraphicsRenderItem } from '../webgl/render-item'; import { AttributeSpec, Values, UniformSpec, GlobalUniformSchema, InternalSchema, TextureSpec, ValueSpec, ElementsSpec, DefineSpec, InternalValues } from './schema'; import { DirectVolumeShaderCode } from '../shader-code'; import { ValueCell } from 'mol-util'; @@ -24,6 +24,11 @@ export const DirectVolumeSchema = { tOverpaint: TextureSpec('image-uint8', 'rgba', 'ubyte', 'nearest'), dOverpaint: DefineSpec('boolean'), + uTransparencyTexDim: UniformSpec('v2'), + tTransparency: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'), + dTransparency: DefineSpec('boolean'), + dTransparencyVariant: DefineSpec('string', ['single', 'multi']), + uInstanceCount: UniformSpec('i'), uGroupCount: UniformSpec('i'), @@ -62,18 +67,18 @@ export const DirectVolumeSchema = { dGridTexType: DefineSpec('string', ['2d', '3d']), uGridTexDim: UniformSpec('v3'), - tGridTex: TextureSpec('texture', 'rgba', 'ubyte', 'linear'), + tGridTex: TextureSpec('texture', 'rgba', 'float', 'nearest'), } export type DirectVolumeSchema = typeof DirectVolumeSchema export type DirectVolumeValues = Values<DirectVolumeSchema> -export function DirectVolumeRenderable(ctx: WebGLContext, id: number, values: DirectVolumeValues, state: RenderableState): Renderable<DirectVolumeValues> { +export function DirectVolumeRenderable(ctx: WebGLContext, id: number, values: DirectVolumeValues, state: RenderableState, materialId: number): Renderable<DirectVolumeValues> { const schema = { ...GlobalUniformSchema, ...InternalSchema, ...DirectVolumeSchema } const internalValues: InternalValues = { uObjectId: ValueCell.create(id), - uPickable: ValueCell.create(state.pickable ? 1 : 0) + uPickable: ValueCell.create(state.pickable ? 1 : 0), } const shaderCode = DirectVolumeShaderCode - const renderItem = createRenderItem(ctx, 'triangles', shaderCode, schema, { ...values, ...internalValues }) + const renderItem = createGraphicsRenderItem(ctx, 'triangles', shaderCode, schema, { ...values, ...internalValues }, materialId) return createRenderable(renderItem, values, state); } \ No newline at end of file diff --git a/src/mol-gl/renderable/gaussian-density.ts b/src/mol-gl/renderable/gaussian-density.ts deleted file mode 100644 index a8b4f1518a862487568da811962de281eb30bc9e..0000000000000000000000000000000000000000 --- a/src/mol-gl/renderable/gaussian-density.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - */ - -import { Renderable, RenderableState, createRenderable } from '../renderable' -import { WebGLContext } from '../webgl/context'; -import { createRenderItem } from '../webgl/render-item'; -import { AttributeSpec, Values, UniformSpec, ValueSpec, DefineSpec, TextureSpec } from './schema'; -import { GaussianDensityShaderCode } from '../shader-code'; - -export const GaussianDensitySchema = { - drawCount: ValueSpec('number'), - instanceCount: ValueSpec('number'), - - aRadius: AttributeSpec('float32', 1, 0), - aPosition: AttributeSpec('float32', 3, 0), - aGroup: AttributeSpec('float32', 1, 0), - - uCurrentSlice: UniformSpec('f'), - uCurrentX: UniformSpec('f'), - uCurrentY: UniformSpec('f'), - uBboxMin: UniformSpec('v3'), - uBboxMax: UniformSpec('v3'), - uBboxSize: UniformSpec('v3'), - uGridDim: UniformSpec('v3'), - uGridTexDim: UniformSpec('v3'), - uAlpha: UniformSpec('f'), - tMinDistanceTex: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'), - - dGridTexType: DefineSpec('string', ['2d', '3d']), - dCalcType: DefineSpec('string', ['density', 'minDistance', 'groupId']), -} -export type GaussianDensitySchema = typeof GaussianDensitySchema -export type GaussianDensityValues = Values<GaussianDensitySchema> - -export function GaussianDensityRenderable(ctx: WebGLContext, id: number, values: GaussianDensityValues, state: RenderableState): Renderable<GaussianDensityValues> { - const schema = { ...GaussianDensitySchema } - const shaderCode = GaussianDensityShaderCode - const renderItem = createRenderItem(ctx, 'points', shaderCode, schema, values) - - return createRenderable(renderItem, values, state); -} \ No newline at end of file diff --git a/src/mol-gl/renderable/lines.ts b/src/mol-gl/renderable/lines.ts index 54e62c5b448bf5b4bf8bcf88037092564fcf52ce..e44a62f727944a953bf33ada6fcc43307fd2b3dc 100644 --- a/src/mol-gl/renderable/lines.ts +++ b/src/mol-gl/renderable/lines.ts @@ -6,7 +6,7 @@ import { Renderable, RenderableState, createRenderable } from '../renderable' import { WebGLContext } from '../webgl/context'; -import { createRenderItem } from '../webgl/render-item'; +import { createGraphicsRenderItem } from '../webgl/render-item'; import { GlobalUniformSchema, BaseSchema, AttributeSpec, DefineSpec, Values, InternalSchema, SizeSchema, ElementsSpec, InternalValues } from './schema'; import { ValueCell } from 'mol-util'; import { LinesShaderCode } from '../shader-code'; @@ -25,14 +25,14 @@ export const LinesSchema = { export type LinesSchema = typeof LinesSchema export type LinesValues = Values<LinesSchema> -export function LinesRenderable(ctx: WebGLContext, id: number, values: LinesValues, state: RenderableState): Renderable<LinesValues> { +export function LinesRenderable(ctx: WebGLContext, id: number, values: LinesValues, state: RenderableState, materialId: number): Renderable<LinesValues> { const schema = { ...GlobalUniformSchema, ...InternalSchema, ...LinesSchema } const internalValues: InternalValues = { uObjectId: ValueCell.create(id), uPickable: ValueCell.create(state.pickable ? 1 : 0) } const shaderCode = LinesShaderCode - const renderItem = createRenderItem(ctx, 'triangles', shaderCode, schema, { ...values, ...internalValues }) + const renderItem = createGraphicsRenderItem(ctx, 'triangles', shaderCode, schema, { ...values, ...internalValues }, materialId) return createRenderable(renderItem, values, state); } \ No newline at end of file diff --git a/src/mol-gl/renderable/mesh.ts b/src/mol-gl/renderable/mesh.ts index a3d70909409dda202f1649ab5397ae0cc47e8d29..bcf65b9f0901227dc3407c7e64832fdb434ecc14 100644 --- a/src/mol-gl/renderable/mesh.ts +++ b/src/mol-gl/renderable/mesh.ts @@ -6,7 +6,7 @@ import { Renderable, RenderableState, createRenderable } from '../renderable' import { WebGLContext } from '../webgl/context'; -import { createRenderItem } from '../webgl/render-item'; +import { createGraphicsRenderItem } from '../webgl/render-item'; import { GlobalUniformSchema, BaseSchema, AttributeSpec, ElementsSpec, DefineSpec, Values, InternalSchema, InternalValues } from './schema'; import { MeshShaderCode } from '../shader-code'; import { ValueCell } from 'mol-util'; @@ -23,14 +23,14 @@ export const MeshSchema = { export type MeshSchema = typeof MeshSchema export type MeshValues = Values<MeshSchema> -export function MeshRenderable(ctx: WebGLContext, id: number, values: MeshValues, state: RenderableState): Renderable<MeshValues> { +export function MeshRenderable(ctx: WebGLContext, id: number, values: MeshValues, state: RenderableState, materialId: number): Renderable<MeshValues> { const schema = { ...GlobalUniformSchema, ...InternalSchema, ...MeshSchema } const internalValues: InternalValues = { uObjectId: ValueCell.create(id), uPickable: ValueCell.create(state.pickable ? 1 : 0) } const shaderCode = MeshShaderCode - const renderItem = createRenderItem(ctx, 'triangles', shaderCode, schema, { ...values, ...internalValues }) + const renderItem = createGraphicsRenderItem(ctx, 'triangles', shaderCode, schema, { ...values, ...internalValues }, materialId) return createRenderable(renderItem, values, state) } \ No newline at end of file diff --git a/src/mol-gl/renderable/points.ts b/src/mol-gl/renderable/points.ts index 60f9f243622fcda3422b43bc1ce5b028f8dd1be2..6b2cf5e1a3e69891055c76e22fd39d28baae3c00 100644 --- a/src/mol-gl/renderable/points.ts +++ b/src/mol-gl/renderable/points.ts @@ -6,7 +6,7 @@ import { Renderable, RenderableState, createRenderable } from '../renderable' import { WebGLContext } from '../webgl/context'; -import { createRenderItem } from '../webgl/render-item'; +import { createGraphicsRenderItem } from '../webgl/render-item'; import { GlobalUniformSchema, BaseSchema, AttributeSpec, UniformSpec, DefineSpec, Values, InternalSchema, SizeSchema, InternalValues } from './schema'; import { PointsShaderCode } from '../shader-code'; import { ValueCell } from 'mol-util'; @@ -22,13 +22,13 @@ export const PointsSchema = { export type PointsSchema = typeof PointsSchema export type PointsValues = Values<PointsSchema> -export function PointsRenderable(ctx: WebGLContext, id: number, values: PointsValues, state: RenderableState): Renderable<PointsValues> { +export function PointsRenderable(ctx: WebGLContext, id: number, values: PointsValues, state: RenderableState, materialId: number): Renderable<PointsValues> { const schema = { ...GlobalUniformSchema, ...InternalSchema, ...PointsSchema } const internalValues: InternalValues = { uObjectId: ValueCell.create(id), uPickable: ValueCell.create(state.pickable ? 1 : 0) } const shaderCode = PointsShaderCode - const renderItem = createRenderItem(ctx, 'points', shaderCode, schema, { ...values, ...internalValues }) + const renderItem = createGraphicsRenderItem(ctx, 'points', shaderCode, schema, { ...values, ...internalValues }, materialId) return createRenderable(renderItem, values, state); } \ No newline at end of file diff --git a/src/mol-gl/renderable/schema.ts b/src/mol-gl/renderable/schema.ts index 28e36306fe14d52b691e0da71cbc5781e3702da1..4c9a9e453dcd536a60efccbc3c7ac8a06be8411c 100644 --- a/src/mol-gl/renderable/schema.ts +++ b/src/mol-gl/renderable/schema.ts @@ -1,11 +1,11 @@ /** - * 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> */ import { ValueCell } from 'mol-util'; -import { ArrayKind, BufferItemSize, ElementsKind, AttributeValues } from '../webgl/buffer'; +import { AttributeItemSize, ElementsKind, AttributeValues, AttributeKind } from '../webgl/buffer'; import { UniformKind, UniformValues } from '../webgl/uniform'; import { DefineKind, DefineValues } from '../shader-code'; import { Vec2, Vec3, Vec4, Mat3, Mat4 } from 'mol-math/linear-algebra'; @@ -68,27 +68,19 @@ export function splitValues(schema: RenderableSchema, values: RenderableValues) const defineValues: DefineValues = {} const textureValues: TextureValues = {} const uniformValues: UniformValues = {} + const materialUniformValues: UniformValues = {} Object.keys(schema).forEach(k => { - if (schema[k].type === 'attribute') attributeValues[k] = values[k] - if (schema[k].type === 'define') defineValues[k] = values[k] - if (schema[k].type === 'texture') textureValues[k] = values[k] - if (schema[k].type === 'uniform') uniformValues[k] = values[k] + const spec = schema[k] + if (spec.type === 'attribute') attributeValues[k] = values[k] + if (spec.type === 'define') defineValues[k] = values[k] + if (spec.type === 'texture') textureValues[k] = values[k] + // check if k exists in values so that global uniforms are excluded here + if (spec.type === 'uniform' && values[k] !== undefined) { + if (spec.isMaterial) materialUniformValues[k] = values[k] + else uniformValues[k] = values[k] + } }) - return { attributeValues, defineValues, textureValues, uniformValues } -} - -export function splitKeys(schema: RenderableSchema) { - const attributeKeys: string[] = [] - const defineKeys: string[] = [] - const textureKeys: string[] = [] - const uniformKeys: string[] = [] - Object.keys(schema).forEach(k => { - if (schema[k].type === 'attribute') attributeKeys.push(k) - if (schema[k].type === 'define') defineKeys.push(k) - if (schema[k].type === 'texture') textureKeys.push(k) - if (schema[k].type === 'uniform') uniformKeys.push(k) - }) - return { attributeKeys, defineKeys, textureKeys, uniformKeys } + return { attributeValues, defineValues, textureValues, uniformValues, materialUniformValues } } export type Versions<T extends RenderableValues> = { [k in keyof T]: number } @@ -102,14 +94,14 @@ export function getValueVersions<T extends RenderableValues>(values: T) { // -export type AttributeSpec<K extends ArrayKind> = { type: 'attribute', kind: K, itemSize: BufferItemSize, divisor: number } -export function AttributeSpec<K extends ArrayKind>(kind: K, itemSize: BufferItemSize, divisor: number): AttributeSpec<K> { +export type AttributeSpec<K extends AttributeKind> = { type: 'attribute', kind: K, itemSize: AttributeItemSize, divisor: number } +export function AttributeSpec<K extends AttributeKind>(kind: K, itemSize: AttributeItemSize, divisor: number): AttributeSpec<K> { return { type: 'attribute', kind, itemSize, divisor } } -export type UniformSpec<K extends UniformKind> = { type: 'uniform', kind: K } -export function UniformSpec<K extends UniformKind>(kind: K): UniformSpec<K> { - return { type: 'uniform', kind } +export type UniformSpec<K extends UniformKind> = { type: 'uniform', kind: K, isMaterial: boolean } +export function UniformSpec<K extends UniformKind>(kind: K, isMaterial = false): UniformSpec<K> { + return { type: 'uniform', kind, isMaterial } } export type TextureSpec<K extends TextureKind> = { type: 'texture', kind: K, format: TextureFormat, dataType: TextureType, filter: TextureFilter } @@ -136,7 +128,7 @@ export function ValueSpec<K extends ValueKind>(kind: K): ValueSpec<K> { export type RenderableSchema = { [k: string]: ( - AttributeSpec<ArrayKind> | UniformSpec<UniformKind> | TextureSpec<TextureKind> | + AttributeSpec<AttributeKind> | UniformSpec<UniformKind> | TextureSpec<TextureKind> | ValueSpec<ValueKind> | DefineSpec<DefineKind> | ElementsSpec<ElementsKind> ) } @@ -154,9 +146,13 @@ export const GlobalUniformSchema = { uInvProjection: UniformSpec('m4'), uModelViewProjection: UniformSpec('m4'), uInvModelViewProjection: UniformSpec('m4'), - // uLightPosition: Uniform('v3'), - uLightColor: UniformSpec('v3'), - uLightAmbient: UniformSpec('v3'), + + uLightIntensity: UniformSpec('f'), + uAmbientIntensity: UniformSpec('f'), + + uMetalness: UniformSpec('f'), + uRoughness: UniformSpec('f'), + uReflectivity: UniformSpec('f'), uPixelRatio: UniformSpec('f'), uViewportHeight: UniformSpec('f'), @@ -170,18 +166,18 @@ export const GlobalUniformSchema = { uPickingAlphaThreshold: UniformSpec('f'), } export type GlobalUniformSchema = typeof GlobalUniformSchema -export type GlobalUniformValues = { [k in keyof GlobalUniformSchema]: ValueCell<any> } +export type GlobalUniformValues = Values<GlobalUniformSchema> // { [k in keyof GlobalUniformSchema]: ValueCell<any> } export const InternalSchema = { uObjectId: UniformSpec('i'), - uPickable: UniformSpec('i'), + uPickable: UniformSpec('i', true), } export type InternalSchema = typeof InternalSchema export type InternalValues = { [k in keyof InternalSchema]: ValueCell<any> } export const ColorSchema = { // aColor: AttributeSpec('float32', 3, 0), // TODO - uColor: UniformSpec('v3'), + uColor: UniformSpec('v3', true), uColorTexDim: UniformSpec('v2'), tColor: TextureSpec('image-uint8', 'rgb', 'ubyte', 'nearest'), dColorType: DefineSpec('string', ['uniform', 'attribute', 'instance', 'group', 'group_instance']), @@ -191,7 +187,7 @@ export type ColorValues = Values<ColorSchema> export const SizeSchema = { // aSize: AttributeSpec('float32', 1, 0), // TODO - uSize: UniformSpec('f'), + uSize: UniformSpec('f', true), uSizeTexDim: UniformSpec('v2'), tSize: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'), dSizeType: DefineSpec('string', ['uniform', 'attribute', 'instance', 'group', 'group_instance']), @@ -215,10 +211,22 @@ export const OverpaintSchema = { export type OverpaintSchema = typeof OverpaintSchema export type OverpaintValues = Values<OverpaintSchema> +export const TransparencySchema = { + // aTransparency: AttributeSpec('float32', 1, 0), // TODO + uTransparencyTexDim: UniformSpec('v2'), + tTransparency: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'), + dTransparency: DefineSpec('boolean'), + // dTransparencyType: DefineSpec('string', ['uniform', 'attribute', 'instance', 'group', 'group_instance']), // TODO + dTransparencyVariant: DefineSpec('string', ['single', 'multi']), +} +export type TransparencySchema = typeof TransparencySchema +export type TransparencyValues = Values<TransparencySchema> + export const BaseSchema = { ...ColorSchema, ...MarkerSchema, ...OverpaintSchema, + ...TransparencySchema, aInstance: AttributeSpec('float32', 1, 1), aGroup: AttributeSpec('float32', 1, 0), @@ -231,12 +239,12 @@ export const BaseSchema = { /** * final alpha, calculated as `values.alpha * state.alpha` */ - uAlpha: UniformSpec('f'), + uAlpha: UniformSpec('f', true), uInstanceCount: UniformSpec('i'), uGroupCount: UniformSpec('i'), - uHighlightColor: UniformSpec('v3'), - uSelectColor: UniformSpec('v3'), + uHighlightColor: UniformSpec('v3', true), + uSelectColor: UniformSpec('v3', true), drawCount: ValueSpec('number'), instanceCount: ValueSpec('number'), diff --git a/src/mol-gl/renderable/spheres.ts b/src/mol-gl/renderable/spheres.ts index 0136658ab4f5451c1610e301a89837a6c3bb759c..1af5348ec7029a76e0dd93dc790eaeac20967cc0 100644 --- a/src/mol-gl/renderable/spheres.ts +++ b/src/mol-gl/renderable/spheres.ts @@ -6,7 +6,7 @@ import { Renderable, RenderableState, createRenderable } from '../renderable' import { WebGLContext } from '../webgl/context'; -import { createRenderItem } from '../webgl/render-item'; +import { createGraphicsRenderItem } from '../webgl/render-item'; import { GlobalUniformSchema, BaseSchema, AttributeSpec, Values, InternalSchema, SizeSchema, InternalValues, ElementsSpec, ValueSpec, DefineSpec } from './schema'; import { SpheresShaderCode } from '../shader-code'; import { ValueCell } from 'mol-util'; @@ -24,13 +24,13 @@ export const SpheresSchema = { export type SpheresSchema = typeof SpheresSchema export type SpheresValues = Values<SpheresSchema> -export function SpheresRenderable(ctx: WebGLContext, id: number, values: SpheresValues, state: RenderableState): Renderable<SpheresValues> { +export function SpheresRenderable(ctx: WebGLContext, id: number, values: SpheresValues, state: RenderableState, materialId: number): Renderable<SpheresValues> { const schema = { ...GlobalUniformSchema, ...InternalSchema, ...SpheresSchema } const internalValues: InternalValues = { uObjectId: ValueCell.create(id), uPickable: ValueCell.create(state.pickable ? 1 : 0) } const shaderCode = SpheresShaderCode - const renderItem = createRenderItem(ctx, 'triangles', shaderCode, schema, { ...values, ...internalValues }) + const renderItem = createGraphicsRenderItem(ctx, 'triangles', shaderCode, schema, { ...values, ...internalValues }, materialId) return createRenderable(renderItem, values, state); } \ No newline at end of file diff --git a/src/mol-gl/renderable/text.ts b/src/mol-gl/renderable/text.ts index 6f10a8fad2fc94589cfe215d39f0bbd8466b5c0e..1bf43a8569bf25c8cb22efa3bff6730b7f99b61e 100644 --- a/src/mol-gl/renderable/text.ts +++ b/src/mol-gl/renderable/text.ts @@ -6,7 +6,7 @@ import { Renderable, RenderableState, createRenderable } from '../renderable' import { WebGLContext } from '../webgl/context'; -import { createRenderItem } from '../webgl/render-item'; +import { createGraphicsRenderItem } from '../webgl/render-item'; import { GlobalUniformSchema, BaseSchema, AttributeSpec, UniformSpec, Values, InternalSchema, SizeSchema, InternalValues, TextureSpec, ElementsSpec, ValueSpec } from './schema'; import { TextShaderCode } from '../shader-code'; import { ValueCell } from 'mol-util'; @@ -34,13 +34,13 @@ export const TextSchema = { export type TextSchema = typeof TextSchema export type TextValues = Values<TextSchema> -export function TextRenderable(ctx: WebGLContext, id: number, values: TextValues, state: RenderableState): Renderable<TextValues> { +export function TextRenderable(ctx: WebGLContext, id: number, values: TextValues, state: RenderableState, materialId: number): Renderable<TextValues> { const schema = { ...GlobalUniformSchema, ...InternalSchema, ...TextSchema } const internalValues: InternalValues = { uObjectId: ValueCell.create(id), uPickable: ValueCell.create(state.pickable ? 1 : 0) } const shaderCode = TextShaderCode - const renderItem = createRenderItem(ctx, 'triangles', shaderCode, schema, { ...values, ...internalValues }) + const renderItem = createGraphicsRenderItem(ctx, 'triangles', shaderCode, schema, { ...values, ...internalValues }, materialId) return createRenderable(renderItem, values, state); } \ No newline at end of file diff --git a/src/mol-gl/renderable/texture-mesh.ts b/src/mol-gl/renderable/texture-mesh.ts new file mode 100644 index 0000000000000000000000000000000000000000..75f6f08b1a66bea15c48cce11e5b24532c1c992d --- /dev/null +++ b/src/mol-gl/renderable/texture-mesh.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { Renderable, RenderableState, createRenderable } from '../renderable' +import { WebGLContext } from '../webgl/context'; +import { createGraphicsRenderItem } from '../webgl/render-item'; +import { GlobalUniformSchema, BaseSchema, DefineSpec, Values, InternalSchema, InternalValues, UniformSpec, TextureSpec } from './schema'; +import { MeshShaderCode } from '../shader-code'; +import { ValueCell } from 'mol-util'; + +export const TextureMeshSchema = { + ...BaseSchema, + + uGeoTexDim: UniformSpec('v2'), + /** texture has vertex positions in XYZ and group id in W */ + tPositionGroup: TextureSpec('texture', 'rgba', 'float', 'nearest'), + tNormal: TextureSpec('texture', 'rgba', 'float', 'nearest'), + + dFlatShaded: DefineSpec('boolean'), + dDoubleSided: DefineSpec('boolean'), + dFlipSided: DefineSpec('boolean'), + dGeoTexture: DefineSpec('boolean'), +} +export type TextureMeshSchema = typeof TextureMeshSchema +export type TextureMeshValues = Values<TextureMeshSchema> + +export function TextureMeshRenderable(ctx: WebGLContext, id: number, values: TextureMeshValues, state: RenderableState, materialId: number): Renderable<TextureMeshValues> { + const schema = { ...GlobalUniformSchema, ...InternalSchema, ...TextureMeshSchema } + const internalValues: InternalValues = { + uObjectId: ValueCell.create(id), + uPickable: ValueCell.create(state.pickable ? 1 : 0) + } + const shaderCode = MeshShaderCode + const renderItem = createGraphicsRenderItem(ctx, 'triangles', shaderCode, schema, { ...values, ...internalValues }, materialId) + + return createRenderable(renderItem, values, state) +} \ No newline at end of file diff --git a/src/mol-gl/renderable/util.ts b/src/mol-gl/renderable/util.ts index a8d4d0c8b0e74f52f05b3d70395380cde3f75040..5c768690c85d276809d48647916c7166a511ead2 100644 --- a/src/mol-gl/renderable/util.ts +++ b/src/mol-gl/renderable/util.ts @@ -29,9 +29,9 @@ export interface TextureVolume<T extends Uint8Array | Float32Array> { readonly depth: number } -export function createTextureImage(n: number, itemSize: number, array?: Uint8Array): TextureImage<Uint8Array> { +export function createTextureImage<T extends Uint8Array | Float32Array>(n: number, itemSize: number, arrayCtor: new (length: number) => T, array?: T): TextureImage<T> { const { length, width, height } = calculateTextureInfo(n, itemSize) - array = array && array.length >= length ? array : new Uint8Array(length) + array = array && array.length >= length ? array : new arrayCtor(length) return { array, width, height } } @@ -65,7 +65,8 @@ export function printImageData(imageData: ImageData, scale = 1) { const img = document.createElement('img') img.src = objectURL img.style.width = imageData.width * scale + 'px' - img.style.height = imageData.height * scale + 'px' + img.style.height = imageData.height * scale + 'px'; + (img.style as any).imageRendering = 'pixelated' // works in Chrome img.style.position = 'absolute' img.style.top = '0px' img.style.left = '0px' diff --git a/src/mol-gl/renderer.ts b/src/mol-gl/renderer.ts index a85a8f3a0278bb2b0e7e1bd73782f7f155866450..2c427b728c71764ce5e0c934276058a7ff70b410 100644 --- a/src/mol-gl/renderer.ts +++ b/src/mol-gl/renderer.ts @@ -1,10 +1,9 @@ /** - * 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> */ -// import { Vec3, Mat4 } from 'mol-math/linear-algebra' import { Viewport } from 'mol-canvas3d/camera/util'; import { Camera } from 'mol-canvas3d/camera'; @@ -15,7 +14,9 @@ import { Renderable } from './renderable'; import { Color } from 'mol-util/color'; import { ValueCell } from 'mol-util'; import { RenderableValues, GlobalUniformValues, BaseValues } from './renderable/schema'; -import { RenderVariant } from './webgl/render-item'; +import { GraphicsRenderVariant } from './webgl/render-item'; +import { ParamDefinition as PD } from 'mol-util/param-definition'; +import { deepClone } from 'mol-util/object'; export interface RendererStats { programCount: number @@ -34,44 +35,36 @@ export interface RendererStats { interface Renderer { readonly stats: RendererStats - readonly props: RendererProps + readonly props: Readonly<RendererProps> clear: () => void - render: (scene: Scene, variant: RenderVariant) => void + render: (scene: Scene, variant: GraphicsRenderVariant) => void + setProps: (props: Partial<RendererProps>) => void setViewport: (x: number, y: number, width: number, height: number) => void - setClearColor: (color: Color) => void - setPickingAlphaThreshold: (value: number) => void getImageData: () => ImageData dispose: () => void } -export const DefaultRendererProps = { - clearColor: Color(0x000000), - viewport: Viewport.create(0, 0, 0, 0), - pickingAlphaThreshold: 0.5, +export const RendererParams = { + backgroundColor: PD.Color(Color(0x000000)), + pickingAlphaThreshold: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }, { description: 'The minimum opacity value needed for an object to be pickable.' }), + + lightIntensity: PD.Numeric(0.6, { min: 0.0, max: 1.0, step: 0.01 }), + ambientIntensity: PD.Numeric(0.4, { min: 0.0, max: 1.0, step: 0.01 }), + + metalness: PD.Numeric(0.0, { min: 0.0, max: 1.0, step: 0.01 }), + roughness: PD.Numeric(0.4, { min: 0.0, max: 1.0, step: 0.01 }), + reflectivity: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }), } -export type RendererProps = typeof DefaultRendererProps +export type RendererProps = PD.Values<typeof RendererParams> namespace Renderer { export function create(ctx: WebGLContext, camera: Camera, props: Partial<RendererProps> = {}): Renderer { - const { gl } = ctx - let { clearColor, viewport: _viewport, pickingAlphaThreshold } = { ...DefaultRendererProps, ...props } - - const viewport = Viewport.clone(_viewport) - const viewportVec4 = Viewport.toVec4(Vec4.zero(), viewport) - - // const lightPosition = Vec3.create(0, 0, -100) - const lightColor = Vec3.create(1.0, 1.0, 1.0) - const lightAmbient = Vec3.create(0.5, 0.5, 0.5) - const fogColor = Vec3.create(0.0, 0.0, 0.0) - - function setClearColor(color: Color) { - clearColor = color - const [ r, g, b ] = Color.toRgbNormalized(color) - gl.clearColor(r, g, b, 1.0) - Vec3.set(fogColor, r, g, b) - } - setClearColor(clearColor) + const { gl, state, stats } = ctx + const p = deepClone({ ...PD.getDefaultValues(RendererParams), ...props }) + + const viewport = Viewport() + const bgColor = Color.toVec3Normalized(Vec3(), p.backgroundColor) const view = Mat4.clone(camera.view) const invView = Mat4.invert(Mat4.identity(), view) @@ -94,63 +87,71 @@ namespace Renderer { uPixelRatio: ValueCell.create(ctx.pixelRatio), uViewportHeight: ValueCell.create(viewport.height), - uViewport: ValueCell.create(viewportVec4), + uViewport: ValueCell.create(Viewport.toVec4(Vec4(), viewport)), + + uLightIntensity: ValueCell.create(p.lightIntensity), + uAmbientIntensity: ValueCell.create(p.ambientIntensity), - uLightColor: ValueCell.create(lightColor), - uLightAmbient: ValueCell.create(lightAmbient), + uMetalness: ValueCell.create(p.metalness), + uRoughness: ValueCell.create(p.roughness), + uReflectivity: ValueCell.create(p.reflectivity), uCameraPosition: ValueCell.create(Vec3.clone(camera.state.position)), uFogNear: ValueCell.create(camera.state.fogNear), uFogFar: ValueCell.create(camera.state.fogFar), - uFogColor: ValueCell.create(fogColor), + uFogColor: ValueCell.create(bgColor), - uPickingAlphaThreshold: ValueCell.create(pickingAlphaThreshold), + uPickingAlphaThreshold: ValueCell.create(p.pickingAlphaThreshold), } + const globalUniformList = Object.entries(globalUniforms) let globalUniformsNeedUpdate = true - const renderObject = (r: Renderable<RenderableValues & BaseValues>, variant: RenderVariant) => { + + const renderObject = (r: Renderable<RenderableValues & BaseValues>, variant: GraphicsRenderVariant) => { const program = r.getProgram(variant) if (r.state.visible) { - if (ctx.currentProgramId !== program.id) { + if (state.currentProgramId !== program.id) { + // console.log('new program') globalUniformsNeedUpdate = true + program.use() } - program.use() if (globalUniformsNeedUpdate) { - program.setUniforms(globalUniforms) + // console.log('globalUniformsNeedUpdate') + program.setUniforms(globalUniformList) globalUniformsNeedUpdate = false } if (r.values.dDoubleSided) { if (r.values.dDoubleSided.ref.value) { - gl.disable(gl.CULL_FACE) + state.disable(gl.CULL_FACE) } else { - gl.enable(gl.CULL_FACE) + state.enable(gl.CULL_FACE) } } else { // webgl default - gl.disable(gl.CULL_FACE) + state.disable(gl.CULL_FACE) } if (r.values.dFlipSided) { if (r.values.dFlipSided.ref.value) { - gl.frontFace(gl.CW) - gl.cullFace(gl.FRONT) + state.frontFace(gl.CW) + state.cullFace(gl.FRONT) } else { - gl.frontFace(gl.CCW) - gl.cullFace(gl.BACK) + state.frontFace(gl.CCW) + state.cullFace(gl.BACK) } } else { // webgl default - gl.frontFace(gl.CCW) - gl.cullFace(gl.BACK) + state.frontFace(gl.CCW) + state.cullFace(gl.BACK) } r.render(variant) } } - const render = (scene: Scene, variant: RenderVariant) => { + const render = (scene: Scene, variant: GraphicsRenderVariant) => { ValueCell.update(globalUniforms.uModel, scene.view) ValueCell.update(globalUniforms.uView, camera.view) ValueCell.update(globalUniforms.uInvView, Mat4.invert(invView, camera.view)) @@ -166,30 +167,33 @@ namespace Renderer { ValueCell.update(globalUniforms.uFogNear, camera.state.fogNear) globalUniformsNeedUpdate = true + state.currentRenderItemId = -1 const { renderables } = scene + state.disable(gl.SCISSOR_TEST) + state.disable(gl.BLEND) + state.depthMask(true) + state.colorMask(true, true, true, true) + state.enable(gl.DEPTH_TEST) + state.clearColor(bgColor[0], bgColor[1], bgColor[2], 1.0) + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) + if (variant === 'draw') { - gl.disable(gl.BLEND) - gl.enable(gl.DEPTH_TEST) - gl.depthMask(true) for (let i = 0, il = renderables.length; i < il; ++i) { const r = renderables[i] if (r.state.opaque) renderObject(r, variant) } - gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) - gl.enable(gl.BLEND) + state.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) + state.enable(gl.BLEND) for (let i = 0, il = renderables.length; i < il; ++i) { const r = renderables[i] - gl.depthMask(r.values.uAlpha.ref.value === 1.0) + state.depthMask(r.values.uAlpha.ref.value === 1.0) if (!r.state.opaque) renderObject(r, variant) } } else { // picking - gl.disable(gl.BLEND) - gl.enable(gl.DEPTH_TEST) - gl.depthMask(true) for (let i = 0, il = renderables.length; i < il; ++i) { renderObject(renderables[i], variant) } @@ -200,51 +204,79 @@ namespace Renderer { return { clear: () => { - gl.depthMask(true) + state.depthMask(true) + state.clearColor(bgColor[0], bgColor[1], bgColor[2], 1.0) gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) }, render, - setClearColor, - setPickingAlphaThreshold: (value: number) => { - pickingAlphaThreshold = value - ValueCell.update(globalUniforms.uPickingAlphaThreshold, pickingAlphaThreshold) + setProps: (props: Partial<RendererProps>) => { + if (props.pickingAlphaThreshold !== undefined && props.pickingAlphaThreshold !== p.pickingAlphaThreshold) { + p.pickingAlphaThreshold = props.pickingAlphaThreshold + ValueCell.update(globalUniforms.uPickingAlphaThreshold, p.pickingAlphaThreshold) + } + if (props.backgroundColor !== undefined && props.backgroundColor !== p.backgroundColor) { + p.backgroundColor = props.backgroundColor + Color.toVec3Normalized(bgColor, p.backgroundColor) + ValueCell.update(globalUniforms.uFogColor, Vec3.copy(globalUniforms.uFogColor.ref.value, bgColor)) + } + if (props.lightIntensity !== undefined && props.lightIntensity !== p.lightIntensity) { + p.lightIntensity = props.lightIntensity + ValueCell.update(globalUniforms.uLightIntensity, p.lightIntensity) + } + if (props.ambientIntensity !== undefined && props.ambientIntensity !== p.ambientIntensity) { + p.ambientIntensity = props.ambientIntensity + ValueCell.update(globalUniforms.uAmbientIntensity, p.ambientIntensity) + } + + if (props.metalness !== undefined && props.metalness !== p.metalness) { + p.metalness = props.metalness + ValueCell.update(globalUniforms.uMetalness, p.metalness) + } + if (props.roughness !== undefined && props.roughness !== p.roughness) { + p.roughness = props.roughness + ValueCell.update(globalUniforms.uRoughness, p.roughness) + } + if (props.reflectivity !== undefined && props.reflectivity !== p.reflectivity) { + p.reflectivity = props.reflectivity + ValueCell.update(globalUniforms.uReflectivity, p.reflectivity) + } }, setViewport: (x: number, y: number, width: number, height: number) => { - Viewport.set(viewport, x, y, width, height) gl.viewport(x, y, width, height) - ValueCell.update(globalUniforms.uViewportHeight, height) - ValueCell.update(globalUniforms.uViewport, Vec4.set(viewportVec4, x, y, width, height)) + if (x !== viewport.x || y !== viewport.y || width !== viewport.width || height !== viewport.height) { + Viewport.set(viewport, x, y, width, height) + ValueCell.update(globalUniforms.uViewportHeight, height) + ValueCell.update(globalUniforms.uViewport, Vec4.set(globalUniforms.uViewport.ref.value, x, y, width, height)) + } }, getImageData: () => { - const { width, height } = viewport - const buffer = new Uint8Array(width * height * 4) + const { x, y, width, height } = viewport + const dw = width - x + const dh = height - y + const buffer = new Uint8Array(dw * dh * 4) ctx.unbindFramebuffer() - ctx.readPixels(0, 0, width, height, buffer) - return createImageData(buffer, width, height) + ctx.readPixels(x, y, width, height, buffer) + return createImageData(buffer, dw, dh) }, get props() { - return { - clearColor, - pickingAlphaThreshold, - viewport - } + return p }, get stats(): RendererStats { return { programCount: ctx.programCache.count, shaderCount: ctx.shaderCache.count, - bufferCount: ctx.bufferCount, - framebufferCount: ctx.framebufferCount, - renderbufferCount: ctx.renderbufferCount, - textureCount: ctx.textureCount, - vaoCount: ctx.vaoCount, + bufferCount: stats.bufferCount, + framebufferCount: stats.framebufferCount, + renderbufferCount: stats.renderbufferCount, + textureCount: stats.textureCount, + vaoCount: stats.vaoCount, - drawCount: ctx.drawCount, - instanceCount: ctx.instanceCount, - instancedDrawCount: ctx.instancedDrawCount, + drawCount: stats.drawCount, + instanceCount: stats.instanceCount, + instancedDrawCount: stats.instancedDrawCount, } }, dispose: () => { diff --git a/src/mol-gl/scene.ts b/src/mol-gl/scene.ts index e8e079ceb5d01b3aeb5e0a07442bc510e0d481a4..749836fe396ff5d1b56cdf87179c9e316300cda4 100644 --- a/src/mol-gl/scene.ts +++ b/src/mol-gl/scene.ts @@ -7,7 +7,7 @@ import { Renderable } from './renderable' import { WebGLContext } from './webgl/context'; import { RenderableValues, BaseValues } from './renderable/schema'; -import { RenderObject, createRenderable, GraphicsRenderObject } from './render-object'; +import { GraphicsRenderObject, createRenderable } from './render-object'; import { Object3D } from './object3d'; import { Sphere3D } from 'mol-math/geometry'; import { Vec3 } from 'mol-math/linear-algebra'; @@ -38,15 +38,19 @@ function calculateBoundingSphere(renderables: Renderable<RenderableValues & Base function renderableSort(a: Renderable<RenderableValues & BaseValues>, b: Renderable<RenderableValues & BaseValues>) { const drawProgramIdA = a.getProgram('draw').id const drawProgramIdB = b.getProgram('draw').id + const materialIdA = a.materialId + const materialIdB = b.materialId const zA = a.values.boundingSphere.ref.value.center[2] - const zB = a.values.boundingSphere.ref.value.center[2] + const zB = b.values.boundingSphere.ref.value.center[2] if (drawProgramIdA !== drawProgramIdB) { - return drawProgramIdA - drawProgramIdB; // sort by program id to minimize gl state changes + return drawProgramIdA - drawProgramIdB // sort by program id to minimize gl state changes + } else if (materialIdA !== materialIdB) { + return materialIdA - materialIdB // sort by material id to minimize gl state changes } else if (zA !== zB) { return a.state.opaque - ? zA - zB // when opaque draw closer elements first to minimize overdraw - : zB - zA // when transparent draw elements last to maximize partial visibility + ? zA - zB // when opaque, draw closer elements first to minimize overdraw + : zB - zA // when transparent, draw elements last to maximize partial visibility } else { return a.id - b.id; } @@ -58,16 +62,16 @@ interface Scene extends Object3D { readonly boundingSphere: Sphere3D update: (objects: ArrayLike<GraphicsRenderObject> | undefined, keepBoundingSphere: boolean) => void - add: (o: RenderObject) => Renderable<any> - remove: (o: RenderObject) => void - has: (o: RenderObject) => boolean + add: (o: GraphicsRenderObject) => Renderable<any> + remove: (o: GraphicsRenderObject) => void + has: (o: GraphicsRenderObject) => boolean clear: () => void - forEach: (callbackFn: (value: Renderable<RenderableValues & BaseValues>, key: RenderObject) => void) => void + forEach: (callbackFn: (value: Renderable<RenderableValues & BaseValues>, key: GraphicsRenderObject) => void) => void } namespace Scene { export function create(ctx: WebGLContext): Scene { - const renderableMap = new Map<RenderObject, Renderable<RenderableValues & BaseValues>>() + const renderableMap = new Map<GraphicsRenderObject, Renderable<RenderableValues & BaseValues>>() const renderables: Renderable<RenderableValues & BaseValues>[] = [] const boundingSphere = Sphere3D.zero() let boundingSphereDirty = true @@ -95,7 +99,7 @@ namespace Scene { } if (!keepBoundingSphere) boundingSphereDirty = true }, - add: (o: RenderObject) => { + add: (o: GraphicsRenderObject) => { if (!renderableMap.has(o)) { const renderable = createRenderable(ctx, o) renderables.push(renderable) @@ -108,7 +112,7 @@ namespace Scene { return renderableMap.get(o)! } }, - remove: (o: RenderObject) => { + remove: (o: GraphicsRenderObject) => { const renderable = renderableMap.get(o) if (renderable) { renderable.dispose() @@ -118,7 +122,7 @@ namespace Scene { boundingSphereDirty = true } }, - has: (o: RenderObject) => { + has: (o: GraphicsRenderObject) => { return renderableMap.has(o) }, clear: () => { @@ -129,7 +133,7 @@ namespace Scene { renderableMap.clear() boundingSphereDirty = true }, - forEach: (callbackFn: (value: Renderable<any>, key: RenderObject) => void) => { + forEach: (callbackFn: (value: Renderable<any>, key: GraphicsRenderObject) => void) => { renderableMap.forEach(callbackFn) }, get count() { diff --git a/src/mol-gl/shader-code.ts b/src/mol-gl/shader-code.ts index 48e635d1271d17a18261167443f74c16b6e26195..3778ba8ff85859b8e29edc6842d3b1d04d4187c9 100644 --- a/src/mol-gl/shader-code.ts +++ b/src/mol-gl/shader-code.ts @@ -1,12 +1,13 @@ /** - * 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> */ import { ValueCell } from 'mol-util'; import { idFactory } from 'mol-util/id-factory'; -import { WebGLContext } from './webgl/context'; +import { WebGLExtensions } from './webgl/context'; +import { isWebGL2, GLRenderingContext } from './webgl/compat'; export type DefineKind = 'boolean' | 'string' | 'number' export type DefineType = boolean | string @@ -15,8 +16,10 @@ export type DefineValues = { [k: string]: ValueCell<DefineType> } const shaderCodeId = idFactory() export interface ShaderExtensions { - readonly standardDerivatives: boolean - readonly fragDepth: boolean + readonly standardDerivatives?: boolean + readonly fragDepth?: boolean + readonly drawBuffers?: boolean + readonly shaderTextureLod?: boolean } export interface ShaderCode { @@ -26,52 +29,46 @@ export interface ShaderCode { readonly extensions: ShaderExtensions } -export function ShaderCode(vert: string, frag: string, extensions: ShaderExtensions): ShaderCode { +export function ShaderCode(vert: string, frag: string, extensions: ShaderExtensions = {}): ShaderCode { return { id: shaderCodeId(), vert, frag, extensions } } export const PointsShaderCode = ShaderCode( - require('mol-gl/shader/points.vert'), - require('mol-gl/shader/points.frag'), - { standardDerivatives: false, fragDepth: false } + require('mol-gl/shader/points.vert').default, + require('mol-gl/shader/points.frag').default ) export const SpheresShaderCode = ShaderCode( - require('mol-gl/shader/spheres.vert'), - require('mol-gl/shader/spheres.frag'), - { standardDerivatives: false, fragDepth: true } + require('mol-gl/shader/spheres.vert').default, + require('mol-gl/shader/spheres.frag').default, + { fragDepth: true } ) export const TextShaderCode = ShaderCode( - require('mol-gl/shader/text.vert'), - require('mol-gl/shader/text.frag'), - { standardDerivatives: true, fragDepth: false } + require('mol-gl/shader/text.vert').default, + require('mol-gl/shader/text.frag').default, + { standardDerivatives: true } ) export const LinesShaderCode = ShaderCode( - require('mol-gl/shader/lines.vert'), - require('mol-gl/shader/lines.frag'), - { standardDerivatives: false, fragDepth: false } + require('mol-gl/shader/lines.vert').default, + require('mol-gl/shader/lines.frag').default ) export const MeshShaderCode = ShaderCode( - require('mol-gl/shader/mesh.vert'), - require('mol-gl/shader/mesh.frag'), - { standardDerivatives: true, fragDepth: false } -) - -export const GaussianDensityShaderCode = ShaderCode( - require('mol-gl/shader/gaussian-density.vert'), - require('mol-gl/shader/gaussian-density.frag'), - { standardDerivatives: false, fragDepth: false } + require('mol-gl/shader/mesh.vert').default, + require('mol-gl/shader/mesh.frag').default, + { standardDerivatives: true } ) export const DirectVolumeShaderCode = ShaderCode( - require('mol-gl/shader/direct-volume.vert'), - require('mol-gl/shader/direct-volume.frag'), - { standardDerivatives: false, fragDepth: true } + require('mol-gl/shader/direct-volume.vert').default, + require('mol-gl/shader/direct-volume.frag').default, + { fragDepth: true } ) + + export type ShaderDefines = { [k: string]: ValueCell<DefineType> } @@ -97,16 +94,34 @@ function getDefinesCode (defines: ShaderDefines) { return lines.join('\n') + '\n' } -function getGlsl100FragPrefix(ctx: WebGLContext, extensions: ShaderExtensions) { +function getGlsl100FragPrefix(extensions: WebGLExtensions, shaderExtensions: ShaderExtensions) { const prefix: string[] = [] - if (extensions.standardDerivatives) { + if (shaderExtensions.standardDerivatives) { prefix.push('#extension GL_OES_standard_derivatives : enable') prefix.push('#define enabledStandardDerivatives') } - if (extensions.fragDepth) { - if (ctx.extensions.fragDepth) { + if (shaderExtensions.fragDepth) { + if (extensions.fragDepth) { prefix.push('#extension GL_EXT_frag_depth : enable') prefix.push('#define enabledFragDepth') + } else { + throw new Error(`requested 'GL_EXT_frag_depth' extension is unavailable`) + } + } + if (shaderExtensions.drawBuffers) { + if (extensions.drawBuffers) { + prefix.push('#extension GL_EXT_draw_buffers : require') + prefix.push('#define requiredDrawBuffers') + } else { + throw new Error(`requested 'GL_EXT_draw_buffers' extension is unavailable`) + } + } + if (shaderExtensions.shaderTextureLod) { + if (extensions.shaderTextureLod) { + prefix.push('#extension GL_EXT_shader_texture_lod : enable') + prefix.push('#define enabledShaderTextureLod') + } else { + throw new Error(`requested 'GL_EXT_shader_texture_lod' extension is unavailable`) } } return prefix.join('\n') + '\n' @@ -119,25 +134,41 @@ const glsl300VertPrefix = `#version 300 es ` const glsl300FragPrefix = `#version 300 es +layout(location = 0) out highp vec4 out_FragData0; +layout(location = 1) out highp vec4 out_FragData1; +layout(location = 2) out highp vec4 out_FragData2; +layout(location = 3) out highp vec4 out_FragData3; +layout(location = 4) out highp vec4 out_FragData4; +layout(location = 5) out highp vec4 out_FragData5; +layout(location = 6) out highp vec4 out_FragData6; +layout(location = 7) out highp vec4 out_FragData7; + #define varying in -layout(location = 0) out highp vec4 out_FragColor; -#define gl_FragColor out_FragColor -#define gl_FragDepthEXT gl_FragDepth #define texture2D texture +#define texture2DLodEXT textureLod + +#define gl_FragColor out_FragData0 +#define gl_FragDepthEXT gl_FragDepth #define enabledStandardDerivatives #define enabledFragDepth +#define requiredDrawBuffers ` -export function addShaderDefines(ctx: WebGLContext, defines: ShaderDefines, shaders: ShaderCode): ShaderCode { - const { isWebGL2 } = ctx +function transformGlsl300Frag(frag: string) { + return frag.replace(/gl_FragData\[([0-7])\]/g, 'out_FragData$1') +} + +export function addShaderDefines(gl: GLRenderingContext, extensions: WebGLExtensions, defines: ShaderDefines, shaders: ShaderCode): ShaderCode { + const webgl2 = isWebGL2(gl) const header = getDefinesCode(defines) - const vertPrefix = isWebGL2 ? glsl300VertPrefix : '' - const fragPrefix = isWebGL2 ? glsl300FragPrefix : getGlsl100FragPrefix(ctx, shaders.extensions) + const vertPrefix = webgl2 ? glsl300VertPrefix : '' + const fragPrefix = webgl2 ? glsl300FragPrefix : getGlsl100FragPrefix(extensions, shaders.extensions) + const frag = webgl2 ? transformGlsl300Frag(shaders.frag) : shaders.frag return { id: shaderCodeId(), vert: `${vertPrefix}${header}${shaders.vert}`, - frag: `${fragPrefix}${header}${shaders.frag}`, + frag: `${fragPrefix}${header}${frag}`, extensions: shaders.extensions } } \ No newline at end of file diff --git a/src/mol-gl/shader/chunks/apply-light-color.glsl b/src/mol-gl/shader/chunks/apply-light-color.glsl new file mode 100644 index 0000000000000000000000000000000000000000..d71afe83dc4f4459587ae31ae52a04503a581819 --- /dev/null +++ b/src/mol-gl/shader/chunks/apply-light-color.glsl @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + * + * adapted from three.js (https://github.com/mrdoob/three.js/) + * which under the MIT License, Copyright © 2010-2019 three.js authors + */ + +// inputs +// - vec4 material +// - vec3 vViewPosition +// - vec3 normal +// - float uMetalness +// - float uRoughness +// - float uReflectivity +// - float uLightIntensity +// - float uAmbientIntensity + +// outputs +// - sets gl_FragColor + +vec4 color = material; + +ReflectedLight reflectedLight = ReflectedLight(vec3(0.0), vec3(0.0), vec3(0.0)); + +PhysicalMaterial physicalMaterial; +physicalMaterial.diffuseColor = color.rgb * (1.0 - uMetalness); +physicalMaterial.specularRoughness = clamp(uRoughness, 0.04, 1.0); +physicalMaterial.specularColor = mix(vec3(0.16 * pow2(uReflectivity)), color.rgb, uMetalness); + +GeometricContext geometry; +geometry.position = -vViewPosition; +geometry.normal = normal; +geometry.viewDir = normalize(vViewPosition); + +IncidentLight directLight; +directLight.direction = geometry.viewDir; +directLight.color = vec3(uLightIntensity); + +RE_Direct_Physical(directLight, geometry, physicalMaterial, reflectedLight); + +vec3 irradiance = vec3(uAmbientIntensity) * PI; +RE_IndirectDiffuse_Physical(irradiance, geometry, physicalMaterial, reflectedLight); + +vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular; + +gl_FragColor = vec4(outgoingLight, color.a); \ No newline at end of file diff --git a/src/mol-gl/shader/chunks/apply-marker-color.glsl b/src/mol-gl/shader/chunks/apply-marker-color.glsl index 327ba74bb881a097df42ef173ec4bc1db168dc3c..59067c0074d3335034a90d7d2fbd5342977700c1 100644 --- a/src/mol-gl/shader/chunks/apply-marker-color.glsl +++ b/src/mol-gl/shader/chunks/apply-marker-color.glsl @@ -1,5 +1,5 @@ // only mark elements with an alpha above the picking threshold -if (uAlpha >= uPickingAlphaThreshold) { +if (gl_FragColor.a >= uPickingAlphaThreshold) { float marker = floor(vMarker * 255.0 + 0.5); // rounding required to work on some cards on win if (marker > 0.1) { if (intMod(marker, 2.0) > 0.1) { diff --git a/src/mol-gl/shader/chunks/assign-color-varying.glsl b/src/mol-gl/shader/chunks/assign-color-varying.glsl index 7175df041fa3cf59c66964a3a0598d49ac20c05f..4dfd59bfd884bcab975c2f03fda8f2df9bb8113a 100644 --- a/src/mol-gl/shader/chunks/assign-color-varying.glsl +++ b/src/mol-gl/shader/chunks/assign-color-varying.glsl @@ -3,17 +3,22 @@ #elif defined(dColorType_instance) vColor.rgb = readFromTexture(tColor, aInstance, uColorTexDim).rgb; #elif defined(dColorType_group) - vColor.rgb = readFromTexture(tColor, aGroup, uColorTexDim).rgb; + vColor.rgb = readFromTexture(tColor, group, uColorTexDim).rgb; #elif defined(dColorType_groupInstance) - vColor.rgb = readFromTexture(tColor, aInstance * float(uGroupCount) + aGroup, uColorTexDim).rgb; + vColor.rgb = readFromTexture(tColor, aInstance * float(uGroupCount) + group, uColorTexDim).rgb; #elif defined(dColorType_objectPicking) vColor = vec4(encodeFloatRGB(float(uObjectId)), 1.0); #elif defined(dColorType_instancePicking) vColor = vec4(encodeFloatRGB(aInstance), 1.0); #elif defined(dColorType_groupPicking) - vColor = vec4(encodeFloatRGB(aGroup), 1.0); + vColor = vec4(encodeFloatRGB(group), 1.0); #endif #ifdef dOverpaint - vOverpaint = readFromTexture(tOverpaint, aInstance * float(uGroupCount) + aGroup, uOverpaintTexDim); + vOverpaint = readFromTexture(tOverpaint, aInstance * float(uGroupCount) + group, uOverpaintTexDim); +#endif + +#ifdef dTransparency + vGroup = group; + vTransparency = readFromTexture(tTransparency, aInstance * float(uGroupCount) + group, uTransparencyTexDim).a; #endif \ No newline at end of file diff --git a/src/mol-gl/shader/chunks/assign-group.glsl b/src/mol-gl/shader/chunks/assign-group.glsl new file mode 100644 index 0000000000000000000000000000000000000000..9a797bc402bcbf9ade19590f03382a57cf1d88c3 --- /dev/null +++ b/src/mol-gl/shader/chunks/assign-group.glsl @@ -0,0 +1,6 @@ +#ifdef dGeoTexture + // aGroup is used as a vertex index here and the group id is retirieved from tPositionGroup + float group = readFromTexture(tPositionGroup, aGroup, uGeoTexDim).w; +#else + float group = aGroup; +#endif \ No newline at end of file diff --git a/src/mol-gl/shader/chunks/assign-marker-varying.glsl b/src/mol-gl/shader/chunks/assign-marker-varying.glsl index 38c3e0a474debc92226378e42368c7b42362f693..69259033f037ba52806451f51e03d19931dd19a1 100644 --- a/src/mol-gl/shader/chunks/assign-marker-varying.glsl +++ b/src/mol-gl/shader/chunks/assign-marker-varying.glsl @@ -1 +1 @@ -vMarker = readFromTexture(tMarker, aInstance * float(uGroupCount) + aGroup, uMarkerTexDim).a; \ No newline at end of file +vMarker = readFromTexture(tMarker, aInstance * float(uGroupCount) + group, uMarkerTexDim).a; \ No newline at end of file diff --git a/src/mol-gl/shader/chunks/assign-material-color.glsl b/src/mol-gl/shader/chunks/assign-material-color.glsl index d79b00d7da917237b2e5c1b657ccf1683adbd14b..68fba305e63861689d69d403e15ca2eea82517a9 100644 --- a/src/mol-gl/shader/chunks/assign-material-color.glsl +++ b/src/mol-gl/shader/chunks/assign-material-color.glsl @@ -9,4 +9,24 @@ // mix material with overpaint #if defined(dOverpaint) && (defined(dColorType_uniform) || defined(dColorType_attribute) || defined(dColorType_instance) || defined(dColorType_group) || defined(dColorType_groupInstance)) material.rgb = mix(material.rgb, vOverpaint.rgb, vOverpaint.a); +#endif + +// apply screendoor transparency +#if defined(dTransparency) && (defined(dColorType_uniform) || defined(dColorType_attribute) || defined(dColorType_instance) || defined(dColorType_group) || defined(dColorType_groupInstance)) + float ta = 1.0 - vTransparency; + float at = 0.0; + + #if defined(dTransparencyVariant_single) + const mat4 thresholdMatrix = mat4( + 1.0 / 17.0, 9.0 / 17.0, 3.0 / 17.0, 11.0 / 17.0, + 13.0 / 17.0, 5.0 / 17.0, 15.0 / 17.0, 7.0 / 17.0, + 4.0 / 17.0, 12.0 / 17.0, 2.0 / 17.0, 10.0 / 17.0, + 16.0 / 17.0, 8.0 / 17.0, 14.0 / 17.0, 6.0 / 17.0 + ); + at = thresholdMatrix[int(intMod(gl_FragCoord.x, 4.0))][int(intMod(gl_FragCoord.y, 4.0))]; + #elif defined(dTransparencyVariant_multi) + at = fract(dot(vec3(gl_FragCoord.xy, vGroup + 0.5), vec3(2.0, 7.0, 23.0) / 17.0f)); + #endif + + if (ta < 0.99 && (ta < 0.01 || ta < at)) discard; #endif \ No newline at end of file diff --git a/src/mol-gl/shader/chunks/assign-normal.glsl b/src/mol-gl/shader/chunks/assign-normal.glsl new file mode 100644 index 0000000000000000000000000000000000000000..56caada40a5324b4ecd8bb9b07bfd57683aa8a89 --- /dev/null +++ b/src/mol-gl/shader/chunks/assign-normal.glsl @@ -0,0 +1,18 @@ +// inputs +// - vViewPosition (if dFlatShaded) +// - vNormal (if NOT dFlatShaded) + +// outputs +// - normal + +// surface normal +#if defined(dFlatShaded) && defined(enabledStandardDerivatives) + vec3 fdx = dFdx(vViewPosition); + vec3 fdy = dFdy(vViewPosition); + vec3 normal = -normalize(cross(fdx, fdy)); +#else + vec3 normal = -normalize(vNormal); + #ifdef dDoubleSided + normal = normal * (float(gl_FrontFacing) * 2.0 - 1.0); + #endif +#endif \ No newline at end of file diff --git a/src/mol-gl/shader/chunks/assign-position.glsl b/src/mol-gl/shader/chunks/assign-position.glsl index 06231cf7ff806afbd988c00961471fe9cb3b81dd..50440d786ecc1685bd37313a9e96f015c27873b0 100644 --- a/src/mol-gl/shader/chunks/assign-position.glsl +++ b/src/mol-gl/shader/chunks/assign-position.glsl @@ -1,4 +1,9 @@ mat4 modelView = uView * uModel * aTransform; -vec4 mvPosition = modelView * vec4(aPosition, 1.0); +#ifdef dGeoTexture + vec3 position = readFromTexture(tPositionGroup, aGroup, uGeoTexDim).xyz; +#else + vec3 position = aPosition; +#endif +vec4 mvPosition = modelView * vec4(position, 1.0); vViewPosition = mvPosition.xyz; gl_Position = uProjection * mvPosition; \ No newline at end of file diff --git a/src/mol-gl/shader/chunks/assign-size.glsl b/src/mol-gl/shader/chunks/assign-size.glsl index be66b9bfff1c10383baaf03dd571066a66772364..3f1fe2582213eb0e551cce472e547d1788ed7920 100644 --- a/src/mol-gl/shader/chunks/assign-size.glsl +++ b/src/mol-gl/shader/chunks/assign-size.glsl @@ -5,9 +5,9 @@ #elif defined(dSizeType_instance) float size = readFromTexture(tSize, aInstance, uSizeTexDim).a; #elif defined(dSizeType_group) - float size = readFromTexture(tSize, aGroup, uSizeTexDim).a; + float size = readFromTexture(tSize, group, uSizeTexDim).a; #elif defined(dSizeType_groupInstance) - float size = readFromTexture(tSize, aInstance * float(uGroupCount) + aGroup, uSizeTexDim).a; + float size = readFromTexture(tSize, aInstance * float(uGroupCount) + group, uSizeTexDim).a; #endif #if defined(dSizeType_instance) || defined(dSizeType_group) || defined(dSizeType_groupInstance) diff --git a/src/mol-gl/shader/chunks/color-frag-params.glsl b/src/mol-gl/shader/chunks/color-frag-params.glsl index 871e2dee3e9551de259c1273ac869154228df38e..8b3e905b4c5134ede7f3e9d69b5b2aae658b4f71 100644 --- a/src/mol-gl/shader/chunks/color-frag-params.glsl +++ b/src/mol-gl/shader/chunks/color-frag-params.glsl @@ -12,4 +12,9 @@ #ifdef dOverpaint varying vec4 vOverpaint; +#endif + +#ifdef dTransparency + varying float vGroup; + varying float vTransparency; #endif \ No newline at end of file diff --git a/src/mol-gl/shader/chunks/color-vert-params.glsl b/src/mol-gl/shader/chunks/color-vert-params.glsl index b3156a22ee6f0cabb1cf1505e09b82570ce865da..3daf743fe42366c229372b9767c532e18f4390e8 100644 --- a/src/mol-gl/shader/chunks/color-vert-params.glsl +++ b/src/mol-gl/shader/chunks/color-vert-params.glsl @@ -20,4 +20,11 @@ varying vec4 vOverpaint; uniform vec2 uOverpaintTexDim; uniform sampler2D tOverpaint; +#endif + +#ifdef dTransparency + varying float vGroup; + varying float vTransparency; + uniform vec2 uTransparencyTexDim; + uniform sampler2D tTransparency; #endif \ No newline at end of file diff --git a/src/mol-gl/shader/chunks/common.glsl b/src/mol-gl/shader/chunks/common.glsl index c26150ec86632fe48034fa8e6265a42d375ed4eb..25d0d9b9c52522b3c70f7909bf8958cbc1472767 100644 --- a/src/mol-gl/shader/chunks/common.glsl +++ b/src/mol-gl/shader/chunks/common.glsl @@ -1,5 +1,13 @@ -float intDiv(float a, float b) { return float(int(a) / int(b)); } -float intMod(float a, float b) { return a - b * float(int(a) / int(b)); } +#define PI 3.14159265 +#define RECIPROCAL_PI 0.31830988618 +#define EPSILON 1e-6 + +#define saturate(a) clamp(a, 0.0, 1.0) + +float intDiv(const in float a, const in float b) { return float(int(a) / int(b)); } +float intMod(const in float a, const in float b) { return a - b * float(int(a) / int(b)); } + +float pow2(const in float x) { return x*x; } #if __VERSION__ != 300 // transpose diff --git a/src/mol-gl/shader/chunks/light-frag-params.glsl b/src/mol-gl/shader/chunks/light-frag-params.glsl new file mode 100644 index 0000000000000000000000000000000000000000..8a7eae37e202880a108212ca6d4b623babbd4c17 --- /dev/null +++ b/src/mol-gl/shader/chunks/light-frag-params.glsl @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + * + * adapted from three.js (https://github.com/mrdoob/three.js/) + * which under the MIT License, Copyright © 2010-2019 three.js authors + */ + +uniform float uLightIntensity; +uniform float uAmbientIntensity; +uniform float uReflectivity; +uniform float uMetalness; +uniform float uRoughness; + +struct PhysicalMaterial { + vec3 diffuseColor; + float specularRoughness; + vec3 specularColor; +}; + +struct IncidentLight { + vec3 color; + vec3 direction; +}; + +struct ReflectedLight { + vec3 directDiffuse; + vec3 directSpecular; + vec3 indirectDiffuse; +}; + +struct GeometricContext { + vec3 position; + vec3 normal; + vec3 viewDir; +}; + +vec3 F_Schlick(const in vec3 specularColor, const in float dotLH) { + // Original approximation by Christophe Schlick '94 + // float fresnel = pow( 1.0 - dotLH, 5.0 ); + // Optimized variant (presented by Epic at SIGGRAPH '13) + // https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf + float fresnel = exp2((-5.55473 * dotLH - 6.98316) * dotLH); + return (1.0 - specularColor) * fresnel + specularColor; +} + +// Moving Frostbite to Physically Based Rendering 3.0 - page 12, listing 2 +// https://seblagarde.files.wordpress.com/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf +float G_GGX_SmithCorrelated(const in float alpha, const in float dotNL, const in float dotNV) { + float a2 = pow2(alpha); + // dotNL and dotNV are explicitly swapped. This is not a mistake. + float gv = dotNL * sqrt(a2 + (1.0 - a2) * pow2(dotNV)); + float gl = dotNV * sqrt(a2 + (1.0 - a2) * pow2(dotNL)); + return 0.5 / max(gv + gl, EPSILON); +} + +// Microfacet Models for Refraction through Rough Surfaces - equation (33) +// http://graphicrants.blogspot.com/2013/08/specular-brdf-reference.html +// alpha is "roughness squared" in Disney’s reparameterization +float D_GGX(const in float alpha, const in float dotNH) { + float a2 = pow2(alpha); + float denom = pow2(dotNH) * (a2 - 1.0) + 1.0; // avoid alpha = 0 with dotNH = 1 + return RECIPROCAL_PI * a2 / pow2(denom); +} + +vec3 BRDF_Diffuse_Lambert(const in vec3 diffuseColor) { + return RECIPROCAL_PI * diffuseColor; +} + +// GGX Distribution, Schlick Fresnel, GGX-Smith Visibility +vec3 BRDF_Specular_GGX(const in IncidentLight incidentLight, const in GeometricContext geometry, const in vec3 specularColor, const in float roughness) { + float alpha = pow2(roughness); // UE4's roughness + vec3 halfDir = normalize(incidentLight.direction + geometry.viewDir); + + float dotNL = saturate(dot(geometry.normal, incidentLight.direction)); + float dotNV = saturate(dot(geometry.normal, geometry.viewDir)); + float dotNH = saturate(dot(geometry.normal, halfDir)); + float dotLH = saturate(dot(incidentLight.direction, halfDir)); + + vec3 F = F_Schlick(specularColor, dotLH); + float G = G_GGX_SmithCorrelated(alpha, dotNL, dotNV); + float D = D_GGX(alpha, dotNH); + return F * (G * D); +} + +// ref: https://www.unrealengine.com/blog/physically-based-shading-on-mobile - environmentBRDF for GGX on mobile +vec3 BRDF_Specular_GGX_Environment(const in GeometricContext geometry, const in vec3 specularColor, const in float roughness) { + float dotNV = saturate(dot(geometry.normal, geometry.viewDir)); + const vec4 c0 = vec4(-1, -0.0275, -0.572, 0.022); + const vec4 c1 = vec4(1, 0.0425, 1.04, -0.04); + vec4 r = roughness * c0 + c1; + float a004 = min(r.x * r.x, exp2(-9.28 * dotNV)) * r.x + r.y; + vec2 AB = vec2(-1.04, 1.04) * a004 + r.zw; + return specularColor * AB.x + AB.y; +} + +void RE_Direct_Physical(const in IncidentLight directLight, const in GeometricContext geometry, const in PhysicalMaterial material, inout ReflectedLight reflectedLight) { + float dotNL = saturate(dot(geometry.normal, directLight.direction)); + vec3 irradiance = dotNL * directLight.color; + irradiance *= PI; // punctual light + + reflectedLight.directSpecular += irradiance * BRDF_Specular_GGX(directLight, geometry, material.specularColor, material.specularRoughness); + reflectedLight.directDiffuse += irradiance * BRDF_Diffuse_Lambert(material.diffuseColor); +} + +void RE_IndirectDiffuse_Physical(const in vec3 irradiance, const in GeometricContext geometry, const in PhysicalMaterial material, inout ReflectedLight reflectedLight) { + reflectedLight.indirectDiffuse += irradiance * BRDF_Diffuse_Lambert(material.diffuseColor); +} \ No newline at end of file diff --git a/src/mol-gl/shader/chunks/normal-frag-params.glsl b/src/mol-gl/shader/chunks/normal-frag-params.glsl new file mode 100644 index 0000000000000000000000000000000000000000..81ce20d5df0e2db35bbf769137bb73102bfcc17f --- /dev/null +++ b/src/mol-gl/shader/chunks/normal-frag-params.glsl @@ -0,0 +1,3 @@ +#if !defined(dFlatShaded) || !defined(enabledStandardDerivatives) + varying vec3 vNormal; +#endif \ No newline at end of file diff --git a/src/mol-gl/shader/direct-volume.frag b/src/mol-gl/shader/direct-volume.frag index 5728c6c41b3e1a0239a305f38024d3034775e604..42ce46af5477c52be865333100b75f29dd4a3d50 100644 --- a/src/mol-gl/shader/direct-volume.frag +++ b/src/mol-gl/shader/direct-volume.frag @@ -1,5 +1,5 @@ /** - * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> * @author Michael Krone <michael.krone@uni-tuebingen.de> @@ -30,11 +30,11 @@ uniform float uPickingAlphaThreshold; uniform int uPickable; #if defined(dGridTexType_2d) - precision mediump sampler2D; + precision highp sampler2D; uniform sampler2D tGridTex; uniform vec3 uGridTexDim; #elif defined(dGridTexType_3d) - precision mediump sampler3D; + precision highp sampler3D; uniform sampler3D tGridTex; #endif @@ -46,26 +46,14 @@ uniform int uPickable; #endif #pragma glslify: import('./chunks/common.glsl') +#pragma glslify: import('./chunks/light-frag-params.glsl') + #pragma glslify: readFromTexture = require(./utils/read-from-texture.glsl, intMod=intMod, intDiv=intDiv, foo=foo) // foo=foo is a workaround for a bug in glslify #pragma glslify: encodeFloatRGB = require(./utils/encode-float-rgb.glsl) #pragma glslify: decodeFloatRGB = require(./utils/decode-float-rgb.glsl) #pragma glslify: texture3dFrom2dNearest = require(./utils/texture3d-from-2d-nearest.glsl, intMod=intMod, intDiv=intDiv, foo=foo) // foo=foo is a workaround for a bug in glslify #pragma glslify: texture3dFrom2dLinear = require(./utils/texture3d-from-2d-linear.glsl, intMod=intMod, intDiv=intDiv, foo=foo) // foo=foo is a workaround for a bug in glslify -// uniform vec3 uLightPosition; -uniform vec3 uLightColor; -uniform vec3 uLightAmbient; -uniform mat4 uView; - -#pragma glslify: attenuation = require(./utils/attenuation.glsl) -#pragma glslify: calculateSpecular = require(./utils/phong-specular.glsl) -#pragma glslify: calculateDiffuse = require(./utils/oren-nayar-diffuse.glsl) - -const float specularScale = 0.15; -const float shininess = 200.0; -const float roughness = 100.0; -const float albedo = 0.95; - #if defined(dGridTexType_2d) vec4 textureVal(vec3 pos) { return texture3dFrom2dLinear(tGridTex, pos, uGridDim, uGridTexDim.xy); @@ -160,29 +148,16 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 viewDir) { color = readFromTexture(tColor, instance * float(uGroupCount) + group, uColorTexDim).rgb; #endif - vec3 L = normalize(viewDir); // light direction - vec3 V = normalize(viewDir); // eye direction - vec3 N = normalize(gradient); // surface normal + vec3 normal = normalize(gradient); + vec3 vViewPosition = normalize(viewDir); + vec4 material = vec4(color, uAlpha); + #pragma glslify: import('./chunks/apply-light-color.glsl') - // compute our diffuse & specular terms - float specular = calculateSpecular(L, V, N, shininess) * specularScale; - vec3 diffuse = uLightColor * calculateDiffuse(L, V, N, roughness, albedo); - vec3 ambient = uLightAmbient; + float vMarker = readFromTexture(tMarker, instance * float(uGroupCount) + group, uMarkerTexDim).a; + #pragma glslify: import('./chunks/apply-marker-color.glsl') - // add the lighting - vec3 finalColor = color.rgb * (diffuse + ambient) + specular; - - src.rgb = finalColor; - src.a = uAlpha; - - float marker = readFromTexture(tMarker, instance * float(uGroupCount) + group, uMarkerTexDim).a * 255.0; - if (marker > 0.1) { - if (mod(marker, 2.0) > 0.1) { - src.rgb = mix(uHighlightColor, src.rgb, 0.3); - } else { - src.rgb = mix(uSelectColor, src.rgb, 0.3); - } - } + src.rgb = gl_FragColor.rgb; + src.a = gl_FragColor.a; // draw interior darker if( (prevValue - uIsoValue) > 0.0 ) { diff --git a/src/mol-gl/shader/gaussian-density.frag b/src/mol-gl/shader/gaussian-density.frag index 652e87891b45aa5e4a4bbafb8a5d654926acfe57..74f6539de55cee5c4d85da388a084aab3cf16d9b 100644 --- a/src/mol-gl/shader/gaussian-density.frag +++ b/src/mol-gl/shader/gaussian-density.frag @@ -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> * @author Michael Krone <michael.krone@uni-tuebingen.de> @@ -8,62 +8,41 @@ precision highp float; varying vec3 vPosition; -varying float vRadius; +varying float vRadiusSqInv; #if defined(dCalcType_groupId) - #if defined(dGridTexType_2d) - precision mediump sampler2D; - uniform sampler2D tMinDistanceTex; - uniform vec3 uGridTexDim; - #elif defined(dGridTexType_3d) - precision highp sampler3D; - uniform sampler3D tMinDistanceTex; - #endif + precision highp sampler2D; + uniform sampler2D tMinDistanceTex; + uniform vec3 uGridTexDim; varying float vGroup; #endif #pragma glslify: import('./chunks/common.glsl') -#pragma glslify: encodeFloatLog = require(./utils/encode-float-log.glsl) -#pragma glslify: decodeFloatLog = require(./utils/decode-float-log.glsl) +// #pragma glslify: encodeFloatLog = require(./utils/encode-float-log.glsl) +// #pragma glslify: decodeFloatLog = require(./utils/decode-float-log.glsl) #pragma glslify: encodeFloatRGB = require(./utils/encode-float-rgb.glsl) -#pragma glslify: texture3dFrom2dNearest = require(./utils/texture3d-from-2d-nearest.glsl, intMod=intMod, intDiv=intDiv, foo=foo) // foo=foo is a workaround for a bug in glslify -uniform vec3 uBboxSize; -uniform vec3 uBboxMin; -uniform vec3 uBboxMax; uniform vec3 uGridDim; +uniform vec2 uGridTexScale; uniform float uCurrentSlice; uniform float uCurrentX; uniform float uCurrentY; uniform float uAlpha; - -#if defined(dCalcType_groupId) - #if defined(dGridTexType_2d) - vec4 textureMinDist(vec3 pos) { - return texture3dFrom2dNearest(tMinDistanceTex, pos, uGridDim, uGridTexDim.xy); - } - #elif defined(dGridTexType_3d) - vec4 textureMinDist(vec3 pos) { - return texture(tMinDistanceTex, pos); - } - #endif -#endif +uniform float uResolution; void main() { vec2 v = gl_FragCoord.xy - vec2(uCurrentX, uCurrentY) - 0.5; - vec3 fragPos = vec3(v.x, v.y, uCurrentSlice) / uGridDim; - float dist = distance(fragPos * uBboxSize, vPosition * uBboxSize); + vec3 fragPos = vec3(v.x, v.y, uCurrentSlice); + float dist = distance(fragPos, vPosition) * uResolution; #if defined(dCalcType_density) - float radiusSq = vRadius * vRadius; - float density = exp(-uAlpha * ((dist * dist) / radiusSq)); - gl_FragColor = vec4(density); + float density = exp(-uAlpha * ((dist * dist) * vRadiusSqInv)); + gl_FragColor.a = density; #elif defined(dCalcType_minDistance) - gl_FragColor.a = 1.0 - encodeFloatLog(dist); + gl_FragColor.a = 10000.0 - dist; + // gl_FragColor.a = 1.0 - encodeFloatLog(dist); #elif defined(dCalcType_groupId) - float minDistance = decodeFloatLog(1.0 - textureMinDist(fragPos).a); - // TODO verify `length(uBboxSize / uGridDim) * 2.0` - // on some machines `* 2.0` is needed while on others `* 0.5` works - if (dist > minDistance + length(uBboxSize / uGridDim) * 0.5) + float minDistance = 10000.0 - texture2D(tMinDistanceTex, (gl_FragCoord.xy) / (uGridTexDim.xy / uGridTexScale)).a; + if (dist > minDistance + uResolution * 0.05) discard; gl_FragColor.rgb = encodeFloatRGB(vGroup); #endif diff --git a/src/mol-gl/shader/gaussian-density.vert b/src/mol-gl/shader/gaussian-density.vert index f7557182942ce69b7861146dd3da32ffc3b5c52d..1048285027c3f595e9a50728bafc7ec66e1fee3e 100644 --- a/src/mol-gl/shader/gaussian-density.vert +++ b/src/mol-gl/shader/gaussian-density.vert @@ -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> * @author Michael Krone <michael.krone@uni-tuebingen.de> @@ -11,7 +11,7 @@ attribute vec3 aPosition; attribute float aRadius; varying vec3 vPosition; -varying float vRadius; +varying float vRadiusSqInv; #if defined(dCalcType_groupId) attribute float aGroup; @@ -20,17 +20,15 @@ varying float vRadius; uniform vec3 uBboxSize; uniform vec3 uBboxMin; -uniform vec3 uBboxMax; -uniform vec3 uGridDim; uniform float uCurrentSlice; +uniform float uResolution; void main() { - vRadius = aRadius; + vRadiusSqInv = 1.0 / (aRadius * aRadius); #if defined(dCalcType_groupId) vGroup = aGroup; #endif - float scale = max(uBboxSize.z, max(uBboxSize.x, uBboxSize.y)); - gl_PointSize = (vRadius / scale) * max(uGridDim.x, uGridDim.y) * 6.0; - vPosition = (aPosition - uBboxMin) / uBboxSize; - gl_Position = vec4(vPosition * 2.0 - 1.0, 1.0); + gl_PointSize = floor(((aRadius * 4.0) / uResolution) + 0.5); + vPosition = (aPosition - uBboxMin) / uResolution; + gl_Position = vec4(((aPosition - uBboxMin) / uBboxSize) * 2.0 - 1.0, 1.0); } \ No newline at end of file diff --git a/src/mol-gl/shader/histogram-pyramid/reduction.frag b/src/mol-gl/shader/histogram-pyramid/reduction.frag new file mode 100644 index 0000000000000000000000000000000000000000..ce3b9bb302c5143c10e17bf8a84b71f254448978 --- /dev/null +++ b/src/mol-gl/shader/histogram-pyramid/reduction.frag @@ -0,0 +1,22 @@ +precision highp float; +precision highp sampler2D; + +// input texture (previous level used to evaluate the new level) +uniform sampler2D tPreviousLevel; + +// 1/size of the previous level texture. +uniform float uSize; +uniform float uTexSize; + +void main(void) { + float k = 0.5 * uSize; + vec2 position = floor((gl_FragCoord.xy / uTexSize) / uSize) * uSize; + float a = texture2D(tPreviousLevel, position).r; + float b = texture2D(tPreviousLevel, position + vec2(k, 0.)).r; + float c = texture2D(tPreviousLevel, position + vec2(0., k)).r; + float d = texture2D(tPreviousLevel, position + vec2(k, k)).r; + gl_FragColor.a = a; + gl_FragColor.b = a + b; + gl_FragColor.g = gl_FragColor.b + c; + gl_FragColor.r = gl_FragColor.g + d; +} \ No newline at end of file diff --git a/src/mol-gl/shader/histogram-pyramid/sum.frag b/src/mol-gl/shader/histogram-pyramid/sum.frag new file mode 100644 index 0000000000000000000000000000000000000000..4f56dd7c1264bd4a2bcc56b949dc711aeecf680f --- /dev/null +++ b/src/mol-gl/shader/histogram-pyramid/sum.frag @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +precision highp float; +precision highp sampler2D; + +uniform sampler2D tTexture; + +#pragma glslify: encodeFloatRGB = require(../utils/encode-float-rgb.glsl) + +void main(void) { + gl_FragColor = vec4(encodeFloatRGB(texture2D(tTexture, vec2(0.5)).r), 1.0); +} \ No newline at end of file diff --git a/src/mol-gl/shader/lines.vert b/src/mol-gl/shader/lines.vert index 91f0ca8d676f43a197f7acd087a88026a5fb46e3..e97b8e20ee41674d326c46cb01091bd9ef808bea 100644 --- a/src/mol-gl/shader/lines.vert +++ b/src/mol-gl/shader/lines.vert @@ -36,6 +36,7 @@ void trimSegment(const in vec4 start, inout vec4 end) { } void main(){ + #pragma glslify: import('./chunks/assign-group.glsl') #pragma glslify: import('./chunks/assign-color-varying.glsl') #pragma glslify: import('./chunks/assign-marker-varying.glsl') #pragma glslify: import('./chunks/assign-size.glsl') diff --git a/src/mol-gl/shader/marching-cubes/active-voxels.frag b/src/mol-gl/shader/marching-cubes/active-voxels.frag new file mode 100644 index 0000000000000000000000000000000000000000..1d3e94a8f9366c8cf5377fb43f56d5e7f33967af --- /dev/null +++ b/src/mol-gl/shader/marching-cubes/active-voxels.frag @@ -0,0 +1,75 @@ +precision highp float; +precision highp sampler2D; + +uniform sampler2D tTriCount; +uniform sampler2D tVolumeData; + +uniform float uIsoValue; +uniform vec3 uGridDim; +uniform vec3 uGridTexDim; +uniform vec2 uScale; + +// cube corners +const vec3 c0 = vec3(0., 0., 0.); +const vec3 c1 = vec3(1., 0., 0.); +const vec3 c2 = vec3(1., 1., 0.); +const vec3 c3 = vec3(0., 1., 0.); +const vec3 c4 = vec3(0., 0., 1.); +const vec3 c5 = vec3(1., 0., 1.); +const vec3 c6 = vec3(1., 1., 1.); +const vec3 c7 = vec3(0., 1., 1.); + +vec3 index3dFrom2d(vec2 coord) { + vec2 gridTexPos = coord * uGridTexDim.xy; + vec2 columnRow = floor(gridTexPos / uGridDim.xy); + vec2 posXY = gridTexPos - columnRow * uGridDim.xy; + float posZ = columnRow.y * floor(uGridTexDim.x / uGridDim.x) + columnRow.x; + vec3 posXYZ = vec3(posXY, posZ) / uGridDim; + return posXYZ; +} + +float intDiv(float a, float b) { return float(int(a) / int(b)); } +float intMod(float a, float b) { return a - b * float(int(a) / int(b)); } + +vec4 texture3dFrom2dNearest(sampler2D tex, vec3 pos, vec3 gridDim, vec2 texDim) { + float zSlice = floor(pos.z * gridDim.z + 0.5); // round to nearest z-slice + float column = intMod(zSlice * gridDim.x, texDim.x) / gridDim.x; + float row = floor(intDiv(zSlice * gridDim.x, texDim.x)); + vec2 coord = (vec2(column * gridDim.x, row * gridDim.y) + (pos.xy * gridDim.xy)) / (texDim / uScale); + // return texture2D(tex, coord + 0.5 / texDim); + return texture2D(tex, coord); +} + +vec4 voxel(vec3 pos) { + return texture3dFrom2dNearest(tVolumeData, pos, uGridDim, uGridTexDim.xy); +} + +void main(void) { + vec2 uv = gl_FragCoord.xy / uGridTexDim.xy; + vec3 posXYZ = index3dFrom2d(uv); + + // get MC case as the sum of corners that are below the given iso level + float c = step(voxel(posXYZ).a, uIsoValue) + + 2. * step(voxel(posXYZ + c1 / uGridDim).a, uIsoValue) + + 4. * step(voxel(posXYZ + c2 / uGridDim).a, uIsoValue) + + 8. * step(voxel(posXYZ + c3 / uGridDim).a, uIsoValue) + + 16. * step(voxel(posXYZ + c4 / uGridDim).a, uIsoValue) + + 32. * step(voxel(posXYZ + c5 / uGridDim).a, uIsoValue) + + 64. * step(voxel(posXYZ + c6 / uGridDim).a, uIsoValue) + + 128. * step(voxel(posXYZ + c7 / uGridDim).a, uIsoValue); + c *= step(c, 254.); + + // get total triangles to generate for calculated MC case from triCount texture + float totalTrianglesToGenerate = texture2D(tTriCount, vec2(intMod(c, 16.), floor(c / 16.)) / 16.).a; + gl_FragColor = vec4(vec3(floor(totalTrianglesToGenerate * 255.0 + 0.5) * 3.0), c); + + // gl_FragColor = vec4(255.0, 0.0, 0.0, voxel(posXYZ + c4 / uGridDim).a * 255.0); + // gl_FragColor = vec4(255.0, 0.0, 0.0, voxel(posXYZ).a * 255.0); + + // vec2 uv = vCoordinate; + // uv = gl_FragCoord.xy / uGridTexDim.xy; + + // if (uv.y < 0.91) discard; + // gl_FragColor = vec4(vCoordinate * 255.0, 0.0, 255.0); + // gl_FragColor = vec4(250.0, 0.0, 0.0, 255.0); +} \ No newline at end of file diff --git a/src/mol-gl/shader/marching-cubes/isosurface.frag b/src/mol-gl/shader/marching-cubes/isosurface.frag new file mode 100644 index 0000000000000000000000000000000000000000..2499bbf7f43221dd94d659646a1050849ab2f1a4 --- /dev/null +++ b/src/mol-gl/shader/marching-cubes/isosurface.frag @@ -0,0 +1,209 @@ +precision highp float; +precision highp sampler2D; + +uniform sampler2D tActiveVoxelsPyramid; +uniform sampler2D tActiveVoxelsBase; +uniform sampler2D tVolumeData; +uniform sampler2D tTriIndices; + +uniform float uIsoValue; +uniform float uLevels; +uniform float uSize; +uniform float uCount; + +uniform vec3 uGridDim; +uniform vec3 uGridTexDim; +uniform mat4 uGridTransform; + +// scale to volume data coord +uniform vec2 uScale; + +// varying vec2 vCoordinate; + +#pragma glslify: import('../chunks/common.glsl') +#pragma glslify: decodeFloatRGB = require(../utils/decode-float-rgb.glsl) + +// cube corners +const vec3 c0 = vec3(0., 0., 0.); +const vec3 c1 = vec3(1., 0., 0.); +const vec3 c2 = vec3(1., 1., 0.); +const vec3 c3 = vec3(0., 1., 0.); +const vec3 c4 = vec3(0., 0., 1.); +const vec3 c5 = vec3(1., 0., 1.); +const vec3 c6 = vec3(1., 1., 1.); +const vec3 c7 = vec3(0., 1., 1.); + +const float EPS = 0.00001; + +vec3 index3dFrom2d(vec2 coord) { + vec2 gridTexPos = coord * uGridTexDim.xy; + vec2 columnRow = floor(gridTexPos / uGridDim.xy); + vec2 posXY = gridTexPos - columnRow * uGridDim.xy; + float posZ = columnRow.y * floor(uGridTexDim.x / uGridDim.x) + columnRow.x; + vec3 posXYZ = vec3(posXY, posZ); + return posXYZ; +} + +vec4 texture3dFrom2dNearest(sampler2D tex, vec3 pos, vec3 gridDim, vec2 texDim) { + float zSlice = floor(pos.z * gridDim.z + 0.5); // round to nearest z-slice + float column = intMod(zSlice * gridDim.x, texDim.x) / gridDim.x; + float row = floor(intDiv(zSlice * gridDim.x, texDim.x)); + vec2 coord = (vec2(column * gridDim.x, row * gridDim.y) + (pos.xy * gridDim.xy)) / (texDim / uScale); + return texture2D(tex, coord + 0.5 / (texDim / uScale)); + // return texture2D(tex, coord); +} + +vec4 voxel(vec3 pos) { + return texture3dFrom2dNearest(tVolumeData, pos, uGridDim, uGridTexDim.xy); +} + +void main(void) { + // get 1D index + float vI = dot(floor(uSize * (gl_FragCoord.xy / uSize)), vec2(1.0, uSize)); + + // ignore 1D indices outside of the grid + if(vI >= uCount) discard; + + float offset = uSize - 2.; + float k = 1. / uSize; + + vec2 relativePosition = k * vec2(offset, 0.); + vec4 partialSums = texture2D(tActiveVoxelsPyramid, relativePosition); + float start = 0.; + vec4 starts = vec4(0.); + vec4 ends = vec4(0.); + float diff = 2.; + vec4 m = vec4(0.); + vec2 position = vec2(0.); + vec4 vI4 = vec4(vI); + + // traverse the different levels of the pyramid + for(int i = 1; i < 12; i++) { + if(float(i) >= uLevels) break; + + offset -= diff; + diff *= 2.; + relativePosition = position + k * vec2(offset, 0.); + + ends = partialSums.wzyx + vec4(start); + starts = vec4(start, ends.xyz); + m = vec4(greaterThanEqual(vI4, starts)) * vec4(lessThan(vI4, ends)); + relativePosition += m.y * vec2(k, 0.) + m.z * vec2(0., k) + m.w * vec2(k, k); + + start = dot(m, starts); + position = 2. * (relativePosition - k * vec2(offset, 0.)); + partialSums = texture2D(tActiveVoxelsPyramid, relativePosition); + } + + ends = partialSums.wzyx + vec4(start); + starts = vec4(start, ends.xyz); + m = vec4(greaterThanEqual(vI4, starts)) * vec4(lessThan(vI4, ends)); + position += m.y * vec2(k, 0.) + m.z * vec2(0., k) + m.w * vec2(k, k); + + vec2 coord2d = position / uScale; + vec3 coord3d = floor(index3dFrom2d(coord2d) + 0.5); + + float edgeIndex = floor(texture2D(tActiveVoxelsBase, position).a + 0.5); + + // current vertex for the up to 15 MC cases + float currentVertex = vI - dot(m, starts); + + // get index into triIndices table + float mcIndex = 16. * edgeIndex + currentVertex; + vec4 mcData = texture2D(tTriIndices, vec2(intMod(mcIndex, 64.), floor(mcIndex / 64.)) / 64.); + + // bit mask to avoid conditionals (see comment below) for getting MC case corner + vec4 m0 = vec4(floor(mcData.a * 255.0 + 0.5)); + + // get edge value masks + vec4 m1 = vec4(equal(m0, vec4(0., 1., 2., 3.))); + vec4 m2 = vec4(equal(m0, vec4(4., 5., 6., 7.))); + vec4 m3 = vec4(equal(m0, vec4(8., 9., 10., 11.))); + + // apply bit masks + vec3 b0 = coord3d + + m1.y * c1 + + m1.z * c2 + + m1.w * c3 + + m2.x * c4 + + m2.y * c5 + + m2.z * c6 + + m2.w * c7 + + m3.y * c1 + + m3.z * c2 + + m3.w * c3; + vec3 b1 = coord3d + + m1.x * c1 + + m1.y * c2 + + m1.z * c3 + + m2.x * c5 + + m2.y * c6 + + m2.z * c7 + + m2.w * c4 + + m3.x * c4 + + m3.y * c5 + + m3.z * c6 + + m3.w * c7; + + // the conditionals that are avoided by above bitmasks + // vec3 b0 = coord3d; + // vec3 b1 = coord3d; + // if (mcIndex == 0.0) { + // b0 += c0; b1 += c1; + // } else if (mcIndex == 1.0) { + // b0 += c1; b1 += c2; + // } else if (mcIndex == 2.0) { + // b0 += c2; b1 += c3; + // } else if (mcIndex == 3.0) { + // b0 += c3; b1 += c0; + // } else if (mcIndex == 4.0) { + // b0 += c4; b1 += c5; + // } else if (mcIndex == 5.0) { + // b0 += c5; b1 += c6; + // } else if (mcIndex == 6.0) { + // b0 += c6; b1 += c7; + // } else if (mcIndex == 7.0) { + // b0 += c7; b1 += c4; + // } else if (mcIndex == 8.0) { + // b0 += c0; b1 += c4; + // } else if (mcIndex == 9.0) { + // b0 += c1; b1 += c5; + // } else if (mcIndex == 10.0) { + // b0 += c2; b1 += c6; + // } else if (mcIndex == 11.0) { + // b0 += c3; b1 += c7; + // } + // b0 = floor(b0 + 0.5); + // b1 = floor(b1 + 0.5); + + vec4 d0 = voxel(b0 / uGridDim); + vec4 d1 = voxel(b1 / uGridDim); + + float v0 = d0.a; + float v1 = d1.a; + + float t = (uIsoValue - v0) / (v0 - v1); + // t = -0.5; + gl_FragData[0].xyz = (uGridTransform * vec4(b0 + t * (b0 - b1), 1.0)).xyz; + gl_FragData[0].w = decodeFloatRGB(d0.rgb); // group id + + // normals from gradients + vec3 n0 = -normalize(vec3( + voxel((b0 - c1) / uGridDim).a - voxel((b0 + c1) / uGridDim).a, + voxel((b0 - c3) / uGridDim).a - voxel((b0 + c3) / uGridDim).a, + voxel((b0 - c4) / uGridDim).a - voxel((b0 + c4) / uGridDim).a + )); + vec3 n1 = -normalize(vec3( + voxel((b1 - c1) / uGridDim).a - voxel((b1 + c1) / uGridDim).a, + voxel((b1 - c3) / uGridDim).a - voxel((b1 + c3) / uGridDim).a, + voxel((b1 - c4) / uGridDim).a - voxel((b1 + c4) / uGridDim).a + )); + gl_FragData[1].xyz = -vec3( + n0.x + t * (n0.x - n1.x), + n0.y + t * (n0.y - n1.y), + n0.z + t * (n0.z - n1.z) + ); + + mat3 normalMatrix = transpose(inverse(mat3(uGridTransform))); + gl_FragData[1].xyz = normalMatrix * gl_FragData[1].xyz; +} \ No newline at end of file diff --git a/src/mol-gl/shader/mesh.frag b/src/mol-gl/shader/mesh.frag index 93858961510496217a7121ee8182aee085218313..5198d5e30ef434aacb170ab68b0dbff3e643425d 100644 --- a/src/mol-gl/shader/mesh.frag +++ b/src/mol-gl/shader/mesh.frag @@ -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> */ @@ -9,24 +9,8 @@ precision highp int; #pragma glslify: import('./chunks/common-frag-params.glsl') #pragma glslify: import('./chunks/color-frag-params.glsl') - -// uniform vec3 uLightPosition; -uniform vec3 uLightColor; -uniform vec3 uLightAmbient; -uniform mat4 uView; - -#if !defined(dFlatShaded) || !defined(enabledStandardDerivatives) - varying vec3 vNormal; -#endif - -#pragma glslify: attenuation = require(./utils/attenuation.glsl) -#pragma glslify: calculateSpecular = require(./utils/phong-specular.glsl) -#pragma glslify: calculateDiffuse = require(./utils/oren-nayar-diffuse.glsl) - -const float specularScale = 0.15; -const float shininess = 200.0; -const float roughness = 100.0; -const float albedo = 0.95; +#pragma glslify: import('./chunks/light-frag-params.glsl') +#pragma glslify: import('./chunks/normal-frag-params.glsl') void main() { // material color @@ -37,40 +21,8 @@ void main() { discard; // ignore so the element below can be picked gl_FragColor = material; #else - // determine surface to light direction - // vec4 viewLightPosition = view * vec4(lightPosition, 1.0); - // vec3 lightVector = viewLightPosition.xyz - vViewPosition; - vec3 lightVector = vViewPosition; - - vec3 L = normalize(lightVector); // light direction - vec3 V = normalize(vViewPosition); // eye direction - - // surface normal - #if defined(dFlatShaded) && defined(enabledStandardDerivatives) - vec3 fdx = dFdx(vViewPosition); - vec3 fdy = dFdy(vViewPosition); - vec3 N = -normalize(cross(fdx, fdy)); - #else - vec3 N = -normalize(vNormal); - #ifdef dDoubleSided - N = N * (float(gl_FrontFacing) * 2.0 - 1.0); - #endif - #endif - - // compute our diffuse & specular terms - float specular = calculateSpecular(L, V, N, shininess) * specularScale; - vec3 diffuse = uLightColor * calculateDiffuse(L, V, N, roughness, albedo); - vec3 ambient = uLightAmbient; - - // add the lighting - vec3 finalColor = material.rgb * (diffuse + ambient) + specular; - - // gl_FragColor.rgb = N; - // gl_FragColor.a = 1.0; - // gl_FragColor.rgb = vec3(1.0, 0.0, 0.0); - gl_FragColor.rgb = finalColor; - gl_FragColor.a = material.a; - + #pragma glslify: import('./chunks/assign-normal.glsl') + #pragma glslify: import('./chunks/apply-light-color.glsl') #pragma glslify: import('./chunks/apply-marker-color.glsl') #pragma glslify: import('./chunks/apply-fog.glsl') #endif diff --git a/src/mol-gl/shader/mesh.vert b/src/mol-gl/shader/mesh.vert index 99cf417396901d1ec6a3d61b1b5978550a8586d9..d49bba6012ca5deb366c5a648a4b8906bce4221b 100644 --- a/src/mol-gl/shader/mesh.vert +++ b/src/mol-gl/shader/mesh.vert @@ -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> */ @@ -10,24 +10,39 @@ precision highp int; #pragma glslify: import('./chunks/common-vert-params.glsl') #pragma glslify: import('./chunks/color-vert-params.glsl') -attribute vec3 aPosition; +#ifdef dGeoTexture + uniform vec2 uGeoTexDim; + uniform sampler2D tPositionGroup; +#else + attribute vec3 aPosition; +#endif attribute mat4 aTransform; attribute float aInstance; attribute float aGroup; #ifndef dFlatShaded - attribute vec3 aNormal; + #ifdef dGeoTexture + uniform sampler2D tNormal; + #else + attribute vec3 aNormal; + #endif varying vec3 vNormal; #endif void main(){ + #pragma glslify: import('./chunks/assign-group.glsl') #pragma glslify: import('./chunks/assign-color-varying.glsl') #pragma glslify: import('./chunks/assign-marker-varying.glsl') #pragma glslify: import('./chunks/assign-position.glsl') #ifndef dFlatShaded + #ifdef dGeoTexture + vec3 normal = readFromTexture(tNormal, aGroup, uGeoTexDim).xyz; + #else + vec3 normal = aNormal; + #endif mat3 normalMatrix = transpose(inverse(mat3(modelView))); - vec3 transformedNormal = normalize(normalMatrix * normalize(aNormal)); + vec3 transformedNormal = normalize(normalMatrix * normalize(normal)); #if defined(dFlipSided) && !defined(dDoubleSided) // TODO checking dDoubleSided should not be required, ASR transformedNormal = -transformedNormal; #endif diff --git a/src/mol-gl/shader/points.vert b/src/mol-gl/shader/points.vert index 27fe6a387b81597f33ab360098a3217068bff2ec..7a87072bff2dd8831f62fd8a708cd26dffff6681 100644 --- a/src/mol-gl/shader/points.vert +++ b/src/mol-gl/shader/points.vert @@ -20,6 +20,7 @@ attribute float aInstance; attribute float aGroup; void main(){ + #pragma glslify: import('./chunks/assign-group.glsl') #pragma glslify: import('./chunks/assign-color-varying.glsl') #pragma glslify: import('./chunks/assign-marker-varying.glsl') #pragma glslify: import('./chunks/assign-position.glsl') diff --git a/src/mol-gl/shader/quad.vert b/src/mol-gl/shader/quad.vert new file mode 100644 index 0000000000000000000000000000000000000000..4c3b888e5b2552c9a5709f843d64c8fef66f05ce --- /dev/null +++ b/src/mol-gl/shader/quad.vert @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +precision highp float; + +attribute vec2 aPosition; +uniform vec2 uQuadScale; + +void main(void) { + vec2 position = aPosition * uQuadScale - vec2(1.0, 1.0) + uQuadScale; + gl_Position = vec4(position, 0.0, 1.0); +} \ No newline at end of file diff --git a/src/mol-gl/shader/spheres.frag b/src/mol-gl/shader/spheres.frag index fc215b5ad16a0e2969feb038fefa47ad7963e507..0f0488b4f66fee13a14590d01dbaa28ce5bc7d6c 100644 --- a/src/mol-gl/shader/spheres.frag +++ b/src/mol-gl/shader/spheres.frag @@ -9,11 +9,7 @@ precision highp int; #pragma glslify: import('./chunks/common-frag-params.glsl') #pragma glslify: import('./chunks/color-frag-params.glsl') - -// uniform vec3 uLightPosition; -uniform vec3 uLightColor; -uniform vec3 uLightAmbient; -uniform mat4 uView; +#pragma glslify: import('./chunks/light-frag-params.glsl') uniform mat4 uProjection; // uniform vec3 interiorColor; @@ -30,27 +26,18 @@ varying float vRadiusSq; varying vec3 vPoint; varying vec3 vPointViewPosition; -#pragma glslify: attenuation = require(./utils/attenuation.glsl) -#pragma glslify: calculateSpecular = require(./utils/phong-specular.glsl) -#pragma glslify: calculateDiffuse = require(./utils/oren-nayar-diffuse.glsl) - -const float specularScale = 0.15; -const float shininess = 200.0; -const float roughness = 100.0; -const float albedo = 0.95; - bool flag2 = false; bool interior = false; vec3 cameraPos; vec3 cameraNormal; // Calculate depth based on the given camera position. -float calcDepth(in vec3 cameraPos){ +float calcDepth(const in vec3 cameraPos){ vec2 clipZW = cameraPos.z * uProjection[2].zw + uProjection[3].zw; return 0.5 + 0.5 * clipZW.x / clipZW.y; } -float calcClip(in vec3 cameraPos) { +float calcClip(const in vec3 cameraPos) { return dot(vec4(cameraPos, 1.0), vec4(0.0, 0.0, 1.0, clipNear - 0.5)); } @@ -143,36 +130,9 @@ void main(void){ discard; // ignore so the element below can be picked gl_FragColor = material; #else - - vec3 vNormal = cameraNormal; + vec3 normal = cameraNormal; vec3 vViewPosition = -cameraPos; - - // determine surface to light direction - // vec4 viewLightPosition = view * vec4(lightPosition, 1.0); - // vec3 lightVector = viewLightPosition.xyz - vViewPosition; - vec3 lightVector = vViewPosition; - - vec3 L = normalize(lightVector); // light direction - vec3 V = normalize(vViewPosition); // eye direction - - vec3 N = normalize(vNormal); - #ifdef dDoubleSided - N = N * (float(gl_FrontFacing) * 2.0 - 1.0); - #endif - - // compute our diffuse & specular terms - float specular = calculateSpecular(L, V, N, shininess) * specularScale; - vec3 diffuse = uLightColor * calculateDiffuse(L, V, N, roughness, albedo); - vec3 ambient = uLightAmbient; - - // add the lighting - vec3 finalColor = material.rgb * (diffuse + ambient) + specular; - - // gl_FragColor.rgb = N; - // gl_FragColor.a = 1.0; - // gl_FragColor.rgb = vec3(1.0, 0.0, 0.0); - gl_FragColor.rgb = finalColor; - gl_FragColor.a = material.a; + #pragma glslify: import('./chunks/apply-light-color.glsl') if(interior){ #ifdef USE_INTERIOR_COLOR diff --git a/src/mol-gl/shader/spheres.vert b/src/mol-gl/shader/spheres.vert index 6203d8a3b2b8d24ca6229eb771ea15570a5e365d..65656354334a22dcaa33ba90cb0cb511209a2c22 100644 --- a/src/mol-gl/shader/spheres.vert +++ b/src/mol-gl/shader/spheres.vert @@ -86,6 +86,7 @@ void quadraticProjection(const in float radius, const in vec3 position){ void main(void){ + #pragma glslify: import('./chunks/assign-group.glsl') #pragma glslify: import('./chunks/assign-color-varying.glsl') #pragma glslify: import('./chunks/assign-marker-varying.glsl') #pragma glslify: import('./chunks/assign-size.glsl') diff --git a/src/mol-gl/shader/text.vert b/src/mol-gl/shader/text.vert index 4bed4c542e26fa3ea27d83f08a5db597e87c5e6d..3e5309bbf83aeacfd4e9a6927be4c5e8b6b26f4f 100644 --- a/src/mol-gl/shader/text.vert +++ b/src/mol-gl/shader/text.vert @@ -34,6 +34,7 @@ varying vec2 vTexCoord; #pragma glslify: matrixScale = require(./utils/matrix-scale.glsl) void main(void){ + #pragma glslify: import('./chunks/assign-group.glsl') #pragma glslify: import('./chunks/assign-color-varying.glsl') #pragma glslify: import('./chunks/assign-marker-varying.glsl') #pragma glslify: import('./chunks/assign-size.glsl') diff --git a/src/mol-gl/shader/utils/attenuation.glsl b/src/mol-gl/shader/utils/attenuation.glsl deleted file mode 100644 index 833423b85b9fcfc3dfd334b1714bfccfec9d78ee..0000000000000000000000000000000000000000 --- a/src/mol-gl/shader/utils/attenuation.glsl +++ /dev/null @@ -1,14 +0,0 @@ -// by Tom Madams -// Simple: -// https://imdoingitwrong.wordpress.com/2011/01/31/light-attenuation/ -// -// Improved -// https://imdoingitwrong.wordpress.com/2011/02/10/improved-light-attenuation/ -float attenuation(const in float r, const in float f, const in float d) { - float denom = d / r + 1.0; - float attenuation = 1.0 / (denom*denom); - float t = (attenuation - f) / (1.0 - f); - return max(t, 0.0); -} - -#pragma glslify: export(attenuation) \ No newline at end of file diff --git a/src/mol-gl/shader/utils/oren-nayar-diffuse.glsl b/src/mol-gl/shader/utils/oren-nayar-diffuse.glsl deleted file mode 100644 index 59a3078b831a51003f990693a0da9ff0ed06a743..0000000000000000000000000000000000000000 --- a/src/mol-gl/shader/utils/oren-nayar-diffuse.glsl +++ /dev/null @@ -1,21 +0,0 @@ -// (c) 2014 Mikola Lysenko. MIT License -// https://github.com/glslify/glsl-diffuse-oren-nayar - -#define PI 3.14159265 - -float orenNayarDiffuse(const in vec3 lightDirection, const in vec3 viewDirection, const in vec3 surfaceNormal, const in float roughness, const in float albedo) { - float LdotV = dot(lightDirection, viewDirection); - float NdotL = dot(lightDirection, surfaceNormal); - float NdotV = dot(surfaceNormal, viewDirection); - - float s = LdotV - NdotL * NdotV; - float t = mix(1.0, max(NdotL, NdotV), step(0.0, s)); - - float sigma2 = roughness * roughness; - float A = 1.0 + sigma2 * (albedo / (sigma2 + 0.13) + 0.5 / (sigma2 + 0.33)); - float B = 0.45 * sigma2 / (sigma2 + 0.09); - - return albedo * max(0.0, NdotL) * (A + B * s / t) / PI; -} - -#pragma glslify: export(orenNayarDiffuse) \ No newline at end of file diff --git a/src/mol-gl/shader/utils/phong-specular.glsl b/src/mol-gl/shader/utils/phong-specular.glsl deleted file mode 100644 index 6d42305acf8e23d6204d7b0a13f91a69d15f9cf3..0000000000000000000000000000000000000000 --- a/src/mol-gl/shader/utils/phong-specular.glsl +++ /dev/null @@ -1,10 +0,0 @@ -// (c) 2014 Mikola Lysenko. MIT License -// https://github.com/glslify/glsl-specular-phong - -float phongSpecular(const in vec3 lightDirection, const in vec3 viewDirection, const in vec3 surfaceNormal, const in float shininess) { - //Calculate Phong power - vec3 R = -reflect(lightDirection, surfaceNormal); - return pow(max(0.0, dot(viewDirection, R)), shininess); -} - -#pragma glslify: export(phongSpecular) \ No newline at end of file diff --git a/src/mol-gl/webgl/buffer.ts b/src/mol-gl/webgl/buffer.ts index 80cd13401bfe84604f844b89b75d5c42d816812d..eb2c4f43553aea7e69c117c5d3e2169b800d01c2 100644 --- a/src/mol-gl/webgl/buffer.ts +++ b/src/mol-gl/webgl/buffer.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> */ @@ -9,12 +9,13 @@ import { ValueCell } from 'mol-util'; import { RenderableSchema } from '../renderable/schema'; import { idFactory } from 'mol-util/id-factory'; import { ValueOf } from 'mol-util/type-helpers'; +import { GLRenderingContext } from './compat'; const getNextBufferId = idFactory() export type UsageHint = 'static' | 'dynamic' | 'stream' export type DataType = 'uint8' | 'int8' | 'uint16' | 'int16' | 'uint32' | 'int32' | 'float32' -export type BufferType = 'attribute' | 'elements' +export type BufferType = 'attribute' | 'elements' | 'uniform' export type DataTypeArrayType = { 'uint8': Uint8Array @@ -28,8 +29,6 @@ export type DataTypeArrayType = { export type ArrayType = ValueOf<DataTypeArrayType> export type ArrayKind = keyof DataTypeArrayType -export type BufferItemSize = 1 | 2 | 3 | 4 | 16 - export function getUsageHint(ctx: WebGLContext, usageHint: UsageHint) { const { gl } = ctx switch (usageHint) { @@ -78,6 +77,7 @@ export function getBufferType(ctx: WebGLContext, bufferType: BufferType) { switch (bufferType) { case 'attribute': return gl.ARRAY_BUFFER case 'elements': return gl.ELEMENT_ARRAY_BUFFER + case 'uniform': return (gl as WebGL2RenderingContext).UNIFORM_BUFFER } } @@ -90,8 +90,6 @@ export interface Buffer { readonly _dataType: number readonly _bpe: number - readonly itemSize: number - readonly itemCount: number readonly length: number updateData: (array: ArrayType) => void @@ -99,8 +97,8 @@ export interface Buffer { destroy: () => void } -export function createBuffer(ctx: WebGLContext, array: ArrayType, itemSize: BufferItemSize, usageHint: UsageHint, bufferType: BufferType): Buffer { - const { gl } = ctx +export function createBuffer(ctx: WebGLContext, array: ArrayType, usageHint: UsageHint, bufferType: BufferType): Buffer { + const { gl, stats } = ctx const _buffer = gl.createBuffer() if (_buffer === null) { throw new Error('Could not create WebGL buffer') @@ -111,16 +109,15 @@ export function createBuffer(ctx: WebGLContext, array: ArrayType, itemSize: Buff const _dataType = dataTypeFromArray(ctx, array) const _bpe = array.BYTES_PER_ELEMENT const _length = array.length - const _itemCount = Math.floor(_length / itemSize) function updateData(array: ArrayType) { gl.bindBuffer(_bufferType, _buffer); - (gl as WebGLRenderingContext).bufferData(_bufferType, array, _usageHint) // TODO remove cast when webgl2 types are fixed + gl.bufferData(_bufferType, array, _usageHint) } updateData(array) let destroyed = false - ctx.bufferCount += 1 + stats.bufferCount += 1 return { id: getNextBufferId(), @@ -131,40 +128,66 @@ export function createBuffer(ctx: WebGLContext, array: ArrayType, itemSize: Buff _dataType, _bpe, - get itemSize () { return itemSize }, - get itemCount () { return _itemCount }, - get length () { return _length }, + length: _length, updateData, updateSubData: (array: ArrayType, offset: number, count: number) => { gl.bindBuffer(_bufferType, _buffer); - (gl as WebGLRenderingContext).bufferSubData(_bufferType, offset * _bpe, array.subarray(offset, offset + count)) // TODO remove cast when webgl2 types are fixed + gl.bufferSubData(_bufferType, offset * _bpe, array.subarray(offset, offset + count)) }, destroy: () => { if (destroyed) return gl.deleteBuffer(_buffer) destroyed = true - ctx.bufferCount -= 1 + stats.bufferCount -= 1 } } } +// + +export type AttributeItemSize = 1 | 2 | 3 | 4 | 16 +export type AttributeKind = 'float32' | 'int32' + +export function getAttribType(gl: GLRenderingContext, kind: AttributeKind, itemSize: AttributeItemSize) { + switch (kind) { + case 'int32': + switch (itemSize) { + case 1: return gl.INT + case 2: return gl.INT_VEC2 + case 3: return gl.INT_VEC3 + case 4: return gl.INT_VEC4 + } + break + case 'float32': + switch (itemSize) { + case 1: return gl.FLOAT + case 2: return gl.FLOAT_VEC2 + case 3: return gl.FLOAT_VEC3 + case 4: return gl.FLOAT_VEC4 + case 16: return gl.FLOAT_MAT4 + } + break + } + throw new Error(`unknown attribute type for kind '${kind}' and itemSize '${itemSize}'`) +} + export type AttributeDefs = { - [k: string]: { kind: ArrayKind, itemSize: BufferItemSize, divisor: number } + [k: string]: { kind: AttributeKind, itemSize: AttributeItemSize, divisor: number } } export type AttributeValues = { [k: string]: ValueCell<ArrayType> } -export type AttributeBuffers = { [k: string]: AttributeBuffer } +export type AttributeBuffers = [string, AttributeBuffer][] export interface AttributeBuffer extends Buffer { bind: (location: number) => void } -export function createAttributeBuffer<T extends ArrayType, S extends BufferItemSize>(ctx: WebGLContext, array: ArrayType, itemSize: S, divisor: number, usageHint: UsageHint = 'dynamic'): AttributeBuffer { +export function createAttributeBuffer<T extends ArrayType, S extends AttributeItemSize>(ctx: WebGLContext, array: T, itemSize: S, divisor: number, usageHint: UsageHint = 'dynamic'): AttributeBuffer { const { gl } = ctx const { instancedArrays } = ctx.extensions - const buffer = createBuffer(ctx, array, itemSize, usageHint, 'attribute') + const buffer = createBuffer(ctx, array, usageHint, 'attribute') const { _buffer, _bufferType, _dataType, _bpe } = buffer return { @@ -187,16 +210,18 @@ export function createAttributeBuffer<T extends ArrayType, S extends BufferItemS } export function createAttributeBuffers(ctx: WebGLContext, schema: RenderableSchema, values: AttributeValues) { - const buffers: AttributeBuffers = {} + const buffers: AttributeBuffers = [] Object.keys(schema).forEach(k => { const spec = schema[k] if (spec.type === 'attribute') { - buffers[k] = createAttributeBuffer(ctx, values[k].ref.value, spec.itemSize, spec.divisor) + buffers[buffers.length] = [k, createAttributeBuffer(ctx, values[k].ref.value, spec.itemSize, spec.divisor)] } }) - return buffers as AttributeBuffers + return buffers } +// + export type ElementsType = Uint16Array | Uint32Array export type ElementsKind = 'uint16' | 'uint32' @@ -206,7 +231,7 @@ export interface ElementsBuffer extends Buffer { export function createElementsBuffer(ctx: WebGLContext, array: ElementsType, usageHint: UsageHint = 'static'): ElementsBuffer { const { gl } = ctx - const buffer = createBuffer(ctx, array, 1, usageHint, 'elements') + const buffer = createBuffer(ctx, array, usageHint, 'elements') const { _buffer } = buffer return { diff --git a/src/mol-gl/webgl/compat.ts b/src/mol-gl/webgl/compat.ts index 7d763614ea33e22cf2c1c5ec32b8d5b85fb64ce2..9021af77e7f829228e8755fbc84d73463651c949 100644 --- a/src/mol-gl/webgl/compat.ts +++ b/src/mol-gl/webgl/compat.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> */ @@ -50,9 +50,7 @@ export function getStandardDerivatives(gl: GLRenderingContext): COMPAT_standard_ return { FRAGMENT_SHADER_DERIVATIVE_HINT: gl.FRAGMENT_SHADER_DERIVATIVE_HINT } } else { const ext = gl.getExtension('OES_standard_derivatives') - if (ext === null) { - throw new Error('Could not get "OES_standard_derivatives" extension') - } + if (ext === null) return null return { FRAGMENT_SHADER_DERIVATIVE_HINT: ext.FRAGMENT_SHADER_DERIVATIVE_HINT_OES } } } @@ -79,7 +77,7 @@ export function getVertexArrayObject(gl: GLRenderingContext): COMPAT_vertex_arra bindVertexArray: gl.bindVertexArray.bind(gl), createVertexArray: gl.createVertexArray.bind(gl), deleteVertexArray: gl.deleteVertexArray.bind(gl), - isVertexArray: gl.isVertexArray.bind(gl) as (value: any) => value is WebGLVertexArrayObject // TODO change when webgl2 types are fixed + isVertexArray: gl.isVertexArray.bind(gl) } } else { const ext = gl.getExtension('OES_vertex_array_object') @@ -128,4 +126,98 @@ export interface COMPAT_frag_depth { export function getFragDepth(gl: GLRenderingContext): COMPAT_frag_depth | null { return isWebGL2(gl) ? {} : gl.getExtension('EXT_frag_depth') +} + +export interface COMPAT_color_buffer_float { + readonly RGBA32F: number; +} + +export function getColorBufferFloat(gl: GLRenderingContext): COMPAT_color_buffer_float | null { + if (isWebGL2(gl)) { + if (gl.getExtension('EXT_color_buffer_float') === null) return null + return { RGBA32F: gl.RGBA32F } + } else { + const ext = gl.getExtension('WEBGL_color_buffer_float') + if (ext === null) return null + return { RGBA32F: ext.RGBA32F_EXT } + } +} + +export interface COMPAT_draw_buffers { + drawBuffers(buffers: number[]): void; + readonly COLOR_ATTACHMENT0: number; + readonly COLOR_ATTACHMENT1: number; + readonly COLOR_ATTACHMENT2: number; + readonly COLOR_ATTACHMENT3: number; + readonly COLOR_ATTACHMENT4: number; + readonly COLOR_ATTACHMENT5: number; + readonly COLOR_ATTACHMENT6: number; + readonly COLOR_ATTACHMENT7: number; + readonly DRAW_BUFFER0: number; + readonly DRAW_BUFFER1: number; + readonly DRAW_BUFFER2: number; + readonly DRAW_BUFFER3: number; + readonly DRAW_BUFFER4: number; + readonly DRAW_BUFFER5: number; + readonly DRAW_BUFFER6: number; + readonly DRAW_BUFFER7: number; + readonly MAX_COLOR_ATTACHMENTS: number; + readonly MAX_DRAW_BUFFERS: number; +} + +export function getDrawBuffers(gl: GLRenderingContext): COMPAT_draw_buffers | null { + if (isWebGL2(gl)) { + return { + drawBuffers: gl.drawBuffers.bind(gl), + COLOR_ATTACHMENT0: gl.COLOR_ATTACHMENT0, + COLOR_ATTACHMENT1: gl.COLOR_ATTACHMENT1, + COLOR_ATTACHMENT2: gl.COLOR_ATTACHMENT2, + COLOR_ATTACHMENT3: gl.COLOR_ATTACHMENT3, + COLOR_ATTACHMENT4: gl.COLOR_ATTACHMENT4, + COLOR_ATTACHMENT5: gl.COLOR_ATTACHMENT5, + COLOR_ATTACHMENT6: gl.COLOR_ATTACHMENT6, + COLOR_ATTACHMENT7: gl.COLOR_ATTACHMENT7, + DRAW_BUFFER0: gl.DRAW_BUFFER0, + DRAW_BUFFER1: gl.DRAW_BUFFER1, + DRAW_BUFFER2: gl.DRAW_BUFFER2, + DRAW_BUFFER3: gl.DRAW_BUFFER3, + DRAW_BUFFER4: gl.DRAW_BUFFER4, + DRAW_BUFFER5: gl.DRAW_BUFFER5, + DRAW_BUFFER6: gl.DRAW_BUFFER6, + DRAW_BUFFER7: gl.DRAW_BUFFER7, + MAX_COLOR_ATTACHMENTS: gl.MAX_COLOR_ATTACHMENTS, + MAX_DRAW_BUFFERS: gl.MAX_DRAW_BUFFERS, + } + } else { + const ext = gl.getExtension('WEBGL_draw_buffers') + if (ext === null) return null + return { + drawBuffers: ext.drawBuffersWEBGL.bind(ext), + COLOR_ATTACHMENT0: ext.COLOR_ATTACHMENT0_WEBGL, + COLOR_ATTACHMENT1: ext.COLOR_ATTACHMENT1_WEBGL, + COLOR_ATTACHMENT2: ext.COLOR_ATTACHMENT2_WEBGL, + COLOR_ATTACHMENT3: ext.COLOR_ATTACHMENT3_WEBGL, + COLOR_ATTACHMENT4: ext.COLOR_ATTACHMENT4_WEBGL, + COLOR_ATTACHMENT5: ext.COLOR_ATTACHMENT5_WEBGL, + COLOR_ATTACHMENT6: ext.COLOR_ATTACHMENT6_WEBGL, + COLOR_ATTACHMENT7: ext.COLOR_ATTACHMENT7_WEBGL, + DRAW_BUFFER0: ext.DRAW_BUFFER0_WEBGL, + DRAW_BUFFER1: ext.DRAW_BUFFER1_WEBGL, + DRAW_BUFFER2: ext.DRAW_BUFFER2_WEBGL, + DRAW_BUFFER3: ext.DRAW_BUFFER3_WEBGL, + DRAW_BUFFER4: ext.DRAW_BUFFER4_WEBGL, + DRAW_BUFFER5: ext.DRAW_BUFFER5_WEBGL, + DRAW_BUFFER6: ext.DRAW_BUFFER6_WEBGL, + DRAW_BUFFER7: ext.DRAW_BUFFER7_WEBGL, + MAX_COLOR_ATTACHMENTS: ext.MAX_COLOR_ATTACHMENTS_WEBGL, + MAX_DRAW_BUFFERS: ext.MAX_DRAW_BUFFERS_WEBGL, + } + } +} + +export interface COMPAT_shader_texture_lod { +} + +export function getShaderTextureLod(gl: GLRenderingContext): COMPAT_shader_texture_lod | null { + return isWebGL2(gl) ? {} : gl.getExtension('EXT_shader_texture_lod') } \ No newline at end of file diff --git a/src/mol-gl/webgl/context.ts b/src/mol-gl/webgl/context.ts index 9b231fdfb20ffafb28829c4bfcd83b75af089a03..aca8e579dcc80a93d21a5edc09af0362c5c6b3c6 100644 --- a/src/mol-gl/webgl/context.ts +++ b/src/mol-gl/webgl/context.ts @@ -1,14 +1,15 @@ /** - * 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> */ import { createProgramCache, ProgramCache } from './program' import { createShaderCache, ShaderCache } from './shader' -import { GLRenderingContext, COMPAT_instanced_arrays, COMPAT_standard_derivatives, COMPAT_vertex_array_object, getInstancedArrays, getStandardDerivatives, getVertexArrayObject, isWebGL2, COMPAT_element_index_uint, getElementIndexUint, COMPAT_texture_float, getTextureFloat, COMPAT_texture_float_linear, getTextureFloatLinear, COMPAT_blend_minmax, getBlendMinMax, getFragDepth, COMPAT_frag_depth } from './compat'; -import { createFramebufferCache, FramebufferCache } from './framebuffer'; +import { GLRenderingContext, COMPAT_instanced_arrays, COMPAT_standard_derivatives, COMPAT_vertex_array_object, getInstancedArrays, getStandardDerivatives, getVertexArrayObject, isWebGL2, COMPAT_element_index_uint, getElementIndexUint, COMPAT_texture_float, getTextureFloat, COMPAT_texture_float_linear, getTextureFloatLinear, COMPAT_blend_minmax, getBlendMinMax, getFragDepth, COMPAT_frag_depth, COMPAT_color_buffer_float, getColorBufferFloat, COMPAT_draw_buffers, getDrawBuffers, getShaderTextureLod, COMPAT_shader_texture_lod } from './compat'; +import { createFramebufferCache, FramebufferCache, checkFramebufferStatus } from './framebuffer'; import { Scheduler } from 'mol-task'; +import { isDebugMode } from 'mol-util/debug'; export function getGLContext(canvas: HTMLCanvasElement, contextAttributes?: WebGLContextAttributes): GLRenderingContext | null { function getContext(contextId: 'webgl' | 'experimental-webgl' | 'webgl2') { @@ -25,6 +26,24 @@ function getPixelRatio() { return (typeof window !== 'undefined') ? window.devicePixelRatio : 1 } +function getErrorDescription(gl: GLRenderingContext, error: number) { + switch (error) { + case gl.NO_ERROR: return 'no error' + case gl.INVALID_ENUM: return 'invalid enum' + case gl.INVALID_VALUE: return 'invalid value' + case gl.INVALID_OPERATION: return 'invalid operation' + case gl.INVALID_FRAMEBUFFER_OPERATION: return 'invalid framebuffer operation' + case gl.OUT_OF_MEMORY: return 'out of memory' + case gl.CONTEXT_LOST_WEBGL: return 'context lost' + } + return 'unknown error' +} + +export function checkError(gl: GLRenderingContext) { + const error = gl.getError() + if (error) throw new Error(`WebGL error: '${getErrorDescription(gl, error)}'`) +} + function unbindResources (gl: GLRenderingContext) { // bind null to all texture units const maxTextureImageUnits = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS) @@ -32,6 +51,10 @@ function unbindResources (gl: GLRenderingContext) { gl.activeTexture(gl.TEXTURE0 + i) gl.bindTexture(gl.TEXTURE_2D, null) gl.bindTexture(gl.TEXTURE_CUBE_MAP, null) + if (isWebGL2(gl)) { + gl.bindTexture(gl.TEXTURE_2D_ARRAY, null) + gl.bindTexture(gl.TEXTURE_3D, null) + } } // assign the smallest possible buffer to all attributes @@ -93,9 +116,20 @@ function waitForGpuCommandsComplete(gl: GLRenderingContext): Promise<void> { } function waitForGpuCommandsCompleteSync(gl: GLRenderingContext): void { + gl.bindFramebuffer(gl.FRAMEBUFFER, null) gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, tmpPixel) } +function readPixels(gl: GLRenderingContext, x: number, y: number, width: number, height: number, buffer: Uint8Array | Float32Array) { + if (isDebugMode) checkFramebufferStatus(gl) + if (buffer instanceof Uint8Array) { + gl.readPixels(x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, buffer) + } else { + gl.readPixels(x, y, width, height, gl.RGBA, gl.FLOAT, buffer) + } + if (isDebugMode) checkError(gl) +} + export function createImageData(buffer: ArrayLike<number>, width: number, height: number) { const w = width * 4 const h = height @@ -113,7 +147,7 @@ export function createImageData(buffer: ArrayLike<number>, width: number, height // -type Extensions = { +export type WebGLExtensions = { instancedArrays: COMPAT_instanced_arrays standardDerivatives: COMPAT_standard_derivatives blendMinMax: COMPAT_blend_minmax @@ -122,43 +156,12 @@ type Extensions = { elementIndexUint: COMPAT_element_index_uint | null vertexArrayObject: COMPAT_vertex_array_object | null fragDepth: COMPAT_frag_depth | null + colorBufferFloat: COMPAT_color_buffer_float | null + drawBuffers: COMPAT_draw_buffers | null + shaderTextureLod: COMPAT_shader_texture_lod | null } -/** A WebGL context object, including the rendering context, resource caches and counts */ -export interface WebGLContext { - readonly gl: GLRenderingContext - readonly isWebGL2: boolean - readonly extensions: Extensions - readonly pixelRatio: number - - readonly shaderCache: ShaderCache - readonly programCache: ProgramCache - readonly framebufferCache: FramebufferCache - - currentProgramId: number - - bufferCount: number - framebufferCount: number - renderbufferCount: number - textureCount: number - vaoCount: number - - drawCount: number - instanceCount: number - instancedDrawCount: number - - readonly maxTextureSize: number - readonly maxDrawBuffers: number - - unbindFramebuffer: () => void - readPixels: (x: number, y: number, width: number, height: number, buffer: Uint8Array) => void - readPixelsAsync: (x: number, y: number, width: number, height: number, buffer: Uint8Array) => Promise<void> - waitForGpuCommandsComplete: () => Promise<void> - waitForGpuCommandsCompleteSync: () => void - destroy: () => void -} - -export function createContext(gl: GLRenderingContext): WebGLContext { +function createExtensions(gl: GLRenderingContext): WebGLExtensions { const instancedArrays = getInstancedArrays(gl) if (instancedArrays === null) { throw new Error('Could not find support for "instanced_arrays"') @@ -191,19 +194,230 @@ export function createContext(gl: GLRenderingContext): WebGLContext { if (fragDepth === null) { console.log('Could not find support for "frag_depth"') } + const colorBufferFloat = getColorBufferFloat(gl) + if (colorBufferFloat === null) { + console.log('Could not find support for "color_buffer_float"') + } + const drawBuffers = getDrawBuffers(gl) + if (drawBuffers === null) { + console.log('Could not find support for "draw_buffers"') + } + const shaderTextureLod = getShaderTextureLod(gl) + if (shaderTextureLod === null) { + console.log('Could not find support for "shader_texture_lod"') + } + + + return { + instancedArrays, + standardDerivatives, + blendMinMax, + textureFloat, + textureFloatLinear, + elementIndexUint, + vertexArrayObject, + fragDepth, + colorBufferFloat, + drawBuffers, + shaderTextureLod + } +} + +export type WebGLStats = { + bufferCount: number + framebufferCount: number + renderbufferCount: number + textureCount: number + vaoCount: number + + drawCount: number + instanceCount: number + instancedDrawCount: number +} + +function createStats(): WebGLStats { + return { + bufferCount: 0, + framebufferCount: 0, + renderbufferCount: 0, + textureCount: 0, + vaoCount: 0, + + drawCount: 0, + instanceCount: 0, + instancedDrawCount: 0, + } +} + +export type WebGLState = { + currentProgramId: number + currentMaterialId: number + currentRenderItemId: number + + enable: (cap: number) => void + disable: (cap: number) => void + + frontFace: (mode: number) => void + cullFace: (mode: number) => void + depthMask: (flag: boolean) => void + colorMask: (red: boolean, green: boolean, blue: boolean, alpha: boolean) => void + clearColor: (red: number, green: number, blue: number, alpha: number) => void + + blendFunc: (src: number, dst: number) => void + blendFuncSeparate: (srcRGB: number, dstRGB: number, srcAlpha: number, dstAlpha: number) => void + + blendEquation: (mode: number) => void + blendEquationSeparate: (modeRGB: number, modeAlpha: number) => void +} + +function createState(gl: GLRenderingContext): WebGLState { + const enabledCapabilities: { [k: number]: boolean } = {} + + let currentFrontFace = gl.getParameter(gl.FRONT_FACE) + let currentCullFace = gl.getParameter(gl.CULL_FACE_MODE) + let currentDepthMask = gl.getParameter(gl.DEPTH_WRITEMASK) + let currentColorMask = gl.getParameter(gl.COLOR_WRITEMASK) + let currentClearColor = gl.getParameter(gl.COLOR_CLEAR_VALUE) + + let currentBlendSrcRGB = gl.getParameter(gl.BLEND_SRC_RGB) + let currentBlendDstRGB = gl.getParameter(gl.BLEND_DST_RGB) + let currentBlendSrcAlpha = gl.getParameter(gl.BLEND_SRC_ALPHA) + let currentBlendDstAlpha = gl.getParameter(gl.BLEND_DST_ALPHA) + + let currentBlendEqRGB = gl.getParameter(gl.BLEND_EQUATION_RGB) + let currentBlendEqAlpha = gl.getParameter(gl.BLEND_EQUATION_ALPHA) + + return { + currentProgramId: -1, + currentMaterialId: -1, + currentRenderItemId: -1, + + enable: (cap: number) => { + if (enabledCapabilities[cap] !== true ) { + gl.enable(cap) + enabledCapabilities[cap] = true + } + }, + disable: (cap: number) => { + if (enabledCapabilities[cap] !== false) { + gl.disable(cap) + enabledCapabilities[cap] = false + } + }, + + frontFace: (mode: number) => { + if (mode !== currentFrontFace) { + gl.frontFace(mode) + currentFrontFace = mode + } + }, + cullFace: (mode: number) => { + if (mode !== currentCullFace) { + gl.cullFace(mode) + currentCullFace = mode + } + }, + depthMask: (flag: boolean) => { + if (flag !== currentDepthMask) { + gl.depthMask(flag) + currentDepthMask = flag + } + }, + colorMask: (red: boolean, green: boolean, blue: boolean, alpha: boolean) => { + if (red !== currentColorMask[0] || green !== currentColorMask[1] || blue !== currentColorMask[2] || alpha !== currentColorMask[3]) + gl.colorMask(red, green, blue, alpha) + currentColorMask[0] = red + currentColorMask[1] = green + currentColorMask[2] = blue + currentColorMask[3] = alpha + }, + clearColor: (red: number, green: number, blue: number, alpha: number) => { + if (red !== currentClearColor[0] || green !== currentClearColor[1] || blue !== currentClearColor[2] || alpha !== currentClearColor[3]) + gl.clearColor(red, green, blue, alpha) + currentClearColor[0] = red + currentClearColor[1] = green + currentClearColor[2] = blue + currentClearColor[3] = alpha + }, + + blendFunc: (src: number, dst: number) => { + if (src !== currentBlendSrcRGB || dst !== currentBlendDstRGB || src !== currentBlendSrcAlpha || dst !== currentBlendDstAlpha) { + gl.blendFunc(src, dst) + currentBlendSrcRGB = src + currentBlendDstRGB = dst + currentBlendSrcAlpha = src + currentBlendDstAlpha = dst + } + }, + blendFuncSeparate: (srcRGB: number, dstRGB: number, srcAlpha: number, dstAlpha: number) => { + if (srcRGB !== currentBlendSrcRGB || dstRGB !== currentBlendDstRGB || srcAlpha !== currentBlendSrcAlpha || dstAlpha !== currentBlendDstAlpha) { + gl.blendFuncSeparate(srcRGB, dstRGB, srcAlpha, dstAlpha) + currentBlendSrcRGB = srcRGB + currentBlendDstRGB = dstRGB + currentBlendSrcAlpha = srcAlpha + currentBlendDstAlpha = dstAlpha + } + }, + + blendEquation: (mode: number) => { + if (mode !== currentBlendEqRGB || mode !== currentBlendEqAlpha) { + gl.blendEquation(mode) + currentBlendEqRGB = mode + currentBlendEqAlpha = mode + } + }, + blendEquationSeparate: (modeRGB: number, modeAlpha: number) => { + if (modeRGB !== currentBlendEqRGB || modeAlpha !== currentBlendEqAlpha) { + gl.blendEquationSeparate(modeRGB, modeAlpha) + currentBlendEqRGB = modeRGB + currentBlendEqAlpha = modeAlpha + } + } + } +} + +/** A WebGL context object, including the rendering context, resource caches and counts */ +export interface WebGLContext { + readonly gl: GLRenderingContext + readonly isWebGL2: boolean + readonly pixelRatio: number + + readonly extensions: WebGLExtensions + readonly state: WebGLState + readonly stats: WebGLStats + + readonly shaderCache: ShaderCache + readonly programCache: ProgramCache + readonly framebufferCache: FramebufferCache + + readonly maxTextureSize: number + readonly maxDrawBuffers: number + + unbindFramebuffer: () => void + readPixels: (x: number, y: number, width: number, height: number, buffer: Uint8Array | Float32Array) => void + readPixelsAsync: (x: number, y: number, width: number, height: number, buffer: Uint8Array) => Promise<void> + waitForGpuCommandsComplete: () => Promise<void> + waitForGpuCommandsCompleteSync: () => void + destroy: () => void +} + +export function createContext(gl: GLRenderingContext): WebGLContext { + const extensions = createExtensions(gl) + const state = createState(gl) + const stats = createStats() - const shaderCache = createShaderCache() - const programCache = createProgramCache() - const framebufferCache = createFramebufferCache() + const shaderCache: ShaderCache = createShaderCache(gl) + const programCache: ProgramCache = createProgramCache(gl, state, extensions, shaderCache) + const framebufferCache: FramebufferCache = createFramebufferCache(gl, stats) const parameters = { - maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE), - maxDrawBuffers: isWebGL2(gl) ? gl.getParameter(gl.MAX_DRAW_BUFFERS) : 0, - maxVertexTextureImageUnits: gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS), + maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE) as number, + maxDrawBuffers: isWebGL2(gl) ? gl.getParameter(gl.MAX_DRAW_BUFFERS) as number : 0, + maxVertexTextureImageUnits: gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS) as number, } - if (parameters.maxVertexTextureImageUnits < 4) { - throw new Error('Need "MAX_VERTEX_TEXTURE_IMAGE_UNITS" >= 4') + if (parameters.maxVertexTextureImageUnits < 8) { + throw new Error('Need "MAX_VERTEX_TEXTURE_IMAGE_UNITS" >= 8') } let readPixelsAsync: (x: number, y: number, width: number, height: number, buffer: Uint8Array) => Promise<void> @@ -239,53 +453,32 @@ export function createContext(gl: GLRenderingContext): WebGLContext { }) } else { readPixelsAsync = async (x: number, y: number, width: number, height: number, buffer: Uint8Array) => { - gl.readPixels(x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, buffer) + readPixels(gl, x, y, width, height, buffer) } } return { gl, isWebGL2: isWebGL2(gl), - extensions: { - instancedArrays, - standardDerivatives, - blendMinMax, - textureFloat, - textureFloatLinear, - elementIndexUint, - vertexArrayObject, - fragDepth + get pixelRatio () { + // this can change during the lifetime of a rendering context, so need to re-obtain on access + return getPixelRatio() }, - get pixelRatio () { return getPixelRatio() }, + + extensions, + state, + stats, shaderCache, programCache, framebufferCache, - currentProgramId: -1, - - bufferCount: 0, - framebufferCount: 0, - renderbufferCount: 0, - textureCount: 0, - vaoCount: 0, - - drawCount: 0, - instanceCount: 0, - instancedDrawCount: 0, - get maxTextureSize () { return parameters.maxTextureSize }, get maxDrawBuffers () { return parameters.maxDrawBuffers }, unbindFramebuffer: () => unbindFramebuffer(gl), - readPixels: (x: number, y: number, width: number, height: number, buffer: Uint8Array) => { - gl.readPixels(x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, buffer) - // TODO check is very expensive - // if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE) { - // gl.readPixels(x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, buffer) - // } else { - // console.error('Reading pixels failed. Framebuffer not complete.') - // } + readPixels: (x: number, y: number, width: number, height: number, buffer: Uint8Array | Float32Array) => { + readPixels(gl, x, y, width, height, buffer) }, readPixelsAsync, waitForGpuCommandsComplete: () => waitForGpuCommandsComplete(gl), diff --git a/src/mol-gl/webgl/framebuffer.ts b/src/mol-gl/webgl/framebuffer.ts index ef3439b723e5eb243281da27e83d78d0d74241b6..997c7412e50ec1d42822a9236507110ceee725c3 100644 --- a/src/mol-gl/webgl/framebuffer.ts +++ b/src/mol-gl/webgl/framebuffer.ts @@ -1,15 +1,41 @@ /** - * 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> */ -import { WebGLContext } from './context' +import { WebGLStats } from './context' import { idFactory } from 'mol-util/id-factory'; import { ReferenceCache, createReferenceCache } from 'mol-util/reference-cache'; +import { GLRenderingContext, isWebGL2 } from './compat'; const getNextFramebufferId = idFactory() +function getFramebufferStatusDescription(gl: GLRenderingContext, status: number) { + switch (status) { + case gl.FRAMEBUFFER_COMPLETE: return 'complete' + case gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT: return 'incomplete attachment' + case gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: return 'incomplete missing attachment' + case gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS: return 'incomplete dimensions' + case gl.FRAMEBUFFER_UNSUPPORTED: return 'unsupported' + } + if (isWebGL2(gl)) { + switch (status) { + case gl.FRAMEBUFFER_INCOMPLETE_MULTISAMPLE: return 'incomplete multisample' + case gl.RENDERBUFFER_SAMPLES: return 'renderbuffer samples' + } + } + return 'unknown error' +} + +export function checkFramebufferStatus(gl: GLRenderingContext) { + const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER) + if (status !== gl.FRAMEBUFFER_COMPLETE) { + const description = getFramebufferStatusDescription(gl, status) + throw new Error(`Framebuffer status: ${description}`) + } +} + export interface Framebuffer { readonly id: number @@ -17,15 +43,14 @@ export interface Framebuffer { destroy: () => void } -export function createFramebuffer (ctx: WebGLContext): Framebuffer { - const { gl } = ctx +export function createFramebuffer (gl: GLRenderingContext, stats: WebGLStats): Framebuffer { const _framebuffer = gl.createFramebuffer() if (_framebuffer === null) { throw new Error('Could not create WebGL framebuffer') } let destroyed = false - ctx.framebufferCount += 1 + stats.framebufferCount += 1 return { id: getNextFramebufferId(), @@ -35,17 +60,17 @@ export function createFramebuffer (ctx: WebGLContext): Framebuffer { if (destroyed) return gl.deleteFramebuffer(_framebuffer) destroyed = true - ctx.framebufferCount -= 1 + stats.framebufferCount -= 1 } } } -export type FramebufferCache = ReferenceCache<Framebuffer, string, WebGLContext> +export type FramebufferCache = ReferenceCache<Framebuffer, string> -export function createFramebufferCache(): FramebufferCache { +export function createFramebufferCache(gl: GLRenderingContext, stats: WebGLStats): FramebufferCache { return createReferenceCache( (name: string) => name, - (ctx: WebGLContext) => createFramebuffer(ctx), + () => createFramebuffer(gl, stats), (framebuffer: Framebuffer) => { framebuffer.destroy() } ) } \ No newline at end of file diff --git a/src/mol-gl/webgl/program.ts b/src/mol-gl/webgl/program.ts index 6cc476d5056cc0f0c76cd7de7c682cb362af0884..5997cc71165cb322693132650ee8412add4fe475 100644 --- a/src/mol-gl/webgl/program.ts +++ b/src/mol-gl/webgl/program.ts @@ -1,18 +1,21 @@ /** - * 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> */ import { ShaderCode, DefineValues, addShaderDefines } from '../shader-code' -import { WebGLContext } from './context'; -import { UniformValues, getUniformSetters } from './uniform'; -import { AttributeBuffers } from './buffer'; -import { Textures, TextureId } from './texture'; +import { WebGLExtensions, WebGLState } from './context'; +import { getUniformSetters, UniformsList, getUniformType } from './uniform'; +import { AttributeBuffers, getAttribType } from './buffer'; +import { TextureId, Textures } from './texture'; import { createReferenceCache, ReferenceCache } from 'mol-util/reference-cache'; import { idFactory } from 'mol-util/id-factory'; import { RenderableSchema } from '../renderable/schema'; import { hashFnv32a, hashString } from 'mol-data/util'; +import { isDebugMode } from 'mol-util/debug'; +import { GLRenderingContext } from './compat'; +import { ShaderCache } from './shader'; const getNextProgramId = idFactory() @@ -20,7 +23,7 @@ export interface Program { readonly id: number use: () => void - setUniforms: (uniformValues: UniformValues) => void + setUniforms: (uniformValues: UniformsList) => void bindAttributes: (attribueBuffers: AttributeBuffers) => void bindTextures: (textures: Textures) => void @@ -29,8 +32,7 @@ export interface Program { type Locations = { [k: string]: number } -function getLocations(ctx: WebGLContext, program: WebGLProgram, schema: RenderableSchema) { - const { gl } = ctx +function getLocations(gl: GLRenderingContext, program: WebGLProgram, schema: RenderableSchema) { const locations: Locations = {} Object.keys(schema).forEach(k => { const spec = schema[k] @@ -47,14 +49,76 @@ function getLocations(ctx: WebGLContext, program: WebGLProgram, schema: Renderab return locations } +function checkActiveAttributes(gl: GLRenderingContext, program: WebGLProgram, schema: RenderableSchema) { + const attribCount = gl.getProgramParameter(program, gl.ACTIVE_ATTRIBUTES); + for (let i = 0; i < attribCount; ++i) { + const info = gl.getActiveAttrib(program, i); + if (info) { + const { name, type } = info + if (name.startsWith('__activeAttribute')) { + // name assigned by `gl.shim.ts`, ignore for checks + continue + } + const spec = schema[name] + if (spec === undefined) { + throw new Error(`missing 'uniform' or 'texture' with name '${name}' in schema`) + } + if (spec.type !== 'attribute') { + throw new Error(`'${name}' must be of type 'attribute' but is '${spec.type}'`) + } + const attribType = getAttribType(gl, spec.kind, spec.itemSize) + if (attribType !== type) { + throw new Error(`unexpected attribute type for ${name}`) + } + } + } +} + +function checkActiveUniforms(gl: GLRenderingContext, program: WebGLProgram, schema: RenderableSchema) { + const attribCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); + for (let i = 0; i < attribCount; ++i) { + const info = gl.getActiveUniform(program, i); + if (info) { + const { name, type } = info + if (name.startsWith('__activeUniform')) { + // name assigned by `gl.shim.ts`, ignore for checks + continue + } + const spec = schema[name] + if (spec === undefined) { + throw new Error(`missing 'uniform' or 'texture' with name '${name}' in schema`) + } + if (spec.type === 'uniform') { + const uniformType = getUniformType(gl, spec.kind) + if (uniformType !== type) { + throw new Error(`unexpected uniform type for ${name}`) + } + } else if (spec.type === 'texture') { + if (spec.kind === 'image-float32' || spec.kind === 'image-uint8') { + if (type !== gl.SAMPLER_2D) { + throw new Error(`unexpected sampler type for '${name}'`) + } + } else if (spec.kind === 'volume-float32' || spec.kind === 'volume-uint8') { + if (type !== (gl as WebGL2RenderingContext).SAMPLER_3D) { + throw new Error(`unexpected sampler type for '${name}'`) + } + } else { + // TODO + } + } else { + throw new Error(`'${name}' must be of type 'uniform' or 'texture' but is '${spec.type}'`) + } + } + } +} + export interface ProgramProps { defineValues: DefineValues, shaderCode: ShaderCode, schema: RenderableSchema } -export function createProgram(ctx: WebGLContext, props: ProgramProps): Program { - const { gl, shaderCache } = ctx +export function createProgram(gl: GLRenderingContext, state: WebGLState, extensions: WebGLExtensions, shaderCache: ShaderCache, props: ProgramProps): Program { const { defineValues, shaderCode: _shaderCode, schema } = props const program = gl.createProgram() @@ -63,20 +127,29 @@ export function createProgram(ctx: WebGLContext, props: ProgramProps): Program { } const programId = getNextProgramId() - const shaderCode = addShaderDefines(ctx, defineValues, _shaderCode) - const vertShaderRef = shaderCache.get(ctx, { type: 'vert', source: shaderCode.vert }) - const fragShaderRef = shaderCache.get(ctx, { type: 'frag', source: shaderCode.frag }) + const shaderCode = addShaderDefines(gl, extensions, defineValues, _shaderCode) + const vertShaderRef = shaderCache.get({ type: 'vert', source: shaderCode.vert }) + const fragShaderRef = shaderCache.get({ type: 'frag', source: shaderCode.frag }) vertShaderRef.value.attach(program) fragShaderRef.value.attach(program) gl.linkProgram(program) - if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { - throw new Error(`Could not compile WebGL program. \n\n${gl.getProgramInfoLog(program)}`); + if (isDebugMode) { + // no-op in FF on Mac, see https://bugzilla.mozilla.org/show_bug.cgi?id=1284425 + // gl.validateProgram(program) + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + throw new Error(`Could not compile WebGL program. \n\n${gl.getProgramInfoLog(program)}`); + } } - const locations = getLocations(ctx, program, schema) + const locations = getLocations(gl, program, schema) const uniformSetters = getUniformSetters(schema) + if (isDebugMode) { + checkActiveAttributes(gl, program, schema) + checkActiveUniforms(gl, program, schema) + } + let destroyed = false return { @@ -84,33 +157,35 @@ export function createProgram(ctx: WebGLContext, props: ProgramProps): Program { use: () => { // console.log('use', programId) - ctx.currentProgramId = programId + state.currentProgramId = programId gl.useProgram(program) }, - setUniforms: (uniformValues: UniformValues) => { - const uniformKeys = Object.keys(uniformValues) - for (let i = 0, il = uniformKeys.length; i < il; ++i) { - const k = uniformKeys[i] - const l = locations[k] - const v = uniformValues[k] - if (v) uniformSetters[k](gl, l, v.ref.value) + setUniforms: (uniformValues: UniformsList) => { + for (let i = 0, il = uniformValues.length; i < il; ++i) { + const [k, v] = uniformValues[i] + if (v) { + const l = locations[k] + if (l !== null) uniformSetters[k](gl, l, v.ref.value) + } } }, bindAttributes: (attribueBuffers: AttributeBuffers) => { - const attributeKeys = Object.keys(attribueBuffers) - for (let i = 0, il = attributeKeys.length; i < il; ++i) { - const k = attributeKeys[i] + for (let i = 0, il = attribueBuffers.length; i < il; ++i) { + const [k, buffer] = attribueBuffers[i] const l = locations[k] - if (l !== -1) attribueBuffers[k].bind(l) + if (l !== -1) buffer.bind(l) } }, bindTextures: (textures: Textures) => { - const textureKeys = Object.keys(textures) - for (let i = 0, il = textureKeys.length; i < il; ++i) { - const k = textureKeys[i] + for (let i = 0, il = textures.length; i < il; ++i) { + const [k, texture] = textures[i] const l = locations[k] - textures[k].bind(i as TextureId) - uniformSetters[k](gl, l, i as TextureId) + if (l !== null) { + // TODO if the order and count of textures in a material can be made invariant + // bind needs to be called only when the material changes + texture.bind(i as TextureId) + uniformSetters[k](gl, l, i as TextureId) + } } }, @@ -124,14 +199,14 @@ export function createProgram(ctx: WebGLContext, props: ProgramProps): Program { } } -export type ProgramCache = ReferenceCache<Program, ProgramProps, WebGLContext> +export type ProgramCache = ReferenceCache<Program, ProgramProps> function defineValueHash(v: boolean | number | string): number { return typeof v === 'boolean' ? (v ? 1 : 0) : typeof v === 'number' ? v : hashString(v) } -export function createProgramCache(): ProgramCache { +export function createProgramCache(gl: GLRenderingContext, state: WebGLState, extensions: WebGLExtensions, shaderCache: ShaderCache): ProgramCache { return createReferenceCache( (props: ProgramProps) => { const array = [ props.shaderCode.id ] @@ -141,7 +216,7 @@ export function createProgramCache(): ProgramCache { }) return hashFnv32a(array).toString() }, - (ctx: WebGLContext, props: ProgramProps) => createProgram(ctx, props), + (props: ProgramProps) => createProgram(gl, state, extensions, shaderCache, props), (program: Program) => { program.destroy() } ) } \ No newline at end of file diff --git a/src/mol-gl/webgl/render-item.ts b/src/mol-gl/webgl/render-item.ts index fad8f866e25789f402f9a8ef880d0d6fbeb0441a..b516fb8099172ed50c21646934ab016174f4a947 100644 --- a/src/mol-gl/webgl/render-item.ts +++ b/src/mol-gl/webgl/render-item.ts @@ -1,20 +1,22 @@ /** - * 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> */ -import { createAttributeBuffers, createElementsBuffer, ElementsBuffer, createAttributeBuffer, ArrayKind } from './buffer'; -import { createTextures } from './texture'; -import { WebGLContext } from './context'; +import { createAttributeBuffers, createElementsBuffer, ElementsBuffer, createAttributeBuffer, AttributeKind } from './buffer'; +import { createTextures, Texture } from './texture'; +import { WebGLContext, checkError } from './context'; import { ShaderCode } from '../shader-code'; import { Program } from './program'; -import { RenderableSchema, RenderableValues, AttributeSpec, getValueVersions, splitValues, Values, splitKeys } from '../renderable/schema'; +import { RenderableSchema, RenderableValues, AttributeSpec, getValueVersions, splitValues, Values } from '../renderable/schema'; import { idFactory } from 'mol-util/id-factory'; import { deleteVertexArray, createVertexArray } from './vertex-array'; import { ValueCell } from 'mol-util'; import { ReferenceItem } from 'mol-util/reference-cache'; import { TextureImage, TextureVolume } from 'mol-gl/renderable/util'; +import { checkFramebufferStatus } from './framebuffer'; +import { isDebugMode } from 'mol-util/debug'; const getNextRenderItemId = idFactory() @@ -33,22 +35,34 @@ export function getDrawMode(ctx: WebGLContext, drawMode: DrawMode) { } } -export interface RenderItem { +export interface RenderItem<T extends string> { readonly id: number - getProgram: (variant: RenderVariant) => Program + readonly materialId: number + getProgram: (variant: T) => Program - render: (variant: RenderVariant) => void + render: (variant: T) => void update: () => Readonly<ValueChanges> destroy: () => void } -const RenderVariantDefines = { +// + +const GraphicsRenderVariantDefines = { 'draw': {}, 'pickObject': { dColorType: ValueCell.create('objectPicking') }, 'pickInstance': { dColorType: ValueCell.create('instancePicking') }, 'pickGroup': { dColorType: ValueCell.create('groupPicking') } } -export type RenderVariant = keyof typeof RenderVariantDefines +export type GraphicsRenderVariant = keyof typeof GraphicsRenderVariantDefines + +const ComputeRenderVariantDefines = { + 'compute': {}, +} +export type ComputeRenderVariant = keyof typeof ComputeRenderVariantDefines + +type RenderVariantDefines = typeof GraphicsRenderVariantDefines | typeof ComputeRenderVariantDefines + +// type ProgramVariants = { [k: string]: ReferenceItem<Program> } type VertexArrayVariants = { [k: string]: WebGLVertexArrayObjectOES | null } @@ -58,7 +72,6 @@ interface ValueChanges { defines: boolean elements: boolean textures: boolean - uniforms: boolean } function createValueChanges() { return { @@ -66,7 +79,6 @@ function createValueChanges() { defines: false, elements: false, textures: false, - uniforms: false, } } function resetValueChanges(valueChanges: ValueChanges) { @@ -74,31 +86,44 @@ function resetValueChanges(valueChanges: ValueChanges) { valueChanges.defines = false valueChanges.elements = false valueChanges.textures = false - valueChanges.uniforms = false } -// TODO make `RenderVariantDefines` a parameter for `createRenderItem` +// + +export type GraphicsRenderItem = RenderItem<keyof typeof GraphicsRenderVariantDefines & string> +export function createGraphicsRenderItem(ctx: WebGLContext, drawMode: DrawMode, shaderCode: ShaderCode, schema: RenderableSchema, values: RenderableValues, materialId: number) { + return createRenderItem(ctx, drawMode, shaderCode, schema, values, materialId, GraphicsRenderVariantDefines) +} + +export type ComputeRenderItem = RenderItem<keyof typeof ComputeRenderVariantDefines & string> +export function createComputeRenderItem(ctx: WebGLContext, drawMode: DrawMode, shaderCode: ShaderCode, schema: RenderableSchema, values: RenderableValues, materialId = -1) { + return createRenderItem(ctx, drawMode, shaderCode, schema, values, materialId, ComputeRenderVariantDefines) +} /** * Creates a render item * * - assumes that `values.drawCount` and `values.instanceCount` exist */ -export function createRenderItem(ctx: WebGLContext, drawMode: DrawMode, shaderCode: ShaderCode, schema: RenderableSchema, values: RenderableValues): RenderItem { +export function createRenderItem<T extends RenderVariantDefines, S extends keyof T & string>(ctx: WebGLContext, drawMode: DrawMode, shaderCode: ShaderCode, schema: RenderableSchema, values: RenderableValues, materialId: number, renderVariantDefines: T): RenderItem<S> { const id = getNextRenderItemId() - const { programCache } = ctx + const { stats, state, programCache } = ctx const { instancedArrays, vertexArrayObject } = ctx.extensions - const { attributeValues, defineValues, textureValues, uniformValues } = splitValues(schema, values) - const { attributeKeys, defineKeys, textureKeys } = splitKeys(schema) + const { attributeValues, defineValues, textureValues, uniformValues, materialUniformValues } = splitValues(schema, values) + + const uniformValueEntries = Object.entries(uniformValues) + const materialUniformValueEntries = Object.entries(materialUniformValues) + const defineValueEntries = Object.entries(defineValues) + const versions = getValueVersions(values) const glDrawMode = getDrawMode(ctx, drawMode) const programs: ProgramVariants = {} - Object.keys(RenderVariantDefines).forEach(k => { - const variantDefineValues: Values<RenderableSchema> = (RenderVariantDefines as any)[k] - programs[k] = programCache.get(ctx, { + Object.keys(renderVariantDefines).forEach(k => { + const variantDefineValues: Values<RenderableSchema> = (renderVariantDefines as any)[k] + programs[k] = programCache.get({ defineValues: { ...defineValues, ...variantDefineValues }, shaderCode, schema @@ -115,51 +140,80 @@ export function createRenderItem(ctx: WebGLContext, drawMode: DrawMode, shaderCo } const vertexArrays: VertexArrayVariants = {} - Object.keys(RenderVariantDefines).forEach(k => { + Object.keys(renderVariantDefines).forEach(k => { vertexArrays[k] = createVertexArray(ctx, programs[k].value, attributeBuffers, elementsBuffer) }) let drawCount = values.drawCount.ref.value let instanceCount = values.instanceCount.ref.value - ctx.drawCount += drawCount - ctx.instanceCount += instanceCount - ctx.instancedDrawCount += instanceCount * drawCount + stats.drawCount += drawCount + stats.instanceCount += instanceCount + stats.instancedDrawCount += instanceCount * drawCount const valueChanges = createValueChanges() let destroyed = false + let currentProgramId = -1 return { id, - getProgram: (variant: RenderVariant) => programs[variant].value, + materialId, + getProgram: (variant: S) => programs[variant].value, - render: (variant: RenderVariant) => { + render: (variant: S) => { if (drawCount === 0 || instanceCount === 0) return const program = programs[variant].value - const vertexArray = vertexArrays[variant] - program.setUniforms(uniformValues) - if (vertexArrayObject && vertexArray) { - vertexArrayObject.bindVertexArray(vertexArray) - // need to bind elements buffer explicitly since it is not always recorded in the VAO - if (elementsBuffer) elementsBuffer.bind() + if (program.id === currentProgramId && state.currentRenderItemId === id) { + program.setUniforms(uniformValueEntries) + program.bindTextures(textures) } else { - if (elementsBuffer) elementsBuffer.bind() - program.bindAttributes(attributeBuffers) + const vertexArray = vertexArrays[variant] + if (program.id !== state.currentProgramId || program.id !== currentProgramId || + materialId === -1 || materialId !== state.currentMaterialId + ) { + // console.log('program.id changed or materialId changed/-1', materialId) + if (program.id !== state.currentProgramId) program.use() + program.setUniforms(materialUniformValueEntries) + state.currentMaterialId = materialId + currentProgramId = program.id + } + program.setUniforms(uniformValueEntries) + program.bindTextures(textures) + if (vertexArrayObject && vertexArray) { + vertexArrayObject.bindVertexArray(vertexArray) + // need to bind elements buffer explicitly since it is not always recorded in the VAO + if (elementsBuffer) elementsBuffer.bind() + } else { + if (elementsBuffer) elementsBuffer.bind() + program.bindAttributes(attributeBuffers) + } + state.currentRenderItemId = id + } + if (isDebugMode) { + checkFramebufferStatus(ctx.gl) } - program.bindTextures(textures) if (elementsBuffer) { instancedArrays.drawElementsInstanced(glDrawMode, drawCount, elementsBuffer._dataType, 0, instanceCount); } else { instancedArrays.drawArraysInstanced(glDrawMode, 0, drawCount, instanceCount) } + if (isDebugMode) { + try { + checkError(ctx.gl) + } catch (e) { + // console.log('shaderCode', shaderCode) + // console.log('schema', schema) + // console.log('attributeBuffers', attributeBuffers) + throw new Error(`Error rendering item id ${id}: '${e}'`) + } + } }, update: () => { resetValueChanges(valueChanges) - for (let i = 0, il = defineKeys.length; i < il; ++i) { - const k = defineKeys[i] - const value = defineValues[k] + for (let i = 0, il = defineValueEntries.length; i < il; ++i) { + const [k, value] = defineValueEntries[i] if (value.ref.version !== versions[k]) { // console.log('define version changed', k) valueChanges.defines = true @@ -169,10 +223,10 @@ export function createRenderItem(ctx: WebGLContext, drawMode: DrawMode, shaderCo if (valueChanges.defines) { // console.log('some defines changed, need to rebuild programs') - Object.keys(RenderVariantDefines).forEach(k => { - const variantDefineValues: Values<RenderableSchema> = (RenderVariantDefines as any)[k] + Object.keys(renderVariantDefines).forEach(k => { + const variantDefineValues: Values<RenderableSchema> = (renderVariantDefines as any)[k] programs[k].free() - programs[k] = programCache.get(ctx, { + programs[k] = programCache.get({ defineValues: { ...defineValues, ...variantDefineValues }, shaderCode, schema @@ -182,32 +236,31 @@ export function createRenderItem(ctx: WebGLContext, drawMode: DrawMode, shaderCo if (values.drawCount.ref.version !== versions.drawCount) { // console.log('drawCount version changed') - ctx.drawCount += values.drawCount.ref.value - drawCount - ctx.instancedDrawCount += instanceCount * values.drawCount.ref.value - instanceCount * drawCount + stats.drawCount += values.drawCount.ref.value - drawCount + stats.instancedDrawCount += instanceCount * values.drawCount.ref.value - instanceCount * drawCount drawCount = values.drawCount.ref.value versions.drawCount = values.drawCount.ref.version } if (values.instanceCount.ref.version !== versions.instanceCount) { // console.log('instanceCount version changed') - ctx.instanceCount += values.instanceCount.ref.value - instanceCount - ctx.instancedDrawCount += values.instanceCount.ref.value * drawCount - instanceCount * drawCount + stats.instanceCount += values.instanceCount.ref.value - instanceCount + stats.instancedDrawCount += values.instanceCount.ref.value * drawCount - instanceCount * drawCount instanceCount = values.instanceCount.ref.value versions.instanceCount = values.instanceCount.ref.version } - for (let i = 0, il = attributeKeys.length; i < il; ++i) { - const k = attributeKeys[i] + for (let i = 0, il = attributeBuffers.length; i < il; ++i) { + const [k, buffer] = attributeBuffers[i] const value = attributeValues[k] if (value.ref.version !== versions[k]) { - const buffer = attributeBuffers[k] if (buffer.length >= value.ref.value.length) { // console.log('attribute array large enough to update', k, value.ref.id, value.ref.version) buffer.updateData(value.ref.value) } else { // console.log('attribute array to small, need to create new attribute', k, value.ref.id, value.ref.version) buffer.destroy() - const { itemSize, divisor } = schema[k] as AttributeSpec<ArrayKind> - attributeBuffers[k] = createAttributeBuffer(ctx, value.ref.value, itemSize, divisor) + const { itemSize, divisor } = schema[k] as AttributeSpec<AttributeKind> + attributeBuffers[i][1] = createAttributeBuffer(ctx, value.ref.value, itemSize, divisor) valueChanges.attributes = true } versions[k] = value.ref.version @@ -231,7 +284,7 @@ export function createRenderItem(ctx: WebGLContext, drawMode: DrawMode, shaderCo // console.log('program/defines or buffers changed, update vaos') const { vertexArrayObject } = ctx.extensions if (vertexArrayObject) { - Object.keys(RenderVariantDefines).forEach(k => { + Object.keys(renderVariantDefines).forEach(k => { vertexArrayObject.bindVertexArray(vertexArrays[k]) if (elementsBuffer && (valueChanges.defines || valueChanges.elements)) { elementsBuffer.bind() @@ -244,16 +297,18 @@ export function createRenderItem(ctx: WebGLContext, drawMode: DrawMode, shaderCo } } - for (let i = 0, il = textureKeys.length; i < il; ++i) { - const k = textureKeys[i] + for (let i = 0, il = textures.length; i < il; ++i) { + const [k, texture] = textures[i] const value = textureValues[k] if (value.ref.version !== versions[k]) { // update of textures with kind 'texture' is done externally if (schema[k].kind !== 'texture') { // console.log('texture version changed, uploading image', k) - textures[k].load(value.ref.value as TextureImage<any> | TextureVolume<any>) + texture.load(value.ref.value as TextureImage<any> | TextureVolume<any>) versions[k] = value.ref.version valueChanges.textures = true + } else { + textures[i][1] = value.ref.value as Texture } } } @@ -262,17 +317,17 @@ export function createRenderItem(ctx: WebGLContext, drawMode: DrawMode, shaderCo }, destroy: () => { if (!destroyed) { - Object.keys(RenderVariantDefines).forEach(k => { + Object.keys(renderVariantDefines).forEach(k => { programs[k].free() deleteVertexArray(ctx, vertexArrays[k]) }) - Object.keys(textures).forEach(k => { + textures.forEach(([k, texture]) => { // lifetime of textures with kind 'texture' is defined externally if (schema[k].kind !== 'texture') { - textures[k].destroy() + texture.destroy() } }) - Object.keys(attributeBuffers).forEach(k => attributeBuffers[k].destroy()) + attributeBuffers.forEach(([_, buffer]) => buffer.destroy()) if (elementsBuffer) elementsBuffer.destroy() destroyed = true } diff --git a/src/mol-gl/webgl/render-target.ts b/src/mol-gl/webgl/render-target.ts index d26ec40b136d87c9669b32d2ddfca8ee55c7285c..01f623ecc8658f02fbf14b913a23a8332197929a 100644 --- a/src/mol-gl/webgl/render-target.ts +++ b/src/mol-gl/webgl/render-target.ts @@ -31,7 +31,7 @@ export interface RenderTarget { } export function createRenderTarget (ctx: WebGLContext, _width: number, _height: number): RenderTarget { - const { gl } = ctx + const { gl, stats } = ctx const image: Mutable<TextureImage<Uint8Array>> = { array: new Uint8Array(_width * _height * 4), @@ -42,7 +42,7 @@ export function createRenderTarget (ctx: WebGLContext, _width: number, _height: const targetTexture = createTexture(ctx, 'image-uint8', 'rgba', 'ubyte', 'linear') targetTexture.load(image) - const framebuffer = createFramebuffer(ctx) + const framebuffer = createFramebuffer(gl, stats) // attach the texture as the first color attachment targetTexture.attachFramebuffer(framebuffer, 'color0') diff --git a/src/mol-gl/webgl/renderbuffer.ts b/src/mol-gl/webgl/renderbuffer.ts index 8cc41d487b06dcdcfc1adc8f5bea866f5e960a32..86eea102485f89f7b01a6910dc572d42b1b99223 100644 --- a/src/mol-gl/webgl/renderbuffer.ts +++ b/src/mol-gl/webgl/renderbuffer.ts @@ -42,7 +42,7 @@ export interface Renderbuffer { } export function createRenderbuffer (ctx: WebGLContext, format: RenderbufferFormat, attachment: RenderbufferAttachment, _width: number, _height: number): Renderbuffer { - const { gl } = ctx + const { gl, stats } = ctx const _renderbuffer = gl.createRenderbuffer() if (_renderbuffer === null) { throw new Error('Could not create WebGL renderbuffer') @@ -57,7 +57,7 @@ export function createRenderbuffer (ctx: WebGLContext, format: RenderbufferForma gl.framebufferRenderbuffer(gl.FRAMEBUFFER, _attachment, gl.RENDERBUFFER, _renderbuffer) let destroyed = false - ctx.renderbufferCount += 1 + stats.renderbufferCount += 1 return { id: getNextRenderbufferId(), @@ -72,7 +72,7 @@ export function createRenderbuffer (ctx: WebGLContext, format: RenderbufferForma if (destroyed) return gl.deleteRenderbuffer(_renderbuffer) destroyed = true - ctx.framebufferCount -= 1 + stats.framebufferCount -= 1 } } } \ No newline at end of file diff --git a/src/mol-gl/webgl/shader.ts b/src/mol-gl/webgl/shader.ts index a209cd017546234b711b4b3069275ed5c015417a..546ba0328b185ddbf452181fadea06887112e9ed 100644 --- a/src/mol-gl/webgl/shader.ts +++ b/src/mol-gl/webgl/shader.ts @@ -1,12 +1,13 @@ /** - * 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> */ import { createReferenceCache, ReferenceCache } from 'mol-util/reference-cache'; -import { WebGLContext } from './context'; import { idFactory } from 'mol-util/id-factory'; +import { GLRenderingContext } from './compat'; +import { isDebugMode } from 'mol-util/debug'; const getNextShaderId = idFactory() @@ -26,8 +27,7 @@ export interface Shader { destroy: () => void } -function createShader(ctx: WebGLContext, props: ShaderProps): Shader { - const { gl } = ctx +function createShader(gl: GLRenderingContext, props: ShaderProps): Shader { const { type, source } = props const shader = gl.createShader(type === 'vert' ? gl.VERTEX_SHADER : gl.FRAGMENT_SHADER) @@ -38,7 +38,7 @@ function createShader(ctx: WebGLContext, props: ShaderProps): Shader { gl.shaderSource(shader, source) gl.compileShader(shader) - if (gl.getShaderParameter(shader, gl.COMPILE_STATUS) === false) { + if (isDebugMode && gl.getShaderParameter(shader, gl.COMPILE_STATUS) === false) { console.warn(`'${type}' shader info log '${gl.getShaderInfoLog(shader)}'\n${addLineNumbers(source)}`) throw new Error(`Error compiling ${type} shader`) } @@ -54,12 +54,12 @@ function createShader(ctx: WebGLContext, props: ShaderProps): Shader { } } -export type ShaderCache = ReferenceCache<Shader, ShaderProps, WebGLContext> +export type ShaderCache = ReferenceCache<Shader, ShaderProps> -export function createShaderCache(): ShaderCache { +export function createShaderCache(gl: GLRenderingContext): ShaderCache { return createReferenceCache( (props: ShaderProps) => JSON.stringify(props), - (ctx: WebGLContext, props: ShaderProps) => createShader(ctx, props), + (props: ShaderProps) => createShader(gl, props), (shader: Shader) => { shader.destroy() } ) } \ No newline at end of file diff --git a/src/mol-gl/webgl/texture.ts b/src/mol-gl/webgl/texture.ts index c5128cc4ad2ed9eb3faee3ba3e2bf887ba858adc..20cc12853ad6a7475e1359899da9d6757d7ad9a3 100644 --- a/src/mol-gl/webgl/texture.ts +++ b/src/mol-gl/webgl/texture.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> */ @@ -42,40 +42,42 @@ export function getTarget(ctx: WebGLContext, kind: TextureKind): number { case 'volume-float32': return gl.TEXTURE_3D } } - throw new Error('unknown texture kind') + throw new Error(`unknown texture kind '${kind}'`) } -export function getFormat(ctx: WebGLContext, format: TextureFormat): number { +export function getFormat(ctx: WebGLContext, format: TextureFormat, type: TextureType): number { const { gl } = ctx switch (format) { - case 'alpha': return gl.ALPHA + case 'alpha': + if (isWebGL2 && type === 'float') return (gl as WebGL2RenderingContext).RED + else return gl.ALPHA case 'rgb': return gl.RGB case 'rgba': return gl.RGBA } } export function getInternalFormat(ctx: WebGLContext, format: TextureFormat, type: TextureType): number { - const { gl, isWebGL2 } = ctx - if (isWebGL2) { + const { gl } = ctx + if (isWebGL2(gl)) { switch (format) { case 'alpha': switch (type) { case 'ubyte': return gl.ALPHA - case 'float': throw new Error('invalid format/type combination alpha/float') + case 'float': return gl.R32F } case 'rgb': switch (type) { case 'ubyte': return gl.RGB - case 'float': return (gl as WebGL2RenderingContext).RGB32F + case 'float': return gl.RGB32F } case 'rgba': switch (type) { case 'ubyte': return gl.RGBA - case 'float': return (gl as WebGL2RenderingContext).RGBA32F + case 'float': return gl.RGBA32F } } } - return getFormat(ctx, format) + return getFormat(ctx, format, type) } export function getType(ctx: WebGLContext, type: TextureType): number { @@ -95,21 +97,21 @@ export function getFilter(ctx: WebGLContext, type: TextureFilter): number { } export function getAttachment(ctx: WebGLContext, attachment: TextureAttachment): number { - const { gl } = ctx + const { gl, extensions } = ctx switch (attachment) { case 'depth': return gl.DEPTH_ATTACHMENT case 'stencil': return gl.STENCIL_ATTACHMENT case 'color0': case 0: return gl.COLOR_ATTACHMENT0 } - if (isWebGL2(gl)) { + if (extensions.drawBuffers) { switch (attachment) { - case 'color1': case 1: return gl.COLOR_ATTACHMENT1 - case 'color2': case 2: return gl.COLOR_ATTACHMENT2 - case 'color3': case 3: return gl.COLOR_ATTACHMENT3 - case 'color4': case 4: return gl.COLOR_ATTACHMENT4 - case 'color5': case 5: return gl.COLOR_ATTACHMENT5 - case 'color6': case 6: return gl.COLOR_ATTACHMENT6 - case 'color7': case 7: return gl.COLOR_ATTACHMENT7 + case 'color1': case 1: return extensions.drawBuffers.COLOR_ATTACHMENT1 + case 'color2': case 2: return extensions.drawBuffers.COLOR_ATTACHMENT2 + case 'color3': case 3: return extensions.drawBuffers.COLOR_ATTACHMENT3 + case 'color4': case 4: return extensions.drawBuffers.COLOR_ATTACHMENT4 + case 'color5': case 5: return extensions.drawBuffers.COLOR_ATTACHMENT5 + case 'color6': case 6: return extensions.drawBuffers.COLOR_ATTACHMENT6 + case 'color7': case 7: return extensions.drawBuffers.COLOR_ATTACHMENT7 } } throw new Error('unknown texture attachment') @@ -139,19 +141,24 @@ export interface Texture { export type TextureId = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 export type TextureValues = { [k: string]: ValueCell<TextureValueType> } -export type Textures = { [k: string]: Texture } +export type Textures = [string, Texture][] export function createTexture(ctx: WebGLContext, kind: TextureKind, _format: TextureFormat, _type: TextureType, _filter: TextureFilter): Texture { const id = getNextTextureId() - const { gl } = ctx + const { gl, stats } = ctx const texture = gl.createTexture() if (texture === null) { throw new Error('Could not create WebGL texture') } + // check texture kind and type compatability + if ((kind.endsWith('float32') && _type !== 'float') || kind.endsWith('uint8') && _type !== 'ubyte') { + throw new Error(`texture kind '${kind}' and type '${_type}' are incompatible`) + } + const target = getTarget(ctx, kind) const filter = getFilter(ctx, _filter) - const format = getFormat(ctx, _format) + const format = getFormat(ctx, _format, _type) const internalFormat = getInternalFormat(ctx, _format, _type) const type = getType(ctx, _type) @@ -166,7 +173,7 @@ export function createTexture(ctx: WebGLContext, kind: TextureKind, _format: Tex let width = 0, height = 0, depth = 0 let destroyed = false - ctx.textureCount += 1 + stats.textureCount += 1 return { id, @@ -184,8 +191,8 @@ export function createTexture(ctx: WebGLContext, kind: TextureKind, _format: Tex gl.bindTexture(target, texture) if (target === gl.TEXTURE_2D) { gl.texImage2D(target, 0, internalFormat, width, height, 0, format, type, null) - } else if (target === (gl as WebGL2RenderingContext).TEXTURE_3D && depth !== undefined) { - (gl as WebGL2RenderingContext).texImage3D(target, 0, internalFormat, width, height, depth, 0, format, type, null) + } else if (isWebGL2(gl) && target === gl.TEXTURE_3D && depth !== undefined) { + gl.texImage3D(target, 0, internalFormat, width, height, depth, 0, format, type, null) } else { throw new Error('unknown texture target') } @@ -200,10 +207,10 @@ export function createTexture(ctx: WebGLContext, kind: TextureKind, _format: Tex const { array, width: _width, height: _height } = data as TextureImage<any> width = _width, height = _height; gl.texImage2D(target, 0, internalFormat, width, height, 0, format, type, array) - } else if (target === (gl as WebGL2RenderingContext).TEXTURE_3D) { + } else if (isWebGL2(gl) && target === gl.TEXTURE_3D) { const { array, width: _width, height: _height, depth: _depth } = data as TextureVolume<any> - width = _width, height = _height, depth = _depth; - (gl as WebGL2RenderingContext).texImage3D(target, 0, internalFormat, width, height, depth, 0, format, type, array) + width = _width, height = _height, depth = _depth + gl.texImage3D(target, 0, internalFormat, width, height, depth, 0, format, type, array) } else { throw new Error('unknown texture target') } @@ -238,22 +245,22 @@ export function createTexture(ctx: WebGLContext, kind: TextureKind, _format: Tex if (destroyed) return gl.deleteTexture(texture) destroyed = true - ctx.textureCount -= 1 + stats.textureCount -= 1 } } } export function createTextures(ctx: WebGLContext, schema: RenderableSchema, values: TextureValues) { - const textures: Textures = {} - Object.keys(schema).forEach((k, i) => { + const textures: Textures = [] + Object.keys(schema).forEach(k => { const spec = schema[k] if (spec.type === 'texture') { if (spec.kind === 'texture') { - textures[k] = values[k].ref.value as Texture + textures[textures.length] = [k, values[k].ref.value as Texture] } else { const texture = createTexture(ctx, spec.kind, spec.format, spec.dataType, spec.filter) texture.load(values[k].ref.value as TextureImage<any> | TextureVolume<any>) - textures[k] = texture + textures[textures.length] = [k, texture] } } }) diff --git a/src/mol-gl/webgl/uniform.ts b/src/mol-gl/webgl/uniform.ts index a979a64fee9228f01f052d39aa94f067a2289807..bdc7bccc1c2ed3dd7e070ceca149d7e75e780134 100644 --- a/src/mol-gl/webgl/uniform.ts +++ b/src/mol-gl/webgl/uniform.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> */ @@ -23,6 +23,20 @@ export type UniformKind = keyof UniformKindValue export type UniformType = number | Vec2 | Vec3 | Vec4 | Mat3 | Mat4 export type UniformValues = { [k: string]: ValueCell<UniformType> } +export type UniformsList = [string, ValueCell<UniformType>][] + +export function getUniformType(gl: GLRenderingContext, kind: UniformKind) { + switch (kind) { + case 'f': return gl.FLOAT + case 'i': return gl.INT + case 'v2': return gl.FLOAT_VEC2 + case 'v3': return gl.FLOAT_VEC3 + case 'v4': return gl.FLOAT_VEC4 + case 'm3': return gl.FLOAT_MAT3 + case 'm4': return gl.FLOAT_MAT4 + default: console.error(`unknown uniform kind '${kind}'`) + } +} export function setUniform(gl: GLRenderingContext, location: WebGLUniformLocation | null, kind: UniformKind, value: any) { switch (kind) { diff --git a/src/mol-gl/webgl/vertex-array.ts b/src/mol-gl/webgl/vertex-array.ts index f6a2315180e2c1cac18bb253fffe65abbd622599..cbb630d2582f9c3b2579bc92785f36932c9c6229 100644 --- a/src/mol-gl/webgl/vertex-array.ts +++ b/src/mol-gl/webgl/vertex-array.ts @@ -6,18 +6,19 @@ import { WebGLContext } from './context'; import { Program } from './program'; -import { AttributeBuffers, ElementsBuffer } from './buffer'; +import { ElementsBuffer, AttributeBuffers } from './buffer'; export function createVertexArray(ctx: WebGLContext, program: Program, attributeBuffers: AttributeBuffers, elementsBuffer?: ElementsBuffer) { const { vertexArrayObject } = ctx.extensions let vertexArray: WebGLVertexArrayObject | null = null if (vertexArrayObject) { vertexArray = vertexArrayObject.createVertexArray() - vertexArrayObject.bindVertexArray(vertexArray) - if (elementsBuffer) elementsBuffer.bind() - program.bindAttributes(attributeBuffers) - ctx.vaoCount += 1 - vertexArrayObject.bindVertexArray(null) + if (vertexArray) { + updateVertexArray(ctx, vertexArray, program, attributeBuffers, elementsBuffer) + ctx.stats.vaoCount += 1 + } else { + console.warn('Could not create WebGL vertex array') + } } return vertexArray } @@ -36,6 +37,6 @@ export function deleteVertexArray(ctx: WebGLContext, vertexArray: WebGLVertexArr const { vertexArrayObject } = ctx.extensions if (vertexArrayObject && vertexArray) { vertexArrayObject.deleteVertexArray(vertexArray) - ctx.vaoCount -= 1 + ctx.stats.vaoCount -= 1 } } \ No newline at end of file diff --git a/src/mol-io/reader/_spec/mol2.spec.ts b/src/mol-io/reader/_spec/mol2.spec.ts index dc88b16043caf6777ca6571656f2c3cfab6eb38f..04570831700fc0db83cae496f660bbf50ef654ef 100644 --- a/src/mol-io/reader/_spec/mol2.spec.ts +++ b/src/mol-io/reader/_spec/mol2.spec.ts @@ -265,10 +265,10 @@ describe('mol2 reader', () => { expect(molecule.num_subst).toBe(0); expect(molecule.num_feat).toBe(0); expect(molecule.num_sets).toBe(0); - expect(molecule.mol_type).toBe("SMALL") - expect(molecule.charge_type).toBe("GASTEIGER"); - expect(molecule.status_bits).toBe(""); - expect(molecule.mol_comment).toBe(""); + expect(molecule.mol_type).toBe('SMALL') + expect(molecule.charge_type).toBe('GASTEIGER'); + expect(molecule.status_bits).toBe(''); + expect(molecule.mol_comment).toBe(''); // required atom fields expect(atoms.count).toBe(26); @@ -277,7 +277,7 @@ describe('mol2 reader', () => { expect(atoms.x.value(0)).toBeCloseTo(1.7394, 0.001); expect(atoms.y.value(0)).toBeCloseTo(-2.1169, 0.0001); expect(atoms.z.value(0)).toBeCloseTo(-1.0893, 0.0001); - expect(atoms.atom_type.value(0)).toBe("O.3"); + expect(atoms.atom_type.value(0)).toBe('O.3'); // optional atom fields expect(atoms.subst_id.value(0)).toBe(1); @@ -316,10 +316,10 @@ describe('mol2 reader', () => { expect(molecule.num_subst).toBe(0); expect(molecule.num_feat).toBe(0); expect(molecule.num_sets).toBe(0); - expect(molecule.mol_type).toBe("SMALL") - expect(molecule.charge_type).toBe("GASTEIGER"); - expect(molecule.status_bits).toBe(""); - expect(molecule.mol_comment).toBe(""); + expect(molecule.mol_type).toBe('SMALL') + expect(molecule.charge_type).toBe('GASTEIGER'); + expect(molecule.status_bits).toBe(''); + expect(molecule.mol_comment).toBe(''); // required atom fields expect(atoms.count).toBe(26); @@ -328,7 +328,7 @@ describe('mol2 reader', () => { expect(atoms.x.value(0)).toBeCloseTo(1.7394, 0.001); expect(atoms.y.value(0)).toBeCloseTo(-2.1169, 0.0001); expect(atoms.z.value(0)).toBeCloseTo(-1.0893, 0.0001); - expect(atoms.atom_type.value(0)).toBe("O.3"); + expect(atoms.atom_type.value(0)).toBe('O.3'); // optional atom fields expect(atoms.subst_id.value(0)).toBe(1); @@ -367,10 +367,10 @@ describe('mol2 reader', () => { expect(molecule.num_subst).toBe(0); expect(molecule.num_feat).toBe(0); expect(molecule.num_sets).toBe(0); - expect(molecule.mol_type).toBe("SMALL") - expect(molecule.charge_type).toBe("GASTEIGER"); - expect(molecule.status_bits).toBe(""); - expect(molecule.mol_comment).toBe(""); + expect(molecule.mol_type).toBe('SMALL') + expect(molecule.charge_type).toBe('GASTEIGER'); + expect(molecule.status_bits).toBe(''); + expect(molecule.mol_comment).toBe(''); // required atom fields expect(atoms.count).toBe(26); @@ -379,7 +379,7 @@ describe('mol2 reader', () => { expect(atoms.x.value(0)).toBeCloseTo(1.7394, 0.001); expect(atoms.y.value(0)).toBeCloseTo(-2.1169, 0.0001); expect(atoms.z.value(0)).toBeCloseTo(-1.0893, 0.0001); - expect(atoms.atom_type.value(0)).toBe("O.3"); + expect(atoms.atom_type.value(0)).toBe('O.3'); // optional atom fields expect(atoms.subst_id.value(0)).toBe(0); diff --git a/src/mol-io/reader/_spec/ply.spec.ts b/src/mol-io/reader/_spec/ply.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..75325914641932f2495716166566fbdc9d084521 --- /dev/null +++ b/src/mol-io/reader/_spec/ply.spec.ts @@ -0,0 +1,152 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import Ply from '../ply/parser' +import { PlyTable, PlyList } from '../ply/schema'; + +const plyString = `ply +format ascii 1.0 +comment file created by MegaMol +element vertex 6 +property float x +property float y +property float z +property uchar red +property uchar green +property uchar blue +property uchar alpha +property float nx +property float ny +property float nz +property int atomid +property uchar contactcount_r +property uchar contactcount_g +property uchar contactcount_b +property uchar contactsteps_r +property uchar contactsteps_g +property uchar contactsteps_b +property uchar hbonds_r +property uchar hbonds_g +property uchar hbonds_b +property uchar hbondsteps_r +property uchar hbondsteps_g +property uchar hbondsteps_b +property uchar molcount_r +property uchar molcount_g +property uchar molcount_b +property uchar spots_r +property uchar spots_g +property uchar spots_b +property uchar rmsf_r +property uchar rmsf_g +property uchar rmsf_b +element face 2 +property list uchar int vertex_index +end_header +130.901 160.016 163.033 90 159 210 255 -0.382 -0.895 -0.231 181 21 100 150 24 102 151 20 100 150 20 100 150 30 106 154 20 100 150 171 196 212 +131.372 159.778 162.83 90 159 210 255 -0.618 -0.776 -0.129 178 21 100 150 24 102 151 20 100 150 20 100 150 30 106 154 20 100 150 141 177 199 +131.682 159.385 163.089 90 159 210 255 -0.773 -0.579 -0.259 180 21 100 150 24 102 151 20 100 150 20 100 150 30 106 154 20 100 150 172 196 212 +131.233 160.386 162.11 90 159 210 255 -0.708 -0.383 -0.594 178 21 100 150 24 102 151 20 100 150 20 100 150 30 106 154 20 100 150 141 177 199 +130.782 160.539 162.415 90 159 210 255 -0.482 -0.459 -0.746 181 21 100 150 24 102 151 20 100 150 20 100 150 30 106 154 20 100 150 171 196 212 +131.482 160.483 161.621 90 159 210 255 -0.832 -0.431 -0.349 179 21 100 150 24 102 151 20 100 150 20 100 150 30 106 154 20 100 150 171 196 212 +3 0 2 1 +3 3 5 4 +` + +const plyCubeString = `ply +format ascii 1.0 +comment test cube +element vertex 24 +property float32 x +property float32 y +property float32 z +property uint32 material_index +element face 6 +property list uint8 int32 vertex_indices +element material 6 +property uint8 red +property uint8 green +property uint8 blue +end_header +-1 -1 -1 0 +1 -1 -1 0 +1 1 -1 0 +-1 1 -1 0 +1 -1 1 1 +-1 -1 1 1 +-1 1 1 1 +1 1 1 1 +1 1 1 2 +1 1 -1 2 +1 -1 -1 2 +1 -1 1 2 +-1 1 -1 3 +-1 1 1 3 +-1 -1 1 3 +-1 -1 -1 3 +-1 1 1 4 +-1 1 -1 4 +1 1 -1 4 +1 1 1 4 +1 -1 1 5 +1 -1 -1 5 +-1 -1 -1 5 +-1 -1 1 5 +4 0 1 2 3 +4 4 5 6 7 +4 8 9 10 11 +4 12 13 14 15 +4 16 17 18 19 +4 20 21 22 23 +255 0 0 +0 255 0 +0 0 255 +255 255 0 +0 255 255 +255 0 255 +` + + +describe('ply reader', () => { + it('basic', async () => { + const parsed = await Ply(plyString).run(); + if (parsed.isError) return; + const plyFile = parsed.result; + + const vertex = plyFile.getElement('vertex') as PlyTable + if (!vertex) return + const x = vertex.getProperty('x') + if (!x) return + expect(x.value(0)).toEqual(130.901) + + const face = plyFile.getElement('face') as PlyList + if (!face) return + expect(face.value(0)).toEqual({ count: 3, entries: [0, 2, 1]}) + expect(face.value(1)).toEqual({ count: 3, entries: [3, 5, 4]}) + + expect.assertions(3) + }); + + it('material', async () => { + const parsed = await Ply(plyCubeString).run(); + if (parsed.isError) return; + const plyFile = parsed.result; + + const vertex = plyFile.getElement('vertex') as PlyTable + if (!vertex) return + expect(vertex.rowCount).toBe(24) + + const face = plyFile.getElement('face') as PlyList + if (!face) return + expect(face.rowCount).toBe(6) + + const material = plyFile.getElement('face') as PlyTable + if (!material) return + expect(face.rowCount).toBe(6) + + expect.assertions(3) + }); +}); \ No newline at end of file diff --git a/src/mol-io/reader/cif/data-model.ts b/src/mol-io/reader/cif/data-model.ts index 2800437dc930bd57af8dafe14913f9d5e15fc105..c5778c7b55a847616c5c36ff57dcf300f9af1926 100644 --- a/src/mol-io/reader/cif/data-model.ts +++ b/src/mol-io/reader/cif/data-model.ts @@ -199,7 +199,7 @@ export namespace CifField { export function ofColumn(column: Column<any>): CifField { const { rowCount, valueKind, areValuesEqual } = column; - + let str: CifField['str'] let int: CifField['int'] let float: CifField['float'] @@ -219,7 +219,6 @@ export namespace CifField { default: throw new Error('unsupported') } - return { __array: void 0, diff --git a/src/mol-io/reader/csv/data-model.ts b/src/mol-io/reader/csv/data-model.ts index 401c7aa2d5855cd530131ac22345bc0052823f9f..7c538467e25bf5eb2143ba569af1af41fe02e3e1 100644 --- a/src/mol-io/reader/csv/data-model.ts +++ b/src/mol-io/reader/csv/data-model.ts @@ -9,12 +9,11 @@ import { CifField as CsvColumn } from '../cif/data-model' export { CsvColumn } export interface CsvFile { - readonly name?: string, readonly table: CsvTable } -export function CsvFile(table: CsvTable, name?: string): CsvFile { - return { name, table }; +export function CsvFile(table: CsvTable): CsvFile { + return { table }; } export interface CsvTable { @@ -27,10 +26,4 @@ export function CsvTable(rowCount: number, columnNames: string[], columns: CsvCo return { rowCount, columnNames: [...columnNames], getColumn(name) { return columns[name]; } }; } -export type CsvColumns = { [name: string]: CsvColumn } - -// export namespace CsvTable { -// export function empty(name: string): Table { -// return { rowCount: 0, name, fieldNames: [], getColumn(name: string) { return void 0; } }; -// }; -// } \ No newline at end of file +export type CsvColumns = { [name: string]: CsvColumn } \ No newline at end of file diff --git a/src/mol-io/reader/ply/parser.ts b/src/mol-io/reader/ply/parser.ts new file mode 100644 index 0000000000000000000000000000000000000000..bd9ce4f2d044a9e0cf62efe7c5f06c87d4258658 --- /dev/null +++ b/src/mol-io/reader/ply/parser.ts @@ -0,0 +1,263 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { ReaderResult as Result } from '../result' +import { Task, RuntimeContext } from 'mol-task' +import { PlyFile, PlyType, PlyElement } from './schema'; +import { Tokenizer, TokenBuilder, Tokens } from '../common/text/tokenizer'; +import { Column } from 'mol-data/db'; +import { TokenColumn } from '../common/text/column/token'; + +interface State { + data: string + tokenizer: Tokenizer + runtimeCtx: RuntimeContext + + comments: string[] + elementSpecs: ElementSpec[] + elements: PlyElement[] +} + +function State(data: string, runtimeCtx: RuntimeContext): State { + const tokenizer = Tokenizer(data) + return { + data, + tokenizer, + runtimeCtx, + + comments: [], + elementSpecs: [], + elements: [] + } +} + +type ColumnProperty = { kind: 'column', type: PlyType, name: string } +type ListProperty = { kind: 'list', countType: PlyType, dataType: PlyType, name: string } +type Property = ColumnProperty | ListProperty + +type TableElementSpec = { kind: 'table', name: string, count: number, properties: ColumnProperty[] } +type ListElementSpec = { kind: 'list', name: string, count: number, property: ListProperty } +type ElementSpec = TableElementSpec | ListElementSpec + +function markHeader(tokenizer: Tokenizer) { + const endHeaderIndex = tokenizer.data.indexOf('end_header', tokenizer.position) + if (endHeaderIndex === -1) throw new Error(`no 'end_header' record found`) + // TODO set `tokenizer.lineNumber` correctly + tokenizer.tokenStart = tokenizer.position + tokenizer.tokenEnd = endHeaderIndex + tokenizer.position = endHeaderIndex + Tokenizer.eatLine(tokenizer) +} + +function parseHeader(state: State) { + const { tokenizer, comments, elementSpecs } = state + + markHeader(tokenizer) + const headerLines = Tokenizer.getTokenString(tokenizer).split(/\r?\n/) + + if (headerLines[0] !== 'ply') throw new Error(`data not starting with 'ply'`) + if (headerLines[1] !== 'format ascii 1.0') throw new Error(`format not 'ascii 1.0'`) + + let currentName: string | undefined + let currentCount: number | undefined + let currentProperties: Property[] | undefined + + + function addCurrentElementSchema() { + if (currentName !== undefined && currentCount !== undefined && currentProperties !== undefined) { + let isList = false + for (let i = 0, il = currentProperties.length; i < il; ++i) { + const p = currentProperties[i] + if (p.kind === 'list') { + isList = true + break + } + } + if (isList && currentProperties.length !== 1) throw new Error('expected single list property') + if (isList) { + elementSpecs.push({ + kind: 'list', + name: currentName, + count: currentCount, + property: currentProperties[0] as ListProperty + }) + } else { + elementSpecs.push({ + kind: 'table', + name: currentName, + count: currentCount, + properties: currentProperties as ColumnProperty[] + }) + } + } + } + + for (let i = 2, il = headerLines.length; i < il; ++i) { + const l = headerLines[i] + const ls = l.split(' ') + if (l.startsWith('comment')) { + comments.push(l.substr(8)) + } else if (l.startsWith('element')) { + addCurrentElementSchema() + currentProperties = [] + currentName = ls[1] + currentCount = parseInt(ls[2]) + } else if (l.startsWith('property')) { + if (currentProperties === undefined) throw new Error(`properties outside of element`) + if (ls[1] === 'list') { + currentProperties.push({ + kind: 'list', + countType: PlyType(ls[2]), + dataType: PlyType(ls[3]), + name: ls[4] + }) + } else { + currentProperties.push({ + kind: 'column', + type: PlyType(ls[1]), + name: ls[2] + }) + } + } else if (l.startsWith('end_header')) { + addCurrentElementSchema() + } else { + console.warn('unknown header line') + } + } +} + +function parseElements(state: State) { + const { elementSpecs } = state + for (let i = 0, il = elementSpecs.length; i < il; ++i) { + const spec = elementSpecs[i] + if (spec.kind === 'table') parseTableElement(state, spec) + else if (spec.kind === 'list') parseListElement(state, spec) + } +} + +function getColumnSchema(type: PlyType): Column.Schema { + switch (type) { + case 'char': case 'uchar': case 'int8': case 'uint8': + case 'short': case 'ushort': case 'int16': case 'uint16': + case 'int': case 'uint': case 'int32': case 'uint32': + return Column.Schema.int + case 'float': case 'double': case 'float32': case 'float64': + return Column.Schema.float + } +} + +function parseTableElement(state: State, spec: TableElementSpec) { + const { elements, tokenizer } = state + const { count, properties } = spec + const propertyCount = properties.length + const propertyNames: string[] = [] + const propertyTypes: PlyType[] = [] + const propertyTokens: Tokens[] = [] + const propertyColumns = new Map<string, Column<number>>() + + for (let i = 0, il = propertyCount; i < il; ++i) { + const tokens = TokenBuilder.create(tokenizer.data, count * 2) + propertyTokens.push(tokens) + } + + for (let i = 0, il = count; i < il; ++i) { + for (let j = 0, jl = propertyCount; j < jl; ++j) { + Tokenizer.skipWhitespace(tokenizer) + Tokenizer.markStart(tokenizer) + Tokenizer.eatValue(tokenizer) + TokenBuilder.addUnchecked(propertyTokens[j], tokenizer.tokenStart, tokenizer.tokenEnd) + } + } + + for (let i = 0, il = propertyCount; i < il; ++i) { + const { type, name } = properties[i] + const column = TokenColumn(propertyTokens[i], getColumnSchema(type)) + propertyNames.push(name) + propertyTypes.push(type) + propertyColumns.set(name, column) + } + + elements.push({ + kind: 'table', + rowCount: count, + propertyNames, + propertyTypes, + getProperty: (name: string) => propertyColumns.get(name) + }) +} + +function parseListElement(state: State, spec: ListElementSpec) { + const { elements, tokenizer } = state + const { count, property } = spec + + // initial tokens size assumes triangle index data + const tokens = TokenBuilder.create(tokenizer.data, count * 2 * 3) + + const offsets = new Uint32Array(count + 1) + let entryCount = 0 + + for (let i = 0, il = count; i < il; ++i) { + // skip over row entry count as it is determined by line break + Tokenizer.skipWhitespace(tokenizer) + Tokenizer.eatValue(tokenizer) + + while (Tokenizer.skipWhitespace(tokenizer) !== 10) { + ++entryCount + Tokenizer.markStart(tokenizer) + Tokenizer.eatValue(tokenizer) + TokenBuilder.addToken(tokens, tokenizer) + } + offsets[i + 1] = entryCount + } + + // console.log(tokens.indices) + // console.log(offsets) + + /** holds row value entries transiently */ + const listValue = { + entries: [] as number[], + count: 0 + } + + const column = TokenColumn(tokens, getColumnSchema(property.dataType)) + + elements.push({ + kind: 'list', + rowCount: count, + name: property.name, + type: property.dataType, + value: (row: number) => { + const start = offsets[row] + const end = offsets[row + 1] + for (let i = start; i < end; ++i) { + listValue.entries[i - start] = column.value(i) + } + listValue.count = end - start + return listValue + } + }) +} + +async function parseInternal(data: string, ctx: RuntimeContext): Promise<Result<PlyFile>> { + const state = State(data, ctx); + ctx.update({ message: 'Parsing...', current: 0, max: data.length }); + parseHeader(state) + // console.log(state.comments) + // console.log(JSON.stringify(state.elementSpecs, undefined, 4)) + parseElements(state) + const { elements, elementSpecs, comments } = state + const elementNames = elementSpecs.map(s => s.name) + const result = PlyFile(elements, elementNames, comments) + return Result.success(result); +} + +export function parse(data: string) { + return Task.create<Result<PlyFile>>('Parse PLY', async ctx => { + return await parseInternal(data, ctx) + }) +} + +export default parse; \ No newline at end of file diff --git a/src/mol-io/reader/ply/schema.ts b/src/mol-io/reader/ply/schema.ts new file mode 100644 index 0000000000000000000000000000000000000000..c5fbcb995ae825d0ea7f829ffd3ea96bca73f715 --- /dev/null +++ b/src/mol-io/reader/ply/schema.ts @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { Column } from 'mol-data/db'; + +// http://paulbourke.net/dataformats/ply/ +// https://en.wikipedia.org/wiki/PLY_(file_format) + +export const PlyTypeByteLength = { + 'char': 1, + 'uchar': 1, + 'short': 2, + 'ushort': 2, + 'int': 4, + 'uint': 4, + 'float': 4, + 'double': 8, + + 'int8': 1, + 'uint8': 1, + 'int16': 2, + 'uint16': 2, + 'int32': 4, + 'uint32': 4, + 'float32': 4, + 'float64': 8 +} +export type PlyType = keyof typeof PlyTypeByteLength +export const PlyTypes = new Set(Object.keys(PlyTypeByteLength)) +export function PlyType(str: string) { + if (!PlyTypes.has(str)) throw new Error(`unknown ply type '${str}'`) + return str as PlyType +} + +export interface PlyFile { + readonly comments: ReadonlyArray<string> + readonly elementNames: ReadonlyArray<string> + getElement(name: string): PlyElement | undefined +} + +export function PlyFile(elements: PlyElement[], elementNames: string[], comments: string[]): PlyFile { + const elementMap = new Map<string, PlyElement>() + for (let i = 0, il = elementNames.length; i < il; ++i) { + elementMap.set(elementNames[i], elements[i]) + } + return { + comments, + elementNames, + getElement: (name: string) => { + return elementMap.get(name) + } + }; +} + +export type PlyElement = PlyTable | PlyList + +export interface PlyTable { + readonly kind: 'table' + readonly rowCount: number + readonly propertyNames: ReadonlyArray<string> + readonly propertyTypes: ReadonlyArray<PlyType> + getProperty(name: string): Column<number> | undefined +} + +export interface PlyListValue { + readonly entries: ArrayLike<number> + readonly count: number +} + +export interface PlyList { + readonly kind: 'list' + readonly rowCount: number, + readonly name: string, + readonly type: PlyType, + value: (row: number) => PlyListValue +} \ No newline at end of file diff --git a/src/mol-math/geometry/common.ts b/src/mol-math/geometry/common.ts index 674e3ede99369615562c0a8056b8735e43390c05..61d34da38fe51f482b96d122c0cb02843e1e8ebe 100644 --- a/src/mol-math/geometry/common.ts +++ b/src/mol-math/geometry/common.ts @@ -1,12 +1,12 @@ /** - * 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> * @author Alexander Rose <alexander.rose@weirdbyte.de> */ import { OrderedSet } from 'mol-data/int' -import { Mat4, Tensor, Vec3 } from '../linear-algebra'; +import { Mat4, Tensor, Vec3, Vec2 } from '../linear-algebra'; import { Box3D } from '../geometry'; import { Texture } from 'mol-gl/webgl/texture'; @@ -30,5 +30,15 @@ export type DensityTextureData = { transform: Mat4, texture: Texture, bbox: Box3D, - gridDimension: Vec3 + gridDim: Vec3, + gridTexDim: Vec3 + gridTexScale: Vec2 +} + +export function fillGridDim(length: number, start: number, step: number) { + const a = new Float32Array(length) + for (let i = 0; i < a.length; i++) { + a[i] = start + (step * i) + } + return a } \ No newline at end of file diff --git a/src/mol-math/geometry/gaussian-density.ts b/src/mol-math/geometry/gaussian-density.ts index 434ba158c1dbc59d5161c7e4d3fe07a12919c448..e152830cc6117c7b52ca146a3c836241ecaafbb2 100644 --- a/src/mol-math/geometry/gaussian-density.ts +++ b/src/mol-math/geometry/gaussian-density.ts @@ -1,20 +1,24 @@ /** - * 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> */ import { Box3D } from '../geometry'; -import { Vec3 } from '../linear-algebra'; import { RuntimeContext, Task } from 'mol-task'; import { PositionData, DensityData } from './common'; import { GaussianDensityCPU } from './gaussian-density/cpu'; import { WebGLContext } from 'mol-gl/webgl/context'; +import { Texture } from 'mol-gl/webgl/texture'; +import { GaussianDensityTexture2d, GaussianDensityTexture3d } from './gaussian-density/gpu'; -// import { GaussianDensityGPU } from './gaussian-density/gpu'; +// import { GaussianDensityGPU, GaussianDensityTexture } from './gaussian-density/gpu'; const GaussianDensityGPU = typeof document !== 'undefined' ? (require('./gaussian-density/gpu') as typeof import('./gaussian-density/gpu')).GaussianDensityGPU : void 0; +const GaussianDensityTexture = typeof document !== 'undefined' + ? (require('./gaussian-density/gpu') as typeof import('./gaussian-density/gpu')).GaussianDensityTexture + : void 0; export const DefaultGaussianDensityGPUProps = { resolution: 1, @@ -29,17 +33,9 @@ export const DefaultGaussianDensityProps = { } export type GaussianDensityProps = typeof DefaultGaussianDensityProps -export function getDelta(box: Box3D, resolution: number) { - const extent = Vec3.sub(Vec3.zero(), box.max, box.min) - const size = Vec3.zero() - Vec3.ceil(size, Vec3.scale(size, extent, resolution)) - const delta = Vec3.div(Vec3.zero(), extent, size) - return delta -} - -export function computeGaussianDensity(position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityProps) { +export function computeGaussianDensity(position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityProps, webgl?: WebGLContext) { return Task.create('Gaussian Density', async ctx => { - return await GaussianDensity(ctx, position, box, radius, props) + return await GaussianDensity(ctx, position, box, radius, props, webgl) }); } @@ -47,8 +43,29 @@ export async function GaussianDensity(ctx: RuntimeContext, position: PositionDat if (props.useGpu) { if (!GaussianDensityGPU) throw 'GPU computation not supported on this platform'; if (!webgl) throw 'No WebGL context provided'; - return await GaussianDensityGPU(ctx, position, box, radius, props, webgl) + return GaussianDensityGPU(position, box, radius, props, webgl) } else { return await GaussianDensityCPU(ctx, position, box, radius, props) } +} + +export function computeGaussianDensityTexture(position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, webgl: WebGLContext, texture?: Texture) { + return _computeGaussianDensityTexture(webgl.isWebGL2 ? '3d' : '2d', position, box, radius, props, webgl, texture) +} + +export function computeGaussianDensityTexture2d(position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, webgl: WebGLContext, texture?: Texture) { + return _computeGaussianDensityTexture('2d', position, box, radius, props, webgl, texture) +} + +export function computeGaussianDensityTexture3d(position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, webgl: WebGLContext, texture?: Texture) { + return _computeGaussianDensityTexture('2d', position, box, radius, props, webgl, texture) +} + +function _computeGaussianDensityTexture(type: '2d' | '3d', position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, webgl: WebGLContext, texture?: Texture) { + if (!GaussianDensityTexture) throw 'GPU computation not supported on this platform'; + return Task.create('Gaussian Density', async ctx => { + return type === '2d' ? + GaussianDensityTexture2d(webgl, position, box, radius, props, texture) : + GaussianDensityTexture3d(webgl, position, box, radius, props, texture); + }); } \ No newline at end of file diff --git a/src/mol-math/geometry/gaussian-density/cpu.ts b/src/mol-math/geometry/gaussian-density/cpu.ts index a52619139e28ff2ba9b39989ea9468b32c326133..3dedac18f884b9a1acc9891f4739bff7428201da 100644 --- a/src/mol-math/geometry/gaussian-density/cpu.ts +++ b/src/mol-math/geometry/gaussian-density/cpu.ts @@ -1,44 +1,37 @@ /** - * 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> */ -import { Box3D } from '../../geometry'; +import { Box3D, fillGridDim } from '../../geometry'; import { Vec3, Mat4, Tensor } from '../../linear-algebra'; import { RuntimeContext } from 'mol-task'; import { PositionData, DensityData } from '../common'; import { OrderedSet } from 'mol-data/int'; -import { GaussianDensityProps, getDelta } from '../gaussian-density'; +import { GaussianDensityProps } from '../gaussian-density'; export async function GaussianDensityCPU(ctx: RuntimeContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityProps): Promise<DensityData> { const { resolution, radiusOffset, smoothness } = props + const scaleFactor = 1 / resolution const { indices, x, y, z } = position const n = OrderedSet.size(indices) - - const v = Vec3.zero() - const p = Vec3.zero() + const radii = new Float32Array(n) let maxRadius = 0 for (let i = 0; i < n; ++i) { const r = radius(OrderedSet.getAt(indices, i)) + radiusOffset if (maxRadius < r) maxRadius = r - - if (i % 10000 === 0 && ctx.shouldUpdate) { - await ctx.update({ message: 'calculating max radius', current: i, max: n }) - } + radii[i] = r } const pad = maxRadius * 2 + resolution - const expandedBox = Box3D.expand(Box3D.empty(), box, Vec3.create(pad, pad, pad)) - const extent = Vec3.sub(Vec3.zero(), expandedBox.max, expandedBox.min) + const expandedBox = Box3D.expand(Box3D(), box, Vec3.create(pad, pad, pad)); const min = expandedBox.min - - const delta = getDelta(Box3D.expand(Box3D.empty(), box, Vec3.create(pad, pad, pad)), resolution) - const dim = Vec3.zero() - Vec3.ceil(dim, Vec3.mul(dim, extent, delta)) - // console.log('grid dim cpu', dim) + const scaledBox = Box3D.scale(Box3D(), expandedBox, scaleFactor) + const dim = Box3D.size(Vec3(), scaledBox) + Vec3.ceil(dim, dim) const space = Tensor.Space(dim, [0, 1, 2], Float32Array) const data = space.create() @@ -47,68 +40,91 @@ export async function GaussianDensityCPU(ctx: RuntimeContext, position: Position const idData = space.create() const idField = Tensor.create(space, idData) - const densData = space.create() - - const c = Vec3.zero() - - const alpha = smoothness - - const _r2 = maxRadius * 2 - const _radius2 = Vec3.create(_r2, _r2, _r2) - Vec3.mul(_radius2, _radius2, delta) - const updateChunk = Math.ceil(10000 / (_radius2[0] * _radius2[1] * _radius2[2])) + const [ dimX, dimY, dimZ ] = dim + const iu = dimZ, iv = dimY, iuv = iu * iv - const beg = Vec3.zero() - const end = Vec3.zero() + const gridx = fillGridDim(dim[0], min[0], resolution) + const gridy = fillGridDim(dim[1], min[1], resolution) + const gridz = fillGridDim(dim[2], min[2], resolution) - const gridPad = 1 / Math.max(...delta) + const densData = space.create() - // console.time('gaussian density cpu') - for (let i = 0; i < n; ++i) { - const j = OrderedSet.getAt(indices, i) - - Vec3.set(v, x[j], y[j], z[j]) - - Vec3.sub(v, v, min) - Vec3.mul(c, v, delta) - - const rad = radius(j) + radiusOffset - const rSq = rad * rad - - const r2 = radiusOffset + rad * 2 + gridPad - const rad2 = Vec3.create(r2, r2, r2) - Vec3.mul(rad2, rad2, delta) - const r2sq = r2 * r2 - - const [ begX, begY, begZ ] = Vec3.floor(beg, Vec3.sub(beg, c, rad2)) - const [ endX, endY, endZ ] = Vec3.ceil(end, Vec3.add(end, c, rad2)) - - for (let xi = begX; xi < endX; ++xi) { - for (let yi = begY; yi < endY; ++yi) { - for (let zi = begZ; zi < endZ; ++zi) { - Vec3.set(p, xi, yi, zi) - Vec3.div(p, p, delta) - const distSq = Vec3.squaredDistance(p, v) - if (distSq <= r2sq) { - const dens = Math.exp(-alpha * (distSq / rSq)) - space.add(data, xi, yi, zi, dens) - if (dens > space.get(densData, xi, yi, zi)) { - space.set(densData, xi, yi, zi, dens) - space.set(idData, xi, yi, zi, i) + const alpha = smoothness + const updateChunk = Math.ceil(100000 / ((Math.pow(Math.pow(maxRadius, 3), 3) * scaleFactor))) + + function accumulateRange(begI: number, endI: number) { + for (let i = begI; i < endI; ++i) { + const j = OrderedSet.getAt(indices, i) + const vx = x[j], vy = y[j], vz = z[j] + + const rad = radii[i] + const rSq = rad * rad + const rSqInv = 1 / rSq + + const r2 = rad * 2 + const r2sq = r2 * r2 + + // Number of grid points, round this up... + const ng = Math.ceil(r2 * scaleFactor) + + // Center of the atom, mapped to grid points (take floor) + const iax = Math.floor(scaleFactor * (vx - min[0])) + const iay = Math.floor(scaleFactor * (vy - min[1])) + const iaz = Math.floor(scaleFactor * (vz - min[2])) + + // Extents of grid to consider for this atom + const begX = Math.max(0, iax - ng) + const begY = Math.max(0, iay - ng) + const begZ = Math.max(0, iaz - ng) + + // Add two to these points: + // - iax are floor'd values so this ensures coverage + // - these are loop limits (exclusive) + const endX = Math.min(dimX, iax + ng + 2) + const endY = Math.min(dimY, iay + ng + 2) + const endZ = Math.min(dimZ, iaz + ng + 2) + + for (let xi = begX; xi < endX; ++xi) { + const dx = gridx[xi] - vx + const xIdx = xi * iuv + for (let yi = begY; yi < endY; ++yi) { + const dy = gridy[yi] - vy + const dxySq = dx * dx + dy * dy + const xyIdx = yi * iu + xIdx + for (let zi = begZ; zi < endZ; ++zi) { + const dz = gridz[zi] - vz + const dSq = dxySq + dz * dz + if (dSq <= r2sq) { + const dens = Math.exp(-alpha * (dSq * rSqInv)) + const idx = zi + xyIdx + data[idx] += dens + if (dens > densData[idx]) { + densData[idx] = dens + idData[idx] = i + } } } } } } + } - if (i % updateChunk === 0 && ctx.shouldUpdate) { - await ctx.update({ message: 'filling density grid', current: i, max: n }) + async function accumulate() { + for (let i = 0; i < n; i += updateChunk) { + accumulateRange(i, Math.min(i + updateChunk, n)) + + if (ctx.shouldUpdate) { + await ctx.update({ message: 'filling density grid', current: i, max: n }) + } } } + + // console.time('gaussian density cpu') + await accumulate() // console.timeEnd('gaussian density cpu') const transform = Mat4.identity() - Mat4.fromScaling(transform, Vec3.inverse(Vec3.zero(), delta)) + Mat4.fromScaling(transform, Vec3.create(resolution, resolution, resolution)) Mat4.setTranslation(transform, expandedBox.min) return { field, idField, transform } diff --git a/src/mol-math/geometry/gaussian-density/gpu.ts b/src/mol-math/geometry/gaussian-density/gpu.ts index 2f91bcd1af1f8228fc22c7979abb0a1493b63a13..80f6314a527711c92b14ad04a847b9a558b355d2 100644 --- a/src/mol-math/geometry/gaussian-density/gpu.ts +++ b/src/mol-math/geometry/gaussian-density/gpu.ts @@ -1,86 +1,142 @@ /** - * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> * @author Michael Krone <michael.krone@uni-tuebingen.de> */ -import { RuntimeContext } from 'mol-task' import { PositionData, DensityData, DensityTextureData } from '../common' import { Box3D } from '../../geometry' -import { GaussianDensityGPUProps, getDelta } from '../gaussian-density' +import { GaussianDensityGPUProps } from '../gaussian-density' import { OrderedSet } from 'mol-data/int' -import { Vec3, Tensor, Mat4 } from '../../linear-algebra' -import { GaussianDensityValues } from 'mol-gl/renderable/gaussian-density' +import { Vec3, Tensor, Mat4, Vec2 } from '../../linear-algebra' import { ValueCell } from 'mol-util' -import { RenderableState, Renderable } from 'mol-gl/renderable' -import { createRenderable, createRenderObject } from 'mol-gl/render-object' +import { createComputeRenderable, ComputeRenderable } from 'mol-gl/renderable' import { WebGLContext } from 'mol-gl/webgl/context'; import { createTexture, Texture } from 'mol-gl/webgl/texture'; -import { GLRenderingContext } from 'mol-gl/webgl/compat'; import { decodeFloatRGB } from 'mol-util/float-packing'; +import { ShaderCode } from 'mol-gl/shader-code'; +import { createComputeRenderItem } from 'mol-gl/webgl/render-item'; +import { ValueSpec, AttributeSpec, UniformSpec, TextureSpec, DefineSpec, Values } from 'mol-gl/renderable/schema'; + +export const GaussianDensitySchema = { + drawCount: ValueSpec('number'), + instanceCount: ValueSpec('number'), + + aRadius: AttributeSpec('float32', 1, 0), + aPosition: AttributeSpec('float32', 3, 0), + aGroup: AttributeSpec('float32', 1, 0), + + uCurrentSlice: UniformSpec('f'), + uCurrentX: UniformSpec('f'), + uCurrentY: UniformSpec('f'), + uBboxMin: UniformSpec('v3', true), + uBboxSize: UniformSpec('v3', true), + uGridDim: UniformSpec('v3', true), + uGridTexDim: UniformSpec('v3', true), + uGridTexScale: UniformSpec('v2', true), + uAlpha: UniformSpec('f', true), + uResolution: UniformSpec('f', true), + tMinDistanceTex: TextureSpec('texture', 'rgba', 'float', 'nearest'), + + dGridTexType: DefineSpec('string', ['2d', '3d']), + dCalcType: DefineSpec('string', ['density', 'minDistance', 'groupId']), +} + +export const GaussianDensityShaderCode = ShaderCode( + require('mol-gl/shader/gaussian-density.vert').default, + require('mol-gl/shader/gaussian-density.frag').default, + { standardDerivatives: false, fragDepth: false } +) /** name for shared framebuffer used for gpu gaussian surface operations */ -const FramebufferName = 'gaussian-density-gpu' +const FramebufferName = 'gaussian-density' -export async function GaussianDensityGPU(ctx: RuntimeContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, webgl: WebGLContext): Promise<DensityData> { +export function GaussianDensityGPU(position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, webgl: WebGLContext): DensityData { // always use texture2d when the gaussian density needs to be downloaded from the GPU, // it's faster than texture3d // console.time('GaussianDensityTexture2d') - const { scale, bbox, texture, dim } = await GaussianDensityTexture2d(ctx, webgl, position, box, radius, props) + const { scale, bbox, texture, gridDim, gridTexDim } = calcGaussianDensityTexture2d(webgl, position, box, radius, props) + // webgl.waitForGpuCommandsCompleteSync() // console.timeEnd('GaussianDensityTexture2d') - const { field, idField } = await fieldFromTexture2d(webgl, texture, dim) + const { field, idField } = fieldFromTexture2d(webgl, texture, gridDim, gridTexDim) - const transform = Mat4.identity() - Mat4.fromScaling(transform, scale) - Mat4.setTranslation(transform, bbox.min) + return { field, idField, transform: getTransform(scale, bbox) } +} + +export function GaussianDensityTexture(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, oldTexture?: Texture): DensityTextureData { + return webgl.isWebGL2 ? + GaussianDensityTexture3d(webgl, position, box, radius, props, oldTexture) : + GaussianDensityTexture2d(webgl, position, box, radius, props, oldTexture) +} + +export function GaussianDensityTexture2d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, oldTexture?: Texture): DensityTextureData { + return finalizeGaussianDensityTexture(calcGaussianDensityTexture2d(webgl, position, box, radius, props, oldTexture)) +} - return { field, idField, transform } +export function GaussianDensityTexture3d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, oldTexture?: Texture): DensityTextureData { + return finalizeGaussianDensityTexture(calcGaussianDensityTexture3d(webgl, position, box, radius, props, oldTexture)) } -export async function GaussianDensityTexture(ctx: RuntimeContext, webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, oldTexture?: Texture): Promise<DensityTextureData> { - // console.time(`GaussianDensityTexture, ${webgl.isWebGL2 ? '3d' : '2d'}`) - const { texture, scale, bbox, dim } = webgl.isWebGL2 ? - await GaussianDensityTexture3d(ctx, webgl, position, box, radius, props, oldTexture) : - await GaussianDensityTexture2d(ctx, webgl, position, box, radius, props, oldTexture) - // console.timeEnd(`GaussianDensityTexture, ${webgl.isWebGL2 ? '3d' : '2d'}`) +function finalizeGaussianDensityTexture({ texture, scale, bbox, gridDim, gridTexDim, gridTexScale }: GaussianDensityTextureData): DensityTextureData { + return { transform: getTransform(scale, bbox), texture, bbox, gridDim, gridTexDim, gridTexScale } +} +function getTransform(scale: Vec3, bbox: Box3D) { const transform = Mat4.identity() Mat4.fromScaling(transform, scale) Mat4.setTranslation(transform, bbox.min) - - return { transform, texture, bbox, gridDimension: dim } + return transform } // -async function GaussianDensityTexture2d(ctx: RuntimeContext, webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, texture?: Texture) { +type GaussianDensityTextureData = { + texture: Texture, + scale: Vec3, + bbox: Box3D, + gridDim: Vec3, + gridTexDim: Vec3 + gridTexScale: Vec2 +} + +function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, texture?: Texture): GaussianDensityTextureData { const { smoothness } = props - const { drawCount, positions, radii, groups, delta, expandedBox, dim } = await prepareGaussianDensityData(ctx, position, box, radius, props) + const { drawCount, positions, radii, groups, scale, expandedBox, dim } = prepareGaussianDensityData(position, box, radius, props) const [ dx, dy, dz ] = dim - const { texDimX, texDimY, texCols } = getTexture2dSize(webgl.maxTextureSize, dim) + const { texDimX, texDimY, texCols, powerOfTwoSize } = getTexture2dSize(dim) + // console.log({ texDimX, texDimY, texCols, powerOfTwoSize, dim }) + const gridTexDim = Vec3.create(texDimX, texDimY, 0) + const gridTexScale = Vec2.create(texDimX / powerOfTwoSize, texDimY / powerOfTwoSize) - const minDistanceTexture = createTexture(webgl, 'image-uint8', 'rgba', 'ubyte', 'nearest') - minDistanceTexture.define(texDimX, texDimY) + const minDistanceTexture = createTexture(webgl, 'image-float32', 'rgba', 'float', 'nearest') + minDistanceTexture.define(powerOfTwoSize, powerOfTwoSize) - const renderObject = getGaussianDensityRenderObject(webgl, drawCount, positions, radii, groups, minDistanceTexture, expandedBox, dim, smoothness) - const renderable = createRenderable(webgl, renderObject) + const renderable = getGaussianDensityRenderable(webgl, drawCount, positions, radii, groups, minDistanceTexture, expandedBox, dim, gridTexDim, gridTexScale, smoothness, props.resolution) // - const { gl, framebufferCache } = webgl - const { uCurrentSlice, uCurrentX, uCurrentY } = renderObject.values + const { gl, framebufferCache, state } = webgl + const { uCurrentSlice, uCurrentX, uCurrentY } = renderable.values - const framebuffer = framebufferCache.get(webgl, FramebufferName).value + const framebuffer = framebufferCache.get(FramebufferName).value framebuffer.bind() - setRenderingDefaults(gl) + setRenderingDefaults(webgl) + + if (!texture) { + texture = createTexture(webgl, 'image-float32', 'rgba', 'float', 'nearest') + texture.define(powerOfTwoSize, powerOfTwoSize) + } else if (texture.width !== powerOfTwoSize || texture.height !== powerOfTwoSize) { + texture.define(powerOfTwoSize, powerOfTwoSize) + } - if (!texture) texture = createTexture(webgl, 'image-uint8', 'rgba', 'ubyte', 'linear') - texture.define(texDimX, texDimY) + // console.log(renderable) - function render(fbTex: Texture) { + function render(fbTex: Texture, clear: boolean) { + state.currentRenderItemId = -1 fbTex.attachFramebuffer(framebuffer, 0) + if (clear) gl.clear(gl.COLOR_BUFFER_BIT) let currCol = 0 let currY = 0 let currX = 0 @@ -89,61 +145,63 @@ async function GaussianDensityTexture2d(ctx: RuntimeContext, webgl: WebGLContext currCol -= texCols currY += dy currX = 0 + ValueCell.update(uCurrentY, currY) } - gl.viewport(currX, currY, dx, dy) - ValueCell.update(uCurrentSlice, i) + // console.log({ i, currX, currY }) ValueCell.update(uCurrentX, currX) - ValueCell.update(uCurrentY, currY) - renderable.render('draw') + ValueCell.update(uCurrentSlice, i) + gl.viewport(currX, currY, dx, dy) + renderable.render() ++currCol currX += dx } + gl.finish() } - setupMinDistanceRendering(webgl, renderable) - render(minDistanceTexture) - setupDensityRendering(webgl, renderable) - render(texture) + render(texture, true) + + setupMinDistanceRendering(webgl, renderable) + render(minDistanceTexture, true) setupGroupIdRendering(webgl, renderable) - render(texture) + render(texture, false) - if (ctx.shouldUpdate) await ctx.update({ message: 'gpu gaussian density calculation' }) - await webgl.waitForGpuCommandsCompleteSync() + // printTexture(webgl, texture, 1) - return { texture, scale: Vec3.inverse(Vec3.zero(), delta), bbox: expandedBox, dim } + return { texture, scale, bbox: expandedBox, gridDim: dim, gridTexDim, gridTexScale } } -async function GaussianDensityTexture3d(ctx: RuntimeContext, webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, texture?: Texture) { +function calcGaussianDensityTexture3d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, texture?: Texture): GaussianDensityTextureData { const { smoothness } = props - const { drawCount, positions, radii, groups, delta, expandedBox, dim } = await prepareGaussianDensityData(ctx, position, box, radius, props) + const { drawCount, positions, radii, groups, scale, expandedBox, dim } = prepareGaussianDensityData(position, box, radius, props) const [ dx, dy, dz ] = dim - const minDistanceTexture = createTexture(webgl, 'volume-uint8', 'rgba', 'ubyte', 'nearest') + const minDistanceTexture = createTexture(webgl, 'volume-float32', 'rgba', 'float', 'nearest') minDistanceTexture.define(dx, dy, dz) - const renderObject = getGaussianDensityRenderObject(webgl, drawCount, positions, radii, groups, minDistanceTexture, expandedBox, dim, smoothness) - const renderable = createRenderable(webgl, renderObject) + const gridTexScale = Vec2.create(1, 1) + + const renderable = getGaussianDensityRenderable(webgl, drawCount, positions, radii, groups, minDistanceTexture, expandedBox, dim, dim, gridTexScale, smoothness, props.resolution) // const { gl, framebufferCache } = webgl - const { uCurrentSlice } = renderObject.values + const { uCurrentSlice } = renderable.values - const framebuffer = framebufferCache.get(webgl, FramebufferName).value + const framebuffer = framebufferCache.get(FramebufferName).value framebuffer.bind() - setRenderingDefaults(gl) + setRenderingDefaults(webgl) gl.viewport(0, 0, dx, dy) - if (!texture) texture = createTexture(webgl, 'volume-uint8', 'rgba', 'ubyte', 'linear') + if (!texture) texture = createTexture(webgl, 'volume-float32', 'rgba', 'float', 'nearest') texture.define(dx, dy, dz) function render(fbTex: Texture) { for (let i = 0; i < dz; ++i) { ValueCell.update(uCurrentSlice, i) fbTex.attachFramebuffer(framebuffer, 0, i) - renderable.render('draw') + renderable.render() } } @@ -156,16 +214,14 @@ async function GaussianDensityTexture3d(ctx: RuntimeContext, webgl: WebGLContext setupGroupIdRendering(webgl, renderable) render(texture) - await ctx.update({ message: 'gpu gaussian density calculation' }); - await webgl.waitForGpuCommandsCompleteSync() - - return { texture, scale: Vec3.inverse(Vec3.zero(), delta), bbox: expandedBox, dim } + return { texture, scale, bbox: expandedBox, gridDim: dim, gridTexDim: dim, gridTexScale } } // -async function prepareGaussianDensityData(ctx: RuntimeContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps) { +function prepareGaussianDensityData(position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps) { const { resolution, radiusOffset } = props + const scaleFactor = 1 / resolution const { indices, x, y, z } = position const n = OrderedSet.size(indices) @@ -186,29 +242,23 @@ async function prepareGaussianDensityData(ctx: RuntimeContext, position: Positio if (maxRadius < r) maxRadius = r radii[i] = r groups[i] = i - - if (i % 10000 === 0 && ctx.shouldUpdate) { - await ctx.update({ message: 'preparing density data', current: i, max: n }) - } } - const pad = maxRadius * 2 + resolution - const expandedBox = Box3D.expand(Box3D.empty(), box, Vec3.create(pad, pad, pad)); - const extent = Vec3.sub(Vec3.zero(), expandedBox.max, expandedBox.min) + const pad = maxRadius * 2 + resolution * 4 + const expandedBox = Box3D.expand(Box3D(), box, Vec3.create(pad, pad, pad)); + const scaledBox = Box3D.scale(Box3D(), expandedBox, scaleFactor) + const dim = Box3D.size(Vec3(), scaledBox) + Vec3.ceil(dim, dim) - const delta = getDelta(expandedBox, resolution) - const dim = Vec3.zero() - Vec3.ceil(dim, Vec3.mul(dim, extent, delta)) - // console.log('grid dim gpu', dim) + const scale = Vec3.create(resolution, resolution, resolution) - return { drawCount: n, positions, radii, groups, delta, expandedBox, dim } + return { drawCount: n, positions, radii, groups, scale, expandedBox, dim } } -function getGaussianDensityRenderObject(webgl: WebGLContext, drawCount: number, positions: Float32Array, radii: Float32Array, groups: Float32Array, minDistanceTexture: Texture, box: Box3D, dimensions: Vec3, smoothness: number) { +function getGaussianDensityRenderable(webgl: WebGLContext, drawCount: number, positions: Float32Array, radii: Float32Array, groups: Float32Array, minDistanceTexture: Texture, box: Box3D, gridDim: Vec3, gridTexDim: Vec3, gridTexScale: Vec2, smoothness: number, resolution: number) { const extent = Vec3.sub(Vec3.zero(), box.max, box.min) - const { texDimX, texDimY } = getTexture2dSize(webgl.maxTextureSize, dimensions) - const values: GaussianDensityValues = { + const values: Values<typeof GaussianDensitySchema> = { drawCount: ValueCell.create(drawCount), instanceCount: ValueCell.create(1), @@ -220,85 +270,92 @@ function getGaussianDensityRenderObject(webgl: WebGLContext, drawCount: number, uCurrentX: ValueCell.create(0), uCurrentY: ValueCell.create(0), uBboxMin: ValueCell.create(box.min), - uBboxMax: ValueCell.create(box.max), uBboxSize: ValueCell.create(extent), - uGridDim: ValueCell.create(dimensions), - uGridTexDim: ValueCell.create(Vec3.create(texDimX, texDimY, 0)), + uGridDim: ValueCell.create(gridDim), + uGridTexDim: ValueCell.create(gridTexDim), + uGridTexScale: ValueCell.create(gridTexScale), uAlpha: ValueCell.create(smoothness), + uResolution: ValueCell.create(resolution), tMinDistanceTex: ValueCell.create(minDistanceTexture), dGridTexType: ValueCell.create(minDistanceTexture.depth > 0 ? '3d' : '2d'), - dCalcType: ValueCell.create('density'), - } - const state: RenderableState = { - visible: true, - alphaFactor: 1, - pickable: false, - opaque: true + dCalcType: ValueCell.create('minDistance'), } - const renderObject = createRenderObject('gaussian-density', values, state) + const schema = { ...GaussianDensitySchema } + const shaderCode = GaussianDensityShaderCode + const renderItem = createComputeRenderItem(webgl, 'points', shaderCode, schema, values) - return renderObject + return createComputeRenderable(renderItem, values) } -function setRenderingDefaults(gl: GLRenderingContext) { - gl.disable(gl.CULL_FACE) - gl.frontFace(gl.CCW) - gl.cullFace(gl.BACK) - gl.enable(gl.BLEND) +function setRenderingDefaults(ctx: WebGLContext) { + const { gl, state } = ctx + state.disable(gl.CULL_FACE) + state.enable(gl.BLEND) + state.disable(gl.DEPTH_TEST) + state.disable(gl.SCISSOR_TEST) + state.depthMask(false) + state.clearColor(0, 0, 0, 0) } -function setupMinDistanceRendering(webgl: WebGLContext, renderable: Renderable<any>) { - const { gl } = webgl +function setupMinDistanceRendering(webgl: WebGLContext, renderable: ComputeRenderable<any>) { + const { gl, state } = webgl ValueCell.update(renderable.values.dCalcType, 'minDistance') renderable.update() - renderable.getProgram('draw').use() - gl.blendFunc(gl.ONE, gl.ONE) + state.colorMask(false, false, false, true) + state.blendFunc(gl.ONE, gl.ONE) // the shader writes 1 - dist so we set blending to MAX - gl.blendEquation(webgl.extensions.blendMinMax.MAX) + state.blendEquation(webgl.extensions.blendMinMax.MAX) } -function setupDensityRendering(webgl: WebGLContext, renderable: Renderable<any>) { - const { gl } = webgl +function setupDensityRendering(webgl: WebGLContext, renderable: ComputeRenderable<any>) { + const { gl, state } = webgl ValueCell.update(renderable.values.dCalcType, 'density') renderable.update() - renderable.getProgram('draw').use() - gl.blendFunc(gl.ONE, gl.ONE) - gl.blendEquation(gl.FUNC_ADD) + state.colorMask(false, false, false, true) + state.blendFunc(gl.ONE, gl.ONE) + // state.colorMask(true, true, true, true) + // state.blendFuncSeparate(gl.ONE, gl.ZERO, gl.ONE, gl.ONE) + state.blendEquation(gl.FUNC_ADD) } -function setupGroupIdRendering(webgl: WebGLContext, renderable: Renderable<any>) { - const { gl } = webgl +function setupGroupIdRendering(webgl: WebGLContext, renderable: ComputeRenderable<any>) { + const { gl, state } = webgl ValueCell.update(renderable.values.dCalcType, 'groupId') renderable.update() - renderable.getProgram('draw').use() // overwrite color, don't change alpha - gl.blendFuncSeparate(gl.ONE, gl.ZERO, gl.ZERO, gl.ONE) - gl.blendEquation(gl.FUNC_ADD) + state.colorMask(true, true, true, false) + state.blendFunc(gl.ONE, gl.ZERO) + state.blendEquation(gl.FUNC_ADD) } -function getTexture2dSize(maxTexSize: number, gridDim: Vec3) { +function getTexture2dSize(gridDim: Vec3) { + const area = gridDim[0] * gridDim[1] * gridDim[2] + const squareDim = Math.sqrt(area) + const powerOfTwoSize = Math.pow(2, Math.ceil(Math.log(squareDim) / Math.log(2))) + let texDimX = 0 let texDimY = gridDim[1] let texRows = 1 let texCols = gridDim[2] - if (maxTexSize < gridDim[0] * gridDim[2]) { - texCols = Math.floor(maxTexSize / gridDim[0]) + if (powerOfTwoSize < gridDim[0] * gridDim[2]) { + texCols = Math.floor(powerOfTwoSize / gridDim[0]) texRows = Math.ceil(gridDim[2] / texCols) texDimX = texCols * gridDim[0] texDimY *= texRows } else { texDimX = gridDim[0] * gridDim[2] } - return { texDimX, texDimY, texRows, texCols } + return { texDimX, texDimY, texRows, texCols, powerOfTwoSize: texDimY < powerOfTwoSize ? powerOfTwoSize : powerOfTwoSize * 2 } } -async function fieldFromTexture2d(ctx: WebGLContext, texture: Texture, dim: Vec3) { +export function fieldFromTexture2d(ctx: WebGLContext, texture: Texture, dim: Vec3, texDim: Vec3) { // console.time('fieldFromTexture2d') const { framebufferCache } = ctx const [ dx, dy, dz ] = dim - const { width, height } = texture + // const { width, height } = texture + const [ width, height ] = texDim const fboTexCols = Math.floor(width / dx) const space = Tensor.Space(dim, [2, 1, 0], Float32Array) @@ -307,16 +364,15 @@ async function fieldFromTexture2d(ctx: WebGLContext, texture: Texture, dim: Vec3 const idData = space.create() const idField = Tensor.create(space, idData) - const image = new Uint8Array(width * height * 4) + // const image = new Uint8Array(width * height * 4) + const image = new Float32Array(width * height * 4) - const framebuffer = framebufferCache.get(ctx, FramebufferName).value + const framebuffer = framebufferCache.get(FramebufferName).value framebuffer.bind() texture.attachFramebuffer(framebuffer, 0) - // TODO too slow, why? Too many checks if gpu ready??? - // await ctx.readPixelsAsync(0, 0, width, height, image) ctx.readPixels(0, 0, width, height, image) - // debugTexture(createImageData(image, width, height), 1/3) + // printImageData(createImageData(image, width, height), 1/3) let j = 0 let tmpCol = 0 @@ -329,8 +385,8 @@ async function fieldFromTexture2d(ctx: WebGLContext, texture: Texture, dim: Vec3 for (let iy = 0; iy < dy; ++iy) { for (let ix = 0; ix < dx; ++ix) { const idx = 4 * (tmpCol * dx + (iy + tmpRow) * width + ix) - data[j] = image[idx + 3] / 255 - idData[j] = decodeFloatRGB(image[idx], image[idx + 1], image[idx + 2]) + data[j] = image[idx + 3] // / 255 + idData[j] = decodeFloatRGB(image[idx] * 255, image[idx + 1] * 255, image[idx + 2] * 255) j++ } } diff --git a/src/mol-math/geometry/lookup3d/common.ts b/src/mol-math/geometry/lookup3d/common.ts index 874735b9ca2659be52324834350cc595f5bc5024..924274b8b0f520a3e1bfb5e6aeab36b7889670bd 100644 --- a/src/mol-math/geometry/lookup3d/common.ts +++ b/src/mol-math/geometry/lookup3d/common.ts @@ -33,4 +33,6 @@ export interface Lookup3D<T = number> { find(x: number, y: number, z: number, radius: number): Result<T>, check(x: number, y: number, z: number, radius: number): boolean, readonly boundary: { readonly box: Box3D, readonly sphere: Sphere3D } + /** transient result */ + readonly result: Result<T> } \ No newline at end of file diff --git a/src/mol-math/geometry/lookup3d/grid.ts b/src/mol-math/geometry/lookup3d/grid.ts index 5270a27600e482edf597e63ec2b821116a16ab47..328f2d00aafa2f6c4e2a2edf0280a08ac2bb8473 100644 --- a/src/mol-math/geometry/lookup3d/grid.ts +++ b/src/mol-math/geometry/lookup3d/grid.ts @@ -27,6 +27,7 @@ class GridLookup3DImpl implements GridLookup3D<number> { private ctx: QueryContext; boundary: Lookup3D['boundary']; buckets: GridLookup3D['buckets']; + result: Result<number> find(x: number, y: number, z: number, radius: number): Result<number> { this.ctx.x = x; @@ -52,6 +53,7 @@ class GridLookup3DImpl implements GridLookup3D<number> { this.ctx = createContext(structure); this.boundary = { box: structure.boundingBox, sphere: structure.boundingSphere }; this.buckets = { offset: structure.bucketOffset, count: structure.bucketCounts, array: structure.bucketArray }; + this.result = this.ctx.result } } diff --git a/src/mol-math/geometry/molecular-surface.ts b/src/mol-math/geometry/molecular-surface.ts new file mode 100644 index 0000000000000000000000000000000000000000..326e863c4e0fbc1ab9626862427eba9e087ea4a3 --- /dev/null +++ b/src/mol-math/geometry/molecular-surface.ts @@ -0,0 +1,366 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Fred Ludlow <fred.ludlow@gmail.com> + * @author Alexander Rose <alexander.rose@weirdbyte.de> + * + * ported from NGL (https://github.com/arose/ngl), licensed under MIT + */ + +import { fillUniform } from 'mol-util/array'; +import { Vec3, Tensor } from 'mol-math/linear-algebra'; +import { ParamDefinition as PD } from 'mol-util/param-definition'; +import { RuntimeContext } from 'mol-task'; +import { OrderedSet } from 'mol-data/int'; +import { PositionData } from './common'; +import { Mat4 } from 'mol-math/linear-algebra/3d'; +import { Box3D, GridLookup3D, fillGridDim } from 'mol-math/geometry'; + +function normalToLine (out: Vec3, p: Vec3) { + out[0] = out[1] = out[2] = 1.0 + if (p[0] !== 0) { + out[0] = (p[1] + p[2]) / -p[0] + } else if (p[1] !== 0) { + out[1] = (p[0] + p[2]) / -p[1] + } else if (p[2] !== 0) { + out[2] = (p[0] + p[1]) / -p[2] + } + return out +} + +type AnglesTables = { cosTable: Float32Array, sinTable: Float32Array } +function getAngleTables (probePositions: number): AnglesTables { + let theta = 0.0 + const step = 2 * Math.PI / probePositions + + const cosTable = new Float32Array(probePositions) + const sinTable = new Float32Array(probePositions) + for (let i = 0; i < probePositions; i++) { + cosTable[i] = Math.cos(theta) + sinTable[i] = Math.sin(theta) + theta += step + } + return { cosTable, sinTable} +} + +// + +export const MolecularSurfaceCalculationParams = { + resolution: PD.Numeric(0.5, { min: 0.01, max: 20, step: 0.01 }), + probeRadius: PD.Numeric(1.4, { min: 0, max: 10, step: 0.1 }), + probePositions: PD.Numeric(30, { min: 12, max: 90, step: 1 }), +} +export const DefaultMolecularSurfaceCalculationProps = PD.getDefaultValues(MolecularSurfaceCalculationParams) +export type MolecularSurfaceCalculationProps = typeof DefaultMolecularSurfaceCalculationProps + + +export async function calcMolecularSurface(ctx: RuntimeContext, position: Required<PositionData>, maxRadius: number, props: MolecularSurfaceCalculationProps) { + // Field generation method adapted from AstexViewer (Mike Hartshorn) by Fred Ludlow. + // Other parts based heavily on NGL (Alexander Rose) EDT Surface class + + let lastClip = -1 + + /** + * Is the point at x,y,z obscured by any of the atoms specifeid by indices in neighbours. + * Ignore indices a and b (these are the relevant atoms in projectPoints/Torii) + * + * Cache the last clipped atom (as very often the same one in subsequent calls) + * + * `a` and `b` must be resolved indices + */ + function obscured(x: number, y: number, z: number, a: number, b: number) { + if (lastClip !== -1) { + const ai = lastClip + if (ai !== a && ai !== b && singleAtomObscures(ai, x, y, z)) { + return ai + } else { + lastClip = -1 + } + } + + for (let j = 0, jl = neighbours.count; j < jl; ++j) { + const ai = OrderedSet.getAt(position.indices, neighbours.indices[j]) + if (ai !== a && ai !== b && singleAtomObscures(ai, x, y, z)) { + lastClip = ai + return ai + } + } + + return -1 + } + + /** + * `ai` must be a resolved index + */ + function singleAtomObscures(ai: number, x: number, y: number, z: number) { + const r = radius[ai] + const dx = px[ai] - x + const dy = py[ai] - y + const dz = pz[ai] - z + const dSq = dx * dx + dy * dy + dz * dz + return dSq < (r * r) + } + + /** + * For each atom: + * Iterate over a subsection of the grid, for each point: + * If current value < 0.0, unvisited, set positive + * + * In any case: Project this point onto surface of the atomic sphere + * If this projected point is not obscured by any other atom + * Calculate delta distance and set grid value to minimum of + * itself and delta + */ + function projectPointsRange (begI: number, endI: number) { + for (let i = begI; i < endI; ++i) { + const j = OrderedSet.getAt(indices, i) + const vx = px[j], vy = py[j], vz = pz[j] + const rad = radius[j] + const rSq = rad * rad + + lookup3d.find(vx, vy, vz, rad) + + // Number of grid points, round this up... + const ng = Math.ceil(rad * scaleFactor) + + // Center of the atom, mapped to grid points (take floor) + const iax = Math.floor(scaleFactor * (vx - minX)) + const iay = Math.floor(scaleFactor * (vy - minY)) + const iaz = Math.floor(scaleFactor * (vz - minZ)) + + // Extents of grid to consider for this atom + const begX = Math.max(0, iax - ng) + const begY = Math.max(0, iay - ng) + const begZ = Math.max(0, iaz - ng) + + // Add two to these points: + // - iax are floor'd values so this ensures coverage + // - these are loop limits (exclusive) + const endX = Math.min(dimX, iax + ng + 2) + const endY = Math.min(dimY, iay + ng + 2) + const endZ = Math.min(dimZ, iaz + ng + 2) + + for (let xi = begX; xi < endX; ++xi) { + const dx = gridx[xi] - vx + const xIdx = xi * iuv + for (let yi = begY; yi < endY; ++yi) { + const dy = gridy[yi] - vy + const dxySq = dx * dx + dy * dy + const xyIdx = yi * iu + xIdx + for (let zi = begZ; zi < endZ; ++zi) { + const dz = gridz[zi] - vz + const dSq = dxySq + dz * dz + + if (dSq < rSq) { + const idx = zi + xyIdx + + // if unvisited, make positive + if (data[idx] < 0.0) data[idx] *= -1 + + // Project on to the surface of the sphere + // sp is the projected point ( dx, dy, dz ) * ( ra / d ) + const d = Math.sqrt(dSq) + const ap = rad / d + const spx = dx * ap + vx + const spy = dy * ap + vy + const spz = dz * ap + vz + + if (obscured(spx, spy, spz, j, -1) === -1) { + const dd = rad - d + if (dd < data[idx]) { + data[idx] = dd + idData[idx] = i + } + } + } + } + } + } + } + } + + async function projectPoints() { + for (let i = 0; i < n; i += updateChunk) { + projectPointsRange(i, Math.min(i + updateChunk, n)) + + if (ctx.shouldUpdate) { + await ctx.update({ message: 'projecting points', current: i, max: n }) + } + } + } + + // Vectors for Torus Projection + const atob = Vec3() + const mid = Vec3() + const n1 = Vec3() + const n2 = Vec3() + /** + * `a` and `b` must be resolved indices + */ + function projectTorus(a: number, b: number) { + const rA = radius[a] + const rB = radius[b] + const dx = atob[0] = px[b] - px[a] + const dy = atob[1] = py[b] - py[a] + const dz = atob[2] = pz[b] - pz[a] + const dSq = dx * dx + dy * dy + dz * dz + + // This check now redundant as already done in AVHash.withinRadii + // if (dSq > ((rA + rB) * (rA + rB))) { return } + + const d = Math.sqrt(dSq) + + // Find angle between a->b vector and the circle + // of their intersection by cosine rule + const cosA = (rA * rA + d * d - rB * rB) / (2.0 * rA * d) + + // distance along a->b at intersection + const dmp = rA * cosA + + Vec3.normalize(atob, atob) + + // Create normal to line + normalToLine(n1, atob) + Vec3.normalize(n1, n1) + + // Cross together for second normal vector + Vec3.cross(n2, atob, n1) + Vec3.normalize(n2, n2) + + // r is radius of circle of intersection + const rInt = Math.sqrt(rA * rA - dmp * dmp) + + Vec3.scale(n1, n1, rInt) + Vec3.scale(n2, n2, rInt) + Vec3.scale(atob, atob, dmp) + + mid[0] = atob[0] + px[a] + mid[1] = atob[1] + py[a] + mid[2] = atob[2] + pz[a] + + lastClip = -1 + + for (let i = 0; i < probePositions; ++i) { + const cost = cosTable[i] + const sint = sinTable[i] + + const px = mid[0] + cost * n1[0] + sint * n2[0] + const py = mid[1] + cost * n1[1] + sint * n2[1] + const pz = mid[2] + cost * n1[2] + sint * n2[2] + + if (obscured(px, py, pz, a, b) === -1) { + const iax = Math.floor(scaleFactor * (px - minX)) + const iay = Math.floor(scaleFactor * (py - minY)) + const iaz = Math.floor(scaleFactor * (pz - minZ)) + + const begX = Math.max(0, iax - ngTorus) + const begY = Math.max(0, iay - ngTorus) + const begZ = Math.max(0, iaz - ngTorus) + + const endX = Math.min(dimX, iax + ngTorus + 2) + const endY = Math.min(dimY, iay + ngTorus + 2) + const endZ = Math.min(dimZ, iaz + ngTorus + 2) + + for (let xi = begX; xi < endX; ++xi) { + const dx = px - gridx[xi] + const xIdx = xi * iuv + + for (let yi = begY; yi < endY; ++yi) { + const dy = py - gridy[yi] + const dxySq = dx * dx + dy * dy + const xyIdx = yi * iu + xIdx + + for (let zi = begZ; zi < endZ; ++zi) { + const dz = pz - gridz[zi] + const dSq = dxySq + dz * dz + + const idx = zi + xyIdx + const current = data[idx] + + if (current > 0.0 && dSq < (current * current)) { + data[idx] = Math.sqrt(dSq) + // Is this grid point closer to a or b? + // Take dot product of atob and gridpoint->p (dx, dy, dz) + const dp = dx * atob[0] + dy * atob[1] + dz * atob[2] + idData[idx] = OrderedSet.indexOf(position.indices, dp < 0.0 ? b : a) + } + } + } + } + } + } + } + + async function projectTorii () { + for (let i = 0; i < n; ++i) { + const k = OrderedSet.getAt(indices, i) + lookup3d.find(px[k], py[k], pz[k], radius[k]) + for (let j = 0, jl = neighbours.count; j < jl; ++j) { + const l = OrderedSet.getAt(indices, neighbours.indices[j]) + if (k < l) projectTorus(k, l) + } + + if (i % updateChunk === 0 && ctx.shouldUpdate) { + await ctx.update({ message: 'projecting torii', current: i, max: n }) + } + } + } + + // console.time('MolecularSurface') + // console.time('MolecularSurface createState') + const { resolution, probeRadius, probePositions } = props + const scaleFactor = 1 / resolution + const ngTorus = Math.max(5, 2 + Math.floor(probeRadius * scaleFactor)) + + const cellSize = Vec3.create(maxRadius, maxRadius, maxRadius) + Vec3.scale(cellSize, cellSize, 2) + const lookup3d = GridLookup3D(position, cellSize) + const neighbours = lookup3d.result + const box = lookup3d.boundary.box + const { indices, x: px, y: py, z: pz, radius } = position + const n = OrderedSet.size(indices) + + const pad = maxRadius * 2 + resolution + const expandedBox = Box3D.expand(Box3D(), box, Vec3.create(pad, pad, pad)); + const [ minX, minY, minZ ] = expandedBox.min + const scaledBox = Box3D.scale(Box3D(), expandedBox, scaleFactor) + const dim = Box3D.size(Vec3(), scaledBox) + Vec3.ceil(dim, dim) + + const [ dimX, dimY, dimZ ] = dim + const iu = dimZ, iv = dimY, iuv = iu * iv + + const { cosTable, sinTable } = getAngleTables(probePositions) + + const space = Tensor.Space(dim, [0, 1, 2], Float32Array) + const data = space.create() + const idData = space.create() + + fillUniform(data, -1001.0) + fillUniform(idData, -1) + + const gridx = fillGridDim(dimX, minX, resolution) + const gridy = fillGridDim(dimY, minY, resolution) + const gridz = fillGridDim(dimZ, minZ, resolution) + + const updateChunk = Math.ceil(100000 / ((Math.pow(Math.pow(maxRadius, 3), 3) * scaleFactor))) + // console.timeEnd('MolecularSurface createState') + + // console.time('MolecularSurface projectPoints') + await projectPoints() + // console.timeEnd('MolecularSurface projectPoints') + + // console.time('MolecularSurface projectTorii') + await projectTorii() + // console.timeEnd('MolecularSurface projectTorii') + // console.timeEnd('MolecularSurface') + + const field = Tensor.create(space, data) + const idField = Tensor.create(space, idData) + + const transform = Mat4.identity() + Mat4.fromScaling(transform, Vec3.create(resolution, resolution, resolution)) + Mat4.setTranslation(transform, expandedBox.min) + // console.log({ field, idField, transform, updateChunk }) + return { field, idField, transform } +} \ No newline at end of file diff --git a/src/mol-math/geometry/primitives/box3d.ts b/src/mol-math/geometry/primitives/box3d.ts index 83c352cb5c64a908ef2b98997b9e28946e73c9ca..90fd7e49e5fa527eb21499cf4a7569104ef90329 100644 --- a/src/mol-math/geometry/primitives/box3d.ts +++ b/src/mol-math/geometry/primitives/box3d.ts @@ -11,9 +11,13 @@ import { OrderedSet } from 'mol-data/int'; interface Box3D { min: Vec3, max: Vec3 } +function Box3D() { + return Box3D.empty(); +} + namespace Box3D { export function create(min: Vec3, max: Vec3): Box3D { return { min, max }; } - export function empty(): Box3D { return { min: Vec3.zero(), max: Vec3.zero() }; } + export function empty(): Box3D { return { min: Vec3(), max: Vec3() }; } export function clone(a: Box3D): Box3D { const out = empty(); @@ -39,12 +43,12 @@ namespace Box3D { return { min, max } } - /** Get size of the box */ + /** Get size/extent of the box */ export function size(size: Vec3, box: Box3D): Vec3 { return Vec3.sub(size, box.max, box.min); } - const tmpSizeV = Vec3.zero() + const tmpSizeV = Vec3() /** Get size of the box */ export function volume(box: Box3D): number { size(tmpSizeV, box) @@ -71,7 +75,13 @@ namespace Box3D { return out } - const tmpTransformV = Vec3.zero() + export function scale(out: Box3D, box: Box3D, scale: number) { + Vec3.scale(out.min, box.min, scale) + Vec3.scale(out.max, box.max, scale) + return out; + } + + const tmpTransformV = Vec3() /** Transform box with a Mat4 */ export function transform(out: Box3D, box: Box3D, m: Mat4): Box3D { const [ minX, minY, minZ ] = box.min diff --git a/src/mol-math/geometry/primitives/sphere3d.ts b/src/mol-math/geometry/primitives/sphere3d.ts index 94c9593f9889b9a68b694cb71968a2cfc221d762..9df2560a973962dc4aa8d74c8ec994b041102260 100644 --- a/src/mol-math/geometry/primitives/sphere3d.ts +++ b/src/mol-math/geometry/primitives/sphere3d.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 David Sehnal <david.sehnal@gmail.com> * @author Alexander Rose <alexander.rose@weirdbyte.de> @@ -9,9 +9,14 @@ import { Vec3, Mat4, EPSILON } from '../../linear-algebra' import { PositionData } from '../common' import { OrderedSet } from 'mol-data/int'; import { NumberArray } from 'mol-util/type-helpers'; +import { Box3D } from './box3d'; interface Sphere3D { center: Vec3, radius: number } +function Sphere3D() { + return Sphere3D.zero(); +} + namespace Sphere3D { export function create(center: Vec3, radius: number): Sphere3D { return { center, radius }; } export function zero(): Sphere3D { return { center: Vec3.zero(), radius: 0 }; } @@ -76,12 +81,31 @@ namespace Sphere3D { return out } - export function addSphere(out: Sphere3D, sphere: Sphere3D) { + export function fromBox3D(out: Sphere3D, box: Box3D) { + Vec3.scale(out.center, Vec3.add(out.center, box.max, box.min), 0.5) + out.radius = Vec3.distance(out.center, box.max) + return out + } + + const tmpAddVec3 = Vec3() + export function addVec3(out: Sphere3D, s: Sphere3D, v: Vec3) { + const d = Vec3.distance(s.center, v) + if (d < s.radius) return Sphere3D.copy(out, s) + Vec3.sub(tmpAddVec3, s.center, v) + Vec3.sub(tmpAddVec3, s.center, tmpAddVec3) + Vec3.setMagnitude(tmpAddVec3, tmpAddVec3, s.radius) + Vec3.scale(out.center, Vec3.add(tmpAddVec3, tmpAddVec3, v), 0.5) + out.radius = Vec3.distance(out.center, v) + return out + } + + /** Expand sphere radius by another sphere */ + export function expandBySphere(out: Sphere3D, sphere: Sphere3D) { out.radius = Math.max(out.radius, Vec3.distance(out.center, sphere.center) + sphere.radius) return out } - /** Expand sphere by delta */ + /** Expand sphere radius by delta */ export function expand(out: Sphere3D, sphere: Sphere3D, delta: number): Sphere3D { out.radius = sphere.radius + delta return out diff --git a/src/mol-math/geometry/symmetry-operator.ts b/src/mol-math/geometry/symmetry-operator.ts index 44ba2975516385dcf35260e7eddc971dd48c80b6..6f3a71fe52e0802fe89cc25d1014ed6d2e707567 100644 --- a/src/mol-math/geometry/symmetry-operator.ts +++ b/src/mol-math/geometry/symmetry-operator.ts @@ -5,6 +5,7 @@ */ import { Vec3, Mat4, Mat3, Quat } from '../linear-algebra/3d' +import { lerp as scalar_lerp } from 'mol-math/interpolate'; interface SymmetryOperator { readonly name: string, @@ -64,7 +65,7 @@ namespace SymmetryOperator { return create(name, t, { id: '', operList: [] }, ncsId); } - const _q1 = Quat.identity(), _q2 = Quat.zero(), _axis = Vec3.zero(); + const _q1 = Quat.identity(), _q2 = Quat.zero(), _q3 = Quat.zero(), _axis = Vec3.zero(); export function lerpFromIdentity(out: Mat4, op: SymmetryOperator, t: number): Mat4 { const m = op.inverse; if (op.isIdentity) return Mat4.copy(out, m); @@ -84,6 +85,25 @@ namespace SymmetryOperator { return out; } + export function slerp(out: Mat4, src: Mat4, tar: Mat4, t: number): Mat4 { + if (Math.abs(t) <= 0.00001) return Mat4.copy(out, src); + if (Math.abs(t - 1) <= 0.00001) return Mat4.copy(out, tar); + + // interpolate rotation + Mat4.getRotation(_q2, src); + Mat4.getRotation(_q3, tar); + Quat.slerp(_q3, _q2, _q3, t); + const angle = Quat.getAxisAngle(_axis, _q3); + Mat4.fromRotation(out, angle, _axis); + + // interpolate translation + Mat4.setValue(out, 0, 3, scalar_lerp(Mat4.getValue(src, 0, 3), Mat4.getValue(tar, 0, 3), t)); + Mat4.setValue(out, 1, 3, scalar_lerp(Mat4.getValue(src, 1, 3), Mat4.getValue(tar, 1, 3), t)); + Mat4.setValue(out, 2, 3, scalar_lerp(Mat4.getValue(src, 2, 3), Mat4.getValue(tar, 2, 3), t)); + + return out; + } + /** * Apply the 1st and then 2nd operator. ( = second.matrix * first.matrix). * Keep `name`, `assembly`, `ncsId` and `hkl` properties from second. diff --git a/src/mol-math/linear-algebra/3d/mat3.ts b/src/mol-math/linear-algebra/3d/mat3.ts index a646193d7d6ecc7403bde52b2ec097610169e964..a7bb2cfdb84a55208b2292e2751a11d03484f297 100644 --- a/src/mol-math/linear-algebra/3d/mat3.ts +++ b/src/mol-math/linear-algebra/3d/mat3.ts @@ -22,6 +22,10 @@ import { NumberArray } from 'mol-util/type-helpers'; interface Mat3 extends Array<number> { [d: number]: number, '@type': 'mat3', length: 9 } +function Mat3() { + return Mat3.zero(); +} + namespace Mat3 { export function zero(): Mat3 { // force double backing array by 0.1. diff --git a/src/mol-math/linear-algebra/3d/mat4.ts b/src/mol-math/linear-algebra/3d/mat4.ts index c8a307e8de9a57ab893dab678ce1bff9d1203076..2bc177fab268d0ba36614a7811ef140ef8a4cdc3 100644 --- a/src/mol-math/linear-algebra/3d/mat4.ts +++ b/src/mol-math/linear-algebra/3d/mat4.ts @@ -1009,28 +1009,32 @@ namespace Mat4 { return Math.sqrt(Math.max(scaleXSq, scaleYSq, scaleZSq)) } + const xAxis = Vec3.create(1, 0, 0) + const yAxis = Vec3.create(1, 0, 0) + const zAxis = Vec3.create(1, 0, 0) + /** Rotation matrix for 90deg around x-axis */ - export const rotX90: ReadonlyMat4 = Mat4.fromRotation(Mat4.identity(), degToRad(90), Vec3.create(1, 0, 0)) + export const rotX90: ReadonlyMat4 = Mat4.fromRotation(Mat4(), degToRad(90), xAxis) /** Rotation matrix for 180deg around x-axis */ - export const rotX180: ReadonlyMat4 = Mat4.fromRotation(Mat4.identity(), degToRad(180), Vec3.create(1, 0, 0)) + export const rotX180: ReadonlyMat4 = Mat4.fromRotation(Mat4(), degToRad(180), xAxis) /** Rotation matrix for 90deg around y-axis */ - export const rotY90: ReadonlyMat4 = Mat4.fromRotation(Mat4.identity(), degToRad(90), Vec3.create(0, 1, 0)) + export const rotY90: ReadonlyMat4 = Mat4.fromRotation(Mat4(), degToRad(90), yAxis) /** Rotation matrix for 180deg around y-axis */ - export const rotY180: ReadonlyMat4 = Mat4.fromRotation(Mat4.identity(), degToRad(180), Vec3.create(0, 1, 0)) + export const rotY180: ReadonlyMat4 = Mat4.fromRotation(Mat4(), degToRad(180), yAxis) /** Rotation matrix for 90deg around z-axis */ - export const rotZ90: ReadonlyMat4 = Mat4.fromRotation(Mat4.identity(), degToRad(90), Vec3.create(0, 0, 1)) + export const rotZ90: ReadonlyMat4 = Mat4.fromRotation(Mat4(), degToRad(90), zAxis) /** Rotation matrix for 180deg around z-axis */ - export const rotZ180: ReadonlyMat4 = Mat4.fromRotation(Mat4.identity(), degToRad(180), Vec3.create(0, 0, 1)) + export const rotZ180: ReadonlyMat4 = Mat4.fromRotation(Mat4(), degToRad(180), zAxis) /** Rotation matrix for 90deg around first x-axis and then y-axis */ - export const rotXY90: ReadonlyMat4 = Mat4.mul(Mat4.identity(), rotX90, rotY90) + export const rotXY90: ReadonlyMat4 = Mat4.mul(Mat4(), rotX90, rotY90) /** Rotation matrix for 90deg around first z-axis and then y-axis */ - export const rotZY90: ReadonlyMat4 = Mat4.mul(Mat4.identity(), rotZ90, rotY90) + export const rotZY90: ReadonlyMat4 = Mat4.mul(Mat4(), rotZ90, rotY90) /** Rotation matrix for 90deg around first z-axis and then y-axis and then z-axis */ - export const rotZYZ90: ReadonlyMat4 = Mat4.mul(Mat4.identity(), rotZY90, rotZ90) + export const rotZYZ90: ReadonlyMat4 = Mat4.mul(Mat4(), rotZY90, rotZ90) /** Rotation matrix for 90deg around first z-axis and then 180deg around x-axis */ - export const rotZ90X180: ReadonlyMat4 = Mat4.mul(Mat4.identity(), rotZ90, rotX180) + export const rotZ90X180: ReadonlyMat4 = Mat4.mul(Mat4(), rotZ90, rotX180) /** Rotation matrix for 90deg around first y-axis and then 180deg around z-axis */ - export const rotY90Z180: ReadonlyMat4 = Mat4.mul(Mat4.identity(), rotY90, rotZ180) + export const rotY90Z180: ReadonlyMat4 = Mat4.mul(Mat4(), rotY90, rotZ180) } export default Mat4 \ No newline at end of file diff --git a/src/mol-math/linear-algebra/3d/quat.ts b/src/mol-math/linear-algebra/3d/quat.ts index 59e68c88bd5fe70b02d7e19bbc4cac22ed614956..abbe498e2a93943273156d6f2142143783553921 100644 --- a/src/mol-math/linear-algebra/3d/quat.ts +++ b/src/mol-math/linear-algebra/3d/quat.ts @@ -276,7 +276,7 @@ namespace Quat { return out; } - const fromUnitVec3Temp = Vec3.zero() + const fromUnitVec3Temp = Vec3() /** Quaternion from two normalized unit vectors. */ export function fromUnitVec3 (out: Quat, a: Vec3, b: Vec3) { // assumes a and b are normalized @@ -374,7 +374,7 @@ namespace Quat { * * Both vectors are assumed to be unit length. */ - const rotTmpVec3 = Vec3.zero(); + const rotTmpVec3 = Vec3(); const rotTmpVec3UnitX = Vec3.create(1, 0, 0); const rotTmpVec3UnitY = Vec3.create(0, 1, 0); export function rotationTo(out: Quat, a: Vec3, b: Vec3) { @@ -405,8 +405,8 @@ namespace Quat { /** * Performs a spherical linear interpolation with two control points */ - let sqlerpTemp1 = Quat.zero(); - let sqlerpTemp2 = Quat.zero(); + let sqlerpTemp1 = Quat(); + let sqlerpTemp2 = Quat(); export function sqlerp(out: Quat, a: Quat, b: Quat, c: Quat, d: Quat, t: number) { slerp(sqlerpTemp1, a, d, t); slerp(sqlerpTemp2, b, c, t); @@ -419,7 +419,7 @@ namespace Quat { * axes. Each axis is a vec3 and is expected to be unit length and * perpendicular to all other specified axes. */ - const axesTmpMat = Mat3.zero(); + const axesTmpMat = Mat3(); export function setAxes(out: Quat, view: Vec3, right: Vec3, up: Vec3) { axesTmpMat[0] = right[0]; axesTmpMat[3] = right[1]; diff --git a/src/mol-math/linear-algebra/3d/vec2.ts b/src/mol-math/linear-algebra/3d/vec2.ts index be7b7cdf93b52dac9b90fca247f42f75485fd383..65a32c3ff61149bd8a9c3e606ee67ab3c3e1b5b3 100644 --- a/src/mol-math/linear-algebra/3d/vec2.ts +++ b/src/mol-math/linear-algebra/3d/vec2.ts @@ -20,6 +20,10 @@ import { NumberArray } from 'mol-util/type-helpers'; interface Vec2 extends Array<number> { [d: number]: number, '@type': 'vec2', length: 2 } +function Vec2() { + return Vec2.zero(); +} + namespace Vec2 { export function zero(): Vec2 { // force double backing array by 0.1. diff --git a/src/mol-math/linear-algebra/3d/vec3.ts b/src/mol-math/linear-algebra/3d/vec3.ts index 3e8fd5d5ee7765fcb88215a1455655b75357b10e..982d323f1933cb5e9ebf17fc1b50c452dfc6581e 100644 --- a/src/mol-math/linear-algebra/3d/vec3.ts +++ b/src/mol-math/linear-algebra/3d/vec3.ts @@ -24,6 +24,10 @@ import { NumberArray } from 'mol-util/type-helpers'; interface Vec3 extends Array<number> { [d: number]: number, '@type': 'vec3', length: 3 } +function Vec3() { + return Vec3.zero(); +} + namespace Vec3 { export function zero(): Vec3 { const out = [0.1, 0.0, 0.0]; @@ -43,6 +47,13 @@ namespace Vec3 { return isNaN(a[0]) || isNaN(a[1]) || isNaN(a[2]) } + export function setNaN(out: Vec3) { + out[0] = NaN; + out[1] = NaN; + out[2] = NaN; + return out + } + export function fromObj(v: { x: number, y: number, z: number }): Vec3 { return create(v.x, v.y, v.z); } @@ -414,6 +425,7 @@ namespace Vec3 { } const angleTempA = zero(), angleTempB = zero(); + /** Computes the angle between 2 vectors, reports in rad. */ export function angle(a: Vec3, b: Vec3) { copy(angleTempA, a); copy(angleTempB, b); @@ -433,6 +445,30 @@ namespace Vec3 { } } + const tmp_dh_ab = zero(); + const tmp_dh_cb = zero(); + const tmp_dh_bc = zero(); + const tmp_dh_dc = zero(); + const tmp_dh_abc = zero(); + const tmp_dh_bcd = zero(); + const tmp_dh_cross = zero(); + /** + * Computes the dihedral angles of 4 points. + */ + export function dihedralAngle(a: Vec3, b: Vec3, c: Vec3, d: Vec3): number { + sub(tmp_dh_ab, a, b); + sub(tmp_dh_cb, c, b); + sub(tmp_dh_bc, b, c); + sub(tmp_dh_dc, d, c); + + cross(tmp_dh_abc, tmp_dh_ab, tmp_dh_cb); + cross(tmp_dh_bcd, tmp_dh_bc, tmp_dh_dc); + + const _angle = angle(tmp_dh_abc, tmp_dh_bcd) * 360.0 / (2 * Math.PI); + cross(tmp_dh_cross, tmp_dh_abc, tmp_dh_bcd); + return dot(tmp_dh_cb, tmp_dh_cross) > 0 ? _angle : -_angle; + } + /** * Returns whether or not the vectors have exactly the same elements in the same position (when compared with ===) */ diff --git a/src/mol-math/linear-algebra/3d/vec4.ts b/src/mol-math/linear-algebra/3d/vec4.ts index c7c4e3380250572a684ce87ce4bdbd90ee8d19df..35346760ee95d740d18a346211f7a2d1920919d7 100644 --- a/src/mol-math/linear-algebra/3d/vec4.ts +++ b/src/mol-math/linear-algebra/3d/vec4.ts @@ -23,6 +23,10 @@ import { NumberArray } from 'mol-util/type-helpers'; interface Vec4 extends Array<number> { [d: number]: number, '@type': 'vec4', length: 4 } +function Vec4() { + return Vec4.zero(); +} + namespace Vec4 { export function zero(): Vec4 { // force double backing array by 0.1. diff --git a/src/mol-math/linear-algebra/_spec/vec3.spec.ts b/src/mol-math/linear-algebra/_spec/vec3.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..bb98228acaf7b051186c8dd54cc985d81159ce52 --- /dev/null +++ b/src/mol-math/linear-algebra/_spec/vec3.spec.ts @@ -0,0 +1,14 @@ +import { Vec3 } from '../3d' + +describe('vec3', () => { + const vec1 = [ 1, 2, 3 ] as Vec3; + const vec2 = [ 2, 3, 1 ] as Vec3; + const orthVec1 = [ 0, 1, 0 ] as Vec3; + const orthVec2 = [ 1, 0, 0 ] as Vec3; + + it('angle calculation', () => { + expect(Vec3.angle(vec1, vec1) * 360 / (2 * Math.PI)).toBe(0.0); + expect(Vec3.angle(orthVec1, orthVec2) * 360 / (2 * Math.PI)).toBe(90.0); + expect(Vec3.angle(vec1, vec2)).toBeCloseTo(0.666946); + }); +}) \ No newline at end of file diff --git a/src/mol-math/misc.ts b/src/mol-math/misc.ts index 98dd209ea168bb3b4e25e897d6e19b424fa6719f..cfcb0d2355350d79adbd424a1b06947fa094caa9 100644 --- a/src/mol-math/misc.ts +++ b/src/mol-math/misc.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> */ @@ -10,4 +10,8 @@ export function degToRad (deg: number) { export function radToDeg (rad: number) { return rad * 57.29578 // rad * 180 / Math.PI +} + +export function isPowerOfTwo (x: number) { + return (x !== 0) && (x & (x - 1)) === 0 } \ No newline at end of file diff --git a/src/mol-model-formats/shape/ply.ts b/src/mol-model-formats/shape/ply.ts new file mode 100644 index 0000000000000000000000000000000000000000..edeceded4abdafa8e59e8370fef32b4c0b7f9f56 --- /dev/null +++ b/src/mol-model-formats/shape/ply.ts @@ -0,0 +1,260 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Schäfer, Marco <marco.schaefer@uni-tuebingen.de> + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { RuntimeContext, Task } from 'mol-task'; +import { ShapeProvider } from 'mol-model/shape/provider'; +import { Color } from 'mol-util/color'; +import { PlyFile, PlyTable, PlyList } from 'mol-io/reader/ply/schema'; +import { MeshBuilder } from 'mol-geo/geometry/mesh/mesh-builder'; +import { Mesh } from 'mol-geo/geometry/mesh/mesh'; +import { Shape } from 'mol-model/shape'; +import { ChunkedArray } from 'mol-data/util'; +import { arrayMax, fillSerial } from 'mol-util/array'; +import { Column } from 'mol-data/db'; +import { ParamDefinition as PD } from 'mol-util/param-definition'; +import { ColorNames } from 'mol-util/color/tables'; +import { deepClone } from 'mol-util/object'; + +// TODO support 'edge' element, see https://www.mathworks.com/help/vision/ug/the-ply-format.html +// TODO support missing face element + +function createPlyShapeParams(plyFile?: PlyFile) { + const vertex = plyFile && plyFile.getElement('vertex') as PlyTable + const material = plyFile && plyFile.getElement('material') as PlyTable + + const defaultValues = { group: '', vRed: '', vGreen: '', vBlue: '', mRed: '', mGreen: '', mBlue: '' } + + const groupOptions: [string, string][] = [['', '']] + const colorOptions: [string, string][] = [['', '']] + if (vertex) { + for (let i = 0, il = vertex.propertyNames.length; i < il; ++i) { + const name = vertex.propertyNames[i] + const type = vertex.propertyTypes[i] + if ( + type === 'uchar' || type === 'uint8' || + type === 'ushort' || type === 'uint16' || + type === 'uint' || type === 'uint32' + ) groupOptions.push([ name, name ]) + if (type === 'uchar' || type === 'uint8') colorOptions.push([ name, name ]) + } + + // TODO hardcoded as convenience for data provided by MegaMol + if (vertex.propertyNames.includes('atomid')) defaultValues.group = 'atomid' + else if (vertex.propertyNames.includes('material_index')) defaultValues.group = 'material_index' + + if (vertex.propertyNames.includes('red')) defaultValues.vRed = 'red' + if (vertex.propertyNames.includes('green')) defaultValues.vGreen = 'green' + if (vertex.propertyNames.includes('blue')) defaultValues.vBlue = 'blue' + } + + const materialOptions: [string, string][] = [['', '']] + if (material) { + for (let i = 0, il = material.propertyNames.length; i < il; ++i) { + const name = material.propertyNames[i] + const type = material.propertyTypes[i] + if (type === 'uchar' || type === 'uint8') materialOptions.push([ name, name ]) + } + + if (material.propertyNames.includes('red')) defaultValues.mRed = 'red' + if (material.propertyNames.includes('green')) defaultValues.mGreen = 'green' + if (material.propertyNames.includes('blue')) defaultValues.mBlue = 'blue' + } + + const defaultColoring = defaultValues.vRed && defaultValues.vGreen && defaultValues.vBlue ? 'vertex' : + defaultValues.mRed && defaultValues.mGreen && defaultValues.mBlue ? 'material' : 'uniform' + + return { + ...Mesh.Params, + + coloring: PD.MappedStatic(defaultColoring, { + vertex: PD.Group({ + red: PD.Select(defaultValues.vRed, colorOptions, { label: 'Red Property' }), + green: PD.Select(defaultValues.vGreen, colorOptions, { label: 'Green Property' }), + blue: PD.Select(defaultValues.vBlue, colorOptions, { label: 'Blue Property' }), + }, { isFlat: true }), + material: PD.Group({ + red: PD.Select(defaultValues.mRed, materialOptions, { label: 'Red Property' }), + green: PD.Select(defaultValues.mGreen, materialOptions, { label: 'Green Property' }), + blue: PD.Select(defaultValues.mBlue, materialOptions, { label: 'Blue Property' }), + }, { isFlat: true }), + uniform: PD.Group({ + color: PD.Color(ColorNames.grey) + }, { isFlat: true }) + }), + grouping: PD.MappedStatic(defaultValues.group ? 'vertex' : 'none', { + vertex: PD.Group({ + group: PD.Select(defaultValues.group, groupOptions, { label: 'Group Property' }), + }, { isFlat: true }), + none: PD.Group({ }) + }), + } +} + +export const PlyShapeParams = createPlyShapeParams() +export type PlyShapeParams = typeof PlyShapeParams + +async function getMesh(ctx: RuntimeContext, vertex: PlyTable, face: PlyList, groupIds: ArrayLike<number>, mesh?: Mesh) { + const builderState = MeshBuilder.createState(vertex.rowCount, vertex.rowCount / 4, mesh) + const { vertices, normals, indices, groups } = builderState + + const x = vertex.getProperty('x') + const y = vertex.getProperty('y') + const z = vertex.getProperty('z') + if (!x || !y || !z) throw new Error('missing coordinate properties') + + const nx = vertex.getProperty('nx') + const ny = vertex.getProperty('ny') + const nz = vertex.getProperty('nz') + + const hasNormals = !!nx && !!ny && !!nz + + for (let i = 0, il = vertex.rowCount; i < il; ++i) { + if (i % 100000 === 0 && ctx.shouldUpdate) await ctx.update({ current: i, max: il, message: `adding vertex ${i}` }) + + ChunkedArray.add3(vertices, x.value(i), y.value(i), z.value(i)) + if (hasNormals) ChunkedArray.add3(normals, nx!.value(i), ny!.value(i), nz!.value(i)); + ChunkedArray.add(groups, groupIds[i]) + } + + for (let i = 0, il = face.rowCount; i < il; ++i) { + if (i % 100000 === 0 && ctx.shouldUpdate) await ctx.update({ current: i, max: il, message: `adding face ${i}` }) + + const { entries, count } = face.value(i) + if (count === 3) { + // triangle + ChunkedArray.add3(indices, entries[0], entries[1], entries[2]) + } else if (count === 4) { + // quadrilateral + ChunkedArray.add3(indices, entries[2], entries[1], entries[0]) + ChunkedArray.add3(indices, entries[2], entries[0], entries[3]) + } + } + + const m = MeshBuilder.getMesh(builderState); + m.normalsComputed = hasNormals + await Mesh.computeNormals(m).runInContext(ctx) + + return m +} + +const int = Column.Schema.int + +type Grouping = { ids: ArrayLike<number>, map: ArrayLike<number> } +function getGrouping(vertex: PlyTable, props: PD.Values<PlyShapeParams>): Grouping { + const { grouping } = props + const { rowCount } = vertex + const column = grouping.name === 'vertex' ? vertex.getProperty(grouping.params.group) : undefined + + const ids = column ? column.toArray({ array: Uint32Array }) : fillSerial(new Uint32Array(rowCount)) + const maxId = arrayMax(ids) // assumes uint ids + const map = new Uint32Array(maxId + 1) + for (let i = 0, il = ids.length; i < il; ++i) map[ids[i]] = i + return { ids, map } +} + +type Coloring = { kind: 'vertex' | 'material' | 'uniform', red: Column<number>, green: Column<number>, blue: Column<number> } +function getColoring(vertex: PlyTable, material: PlyTable | undefined, props: PD.Values<PlyShapeParams>): Coloring { + const { coloring } = props + const { rowCount } = vertex + + let red: Column<number>, green: Column<number>, blue: Column<number> + if (coloring.name === 'vertex') { + red = vertex.getProperty(coloring.params.red) || Column.ofConst(127, rowCount, int) + green = vertex.getProperty(coloring.params.green) || Column.ofConst(127, rowCount, int) + blue = vertex.getProperty(coloring.params.blue) || Column.ofConst(127, rowCount, int) + } else if (coloring.name === 'material') { + red = (material && material.getProperty(coloring.params.red)) || Column.ofConst(127, rowCount, int) + green = (material && material.getProperty(coloring.params.green)) || Column.ofConst(127, rowCount, int) + blue = (material && material.getProperty(coloring.params.blue)) || Column.ofConst(127, rowCount, int) + } else { + const [r, g, b] = Color.toRgb(coloring.params.color) + red = Column.ofConst(r, rowCount, int) + green = Column.ofConst(g, rowCount, int) + blue = Column.ofConst(b, rowCount, int) + } + return { kind: coloring.name, red, green, blue } +} + +function createShape(plyFile: PlyFile, mesh: Mesh, coloring: Coloring, grouping: Grouping) { + const { kind, red, green, blue } = coloring + const { ids, map } = grouping + return Shape.create( + 'ply-mesh', plyFile, mesh, + (groupId: number) => { + const idx = kind === 'material' ? groupId : map[groupId] + return Color.fromRgb(red.value(idx), green.value(idx), blue.value(idx)) + }, + () => 1, // size: constant + (groupId: number) => { + return ids[groupId].toString() + } + ) +} + +function makeShapeGetter() { + let _plyFile: PlyFile | undefined + let _props: PD.Values<PlyShapeParams> | undefined + + let _shape: Shape<Mesh> + let _mesh: Mesh + let _coloring: Coloring + let _grouping: Grouping + + const getShape = async (ctx: RuntimeContext, plyFile: PlyFile, props: PD.Values<PlyShapeParams>, shape?: Shape<Mesh>) => { + + const vertex = plyFile.getElement('vertex') as PlyTable + if (!vertex) throw new Error('missing vertex element') + + const face = plyFile.getElement('face') as PlyList + if (!face) throw new Error('missing face element') + + const material = plyFile.getElement('material') as PlyTable + + let newMesh = false + let newColor = false + + if (!_plyFile || _plyFile !== plyFile) { + newMesh = true + } + + if (!_props || !PD.isParamEqual(PlyShapeParams.grouping, _props.grouping, props.grouping)) { + newMesh = true + } + + if (!_props || !PD.isParamEqual(PlyShapeParams.coloring, _props.coloring, props.coloring)) { + newColor = true + } + + if (newMesh) { + _coloring = getColoring(vertex, material, props) + _grouping = getGrouping(vertex, props) + _mesh = await getMesh(ctx, vertex, face, _grouping.ids, shape && shape.geometry) + _shape = createShape(plyFile, _mesh, _coloring, _grouping) + } else if (newColor) { + _coloring = getColoring(vertex, material, props) + _shape = createShape(plyFile, _mesh, _coloring, _grouping) + } + + _plyFile = plyFile + _props = deepClone(props) + + return _shape + } + return getShape +} + +export function shapeFromPly(source: PlyFile, params?: {}) { + return Task.create<ShapeProvider<PlyFile, Mesh, PlyShapeParams>>('Shape Provider', async ctx => { + return { + label: 'Mesh', + data: source, + params: createPlyShapeParams(source), + getShape: makeShapeGetter(), + geometryUtils: Mesh.Utils + } + }) +} \ No newline at end of file diff --git a/src/mol-model-formats/structure/_spec/pdb.spec.ts b/src/mol-model-formats/structure/_spec/pdb.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..8e365ed4c9c8f2fc1b01e447e32e5314eeb6bd45 --- /dev/null +++ b/src/mol-model-formats/structure/_spec/pdb.spec.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { guessElementSymbol } from '../pdb/to-cif'; +import { TokenBuilder } from 'mol-io/reader/common/text/tokenizer'; + +const records = [ + ['ATOM 19 HD23 LEU A 1 151.940 143.340 155.670 0.00 0.00', 'H'], + ['ATOM 38 CA SER A 3 146.430 138.150 162.270 0.00 0.00', 'C'], + ['ATOM 38 NA SER A 3 146.430 138.150 162.270 0.00 0.00', 'NA'], + ['ATOM 38 NAA SER A 3 146.430 138.150 162.270 0.00 0.00', 'N'], +] + +describe('PDB to-cif', () => { + it('guess-element-symbol', () => { + for (let i = 0, il = records.length; i < il; ++i) { + const [ data, element ] = records[i] + const tokens = TokenBuilder.create(data, 2) + guessElementSymbol(tokens, data, 12, 16) + expect(data.substring(tokens.indices[0], tokens.indices[1])).toBe(element) + } + }); +}); \ No newline at end of file diff --git a/src/mol-model-formats/structure/mmcif/bonds/comp.ts b/src/mol-model-formats/structure/mmcif/bonds/comp.ts index eceb64760607d51d544a8a9a587a46471bd9334d..9ba0b69eb645f03e1fc9f10946eba6bcb09eeff4 100644 --- a/src/mol-model-formats/structure/mmcif/bonds/comp.ts +++ b/src/mol-model-formats/structure/mmcif/bonds/comp.ts @@ -7,7 +7,7 @@ import { Model } from 'mol-model/structure/model/model' import { LinkType } from 'mol-model/structure/model/types' -import { ModelPropertyDescriptor } from 'mol-model/structure/model/properties/custom'; +import { CustomPropertyDescriptor } from 'mol-model/structure'; import { mmCIF_Database } from 'mol-io/reader/cif/schema/mmcif'; import { Structure, Unit, StructureProperties, StructureElement } from 'mol-model/structure'; import { Segmentation } from 'mol-data/int'; @@ -18,7 +18,7 @@ export interface ComponentBond { } export namespace ComponentBond { - export const Descriptor: ModelPropertyDescriptor = { + export const Descriptor: CustomPropertyDescriptor = { isStatic: true, name: 'chem_comp_bond', cifExport: { diff --git a/src/mol-model-formats/structure/mmcif/bonds/struct_conn.ts b/src/mol-model-formats/structure/mmcif/bonds/struct_conn.ts index 91c819e5a3b918e854029231df48cea4e3619fad..69689c51dbae68518e2d4ad14edc55303e6b5ade 100644 --- a/src/mol-model-formats/structure/mmcif/bonds/struct_conn.ts +++ b/src/mol-model-formats/structure/mmcif/bonds/struct_conn.ts @@ -10,7 +10,7 @@ import { Structure } from 'mol-model/structure' import { LinkType } from 'mol-model/structure/model/types' import { findEntityIdByAsymId, findAtomIndexByLabelName } from '../util' import { Column } from 'mol-data/db' -import { ModelPropertyDescriptor } from 'mol-model/structure/model/properties/custom'; +import { CustomPropertyDescriptor } from 'mol-model/structure'; import { mmCIF_Database, mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif'; import { SortedArray } from 'mol-data/int'; import { CifWriter } from 'mol-io/writer/cif' @@ -23,7 +23,7 @@ export interface StructConn { } export namespace StructConn { - export const Descriptor: ModelPropertyDescriptor = { + export const Descriptor: CustomPropertyDescriptor = { isStatic: true, name: 'struct_conn', cifExport: { diff --git a/src/mol-model-formats/structure/mmcif/parser.ts b/src/mol-model-formats/structure/mmcif/parser.ts index d31b63d1a8a6d0eaa4368ee6660178a237f0f59b..71063f94d82d6df46bdcafa3bc5bb73da9102ba7 100644 --- a/src/mol-model-formats/structure/mmcif/parser.ts +++ b/src/mol-model-formats/structure/mmcif/parser.ts @@ -13,7 +13,7 @@ import { RuntimeContext } from 'mol-task'; import UUID from 'mol-util/uuid'; import { Model } from 'mol-model/structure/model/model'; import { Entities } from 'mol-model/structure/model/properties/common'; -import { CustomProperties } from 'mol-model/structure/model/properties/custom'; +import { CustomProperties } from 'mol-model/structure'; import { ModelSymmetry } from 'mol-model/structure/model/properties/symmetry'; import { createAssemblies } from './assembly'; import { getAtomicHierarchyAndConformation } from './atomic'; diff --git a/src/mol-model-formats/structure/pdb/to-cif.ts b/src/mol-model-formats/structure/pdb/to-cif.ts index 853a1b9319eb97121a5394a55e772dde2621608a..0f699f297a4c3f03095cd1514bfb8d7db7c129b8 100644 --- a/src/mol-model-formats/structure/pdb/to-cif.ts +++ b/src/mol-model-formats/structure/pdb/to-cif.ts @@ -8,7 +8,7 @@ import { substringStartsWith } from 'mol-util/string'; import { CifField, CifCategory, CifFrame } from 'mol-io/reader/cif'; import { mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif'; -import { TokenBuilder, Tokenizer } from 'mol-io/reader/common/text/tokenizer'; +import { TokenBuilder, Tokenizer, Tokens } from 'mol-io/reader/common/text/tokenizer'; import { PdbFile } from 'mol-io/reader/pdb/schema'; import { parseCryst1, parseRemark350, parseMtrix } from './assembly'; import { WaterNames } from 'mol-model/structure/model/types'; @@ -89,6 +89,43 @@ function getEntityId(residueName: string, isHet: boolean) { return '1'; } +export function guessElementSymbol(tokens: Tokens, str: string, start: number, end: number) { + let s = start, e = end - 1 + + // trim spaces and numbers + let c = str.charCodeAt(s) + while ((c === 32 || (c >= 48 && c <= 57)) && s <= e) c = str.charCodeAt(++s) + c = str.charCodeAt(e) + while ((c === 32 || (c >= 48 && c <= 57)) && e >= s) c = str.charCodeAt(--e) + + ++e + + if (s === e) return TokenBuilder.add(tokens, s, e) // empty + if (s + 1 === e) return TokenBuilder.add(tokens, s, e) // one char + + c = str.charCodeAt(s) + + if (s + 2 === e) { // two chars + const c2 = str.charCodeAt(s + 1) + if ( + ((c === 78 || c === 110) && (c2 === 65 || c2 === 97)) || // NA na Na nA + ((c === 67 || c === 99) && (c2 === 76 || c2 === 108)) || // CL + ((c === 70 || c === 102) && (c2 === 69 || c2 === 101)) // FE + ) return TokenBuilder.add(tokens, s, s + 2) + } + + if ( + c === 67 || c === 99 || // C c + c === 72 || c === 104 || // H h + c === 78 || c === 110 || // N n + c === 79 || c === 111 || // O o + c === 80 || c === 112 || // P p + c === 83 || c === 115 // S s + ) return TokenBuilder.add(tokens, s, s + 1) + + TokenBuilder.add(tokens, s, s) // no reasonable guess, add empty token +} + function addAtom(sites: AtomSiteTemplate, model: string, data: Tokenizer, s: number, e: number, isHet: boolean) { const { data: str } = data; const length = e - s; @@ -162,11 +199,10 @@ function addAtom(sites: AtomSiteTemplate, model: string, data: Tokenizer, s: num if (data.tokenStart < data.tokenEnd) { TokenBuilder.addToken(sites.type_symbol, data); } else { - // "guess" the symbol - TokenBuilder.add(sites.type_symbol, s + 12, s + 13); + guessElementSymbol(sites.type_symbol, str, s + 12, s + 16) } } else { - TokenBuilder.add(sites.type_symbol, s + 12, s + 13); + guessElementSymbol(sites.type_symbol, str, s + 12, s + 16) } sites.label_entity_id[sites.index] = getEntityId(residueName, isHet); diff --git a/src/mol-model-props/common/custom-element-property.ts b/src/mol-model-props/common/custom-element-property.ts index 85f9306753784f838dbc00dce60b168394c5b6be..f5e2a6ff7e86a9a6a1845acb5d186cc851fe4529 100644 --- a/src/mol-model-props/common/custom-element-property.ts +++ b/src/mol-model-props/common/custom-element-property.ts @@ -4,7 +4,7 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { ElementIndex, Model, ModelPropertyDescriptor } from 'mol-model/structure'; +import { ElementIndex, Model, CustomPropertyDescriptor } from 'mol-model/structure'; import { StructureElement } from 'mol-model/structure/structure'; import { Location } from 'mol-model/location'; import { CustomPropertyRegistry } from './custom-property-registry'; @@ -36,7 +36,7 @@ namespace CustomElementProperty { export function create<T>(params: CreateParams<T>) { const name = params.name; - const Descriptor = ModelPropertyDescriptor({ + const Descriptor = CustomPropertyDescriptor({ isStatic: params.isStatic, name: params.name, }); diff --git a/src/mol-model-props/common/custom-property-registry.ts b/src/mol-model-props/common/custom-property-registry.ts index b537d0df3752caf9a4679d8deae0b0909c568bb6..977cade54990a71dd9b3f821cc89bd02a91c78c6 100644 --- a/src/mol-model-props/common/custom-property-registry.ts +++ b/src/mol-model-props/common/custom-property-registry.ts @@ -4,7 +4,7 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { ModelPropertyDescriptor, Model } from 'mol-model/structure'; +import { CustomPropertyDescriptor, Model } from 'mol-model/structure'; import { OrderedMap } from 'immutable'; import { ParamDefinition } from 'mol-util/param-definition'; import { Task } from 'mol-task'; @@ -58,7 +58,7 @@ namespace CustomPropertyRegistry { export interface Provider { option: [string, string], defaultSelected: boolean, - descriptor: ModelPropertyDescriptor<any, any>, + descriptor: CustomPropertyDescriptor<any, any>, attachableTo: (model: Model) => boolean, attach: (model: Model) => Task<boolean> } diff --git a/src/mol-model-props/pdbe/preferred-assembly.ts b/src/mol-model-props/pdbe/preferred-assembly.ts index fb5f5dc69d30b0ee3f12611aa9890086bed3fd8c..a5c540566b2cbd850c36a5f91ad90ce7447cfad8 100644 --- a/src/mol-model-props/pdbe/preferred-assembly.ts +++ b/src/mol-model-props/pdbe/preferred-assembly.ts @@ -7,7 +7,7 @@ import { Column, Table } from 'mol-data/db'; import { toTable } from 'mol-io/reader/cif/schema'; import { CifWriter } from 'mol-io/writer/cif'; -import { Model, ModelPropertyDescriptor } from 'mol-model/structure'; +import { Model, CustomPropertyDescriptor } from 'mol-model/structure'; export namespace PDBePreferredAssembly { export type Property = string @@ -31,7 +31,7 @@ export namespace PDBePreferredAssembly { }; export type Schema = typeof Schema - export const Descriptor = ModelPropertyDescriptor({ + export const Descriptor = CustomPropertyDescriptor({ isStatic: true, name: 'pdbe_preferred_assembly', cifExport: { diff --git a/src/mol-model-props/pdbe/struct-ref-domain.ts b/src/mol-model-props/pdbe/struct-ref-domain.ts index 3872d5a4d470fe0056024f961385846925f79bfe..8c3a8a7f34ef9762832395db1c6819a7627ad361 100644 --- a/src/mol-model-props/pdbe/struct-ref-domain.ts +++ b/src/mol-model-props/pdbe/struct-ref-domain.ts @@ -7,7 +7,7 @@ import { Column, Table } from 'mol-data/db'; import { toTable } from 'mol-io/reader/cif/schema'; import { CifWriter } from 'mol-io/writer/cif'; -import { Model, ModelPropertyDescriptor } from 'mol-model/structure'; +import { Model, CustomPropertyDescriptor } from 'mol-model/structure'; import { PropertyWrapper } from '../common/wrapper'; export namespace PDBeStructRefDomain { @@ -39,7 +39,7 @@ export namespace PDBeStructRefDomain { }; export type Schema = typeof Schema - export const Descriptor = ModelPropertyDescriptor({ + export const Descriptor = CustomPropertyDescriptor({ isStatic: true, name: 'pdbe_struct_ref_domain', cifExport: { diff --git a/src/mol-model-props/pdbe/structure-quality-report.ts b/src/mol-model-props/pdbe/structure-quality-report.ts index b534c465ff624e5793f4de87e7bc437c099073cd..483b957047120c0118d58b966805bc51ef92ac22 100644 --- a/src/mol-model-props/pdbe/structure-quality-report.ts +++ b/src/mol-model-props/pdbe/structure-quality-report.ts @@ -8,7 +8,7 @@ import { Column, Table } from 'mol-data/db'; import { toTable } from 'mol-io/reader/cif/schema'; import { mmCIF_residueId_schema } from 'mol-io/reader/cif/schema/mmcif-extras'; import { CifWriter } from 'mol-io/writer/cif'; -import { Model, ModelPropertyDescriptor, ResidueIndex, Unit, IndexedCustomProperty } from 'mol-model/structure'; +import { Model, CustomPropertyDescriptor, ResidueIndex, Unit, IndexedCustomProperty } from 'mol-model/structure'; import { residueIdFields } from 'mol-model/structure/export/categories/atom_site'; import { StructureElement, CifExportContext } from 'mol-model/structure/structure'; import { CustomPropSymbol } from 'mol-script/language/symbol'; @@ -43,7 +43,7 @@ export namespace StructureQualityReport { }; export type Schema = typeof Schema - export const Descriptor = ModelPropertyDescriptor({ + export const Descriptor = CustomPropertyDescriptor({ isStatic: false, name: 'pdbe_structure_quality_report', cifExport: { diff --git a/src/mol-model-props/rcsb/assembly-symmetry.ts b/src/mol-model-props/rcsb/assembly-symmetry.ts index c67ed0aca0b78727b2d073b60563f7b1f128d76b..4eab862518f6da97b1adf445a6d24ec16ab4d2ad 100644 --- a/src/mol-model-props/rcsb/assembly-symmetry.ts +++ b/src/mol-model-props/rcsb/assembly-symmetry.ts @@ -7,7 +7,7 @@ import { AssemblySymmetry as AssemblySymmetryGraphQL } from './graphql/types'; import query from './graphql/symmetry.gql'; -import { Model, ModelPropertyDescriptor } from 'mol-model/structure'; +import { Model, CustomPropertyDescriptor } from 'mol-model/structure'; import { CifWriter } from 'mol-io/writer/cif'; import { Database as _Database, Column, Table } from 'mol-data/db' import { Category } from 'mol-io/writer/cif/encoder'; @@ -140,7 +140,7 @@ function createDatabaseFromCif(model: Model): AssemblySymmetry.Database { }) } -const _Descriptor: ModelPropertyDescriptor = { +const _Descriptor: CustomPropertyDescriptor = { isStatic: true, name: 'rcsb_assembly_symmetry', cifExport: { diff --git a/src/mol-model-props/rcsb/representations/assembly-symmetry-axes.ts b/src/mol-model-props/rcsb/representations/assembly-symmetry-axes.ts index 09abfebeb49ec7b35be09744fa09ef1c202cbc30..1ba6eb9512c43ac603fcbe19d9679a079a684979 100644 --- a/src/mol-model-props/rcsb/representations/assembly-symmetry-axes.ts +++ b/src/mol-model-props/rcsb/representations/assembly-symmetry-axes.ts @@ -59,7 +59,7 @@ export const AssemblySymmetryAxesRepresentationProvider: StructureRepresentation // -export function AssemblySymmetryAxesVisual(): ComplexVisual<AssemblySymmetryAxesParams> { +export function AssemblySymmetryAxesVisual(materialId: number): ComplexVisual<AssemblySymmetryAxesParams> { return ComplexMeshVisual<AssemblySymmetryAxesParams>({ defaultProps: PD.getDefaultValues(AssemblySymmetryAxesParams), createGeometry: createAssemblySymmetryAxesMesh, @@ -73,7 +73,7 @@ export function AssemblySymmetryAxesVisual(): ComplexVisual<AssemblySymmetryAxes newProps.symmetryId !== currentProps.symmetryId ) } - }) + }, materialId) } function createLocationIterator(structure: Structure) { diff --git a/src/mol-model/location.ts b/src/mol-model/location.ts index 39546ddac69d0642e20e7f5d232b824d798e2c90..d5006c61710d8684b26c982e1529703a3cfcceda 100644 --- a/src/mol-model/location.ts +++ b/src/mol-model/location.ts @@ -1,12 +1,12 @@ /** - * 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> */ import { StructureElement } from './structure' import { Link } from './structure/structure/unit/links' -import { Shape } from './shape/shape'; +import { ShapeGroup } from './shape/shape'; /** A null value Location */ export const NullLocation = { kind: 'null-location' as 'null-location' } @@ -15,4 +15,4 @@ export function isNullLocation(x: any): x is NullLocation { return !!x && x.kind === 'null-location'; } -export type Location = StructureElement | Link.Location | Shape.Location | NullLocation \ No newline at end of file +export type Location = StructureElement | Link.Location | ShapeGroup.Location | NullLocation \ No newline at end of file diff --git a/src/mol-model/loci.ts b/src/mol-model/loci.ts index 35cad50ed5ecb596bf791d532455964f921db7af..517b0631ac0456006bc7fe62ec7a54690587980b 100644 --- a/src/mol-model/loci.ts +++ b/src/mol-model/loci.ts @@ -1,12 +1,12 @@ /** - * 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> */ import { StructureElement } from './structure' import { Link } from './structure/structure/unit/links' -import { Shape } from './shape'; +import { Shape, ShapeGroup } from './shape'; import { Sphere3D } from 'mol-math/geometry'; import { CentroidHelper } from 'mol-math/geometry/centroid-helper'; import { Vec3 } from 'mol-math/linear-algebra'; @@ -46,7 +46,7 @@ export function createDataLoci(data: any, tag: string, indices: OrderedSet<numbe export { Loci } -type Loci = StructureElement.Loci | Structure.Loci | Link.Loci | EveryLoci | EmptyLoci | DataLoci | Shape.Loci +type Loci = StructureElement.Loci | Structure.Loci | Link.Loci | EveryLoci | EmptyLoci | DataLoci | Shape.Loci | ShapeGroup.Loci namespace Loci { export function areEqual(lociA: Loci, lociB: Loci) { @@ -67,6 +67,9 @@ namespace Loci { if (Shape.isLoci(lociA) && Shape.isLoci(lociB)) { return Shape.areLociEqual(lociA, lociB) } + if (ShapeGroup.isLoci(lociA) && ShapeGroup.isLoci(lociB)) { + return ShapeGroup.areLociEqual(lociA, lociB) + } return false } @@ -96,6 +99,9 @@ namespace Loci { e.aUnit.conformation.position(e.bUnit.elements[e.bIndex], tempPos); sphereHelper.radiusStep(tempPos); } + } else if (loci.kind === 'shape-loci') { + // TODO + return void 0; } else if (loci.kind === 'group-loci') { // TODO return void 0; diff --git a/src/mol-model/shape/provider.ts b/src/mol-model/shape/provider.ts new file mode 100644 index 0000000000000000000000000000000000000000..dea45e8ba202b2a09da762a1b9a84926e836b64b --- /dev/null +++ b/src/mol-model/shape/provider.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { ShapeGetter } from 'mol-repr/shape/representation'; +import { Geometry, GeometryUtils } from 'mol-geo/geometry/geometry'; + +export interface ShapeProvider<D, G extends Geometry, P extends Geometry.Params<G>> { + label: string + data: D + params: P + getShape: ShapeGetter<D, G, P> + geometryUtils: GeometryUtils<G> +} \ No newline at end of file diff --git a/src/mol-model/shape/shape.ts b/src/mol-model/shape/shape.ts index 314af70fbfec3c4df1a7379c9359ead45f4fcc05..0d740ef14a47a24fab8dbb37f0a5c994c466645e 100644 --- a/src/mol-model/shape/shape.ts +++ b/src/mol-model/shape/shape.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> */ @@ -15,6 +15,8 @@ export interface Shape<G extends Geometry = Geometry> { readonly id: UUID /** A name to describe the shape */ readonly name: string + /** The data used to create the shape */ + readonly sourceData: unknown /** The geometry of the shape, e.g. `Mesh` or `Lines` */ readonly geometry: G /** An array of transformation matrices to describe multiple instances of the geometry */ @@ -30,10 +32,11 @@ export interface Shape<G extends Geometry = Geometry> { } export namespace Shape { - export function create<G extends Geometry>(name: string, geometry: G, getColor: Shape['getColor'], getSize: Shape['getSize'], getLabel: Shape['getLabel'], transforms?: Mat4[]): Shape<G> { + export function create<G extends Geometry>(name: string, sourceData: unknown, geometry: G, getColor: Shape['getColor'], getSize: Shape['getSize'], getLabel: Shape['getLabel'], transforms?: Mat4[]): Shape<G> { return { id: UUID.create22(), name, + sourceData, geometry, transforms: transforms || [Mat4.identity()], get groupCount() { return Geometry.getGroupCount(geometry) }, @@ -43,6 +46,13 @@ export namespace Shape { } } + export interface Loci { readonly kind: 'shape-loci', readonly shape: Shape } + export function Loci(shape: Shape): Loci { return { kind: 'shape-loci', shape } } + export function isLoci(x: any): x is Loci { return !!x && x.kind === 'shape-loci' } + export function areLociEqual(a: Loci, b: Loci) { return a.shape === b.shape } +} + +export namespace ShapeGroup { export interface Location { readonly kind: 'group-location' shape: Shape diff --git a/src/mol-model/structure.ts b/src/mol-model/structure.ts index 38d5890d5f0fada2ea72a32c23e6e9d3f2e0261b..3dff04a4684f0c1ad2062fb255eb10755ffa7ea8 100644 --- a/src/mol-model/structure.ts +++ b/src/mol-model/structure.ts @@ -6,4 +6,5 @@ export * from './structure/model' export * from './structure/structure' -export * from './structure/query' \ No newline at end of file +export * from './structure/query' +export * from './structure/common/custom-property' \ No newline at end of file diff --git a/src/mol-model/structure/common/custom-property.ts b/src/mol-model/structure/common/custom-property.ts new file mode 100644 index 0000000000000000000000000000000000000000..12f6f557c4aa5da2e91bd2cb6600b74dd2a08376 --- /dev/null +++ b/src/mol-model/structure/common/custom-property.ts @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { CifWriter } from 'mol-io/writer/cif' +import { CifExportContext } from '../export/mmcif'; +import { QuerySymbolRuntime } from 'mol-script/runtime/query/compiler'; +import { UUID } from 'mol-util'; + +export { CustomPropertyDescriptor, CustomProperties } + +interface CustomPropertyDescriptor<ExportCtx = CifExportContext, Symbols extends { [name: string]: QuerySymbolRuntime } = { }> { + readonly isStatic: boolean, + readonly name: string, + + cifExport?: { + // Prefix enforced during export. + prefix: string, + context?: (ctx: CifExportContext) => ExportCtx | undefined, + categories: CifWriter.Category<ExportCtx>[] + }, + + // TODO: add aliases when lisp-like mol-script is done + symbols?: Symbols +} + +function CustomPropertyDescriptor<Ctx, Desc extends CustomPropertyDescriptor<Ctx>>(desc: Desc) { + return desc; +} + +namespace CustomPropertyDescriptor { + export function getUUID(prop: CustomPropertyDescriptor): UUID { + if (!(prop as any).__key) { + (prop as any).__key = UUID.create22(); + } + return (prop as any).__key; + } +} + +class CustomProperties { + private _list: CustomPropertyDescriptor[] = []; + private _set = new Set<CustomPropertyDescriptor>(); + + get all(): ReadonlyArray<CustomPropertyDescriptor> { + return this._list; + } + + add(desc: CustomPropertyDescriptor<any>) { + if (this._set.has(desc)) return; + + this._list.push(desc); + this._set.add(desc); + } + + has(desc: CustomPropertyDescriptor<any>): boolean { + return this._set.has(desc); + } +} \ No newline at end of file diff --git a/src/mol-model/structure/export/mmcif.ts b/src/mol-model/structure/export/mmcif.ts index e0c189ab4afa7088656e3f0312e2539f7335e4de..b94604b104dae5a250080db2bfc3c94c0c9c036a 100644 --- a/src/mol-model/structure/export/mmcif.ts +++ b/src/mol-model/structure/export/mmcif.ts @@ -16,7 +16,7 @@ import { _chem_comp, _pdbx_chem_comp_identifier, _pdbx_nonpoly_scheme } from './ import { Model } from '../model'; import { getUniqueEntityIndicesFromStructures, copy_mmCif_category } from './categories/utils'; import { _struct_asym, _entity_poly, _entity_poly_seq } from './categories/sequence'; -import { ModelPropertyDescriptor } from '../model/properties/custom'; +import { CustomPropertyDescriptor } from '../common/custom-property'; export interface CifExportContext { structures: Structure[], @@ -100,8 +100,32 @@ export const mmCIF_Export_Filters = { } } +function encodeCustomProp(customProp: CustomPropertyDescriptor, ctx: CifExportContext, encoder: CifWriter.Encoder, params: encode_mmCIF_categories_Params) { + if (!customProp.cifExport || customProp.cifExport.categories.length === 0) return; + + const prefix = customProp.cifExport.prefix; + const cats = customProp.cifExport.categories; + + let propCtx = ctx; + if (customProp.cifExport.context) { + const propId = CustomPropertyDescriptor.getUUID(customProp); + if (ctx.cache[propId + '__ctx']) propCtx = ctx.cache[propId + '__ctx']; + else { + propCtx = customProp.cifExport.context(ctx) || ctx; + ctx.cache[propId + '__ctx'] = propCtx; + } + } + for (const cat of cats) { + if (params.skipCategoryNames && params.skipCategoryNames.has(cat.name)) continue; + if (cat.name.indexOf(prefix) !== 0) throw new Error(`Custom category '${cat.name}' name must start with prefix '${prefix}.'`); + encoder.writeCategory(cat, propCtx); + } +} + +type encode_mmCIF_categories_Params = { skipCategoryNames?: Set<string>, exportCtx?: CifExportContext } + /** Doesn't start a data block */ -export function encode_mmCIF_categories(encoder: CifWriter.Encoder, structures: Structure | Structure[], params?: { skipCategoryNames?: Set<string>, exportCtx?: CifExportContext }) { +export function encode_mmCIF_categories(encoder: CifWriter.Encoder, structures: Structure | Structure[], params?: encode_mmCIF_categories_Params) { const first = Array.isArray(structures) ? structures[0] : (structures as Structure); const models = first.models; if (models.length !== 1) throw 'Can\'t export stucture composed from multiple models.'; @@ -115,26 +139,15 @@ export function encode_mmCIF_categories(encoder: CifWriter.Encoder, structures: } for (const customProp of models[0].customProperties.all) { - if (!customProp.cifExport || customProp.cifExport.categories.length === 0) continue; - - const prefix = customProp.cifExport.prefix; - const cats = customProp.cifExport.categories; - - let propCtx = ctx; - if (customProp.cifExport.context) { - const propId = ModelPropertyDescriptor.getUUID(customProp); - if (ctx.cache[propId + '__ctx']) propCtx = ctx.cache[propId + '__ctx']; - else { - propCtx = customProp.cifExport.context(ctx) || ctx; - ctx.cache[propId + '__ctx'] = propCtx; - } - } - for (const cat of cats) { - if (_params.skipCategoryNames && _params.skipCategoryNames.has(cat.name)) continue; - if (cat.name.indexOf(prefix) !== 0) throw new Error(`Custom category '${cat.name}' name must start with prefix '${prefix}.'`); - encoder.writeCategory(cat, propCtx); - } + encodeCustomProp(customProp, ctx, encoder, _params); + } + + const structureCustomProps = new Set<CustomPropertyDescriptor>(); + for (const s of ctx.structures) { + if (!s.hasCustomProperties) continue; + for (const p of s.customPropertyDescriptors.all) structureCustomProps.add(p); } + structureCustomProps.forEach(customProp => encodeCustomProp(customProp, ctx, encoder, _params)); } function to_mmCIF(name: string, structure: Structure, asBinary = false) { diff --git a/src/mol-model/structure/model.ts b/src/mol-model/structure/model.ts index df9e98d18fa289780ace42f1326250fd232ce92e..8a1dccfdebe0bda698574a261203ab9741156036 100644 --- a/src/mol-model/structure/model.ts +++ b/src/mol-model/structure/model.ts @@ -9,6 +9,6 @@ import * as Types from './model/types' import { ModelSymmetry } from './model/properties/symmetry' import StructureSequence from './model/properties/sequence' -export * from './model/properties/custom' +export * from './model/properties/custom/indexed' export * from './model/indexing' export { Model, Types, ModelSymmetry, StructureSequence } \ No newline at end of file diff --git a/src/mol-model/structure/model/model.ts b/src/mol-model/structure/model/model.ts index c777d8d55fe61d75afc5b24f07e58dbfe0b4748e..303048fdde6740ec47502a9c3baee05a0d927dc7 100644 --- a/src/mol-model/structure/model/model.ts +++ b/src/mol-model/structure/model/model.ts @@ -10,7 +10,7 @@ import { AtomicHierarchy, AtomicConformation } from './properties/atomic'; import { ModelSymmetry } from './properties/symmetry'; import { CoarseHierarchy, CoarseConformation } from './properties/coarse'; import { Entities } from './properties/common'; -import { CustomProperties } from './properties/custom'; +import { CustomProperties } from '../common/custom-property'; import { SecondaryStructure } from './properties/seconday-structure'; import { SaccharideComponentMap } from '../structure/carbohydrates/constants'; import { ModelFormat } from 'mol-model-formats/structure/format'; diff --git a/src/mol-model/structure/model/properties/custom.ts b/src/mol-model/structure/model/properties/custom.ts deleted file mode 100644 index 9b9ab7c3dc7430388fe7b5bf4a6977c6936454a4..0000000000000000000000000000000000000000 --- a/src/mol-model/structure/model/properties/custom.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author David Sehnal <david.sehnal@gmail.com> - */ - -export * from './custom/descriptor' -export * from './custom/collection' -export * from './custom/indexed' \ No newline at end of file diff --git a/src/mol-model/structure/model/properties/custom/chain.ts b/src/mol-model/structure/model/properties/custom/chain.ts deleted file mode 100644 index 4e711c32e7ad450c3d409de7b38169f79b7dbab8..0000000000000000000000000000000000000000 --- a/src/mol-model/structure/model/properties/custom/chain.ts +++ /dev/null @@ -1,91 +0,0 @@ -// /** -// * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. -// * -// * @author David Sehnal <david.sehnal@gmail.com> -// */ - -// import { ChainIndex } from '../../indexing'; -// import { Unit, Structure, StructureElement } from '../../../structure'; -// import { Segmentation } from 'mol-data/int'; -// import { UUID } from 'mol-util'; -// import { CifWriter } from 'mol-io/writer/cif'; - -// export interface ChainCustomProperty<T = any> { -// readonly id: UUID, -// readonly kind: Unit.Kind, -// has(idx: ChainIndex): boolean -// get(idx: ChainIndex): T | undefined -// } - -// export namespace ChainCustomProperty { -// export interface ExportCtx<T> { -// elements: StructureElement[], -// property(index: number): T -// }; - -// function getExportCtx<T>(prop: ChainCustomProperty<T>, structure: Structure): ExportCtx<T> { -// const chainIndex = structure.model.atomicHierarchy.chainAtomSegments.index; -// const elements = getStructureElements(structure, prop); -// return { elements, property: i => prop.get(chainIndex[elements[i].element])! }; -// } - -// export function getCifDataSource<T>(structure: Structure, prop: ChainCustomProperty<T> | undefined, cache: any): CifWriter.Category.Instance['source'][0] { -// if (!prop) return { rowCount: 0 }; -// if (cache && cache[prop.id]) return cache[prop.id]; -// const data = getExportCtx(prop, structure); -// const ret = { data, rowCount: data.elements.length }; -// if (cache) cache[prop.id] = ret; -// return ret; -// } - -// class FromMap<T> implements ChainCustomProperty<T> { -// readonly id = UUID.create(); - -// has(idx: ChainIndex): boolean { -// return this.map.has(idx); -// } - -// get(idx: ChainIndex) { -// return this.map.get(idx); -// } - -// constructor(private map: Map<ChainIndex, T>, public kind: Unit.Kind) { -// } -// } - -// export function fromMap<T>(map: Map<ChainIndex, T>, kind: Unit.Kind) { -// return new FromMap(map, kind); -// } - -// /** -// * Gets all StructureElements that correspond to 1st atoms of residues that have an property assigned. -// * Only works correctly for structures with a single model. -// */ -// export function getStructureElements(structure: Structure, property: ChainCustomProperty) { -// const models = structure.models; -// if (models.length !== 1) throw new Error(`Only works on structures with a single model.`); - -// const seenChains = new Set<ChainIndex>(); -// const unitGroups = structure.unitSymmetryGroups; -// const loci: StructureElement[] = []; - -// for (const unitGroup of unitGroups) { -// const unit = unitGroup.units[0]; -// if (unit.kind !== property.kind) { -// continue; -// } - -// const chains = Segmentation.transientSegments(unit.model.atomicHierarchy.chainAtomSegments, unit.elements); -// while (chains.hasNext) { -// const seg = chains.move(); -// if (!property.has(seg.index) || seenChains.has(seg.index)) continue; - -// seenChains.add(seg.index); -// loci[loci.length] = StructureElement.create(unit, unit.elements[seg.start]); -// } -// } - -// loci.sort((x, y) => x.element - y.element); -// return loci; -// } -// } \ No newline at end of file diff --git a/src/mol-model/structure/model/properties/custom/collection.ts b/src/mol-model/structure/model/properties/custom/collection.ts deleted file mode 100644 index e6602fe192108b654abfaaf90667d51a4e8a8482..0000000000000000000000000000000000000000 --- a/src/mol-model/structure/model/properties/custom/collection.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author David Sehnal <david.sehnal@gmail.com> - */ - -import { ModelPropertyDescriptor } from './descriptor' - -export class CustomProperties { - private _list: ModelPropertyDescriptor[] = []; - private _set = new Set<ModelPropertyDescriptor>(); - - get all(): ReadonlyArray<ModelPropertyDescriptor> { - return this._list; - } - - add(desc: ModelPropertyDescriptor<any>) { - if (this._set.has(desc)) return; - - this._list.push(desc); - this._set.add(desc); - } - - has(desc: ModelPropertyDescriptor<any>): boolean { - return this._set.has(desc); - } -} \ No newline at end of file diff --git a/src/mol-model/structure/model/properties/custom/descriptor.ts b/src/mol-model/structure/model/properties/custom/descriptor.ts deleted file mode 100644 index 1d2abb21c712dc17dcfd8129e3cf6faf7abc31c7..0000000000000000000000000000000000000000 --- a/src/mol-model/structure/model/properties/custom/descriptor.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author David Sehnal <david.sehnal@gmail.com> - */ - -import { CifWriter } from 'mol-io/writer/cif' -import { CifExportContext } from '../../../export/mmcif'; -import { QuerySymbolRuntime } from 'mol-script/runtime/query/compiler'; -import { UUID } from 'mol-util'; - -interface ModelPropertyDescriptor<ExportCtx = CifExportContext, Symbols extends { [name: string]: QuerySymbolRuntime } = { }> { - readonly isStatic: boolean, - readonly name: string, - - cifExport?: { - // Prefix enforced during export. - prefix: string, - context?: (ctx: CifExportContext) => ExportCtx | undefined, - categories: CifWriter.Category<ExportCtx>[] - }, - - // TODO: add aliases when lisp-like mol-script is done - symbols?: Symbols -} - -function ModelPropertyDescriptor<Ctx, Desc extends ModelPropertyDescriptor<Ctx>>(desc: Desc) { - return desc; -} - -namespace ModelPropertyDescriptor { - export function getUUID(prop: ModelPropertyDescriptor): UUID { - if (!(prop as any).__key) { - (prop as any).__key = UUID.create22(); - } - return (prop as any).__key; - } -} - -export { ModelPropertyDescriptor } \ No newline at end of file diff --git a/src/mol-model/structure/model/properties/custom/residue.ts b/src/mol-model/structure/model/properties/custom/residue.ts deleted file mode 100644 index 4dd7cb130e9e06d94e2337d1928405faec51422a..0000000000000000000000000000000000000000 --- a/src/mol-model/structure/model/properties/custom/residue.ts +++ /dev/null @@ -1,91 +0,0 @@ -// /** -// * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. -// * -// * @author David Sehnal <david.sehnal@gmail.com> -// */ - -// import { ResidueIndex } from '../../indexing'; -// import { Unit, Structure, StructureElement } from '../../../structure'; -// import { Segmentation } from 'mol-data/int'; -// import { UUID } from 'mol-util'; -// import { CifWriter } from 'mol-io/writer/cif'; - -// export interface ResidueCustomProperty<T = any> { -// readonly id: UUID, -// readonly kind: Unit.Kind, -// has(idx: ResidueIndex): boolean -// get(idx: ResidueIndex): T | undefined -// } - -// export namespace ResidueCustomProperty { -// export interface ExportCtx<T> { -// elements: StructureElement[], -// property(index: number): T -// }; - -// function getExportCtx<T>(prop: ResidueCustomProperty<T>, structure: Structure): ExportCtx<T> { -// const residueIndex = structure.model.atomicHierarchy.residueAtomSegments.index; -// const elements = getStructureElements(structure, prop); -// return { elements, property: i => prop.get(residueIndex[elements[i].element])! }; -// } - -// export function getCifDataSource<T>(structure: Structure, prop: ResidueCustomProperty<T> | undefined, cache: any): CifWriter.Category.Instance['source'][0] { -// if (!prop) return { rowCount: 0 }; -// if (cache && cache[prop.id]) return cache[prop.id]; -// const data = getExportCtx(prop, structure); -// const ret = { data, rowCount: data.elements.length }; -// if (cache) cache[prop.id] = ret; -// return ret; -// } - -// class FromMap<T> implements ResidueCustomProperty<T> { -// readonly id = UUID.create(); - -// has(idx: ResidueIndex): boolean { -// return this.map.has(idx); -// } - -// get(idx: ResidueIndex) { -// return this.map.get(idx); -// } - -// constructor(private map: Map<ResidueIndex, T>, public kind: Unit.Kind) { -// } -// } - -// export function fromMap<T>(map: Map<ResidueIndex, T>, kind: Unit.Kind) { -// return new FromMap(map, kind); -// } - -// /** -// * Gets all StructureElements that correspond to 1st atoms of residues that have an property assigned. -// * Only works correctly for structures with a single model. -// */ -// export function getStructureElements(structure: Structure, property: ResidueCustomProperty) { -// const models = structure.models; -// if (models.length !== 1) throw new Error(`Only works on structures with a single model.`); - -// const seenResidues = new Set<ResidueIndex>(); -// const unitGroups = structure.unitSymmetryGroups; -// const loci: StructureElement[] = []; - -// for (const unitGroup of unitGroups) { -// const unit = unitGroup.units[0]; -// if (unit.kind !== property.kind) { -// continue; -// } - -// const residues = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements); -// while (residues.hasNext) { -// const seg = residues.move(); -// if (!property.has(seg.index) || seenResidues.has(seg.index)) continue; - -// seenResidues.add(seg.index); -// loci[loci.length] = StructureElement.create(unit, unit.elements[seg.start]); -// } -// } - -// loci.sort((x, y) => x.element - y.element); -// return loci; -// } -// } \ No newline at end of file diff --git a/src/mol-model/structure/model/properties/seconday-structure.ts b/src/mol-model/structure/model/properties/seconday-structure.ts index 49618f0071fc1974657ffd2f01cd1ffaf5c18d31..9434507ebc9ae1a11b2fc02f4cb9e00d0d4c4d0c 100644 --- a/src/mol-model/structure/model/properties/seconday-structure.ts +++ b/src/mol-model/structure/model/properties/seconday-structure.ts @@ -17,12 +17,17 @@ interface SecondaryStructure { } namespace SecondaryStructure { - export type Element = None | Helix | Sheet + export type Element = None | Turn | Helix | Sheet export interface None { kind: 'none' } + export interface Turn { + kind: 'turn', + flags: SecondaryStructureType + } + export interface Helix { kind: 'helix', flags: SecondaryStructureType, diff --git a/src/mol-model/structure/model/properties/utils/guess-element.ts b/src/mol-model/structure/model/properties/utils/guess-element.ts index 05658249f5aed93f771b66cae471b9a74be22955..54a66a8f99537da61f09ebcdab5c4a30e33237fc 100644 --- a/src/mol-model/structure/model/properties/utils/guess-element.ts +++ b/src/mol-model/structure/model/properties/utils/guess-element.ts @@ -12,7 +12,7 @@ function charAtIsNumber(str: string, index: number) { return code >= 48 && code <= 57 } -export function guessElement (str: string) { +export function guessElement(str: string) { let at = str.trim().toUpperCase() if (charAtIsNumber(at, 0)) at = at.substr(1) diff --git a/src/mol-model/structure/model/properties/utils/secondary-structure.ts b/src/mol-model/structure/model/properties/utils/secondary-structure.ts index afa8199475f42a616da01e9c8ec056a416c0fd53..e61dd42415969b91a9e065e2a5ba6e18522d1c16 100644 --- a/src/mol-model/structure/model/properties/utils/secondary-structure.ts +++ b/src/mol-model/structure/model/properties/utils/secondary-structure.ts @@ -2,6 +2,7 @@ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> + * @author Sebastian Bittrich <sebastian.bittrich@rcsb.org> */ import { SecondaryStructure } from 'mol-model/structure/model/properties/seconday-structure'; @@ -14,13 +15,120 @@ import { IntAdjacencyGraph } from 'mol-math/graph'; import { BitFlags } from 'mol-util'; import { ElementIndex } from 'mol-model/structure/model/indexing'; import { AtomicHierarchy, AtomicConformation } from '../atomic'; +import { ParamDefinition as PD } from 'mol-util/param-definition' -export function computeSecondaryStructure(hierarchy: AtomicHierarchy, conformation: AtomicConformation): SecondaryStructure { +/** + * TODO bugs to fix: + * - some turns are not detected correctly: see e.g. pdb:1acj - maybe more than 2 hbonds require some residue to donate electrons + * - some sheets are not extended correctly: see e.g. pdb:1acj + * - validate new helix definition + * - validate new ordering of secondary structure elements + */ + + /** max distance between two C-alpha atoms to check for hbond */ +const caMaxDist = 9.0; + +/** + * Constant for electrostatic energy in kcal/mol + * f * q1 * q2 + * Q = -332 * 0.42 * 0.20 + * + * f is the dimensional factor + * + * q1 and q2 are partial charges which are placed on the C,O + * (+q1,-q1) and N,H (-q2,+q2) + */ +const Q = -27.888 + +/** cutoff for hbonds in kcal/mol, must be lower to be consider as an hbond */ +const hbondEnergyCutoff = -0.5 +/** prevent extremely low hbond energies */ +const hbondEnergyMinimal = -9.9 + +interface DSSPContext { + params: Partial<PD.Values<SecondaryStructureComputationParams>>, + getResidueFlag: (f: DSSPType) => SecondaryStructureType, + getFlagName: (f: DSSPType) => String, + + hierarchy: AtomicHierarchy + proteinResidues: SortedArray<ResidueIndex> + /** flags for each residue */ + flags: Uint32Array + hbonds: DsspHbonds, + + torsionAngles: { phi: Float32Array, psi: Float32Array }, + backboneIndices: BackboneAtomIndices, + conformation: AtomicConformation, + ladders: Ladder[], + bridges: Bridge[] +} + +interface Ladder { + previousLadder: number, + nextLadder: number, + firstStart: number, + secondStart: number, + secondEnd: number, + firstEnd: number, + type: BridgeType +} + +const enum BridgeType { + PARALLEL = 0x0, + ANTI_PARALLEL = 0x1 +} + +class Bridge { + partner1: number; + partner2: number; + type: BridgeType; + + constructor(p1: number, p2: number, type: BridgeType) { + this.partner1 = Math.min(p1, p2) + this.partner2 = Math.max(p1, p2) + this.type = type + } +} + +type DSSPType = BitFlags<DSSPType.Flag> +namespace DSSPType { + export const is: (t: DSSPType, f: Flag) => boolean = BitFlags.has + export const create: (f: Flag) => DSSPType = BitFlags.create + export const enum Flag { + _ = 0x0, + H = 0x1, + B = 0x2, + E = 0x4, + G = 0x8, + I = 0x10, + S = 0x20, + T = 0x40, + T3 = 0x80, + T4 = 0x100, + T5 = 0x200, + T3S = 0x400, // marks 3-turn start + T4S = 0x800, + T5S = 0x1000 + } +} + +export const SecondaryStructureComputationParams = { + oldDefinition: PD.Boolean(true, { description: 'Whether to use the old DSSP convention for the annotation of turns and helices, causes them to be two residues shorter' }), + oldOrdering: PD.Boolean(true, { description: 'Alpha-helices are preferred over 3-10 helices' }) +} +export type SecondaryStructureComputationParams = typeof SecondaryStructureComputationParams + +export function computeSecondaryStructure(hierarchy: AtomicHierarchy, + conformation: AtomicConformation) { // TODO use Zhang-Skolnik for CA alpha only parts or for coarse parts with per-residue elements return computeModelDSSP(hierarchy, conformation) } -export function computeModelDSSP(hierarchy: AtomicHierarchy, conformation: AtomicConformation) { +export function computeModelDSSP(hierarchy: AtomicHierarchy, + conformation: AtomicConformation, + params: Partial<PD.Values<SecondaryStructureComputationParams>> = {}): SecondaryStructure { + params = { ...PD.getDefaultValues(SecondaryStructureComputationParams), ...params }; + const { lookup3d, proteinResidues } = calcAtomicTraceLookup3D(hierarchy, conformation) const backboneIndices = calcBackboneAtomIndices(hierarchy, proteinResidues) const hbonds = calcBackboneHbonds(hierarchy, conformation, proteinResidues, backboneIndices, lookup3d) @@ -28,69 +136,103 @@ export function computeModelDSSP(hierarchy: AtomicHierarchy, conformation: Atomi const residueCount = proteinResidues.length const flags = new Uint32Array(residueCount) + // console.log(`calculating secondary structure elements using ${ params.oldDefinition ? 'old' : 'revised'} definition and ${ params.oldOrdering ? 'old' : 'revised'} ordering of secondary structure elements`) + + const torsionAngles = calculateDihedralAngles(hierarchy, conformation, proteinResidues, backboneIndices) + + const ladders: Ladder[] = [] + const bridges: Bridge[] = [] + + const getResidueFlag = params.oldDefinition ? getOriginalResidueFlag : getUpdatedResidueFlag + const getFlagName = params.oldOrdering ? getOriginalFlagName : getUpdatedFlagName + const ctx: DSSPContext = { + params, + getResidueFlag, + getFlagName, + hierarchy, proteinResidues, flags, - hbonds + hbonds, + + torsionAngles, + backboneIndices, + conformation, + ladders, + bridges } - assignBends(ctx) assignTurns(ctx) assignHelices(ctx) + assignBends(ctx) assignBridges(ctx) assignLadders(ctx) assignSheets(ctx) - const assignment = getDSSPAssignment(flags) - + const assignment = getDSSPAssignment(flags, getResidueFlag) const type = new Uint32Array(hierarchy.residues._rowCount) as unknown as SecondaryStructureType[] + const keys: number[] = [] + const elements: SecondaryStructure.Element[] = [] + for (let i = 0, il = proteinResidues.length; i < il; ++i) { - type[proteinResidues[i]] = assignment[i] + const assign = assignment[i] + type[proteinResidues[i]] = assign + const flag = getResidueFlag(flags[i]) + // TODO is this expected behavior? elements will be strictly split depending on 'winning' flag + if (elements.length === 0 /* would fail at very start */ || flag !== (elements[elements.length - 1] as SecondaryStructure.Helix | SecondaryStructure.Sheet | SecondaryStructure.Turn).flags /* flag changed */) { + elements[elements.length] = createElement(mapToKind(assign), flags[i], getResidueFlag) + } + keys[i] = elements.length - 1 } const secondaryStructure: SecondaryStructure = { type, - key: [], // TODO - elements: [] // TODO + key: keys, + elements: elements } + return secondaryStructure } -interface DSSPContext { - hierarchy: AtomicHierarchy - proteinResidues: SortedArray<ResidueIndex> - /** flags for each residue */ - flags: Uint32Array - - hbonds: DsspHbonds +function createElement(kind: string, flag: DSSPType.Flag, getResidueFlag: (f: DSSPType) => SecondaryStructureType): SecondaryStructure.Element { + // TODO would be nice to add more detailed information + if (kind === 'helix') { + return { + kind: 'helix', + flags: getResidueFlag(flag) + } as SecondaryStructure.Helix + } else if (kind === 'sheet') { + return { + kind: 'sheet', + flags: getResidueFlag(flag) + } as SecondaryStructure.Sheet + } else if (kind === 'turn' || kind === 'bend') { + return { + kind: 'turn', + flags: getResidueFlag(flag) + } + } else { + return { + kind: 'none' + } + } } -type DSSPType = BitFlags<DSSPType.Flag> -namespace DSSPType { - export const is: (t: DSSPType, f: Flag) => boolean = BitFlags.has - export const create: (f: Flag) => DSSPType = BitFlags.create - export const enum Flag { - _ = 0x0, - H = 0x1, - B = 0x2, - E = 0x4, - G = 0x8, - I = 0x10, - S = 0x20, - T = 0x40, - T3 = 0x80, - T4 = 0x100, - T5 = 0x200, +function mapToKind(assignment: SecondaryStructureType.Flag) { + if (assignment === SecondaryStructureType.SecondaryStructureDssp.H || assignment === SecondaryStructureType.SecondaryStructureDssp.G || assignment === SecondaryStructureType.SecondaryStructureDssp.I) { + return 'helix' + } else if (assignment === SecondaryStructureType.SecondaryStructureDssp.B || assignment === SecondaryStructureType.SecondaryStructureDssp.E) { + return 'sheet' + } else if (assignment === SecondaryStructureType.SecondaryStructureDssp.T) { + return 'turn' + } else if (assignment === SecondaryStructureType.SecondaryStructureDssp.S) { + return 'bend' + } else { + return 'none' } } -/** max distance between two C-alpha atoms to check for hbond */ -const caMaxDist = 7.0; - -/** min distance between two C-alpha atoms to check for hbond */ -const caMinDist = 4.0; - function calcAtomicTraceLookup3D(hierarchy: AtomicHierarchy, conformation: AtomicConformation) { const { x, y, z } = conformation; const { moleculeType, traceElementIndex } = hierarchy.derived.residue @@ -141,6 +283,114 @@ function calcBackboneAtomIndices(hierarchy: AtomicHierarchy, proteinResidues: So type DsspHbonds = IntAdjacencyGraph<{ readonly energies: ArrayLike<number> }> +/** + * Bend(i) =: [angle ((CW - Ca(i - 2)),(C"(i + 2) - C"(i))) > 70"] + * + * Type: S + */ +function assignBends(ctx: DSSPContext) { + const flags = ctx.flags + const { x, y, z } = ctx.conformation + const { traceElementIndex } = ctx.hierarchy.derived.residue + + const proteinResidues = ctx.proteinResidues + const residueCount = proteinResidues.length + + const position = (i: number, v: Vec3) => Vec3.set(v, x[i], y[i], z[i]) + + const caPosPrev2 = Vec3() + const caPos = Vec3() + const caPosNext2 = Vec3() + + const nIndices = ctx.backboneIndices.nIndices + const cPos = Vec3() + const nPosNext = Vec3() + + const caMinus2 = Vec3() + const caPlus2 = Vec3() + + f1: for (let i = 2; i < residueCount - 2; i++) { + // check for peptide bond + for (let k = 0; k < 4; k++) { + let index = i + k - 2 + position(traceElementIndex[index], cPos) + position(nIndices[index + 1], nPosNext) + if (Vec3.squaredDistance(cPos, nPosNext) > 6.25 /* max squared peptide bond distance allowed */) { + continue f1 + } + } + + const oRIprev2 = proteinResidues[i - 2] + const oRI = proteinResidues[i] + const oRInext2 = proteinResidues[i + 2] + + const caAtomPrev2 = traceElementIndex[oRIprev2] + const caAtom = traceElementIndex[oRI] + const caAtomNext2 = traceElementIndex[oRInext2] + + position(caAtomPrev2, caPosPrev2) + position(caAtom, caPos) + position(caAtomNext2, caPosNext2) + + Vec3.sub(caMinus2, caPosPrev2, caPos) + Vec3.sub(caPlus2, caPos, caPosNext2) + + const angle = Vec3.angle(caMinus2, caPlus2) * 360 / (2 * Math.PI) + if (angle && angle > 70.00) { + flags[i] |= DSSPType.Flag.S + } + } +} + +function calculateDihedralAngles(hierarchy: AtomicHierarchy, conformation: AtomicConformation, proteinResidues: SortedArray<ResidueIndex>, backboneIndices: BackboneAtomIndices): { phi: Float32Array, psi: Float32Array } { + const { cIndices, nIndices } = backboneIndices + const { index } = hierarchy + const { x, y, z } = conformation + const { traceElementIndex } = hierarchy.derived.residue + + const residueCount = proteinResidues.length + const position = (i: number, v: Vec3) => i === -1 ? Vec3.setNaN(v) : Vec3.set(v, x[i], y[i], z[i]) + + let cPosPrev = Vec3(), caPosPrev = Vec3(), nPosPrev = Vec3() + let cPos = Vec3(), caPos = Vec3(), nPos = Vec3() + let cPosNext = Vec3(), caPosNext = Vec3(), nPosNext = Vec3() + + if (residueCount === 0) return { phi: new Float32Array(0), psi: new Float32Array(0) } + + const phi: Float32Array = new Float32Array(residueCount - 1) + const psi: Float32Array = new Float32Array(residueCount - 1) + + position(-1, cPosPrev) + position(-1, caPosPrev) + position(-1, nPosPrev) + + position(cIndices[0], cPos) + position(traceElementIndex[proteinResidues[0]], caPos) + position(nIndices[0], nPos) + + position(cIndices[1], cPosNext) + position(traceElementIndex[proteinResidues[1]], caPosNext) + position(nIndices[1], nPosNext) + + for (let i = 0; i < residueCount - 1; ++i) { + // ignore C-terminal residue as acceptor + if (index.findAtomOnResidue(proteinResidues[i], 'OXT') !== -1) continue + + // returns NaN for missing atoms + phi[i] = Vec3.dihedralAngle(cPosPrev, nPos, caPos, cPos) + psi[i] = Vec3.dihedralAngle(nPos, caPos, cPos, nPosNext) + + cPosPrev = cPos, caPosPrev = caPos, nPosPrev = nPos + cPos = cPosNext, caPos = caPosNext, nPos = nPosNext + + position(cIndices[i + 1], cPosNext) + position(traceElementIndex[proteinResidues[i + 1]], caPosNext) + position(nIndices[i + 1], nPosNext) + } + + return { phi, psi }; +} + function calcBackboneHbonds(hierarchy: AtomicHierarchy, conformation: AtomicConformation, proteinResidues: SortedArray<ResidueIndex>, backboneIndices: BackboneAtomIndices, lookup3d: GridLookup3D): DsspHbonds { const { cIndices, hIndices, nIndices, oIndices } = backboneIndices const { index } = hierarchy @@ -154,16 +404,14 @@ function calcBackboneHbonds(hierarchy: AtomicHierarchy, conformation: AtomicConf const nAtomResidues: number[] = []; const energies: number[] = []; - const oPos = Vec3.zero() - const cPos = Vec3.zero() - const caPos = Vec3.zero() - const nPos = Vec3.zero() - const hPos = Vec3.zero() - - const cPosPrev = Vec3.zero() - const oPosPrev = Vec3.zero() + const oPos = Vec3() + const cPos = Vec3() + const caPos = Vec3() + const nPos = Vec3() + const hPos = Vec3() - const caMinDistSq = caMinDist * caMinDist + const cPosPrev = Vec3() + const oPosPrev = Vec3() for (let i = 0, il = proteinResidues.length; i < il; ++i) { const oPI = i @@ -183,11 +431,9 @@ function calcBackboneHbonds(hierarchy: AtomicHierarchy, conformation: AtomicConf position(cAtom, cPos) position(caAtom, caPos) - const { indices, count, squaredDistances } = lookup3d.find(caPos[0], caPos[1], caPos[2], caMaxDist) + const { indices, count } = lookup3d.find(caPos[0], caPos[1], caPos[2], caMaxDist) for (let j = 0; j < count; ++j) { - if (squaredDistances[j] < caMinDistSq) continue - const nPI = indices[j] // ignore bonds within a residue or to prev or next residue, TODO take chain border into account @@ -245,8 +491,8 @@ function buildHbondGraph(residueCount: number, oAtomResidues: number[], nAtomRes /** Original priority: H,B,E,G,I,T,S */ function getOriginalResidueFlag(f: DSSPType) { if (DSSPType.is(f, DSSPType.Flag.H)) return SecondaryStructureType.SecondaryStructureDssp.H - if (DSSPType.is(f, DSSPType.Flag.B)) return SecondaryStructureType.SecondaryStructureDssp.B if (DSSPType.is(f, DSSPType.Flag.E)) return SecondaryStructureType.SecondaryStructureDssp.E + if (DSSPType.is(f, DSSPType.Flag.B)) return SecondaryStructureType.SecondaryStructureDssp.B if (DSSPType.is(f, DSSPType.Flag.G)) return SecondaryStructureType.SecondaryStructureDssp.G if (DSSPType.is(f, DSSPType.Flag.I)) return SecondaryStructureType.SecondaryStructureDssp.I if (DSSPType.is(f, DSSPType.Flag.T)) return SecondaryStructureType.SecondaryStructureDssp.T @@ -254,55 +500,50 @@ function getOriginalResidueFlag(f: DSSPType) { return SecondaryStructureType.Flag.None } +function getOriginalFlagName(f: DSSPType) { + if (DSSPType.is(f, DSSPType.Flag.H)) return 'H' + if (DSSPType.is(f, DSSPType.Flag.E)) return 'E' + if (DSSPType.is(f, DSSPType.Flag.B)) return 'B' + if (DSSPType.is(f, DSSPType.Flag.G)) return 'G' + if (DSSPType.is(f, DSSPType.Flag.I)) return 'I' + if (DSSPType.is(f, DSSPType.Flag.T)) return 'T' + if (DSSPType.is(f, DSSPType.Flag.S)) return 'S' + return '-' +} + /** Version 2.1.0 priority: I,H,B,E,G,T,S */ function getUpdatedResidueFlag(f: DSSPType) { if (DSSPType.is(f, DSSPType.Flag.I)) return SecondaryStructureType.SecondaryStructureDssp.I if (DSSPType.is(f, DSSPType.Flag.H)) return SecondaryStructureType.SecondaryStructureDssp.H - if (DSSPType.is(f, DSSPType.Flag.B)) return SecondaryStructureType.SecondaryStructureDssp.B if (DSSPType.is(f, DSSPType.Flag.E)) return SecondaryStructureType.SecondaryStructureDssp.E + if (DSSPType.is(f, DSSPType.Flag.B)) return SecondaryStructureType.SecondaryStructureDssp.B if (DSSPType.is(f, DSSPType.Flag.G)) return SecondaryStructureType.SecondaryStructureDssp.G if (DSSPType.is(f, DSSPType.Flag.T)) return SecondaryStructureType.SecondaryStructureDssp.T if (DSSPType.is(f, DSSPType.Flag.S)) return SecondaryStructureType.SecondaryStructureDssp.S return SecondaryStructureType.Flag.None } -// function geFlagName(f: DSSPType) { -// if (DSSPType.is(f, DSSPType.Flag.I)) return 'I' -// if (DSSPType.is(f, DSSPType.Flag.H)) return 'H' -// if (DSSPType.is(f, DSSPType.Flag.B)) return 'B' -// if (DSSPType.is(f, DSSPType.Flag.E)) return 'E' -// if (DSSPType.is(f, DSSPType.Flag.G)) return 'G' -// if (DSSPType.is(f, DSSPType.Flag.T)) return 'T' -// if (DSSPType.is(f, DSSPType.Flag.S)) return 'S' -// return '-' -// } - -function getDSSPAssignment(flags: Uint32Array, useOriginal = false) { - const getResidueFlag = useOriginal ? getOriginalResidueFlag : getUpdatedResidueFlag +function getUpdatedFlagName(f: DSSPType) { + if (DSSPType.is(f, DSSPType.Flag.I)) return 'I' + if (DSSPType.is(f, DSSPType.Flag.H)) return 'H' + if (DSSPType.is(f, DSSPType.Flag.E)) return 'E' + if (DSSPType.is(f, DSSPType.Flag.B)) return 'B' + if (DSSPType.is(f, DSSPType.Flag.G)) return 'G' + if (DSSPType.is(f, DSSPType.Flag.T)) return 'T' + if (DSSPType.is(f, DSSPType.Flag.S)) return 'S' + return '-' +} + +function getDSSPAssignment(flags: Uint32Array, getResidueFlag: (f: DSSPType) => SecondaryStructureType) { const type = new Uint32Array(flags.length) for (let i = 0, il = flags.length; i < il; ++i) { const f = DSSPType.create(flags[i]) - // console.log(i, geFlagName(f)) type[i] = getResidueFlag(f) } + return type as unknown as ArrayLike<SecondaryStructureType> } -/** - * Constant for electrostatic energy in kcal/mol - * f * q1 * q2 - * Q = -332 * 0.42 * 0.20 - * - * f is the dimensional factor - * - * q1 and q2 are partial charges which are placed on the C,O - * (+q1,-q1) and N,H (-q2,+q2) - */ -const Q = -27.888 - -/** cutoff for hbonds in kcal/mol, must be lower to be consider as an hbond */ -const hbondEnergyCutoff = -0.5 - /** * E = Q * (1/r(ON) + l/r(CH) - l/r(OH) - l/r(CN)) */ @@ -314,7 +555,13 @@ function calcHbondEnergy(oPos: Vec3, cPos: Vec3, nPos: Vec3, hPos: Vec3) { const e1 = Q / distOH - Q / distCH const e2 = Q / distCN - Q / distON - return e1 + e2 + const e = e1 + e2 + + // cap lowest possible energy + if (e < hbondEnergyMinimal) + return hbondEnergyMinimal + + return e } /** @@ -329,24 +576,31 @@ function assignTurns(ctx: DSSPContext) { const { chains, residueAtomSegments, chainAtomSegments } = hierarchy const { label_asym_id } = chains - const turnFlag = [0, 0, 0, DSSPType.Flag.T3, DSSPType.Flag.T4, DSSPType.Flag.T5] + const turnFlag = [DSSPType.Flag.T3S, DSSPType.Flag.T4S, DSSPType.Flag.T5S, DSSPType.Flag.T3, DSSPType.Flag.T4, DSSPType.Flag.T5] - for (let i = 0, il = proteinResidues.length; i < il; ++i) { - const rI = proteinResidues[i] - const cI = chainAtomSegments.index[residueAtomSegments.offsets[rI]] - - // TODO should take sequence gaps into account - for (let k = 3; k <= 5; ++k) { - if (i + k >= proteinResidues.length) continue + for (let idx = 0; idx < 3; idx++) { + for (let i = 0, il = proteinResidues.length - 1; i < il; ++i) { + const rI = proteinResidues[i] + const cI = chainAtomSegments.index[residueAtomSegments.offsets[rI]] - const rN = proteinResidues[i + k] + // TODO should take sequence gaps into account + const rN = proteinResidues[i + idx + 3] const cN = chainAtomSegments.index[residueAtomSegments.offsets[rN]] // check if on same chain if (!label_asym_id.areValuesEqual(cI, cN)) continue // check if hbond exists - if (hbonds.getDirectedEdgeIndex(i, i + k) !== -1) { - flags[i] |= turnFlag[k] | DSSPType.Flag.T + if (hbonds.getDirectedEdgeIndex(i, i + idx + 3) !== -1) { + flags[i] |= turnFlag[idx + 3] | turnFlag[idx] + if (ctx.params.oldDefinition) { + for (let k = 1; k < idx + 3; ++k) { + flags[i + k] |= turnFlag[idx + 3] | DSSPType.Flag.T + } + } else { + for (let k = 0; k <= idx + 3; ++k) { + flags[i + k] |= turnFlag[idx + 3] | DSSPType.Flag.T + } + } } } } @@ -369,7 +623,7 @@ function assignTurns(ctx: DSSPContext) { * Type: B */ function assignBridges(ctx: DSSPContext) { - const { proteinResidues, hbonds, flags } = ctx + const { proteinResidues, hbonds, flags, bridges } = ctx const { offset, b } = hbonds let i: number, j: number @@ -385,6 +639,8 @@ function assignBridges(ctx: DSSPContext) { if (i !== j && hbonds.getDirectedEdgeIndex(j, i + 1) !== -1) { flags[i] |= DSSPType.Flag.B flags[j] |= DSSPType.Flag.B + // TODO move to constructor, actually omit object all together + bridges[bridges.length] = new Bridge(i, j, BridgeType.PARALLEL) } // Parallel Bridge(i, j) =: [Hbond(j - 1, i) and Hbond(i, j + 1)] @@ -393,6 +649,7 @@ function assignBridges(ctx: DSSPContext) { if (i !== j && hbonds.getDirectedEdgeIndex(j - 1, i) !== -1) { flags[i] |= DSSPType.Flag.B flags[j] |= DSSPType.Flag.B + bridges[bridges.length] = new Bridge(j, i, BridgeType.PARALLEL) } // Antiparallel Bridge(i, j) =: [Hbond(i, j) and Hbond(j, i)] @@ -401,6 +658,7 @@ function assignBridges(ctx: DSSPContext) { if (i !== j && hbonds.getDirectedEdgeIndex(j, i) !== -1) { flags[i] |= DSSPType.Flag.B flags[j] |= DSSPType.Flag.B + bridges[bridges.length] = new Bridge(j, i, BridgeType.ANTI_PARALLEL) } // Antiparallel Bridge(i, j) =: [Hbond(i - 1, j + 1) and Hbond(j - 1, i + l)] @@ -409,9 +667,12 @@ function assignBridges(ctx: DSSPContext) { if (i !== j && hbonds.getDirectedEdgeIndex(j - 1, i + 1) !== -1) { flags[i] |= DSSPType.Flag.B flags[j] |= DSSPType.Flag.B + bridges[bridges.length] = new Bridge(j, i, BridgeType.ANTI_PARALLEL) } } } + + bridges.sort((a, b) => a.partner1 > b.partner1 ? 1 : a.partner1 < b.partner1 ? -1 : 0) } /** @@ -428,17 +689,41 @@ function assignBridges(ctx: DSSPContext) { function assignHelices(ctx: DSSPContext) { const { proteinResidues, flags } = ctx - const turnFlag = [0, 0, 0, DSSPType.Flag.T3, DSSPType.Flag.T4, DSSPType.Flag.T5] + const turnFlag = [DSSPType.Flag.T3S, DSSPType.Flag.T4S, DSSPType.Flag.T5S, DSSPType.Flag.T3, DSSPType.Flag.T4, DSSPType.Flag.T5] const helixFlag = [0, 0, 0, DSSPType.Flag.G, DSSPType.Flag.H, DSSPType.Flag.I] - for (let i = 1, il = proteinResidues.length; i < il; ++i) { - const fI = DSSPType.create(flags[i]) - const fI1 = DSSPType.create(flags[i - 1]) + const helixCheckOrder = ctx.params.oldOrdering ? [4, 3, 5] : [3, 4, 5] + for (let ni = 0; ni < helixCheckOrder.length; ni++) { + const n = helixCheckOrder[ni] + + for (let i = 1, il = proteinResidues.length - n; i < il; i++) { + const fI = DSSPType.create(flags[i]) + const fI1 = DSSPType.create(flags[i - 1]) + const fI2 = DSSPType.create(flags[i + 1]) + + // TODO rework to elegant solution which will not break instantly + if (ctx.params.oldOrdering) { + if ((n === 3 && (DSSPType.is(fI, DSSPType.Flag.H) || DSSPType.is(fI2, DSSPType.Flag.H)) || // for 3-10 yield to alpha helix + (n === 5 && ((DSSPType.is(fI, DSSPType.Flag.H) || DSSPType.is(fI, DSSPType.Flag.G)) || (DSSPType.is(fI2, DSSPType.Flag.H) || DSSPType.is(fI2, DSSPType.Flag.G)))))) { // for pi yield to all other helices + continue + } + } else { + if ((n === 4 && (DSSPType.is(fI, DSSPType.Flag.G) || DSSPType.is(fI2, DSSPType.Flag.G)) || // for alpha helix yield to 3-10 + (n === 5 && ((DSSPType.is(fI, DSSPType.Flag.H) || DSSPType.is(fI, DSSPType.Flag.G)) || (DSSPType.is(fI2, DSSPType.Flag.H) || DSSPType.is(fI2, DSSPType.Flag.G)))))) { // for pi yield to all other helices + continue + } + } - for (let k = 3; k <= 5; ++k) { - if (DSSPType.is(fI, turnFlag[k]) && DSSPType.is(fI1, turnFlag[k])) { - for (let l = 0; l < k; ++l) { - flags[i + l] |= helixFlag[k] + if (DSSPType.is(fI, turnFlag[n]) && DSSPType.is(fI, turnFlag[n - 3]) && // check fI for turn start of proper type + DSSPType.is(fI1, turnFlag[n]) && DSSPType.is(fI1, turnFlag[n - 3])) { // check fI1 accordingly + if (ctx.params.oldDefinition) { + for (let k = 0; k < n; k++) { + flags[i + k] |= helixFlag[n] + } + } else { + for (let k = -1; k <= n; k++) { + flags[i + k] |= helixFlag[n] + } } } } @@ -451,23 +736,137 @@ function assignHelices(ctx: DSSPContext) { * Type: E */ function assignLadders(ctx: DSSPContext) { - // TODO + const { bridges, ladders } = ctx + + // create ladders + for (let bridgeIndex = 0; bridgeIndex < bridges.length; bridgeIndex++) { + const bridge = bridges[bridgeIndex] + let found = false + for (let ladderIndex = 0; ladderIndex < ladders.length; ladderIndex++) { + const ladder = ladders[ladderIndex] + if (shouldExtendLadder(ladder, bridge)) { + found = true + ladder.firstEnd++ + if (bridge.type === BridgeType.PARALLEL) { + ladder.secondEnd++ + } else { + ladder.secondStart-- + } + } + } + + // no suitable assignment: create new ladder with single bridge as content + if (!found) { + ladders[ladders.length] = { + previousLadder: 0, + nextLadder: 0, + firstStart: bridge.partner1, + firstEnd: bridge.partner1, + secondStart: bridge.partner2, + secondEnd: bridge.partner2, + type: bridge.type + } + } + } + + // connect ladders + for (let ladderIndex1 = 0; ladderIndex1 < ladders.length; ladderIndex1++) { + const ladder1 = ladders[ladderIndex1] + for (let ladderIndex2 = ladderIndex1; ladderIndex2 < ladders.length; ladderIndex2++) { + const ladder2 = ladders[ladderIndex2] + if (resemblesBulge(ladder1, ladder2)) { + ladder1.nextLadder = ladderIndex2 + ladder2.previousLadder = ladderIndex1 + } + } + } } /** - * sheet=: set of one or more ladders connected by shared residues - * - * Type: E + * For beta structures, we define: a bulge-linked ladder consists of two ladders or bridges of the same type + * connected by at most one extra residue of one strand and at most four extra residues on the other strand, + * all residues in bulge-linked ladders are marked E, including any extra residues. */ -function assignSheets(ctx: DSSPContext) { - // TODO +function resemblesBulge(ladder1: Ladder, ladder2: Ladder) { + if (!(ladder1.type === ladder2.type && ladder2.firstStart - ladder1.firstEnd < 6 && + ladder1.firstStart < ladder2.firstStart && ladder2.nextLadder === 0)) return false + + if (ladder1.type === BridgeType.PARALLEL) { + return bulgeCriterion2(ladder1, ladder2) + } else { + return bulgeCriterion2(ladder2, ladder1) + } +} + +function bulgeCriterion2(ladder1: Ladder, ladder2: Ladder) { + return ladder2.secondStart - ladder1.secondEnd > 0 && ((ladder2.secondStart - ladder1.secondEnd < 6 && + ladder2.firstStart - ladder1.firstEnd < 3) || ladder2.secondStart - ladder1.secondEnd < 3) +} + +function shouldExtendLadder(ladder: Ladder, bridge: Bridge): boolean { + // in order to extend ladders, same type must be present + if (bridge.type !== ladder.type) return false + + // only extend if residue 1 is sequence neighbor with regard to ladder + if (bridge.partner1 !== ladder.firstEnd + 1) return false + + if (bridge.type === BridgeType.PARALLEL) { + if (bridge.partner2 === ladder.secondEnd + 1) { + return true + } + } else { + if (bridge.partner2 === ladder.secondStart - 1) { + return true + } + } + + return false +} + +function isHelixType(f: DSSPType) { + return DSSPType.is(f, DSSPType.Flag.G) || DSSPType.is(f, DSSPType.Flag.H) || DSSPType.is(f, DSSPType.Flag.I) } /** - * Bend(i) =: [angle ((CW - Ca(i - 2)),(C"(i + 2) - C"(i))) > 70"] + * sheet=: set of one or more ladders connected by shared residues * - * Type: S + * Type: E */ -function assignBends(ctx: DSSPContext) { - // TODO +function assignSheets(ctx: DSSPContext) { + const { ladders, flags } = ctx + for (let ladderIndex = 0; ladderIndex < ladders.length; ladderIndex++) { + const ladder = ladders[ladderIndex] + for (let lcount = ladder.firstStart; lcount <= ladder.firstEnd; lcount++) { + const diff = ladder.firstStart - lcount + const l2count = ladder.secondStart - diff + + if (ladder.firstStart !== ladder.firstEnd) { + flags[lcount] |= DSSPType.Flag.E + flags[l2count] |= DSSPType.Flag.E + } else { + if (!isHelixType(flags[lcount]) && DSSPType.is(flags[lcount], DSSPType.Flag.E)) { + flags[lcount] |= DSSPType.Flag.B + } + if (!isHelixType(flags[l2count]) && DSSPType.is(flags[l2count], DSSPType.Flag.E)) { + flags[l2count] |= DSSPType.Flag.B + } + } + } + + if (ladder.nextLadder === 0) continue + + const conladder = ladders[ladder.nextLadder] + for (let lcount = ladder.firstStart; lcount <= conladder.firstEnd; lcount++) { + flags[lcount] |= DSSPType.Flag.E + } + if (ladder.type === BridgeType.PARALLEL) { + for (let lcount = ladder.secondStart; lcount <= conladder.secondEnd; lcount++) { + flags[lcount] |= DSSPType.Flag.E + } + } else { + for (let lcount = conladder.secondEnd; lcount <= ladder.secondStart; lcount++) { + flags[lcount] |= DSSPType.Flag.E + } + } + } } \ No newline at end of file diff --git a/src/mol-model/structure/model/properties/utils/secondary-structure.validation b/src/mol-model/structure/model/properties/utils/secondary-structure.validation new file mode 100644 index 0000000000000000000000000000000000000000..4f1a1f1bba2be14065663ee39ef36e21fb2d575a --- /dev/null +++ b/src/mol-model/structure/model/properties/utils/secondary-structure.validation @@ -0,0 +1,75 @@ +compares Mol* port of DSSP (with default parameters) to the BioJava implementation + +### pdb:1pga ### +# turns # +Mol*: ----------------------TTTTTTTTTTTTTTTT--------TTTT------ +53 turns, 18 openings +DSSP: ----------------------TTTTTTTTTTTTTTTT--------TTTT------ +53 turns, 18 openings + +# bends # +Mol*: ---------SS---------SSSSSSSSSSSSSSSSSS--------SSS------- +23 bends +DSSP: ---------SS---------SSSSSSSSSSSSSSSSSS--------SSS------- +23 bends + +# helices # +Mol*: ----------------------HHHHHHHHHHHHHHTT--------TTTT------ +44 helix elements - 0 3-10, 44 alpha, 0 pi +DSSP: ----------------------HHHHHHHHHHHHHHTT--------TTTT------ +44 helix elements - 0 3-10, 44 alpha, 0 pi + +# all # +Mol*: -EEEEEEE-SS-EEEEEEE-SSHHHHHHHHHHHHHHTT---EEEEETTTTEEEEE- +DSSP: -EEEEEEE-SS-EEEEEEE-SSHHHHHHHHHHHHHHTT---EEEEETTTTEEEEE- + + +### pdb:1bta ### +# turns # +Mol*: ------TTT---TTTTTTTTTTTTT--TT----TTTTTTTTTTT-----------TTTTTTTTT--TTTTTTTTTTTTTTT-------- +127 turns, 44 openings +DSSP: ------TTT---TTTTTTTTTTTTT--TT----TTTTTTTTTTT-----------TTTTTTTTT--TTTTTTTTTTTTTTT-------- +127 turns, 44 openings + +# bends # +Mol*: ------SSS--SSSSSSSSSSSSS---SS---SSSSSSSSSSSSS-SS------SSSSSSSSSSSSSSSSSSSSSSSSSSS-------- +60 bends +DSSP: ------SSS--SSSSSSSSSSSSS---SS---SSSSSSSSSSSSS-SS------SSSSSSSSSSSSSSSSSSSSSSSSSSS-------- +60 bends + +# helices # +Mol*: ------TTT---HHHHHHHHHHHHT--TT----HHHHHHHHTTT-----------TTHHHHTTT--HHHHHHHHHHHHHTT-------- +100 helix elements - 0 3-10, 100 alpha, 0 pi +DSSP: ------TTT---HHHHHHHHHHHHT--TT----HHHHHHHHTTT-----------TTHHHHTTT--HHHHHHHHHHHHHTT-------- +100 helix elements - 0 3-10, 100 alpha, 0 pi + +# all # +Mol*: -EEEEETTT--SHHHHHHHHHHHHT--TT---SHHHHHHHHTTTS-SSEEEEEESTTHHHHTTTSSHHHHHHHHHHHHHTT--EEEEE- +DSSP: -EEEEETTT--SHHHHHHHHHHHHT--TT---SHHHHHHHHTTTS-SSEEEEEESTTHHHHTTTSSHHHHHHHHHHHHHTT--EEEEE- + + +### pdb:1acj ### +# turns # +Mol*: -------TT----------TT----------------TTTTT----------------------------TTTT-TTTTTT----------------------------------TTT------TTT-TTTTTTTTT-----------TTTT---TT-------TTTTTTTTTTTTTTTTTTTTT--TT-------TTTTTTTTTTTT-TTTTTT----------TT-TT----TTTTTTTTTTTTTTTT-----TTTTTTTTTT--TTTTTTTTTTT-----------------------TTTTTTTT----------------TTTTTTT-TT--TT------TTTTTTTTTTTTTT--TTTTTTTTTTT--TTTTT-TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT-------------TT----TTT---TTTTTTTTTTTTT-TTT---TTTTTTTTTTTTTTTTTTTT--------------------------------TTTTTTTTTTTTTTTTTTT- +614 turns, 223 openings +DSSP: -------TT----------TT----------------TTTTT------------------------------TT-TTTTTT----------------------------------TTT------TTT-TTTTTTTTT-----------TTTT---TT-------TTTTTTTTTTTTTTTTTTTTT--TT-------TTTTTTTTTTTT-TTTTTT----------TT-TT----TTTTTTTTTTTTTTTT-----TTTTTTTTTT--TTTTTTTTTTT-----------------------TTTTTTTT----------------TTTTTTT-TT--TT------TTTTTTTTTTT-TT--TTTTTTTTTTT--TTTTT-TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT-------------TT----TTT---TTTTTTTTTTTTT-TTT---TTTTTTTTTTTTTTTTTTTT--------------------------------TTTTTTTTTTTTTTTTTTT- +606 turns, 220 openings + +# bends # +Mol*: --S----SS----------SSS----------S----SS-SSS--------SS-----S-----------SSSS-SSSSSSS--S---S----------SS--SS---------SSSS---S-SSSS--SSSSSSS-----------SSSS----SS-SSS-S-SSSSSSSSSSSSSSSSSSSSS--SSS------SSSSSSSSSSSS-SSSSSS-S----SS--SSSSSS---SSSSSSSSSSSSSSS----S-SSSSSSSSSSS-SSSSSSSSSS--SS--SS--S------SSSSSS-SSSSSSS--S--S-------S--SSSSSSSSSSS--SSS-----SSSSSSSSSSS-SS--SSSSSSSSSSS--SSSSS-SSSSSSSSSSSSSSSSS----SSSSSSSSSSSS-----------SS--S-SSS-S-SS-SSSSSS--SS-SSS---SSSSSSSSSSSSSSSSSSSSSSSS---------SSS------SSSS----SSSS-S----SSSSSSSSSS-- +305 bends +DSSP: --S----SS----------SSS----------S----SS-SSS--------SS-----S-----------SSSS-SSSSSSS--S---S----------SS--SS---------SSSS---S-SSSS--SSSSSSS-----------SSSS----SS-SSS-S-SSSSSSSSSSSSSSSSSSSSS--SSS------SSSSSSSSSSSS-SSSSSS-S----SS--SSSSSS---SSSSSSSSSSSSSSS----S-SSSSSSSSSSS-SSSSSSSSSS--SS--SS--S------SSSSSS-SSSSSSS--S--S-------S--SSSSSSSSSSS--SSS-----SSSSSSSSSSS-SS--SSSSSSSSSSS--SSSSS-SSSSSSSSSSSSSSSSS----SSSSSSSSSSSS-----------SS--S-SSS-S-SS-SSSSSS--SS-SSS---SSSSSSSSSSSSSSSSSSSSSSSS---------SSS------SSSS----SSSS-S----SSSSSSSSSS-- +305 bends + +# helices # +Mol*: -------TT----------TT----------------GGGTT----------------------------TTTT-HHHHTT----------------------------------TTT------GGG-THHHHHHHT-----------HHHH---TT-------HHHHHHHHHHHHHHHHGGGGT--TT-------THHHHHHHHHHH-HHHHTT----------TT-TT----HHHHHHHHHHHHHHTT-----HHHHHHHHHH--HHHHHHHHGGG-----------------------HHHHHHHT----------------HHHHHHH-TT--TT------HHHHHHHHHHHTTT--HHHHHHHHHHH--GGGTT-HHHHHHHHHHHHHHHHTHHHHHHHHHHHHTT-------------TT----GGG---TTTTHHHHTTGGG-GGG---HHHHHHHHHHHHHHHHHHHT--------------------------------TTHHHHHHHHTHHHHHHHH- +523 helix elements - 27 3-10, 496 alpha, 0 pi +DSSP: -------TT----------TT----------------GGGTT------------------------------TT-HHHHTT----------------------------------TTT------GGG-THHHHHHHT-----------HHHH---TT-------HHHHHHHHHHHHHHHHGGGGT--TT-------THHHHHHHHHHH-HHHHTT----------TT-TT----HHHHHHHHHHHHHHTT-----HHHHHHHHHH--HHHHHHHHGGG-----------------------HHHHHHHT----------------HHHHHHH-TT--TT------HHHHHHHHHHH-TT--HHHHHHHHHHH--GGGTT-HHHHHHHHHHHHHHHHTHHHHHHHHHHHHTT-------------TT----GGG---TTTTHHHHTTGGG-GGG---HHHHHHHHHHHHHHHHHHHT--------------------------------TTHHHHHHHHTHHHHHHHH- +523 helix elements - 27 3-10, 496 alpha, 0 pi + +# all # +Mol*: --SEEEETTEEEE-EEEEETTEEEEEEEEEE-EE---GGGTTS--EE----SSEEE--S---B-------TTTT-HHHHTTS--S-B-S---EEEEEE-SS--SSEEEEEEE--STTT---S-SGGG-THHHHHHHT-EEEE-----SHHHH---TT-SSS-S-HHHHHHHHHHHHHHHHGGGGTEEEEEEEEEEETHHHHHHHHHHH-HHHHTT-SEEEEES--TTSTTSEEEHHHHHHHHHHHHHHTT---S-HHHHHHHHHHS-HHHHHHHHGGG-SS--SS--S--EEE-SSSSSS-HHHHHHHT-S--S-EEEEEESB-SHHHHHHHSTT--TTS-----HHHHHHHHHHHTTT--HHHHHHHHHHH--GGGTT-HHHHHHHHHHHHHHHHTHHHHHHHHHHHHTTSS-EEEEEE----TT--S-GGG-SBTTTTHHHHTTGGG-GGG---HHHHHHHHHHHHHHHHHHHTSSSS---------SSS-EEEEESSSS--EEESTTHHHHHHHHTHHHHHHHH- +DSSP: --SEEEETTEEEE-EEEEETTEEEEEEEEEE-EE---GGGTTS--EE----SSEEE--S---B-------SSTT-HHHHTTS--S-B-S---EEEEEE-SS--SSEEEEEEE--STTT---S-SGGG-THHHHHHHT-EEEE-----SHHHH---TT-SSS-S-HHHHHHHHHHHHHHHHGGGGTEEEEEEEEEEETHHHHHHHHHHH-HHHHTT-SEEEEES--TTSTTS-EEHHHHHHHHHHHHHHTT---S-HHHHHHHHHHS-HHHHHHHHGGG-SS--SS--S---EE-SSSSSS-HHHHHHHT-S--S-EEEEEESB-SHHHHHHHSTT--TTS-----HHHHHHHHHHH-TT--HHHHHHHHHHH--GGGTT-HHHHHHHHHHHHHHHHTHHHHHHHHHHHHTTSS-EEEEEE----TT--S-GGG-SBTTTTHHHHTTGGG-GGG---HHHHHHHHHHHHHHHHHHHTSSSS---------SSS-EEEEESSSS--EEESTTHHHHHHHHTHHHHHHHH- + +TODO fix mismatches e.g. here +TODO move to spec.ts once tests are running \ No newline at end of file diff --git a/src/mol-model/structure/model/types.ts b/src/mol-model/structure/model/types.ts index 1361395d7064bf4607f18930dfbdb7e8b193dcdc..8b5e1b947e970cc7bf8fcb6533801849423e972e 100644 --- a/src/mol-model/structure/model/types.ts +++ b/src/mol-model/structure/model/types.ts @@ -275,41 +275,42 @@ export namespace SecondaryStructureType { DoubleHelix = 0x1, Helix = 0x2, Beta = 0x4, - Turn = 0x8, + Bend = 0x8, + Turn = 0x10, // category variant - LeftHanded = 0x10, // helix - RightHanded = 0x20, + LeftHanded = 0x20, // helix + RightHanded = 0x40, - ClassicTurn = 0x40, // turn - InverseTurn = 0x80, + ClassicTurn = 0x80, // turn + InverseTurn = 0x100, // sub-category - HelixOther = 0x100, // protein - Helix27 = 0x200, - Helix3Ten = 0x400, - HelixAlpha = 0x800, - HelixGamma = 0x1000, - HelixOmega = 0x2000, - HelixPi = 0x4000, - HelixPolyproline = 0x8000, - - DoubleHelixOther = 0x10000, // nucleic - DoubleHelixZ = 0x20000, - DoubleHelixA = 0x40000, - DoubleHelixB = 0x80000, - - BetaOther = 0x100000, // protein - BetaStrand = 0x200000, // single strand - BetaSheet = 0x400000, // multiple hydrogen bonded strands - BetaBarell = 0x800000, // closed series of sheets - - TurnOther = 0x1000000, // protein - Turn1 = 0x2000000, - Turn2 = 0x4000000, - Turn3 = 0x8000000, - - NA = 0x10000000, // not applicable/available + HelixOther = 0x200, // protein + Helix27 = 0x400, + Helix3Ten = 0x800, + HelixAlpha = 0x1000, + HelixGamma = 0x2000, + HelixOmega = 0x4000, + HelixPi = 0x8000, + HelixPolyproline = 0x10000, + + DoubleHelixOther = 0x20000, // nucleic + DoubleHelixZ = 0x40000, + DoubleHelixA = 0x80000, + DoubleHelixB = 0x100000, + + BetaOther = 0x200000, // protein + BetaStrand = 0x400000, // single strand + BetaSheet = 0x800000, // multiple hydrogen bonded strands + BetaBarell = 0x1000000, // closed series of sheets + + TurnOther = 0x2000000, // protein + Turn1 = 0x4000000, + Turn2 = 0x8000000, + Turn3 = 0x10000000, + + NA = 0x20000000, // not applicable/available } export const SecondaryStructureMmcif: { [value: string]: number } = { @@ -386,7 +387,7 @@ export namespace SecondaryStructureType { G: Flag.Helix | Flag.Helix3Ten, // 3-helix (310 helix) I: Flag.Helix | Flag.HelixPi, // 5 helix (pi-helix) T: Flag.Turn, // hydrogen bonded turn - S: Flag.Turn, // bend + S: Flag.Bend, // bend } } diff --git a/src/mol-model/structure/query.ts b/src/mol-model/structure/query.ts index df09d47cb60acb44696a3ebe3ea7d7bb26480c79..8538f0bc795a9043518a5109bf5df6f86609586e 100644 --- a/src/mol-model/structure/query.ts +++ b/src/mol-model/structure/query.ts @@ -9,12 +9,14 @@ import { StructureQuery } from './query/query' export * from './query/context' import * as generators from './query/queries/generators' import * as modifiers from './query/queries/modifiers' +import * as filters from './query/queries/filters' import * as combinators from './query/queries/combinators' import * as internal from './query/queries/internal' import pred from './query/predicates' export const Queries = { generators, + filters, modifiers, combinators, pred, diff --git a/src/mol-model/structure/query/queries/filters.ts b/src/mol-model/structure/query/queries/filters.ts index a64cffc38bf15eae0a037b80153a2ac03b8d6fd4..4b08350f68967e22fa5002cc6babafb37121756a 100644 --- a/src/mol-model/structure/query/queries/filters.ts +++ b/src/mol-model/structure/query/queries/filters.ts @@ -31,6 +31,25 @@ export function pick(query: StructureQuery, pred: QueryPredicate): StructureQuer }; } +export function first(query: StructureQuery): StructureQuery { + return ctx => { + const sel = query(ctx); + const ret = StructureSelection.LinearBuilder(ctx.inputStructure); + if (sel.kind === 'singletons') { + if (sel.structure.elementCount > 0) { + const u = sel.structure.units[0]; + const s = Structure.create([u.getChild(SortedArray.ofSingleton(u.elements[0]))], ctx.inputStructure); + ret.add(s); + } + } else { + if (sel.structures.length > 0) { + ret.add(sel.structures[0]); + } + } + return ret.getSelection(); + }; +} + export interface UnitTypeProperties { atomic?: QueryFn, coarse?: QueryFn } export function getCurrentStructureProperties(ctx: QueryContext, props: UnitTypeProperties, set: Set<any>) { diff --git a/src/mol-model/structure/query/queries/generators.ts b/src/mol-model/structure/query/queries/generators.ts index 34ce3fd620119edf4ce852b80bd19e146f205489..24f49db0f080c80869b55d31efdfa8c481b46ef2 100644 --- a/src/mol-model/structure/query/queries/generators.ts +++ b/src/mol-model/structure/query/queries/generators.ts @@ -168,10 +168,10 @@ function atomGroupsGrouped({ entityTest, chainTest, residueTest, atomTest, group }; } -function getRingStructure(unit: Unit.Atomic, ring: UnitRing) { +function getRingStructure(unit: Unit.Atomic, ring: UnitRing, inputStructure: Structure) { const elements = new Int32Array(ring.length) as any as ElementIndex[]; for (let i = 0, _i = ring.length; i < _i; i++) elements[i] = unit.elements[ring[i]]; - return Structure.create([unit.getChild(SortedArray.ofSortedArray(elements))]) + return Structure.create([unit.getChild(SortedArray.ofSortedArray(elements))], inputStructure); } export function rings(fingerprints?: ArrayLike<UnitRing.Fingerprint>): StructureQuery { @@ -184,7 +184,7 @@ export function rings(fingerprints?: ArrayLike<UnitRing.Fingerprint>): Structure if (!Unit.isAtomic(u)) continue; for (const r of u.rings.all) { - ret.add(getRingStructure(u, r)); + ret.add(getRingStructure(u, r, ctx.inputStructure)); } } } else { @@ -198,7 +198,7 @@ export function rings(fingerprints?: ArrayLike<UnitRing.Fingerprint>): Structure for (const fp of uniqueFps.array) { if (!rings.byFingerprint.has(fp)) continue; for (const r of rings.byFingerprint.get(fp)!) { - ret.add(getRingStructure(u, rings.all[r])); + ret.add(getRingStructure(u, rings.all[r], ctx.inputStructure)); } } } diff --git a/src/mol-model/structure/query/queries/internal.ts b/src/mol-model/structure/query/queries/internal.ts index 8cf1078110a6c3c929f56bada9d17a1e2ea8d8bf..d451ca422df1647be96601af99d03351f986556c 100644 --- a/src/mol-model/structure/query/queries/internal.ts +++ b/src/mol-model/structure/query/queries/internal.ts @@ -35,7 +35,7 @@ export function atomicSequence(): StructureQuery { units.push(unit); } - return StructureSelection.Singletons(inputStructure, new Structure(units)); + return StructureSelection.Singletons(inputStructure, new Structure(units, inputStructure, )); }; } @@ -54,7 +54,7 @@ export function water(): StructureQuery { if (P.entity.type(l) !== 'water') continue; units.push(unit); } - return StructureSelection.Singletons(inputStructure, new Structure(units)); + return StructureSelection.Singletons(inputStructure, new Structure(units, inputStructure)); }; } @@ -84,7 +84,7 @@ export function atomicHet(): StructureQuery { units.push(unit); } - return StructureSelection.Singletons(inputStructure, new Structure(units)); + return StructureSelection.Singletons(inputStructure, new Structure(units, inputStructure)); }; } @@ -97,6 +97,6 @@ export function spheres(): StructureQuery { if (unit.kind !== Unit.Kind.Spheres) continue; units.push(unit); } - return StructureSelection.Singletons(inputStructure, new Structure(units)); + return StructureSelection.Singletons(inputStructure, new Structure(units, inputStructure)); }; } diff --git a/src/mol-model/structure/query/selection.ts b/src/mol-model/structure/query/selection.ts index c13c370daaa5ea8783845a10d5303611cefd2c99..9fe6201be03c59e39f096f9650b9c08127988e35 100644 --- a/src/mol-model/structure/query/selection.ts +++ b/src/mol-model/structure/query/selection.ts @@ -135,7 +135,7 @@ namespace StructureSelection { const { elements } = unit; for (let i = 0, _i = elements.length; i < _i; i++) { // TODO: optimize this somehow??? - const s = Structure.create([unit.getChild(SortedArray.ofSingleton(elements[i]))]); + const s = Structure.create([unit.getChild(SortedArray.ofSingleton(elements[i]))], sel.source); fn(s, idx++); } } diff --git a/src/mol-model/structure/query/utils/structure-set.ts b/src/mol-model/structure/query/utils/structure-set.ts index 0ab6a9bbfafe296799320aa3f5d22e7f5d6b28b8..4f61dbc78f7223a01597dce742942cf657044e57 100644 --- a/src/mol-model/structure/query/utils/structure-set.ts +++ b/src/mol-model/structure/query/utils/structure-set.ts @@ -80,7 +80,7 @@ export function structureIntersect(sA: Structure, sB: Structure): Structure { } } - return Structure.create(units); + return Structure.create(units, sA.parent || sB.parent); } export function structureSubtract(a: Structure, b: Structure): Structure { @@ -100,5 +100,5 @@ export function structureSubtract(a: Structure, b: Structure): Structure { } } - return Structure.create(units); + return Structure.create(units, a.parent || b.parent); } \ No newline at end of file diff --git a/src/mol-model/structure/structure/structure.ts b/src/mol-model/structure/structure/structure.ts index 8d45b51467ac1f67d2d734e7fd12c70efddbd444..5243b670ff63ffe4c5d13052525010479372ef96 100644 --- a/src/mol-model/structure/structure/structure.ts +++ b/src/mol-model/structure/structure/structure.ts @@ -26,6 +26,7 @@ import { Vec3, Mat4 } from 'mol-math/linear-algebra'; import { idFactory } from 'mol-util/id-factory'; import { GridLookup3D } from 'mol-math/geometry'; import { UUID } from 'mol-util'; +import { CustomProperties } from '../common/custom-property'; class Structure { /** Maps unit.id to unit */ @@ -34,6 +35,7 @@ class Structure { readonly units: ReadonlyArray<Unit>; private _props: { + parent?: Structure, lookup3d?: StructureLookup3D, links?: InterUnitBonds, crossLinkRestraints?: PairRestraints<CrossLinkRestraint>, @@ -49,7 +51,16 @@ class Structure { transformHash: number, elementCount: number, polymerResidueCount: number, - } = { hashCode: -1, transformHash: -1, elementCount: 0, polymerResidueCount: 0 }; + coordinateSystem: SymmetryOperator, + propertyData?: any, + customProps?: CustomProperties + } = { + hashCode: -1, + transformHash: -1, + elementCount: 0, + polymerResidueCount: 0, + coordinateSystem: SymmetryOperator.Default + }; subsetBuilder(isSorted: boolean) { return new StructureSubsetBuilder(this, isSorted); @@ -60,6 +71,30 @@ class Structure { return this._props.elementCount; } + get hasCustomProperties() { + return !!this._props.customProps && this._props.customProps.all.length > 0; + } + + get customPropertyDescriptors() { + if (!this._props.customProps) this._props.customProps = new CustomProperties(); + return this._props.customProps; + } + + /** + * Property data unique to this instance of the structure. + */ + get currentPropertyData() { + if (!this._props.propertyData) this._props.propertyData = Object.create(null); + return this._props.propertyData; + } + + /** + * Property data of the parent structure if it exists, currentPropertyData otherwise. + */ + get inheritedPropertyData() { + return this.parent ? this.parent.currentPropertyData : this.currentPropertyData; + } + /** Count of all polymer residues in the structure */ get polymerResidueCount() { return this._props.polymerResidueCount; @@ -106,6 +141,14 @@ class Structure { return new Structure.ElementLocationIterator(this); } + get parent() { + return this._props.parent; + } + + get coordinateSystem() { + return this._props.coordinateSystem; + } + get boundary() { return this.lookup3d.boundary; } @@ -174,7 +217,7 @@ class Structure { return SortedArray.has(this.unitMap.get(e.unit.id).elements, e.element); } - constructor(units: ArrayLike<Unit>) { + private initUnits(units: ArrayLike<Unit>) { const map = IntMap.Mutable<Unit>(); let elementCount = 0; let polymerResidueCount = 0; @@ -188,11 +231,18 @@ class Structure { if (u.id < lastId) isSorted = false; lastId = u.id; } - if (!isSorted) sort(units, 0, units.length, cmpUnits, arraySwap) - this.unitMap = map; - this.units = units as ReadonlyArray<Unit>; + if (!isSorted) sort(units, 0, units.length, cmpUnits, arraySwap); this._props.elementCount = elementCount; this._props.polymerResidueCount = polymerResidueCount; + return map; + } + + constructor(units: ArrayLike<Unit>, parent: Structure | undefined, coordinateSystem?: SymmetryOperator) { + this.unitMap = this.initUnits(units); + this.units = units as ReadonlyArray<Unit>; + if (parent) this._props.parent = parent; + if (coordinateSystem) this._props.coordinateSystem = coordinateSystem; + else if (parent) this._props.coordinateSystem = parent.coordinateSystem; } } @@ -283,7 +333,7 @@ function getUniqueAtomicResidueIndices(structure: Structure): ReadonlyMap<UUID, } namespace Structure { - export const Empty = new Structure([]); + export const Empty = new Structure([], void 0, void 0); /** Represents a single structure */ export interface Loci { @@ -302,7 +352,9 @@ namespace Structure { return a.structure === b.structure } - export function create(units: ReadonlyArray<Unit>): Structure { return new Structure(units); } + export function create(units: ReadonlyArray<Unit>, parent: Structure | undefined, coordinateSystem?: SymmetryOperator): Structure { + return new Structure(units, parent, coordinateSystem); + } /** * Construct a Structure from a model. @@ -312,7 +364,7 @@ namespace Structure { */ export function ofModel(model: Model): Structure { const chains = model.atomicHierarchy.chainAtomSegments; - const builder = new StructureBuilder(); + const builder = new StructureBuilder(void 0, void 0); for (let c = 0; c < chains.count; c++) { const start = chains.offsets[c]; @@ -381,11 +433,13 @@ namespace Structure { const units: Unit[] = []; for (const u of s.units) { const old = u.conformation.operator; - const op = SymmetryOperator.create(old.name, transform, { id: '', operList: [] }, old.ncsId, old.hkl); + const op = SymmetryOperator.create(old.name, transform, old.assembly, old.ncsId, old.hkl); units.push(u.applyOperator(u.id, op)); } - return new Structure(units); + const cs = s.coordinateSystem; + const newCS = SymmetryOperator.compose(SymmetryOperator.create(cs.name, transform, cs.assembly, cs.ncsId, cs.hkl), cs); + return new Structure(units, s, newCS); } export class StructureBuilder { @@ -405,15 +459,21 @@ namespace Structure { } getStructure(): Structure { - return create(this.units); + return create(this.units, this.parent, this.coordinateSystem); } get isEmpty() { return this.units.length === 0; } + + constructor(private parent: Structure | undefined, private coordinateSystem: SymmetryOperator | undefined) { + + } } - export function Builder() { return new StructureBuilder(); } + export function Builder(parent: Structure | undefined, coordinateSystem: SymmetryOperator | undefined) { + return new StructureBuilder(parent, coordinateSystem); + } export function hashCode(s: Structure) { return s.hashCode; diff --git a/src/mol-model/structure/structure/symmetry.ts b/src/mol-model/structure/structure/symmetry.ts index 1b917041444704a539124a1d2800354e8fdebb6e..eee13f1b61ee70727fc562a6cc38b9490b674ad3 100644 --- a/src/mol-model/structure/structure/symmetry.ts +++ b/src/mol-model/structure/structure/symmetry.ts @@ -24,7 +24,7 @@ namespace StructureSymmetry { const assembly = ModelSymmetry.findAssembly(models[0], asmName); if (!assembly) throw new Error(`Assembly '${asmName}' is not defined.`); - const assembler = Structure.Builder(); + const assembler = Structure.Builder(void 0, SymmetryOperator.create(assembly.id, Mat4.identity(), { id: assembly.id, operList: [] })); const queryCtx = new QueryContext(structure); @@ -83,7 +83,12 @@ namespace StructureSymmetry { export function areTransformGroupsEquivalent(a: ReadonlyArray<Unit.SymmetryGroup>, b: ReadonlyArray<Unit.SymmetryGroup>) { if (a.length !== b.length) return false for (let i = 0, il = a.length; i < il; ++i) { + const au = a[i].units, bu = b[i].units; + if (au.length !== bu.length) return false; if (a[i].hashCode !== b[i].hashCode) return false + for (let j = 0, _j = au.length; j < _j; j++) { + if (au[j].conformation !== bu[j].conformation) return false; + } } return true } @@ -132,7 +137,7 @@ function getOperatorsCached333(symmetry: ModelSymmetry) { } function assembleOperators(structure: Structure, operators: ReadonlyArray<SymmetryOperator>) { - const assembler = Structure.Builder(); + const assembler = Structure.Builder(void 0, void 0); const { units } = structure; for (const oper of operators) { for (const unit of units) { @@ -174,7 +179,7 @@ async function findMatesRadius(ctx: RuntimeContext, structure: Structure, radius const operators = getOperatorsCached333(symmetry); const lookup = structure.lookup3d; - const assembler = Structure.Builder(); + const assembler = Structure.Builder(void 0, void 0); const { units } = structure; const center = Vec3.zero(); diff --git a/src/mol-model/structure/structure/util/subset-builder.ts b/src/mol-model/structure/structure/util/subset-builder.ts index 77c228902fc14c27a75e070df1868498ca68057a..dbdf0cf774c647e4380a1c3a81f0b4ebff1fc8d0 100644 --- a/src/mol-model/structure/structure/util/subset-builder.ts +++ b/src/mol-model/structure/structure/util/subset-builder.ts @@ -90,7 +90,7 @@ export class StructureSubsetBuilder { newUnits[newUnits.length] = child; } - return Structure.create(newUnits); + return Structure.create(newUnits, this.parent); } getStructure() { diff --git a/src/mol-model/structure/structure/util/unique-subset-builder.ts b/src/mol-model/structure/structure/util/unique-subset-builder.ts index bc18e6519217570f2bb6e6b8097e5baa3e2d6f96..8b808e2466b9b768288de4fc22e6ba783f99f0ab 100644 --- a/src/mol-model/structure/structure/util/unique-subset-builder.ts +++ b/src/mol-model/structure/structure/util/unique-subset-builder.ts @@ -85,7 +85,7 @@ export class StructureUniqueSubsetBuilder { newUnits[newUnits.length] = child; } - return Structure.create(newUnits); + return Structure.create(newUnits, this.parent, this.parent.coordinateSystem); } get isEmpty() { diff --git a/src/mol-model/volume/data.ts b/src/mol-model/volume/data.ts index df2964b686d68ff7fc6e7f7b8cd9ec425856d099..4f414675ee8b6c5bd6ba57dcfc59a674a3aa95bc 100644 --- a/src/mol-model/volume/data.ts +++ b/src/mol-model/volume/data.ts @@ -1,11 +1,13 @@ /** - * 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> + * @author Alexander Rose <alexander.rose@weirdbyte.de> */ import { SpacegroupCell, Box3D } from 'mol-math/geometry' import { Tensor, Mat4, Vec3 } from 'mol-math/linear-algebra' +import { equalEps } from 'mol-math/linear-algebra/3d/common'; /** The basic unit cell that contains the data. */ interface VolumeData { @@ -48,7 +50,7 @@ namespace VolumeIsoValue { export type Absolute = Readonly<{ kind: 'absolute', absoluteValue: number }> export function areSame(a: VolumeIsoValue, b: VolumeIsoValue, stats: VolumeData['dataStats']) { - return toAbsolute(a, stats).absoluteValue === toAbsolute(b, stats).absoluteValue + return equalEps(toAbsolute(a, stats).absoluteValue, toAbsolute(b, stats).absoluteValue, stats.sigma / 100) } export function absolute(value: number): Absolute { return { kind: 'absolute', absoluteValue: value }; } diff --git a/src/mol-plugin/behavior/dynamic/labels.ts b/src/mol-plugin/behavior/dynamic/labels.ts index be54651502d57168464b9a568c7a69eb7043cec6..b3597932ce0e3f68dbfcb74d00c6fc9c8e27a1bf 100644 --- a/src/mol-plugin/behavior/dynamic/labels.ts +++ b/src/mol-plugin/behavior/dynamic/labels.ts @@ -107,7 +107,7 @@ export const SceneLabels = PluginBehavior.create<SceneLabelsProps>({ private getLabelsShape = (ctx: RuntimeContext, data: LabelsData, props: SceneLabelsProps, shape?: Shape<Text>) => { this.geo = getLabelsText(data, props, this.geo) - return Shape.create('Scene Labels', this.geo, this.getColor, this.getSize, this.getLabel, data.transforms) + return Shape.create('Scene Labels', data, this.geo, this.getColor, this.getSize, this.getLabel, data.transforms) } /** Update structures to be labeled, returns true if changed */ @@ -118,7 +118,7 @@ export const SceneLabels = PluginBehavior.create<SceneLabelsProps>({ for (const s of structures) { const rootStructure = getRootStructure(s, state) if (!rootStructure || !SO.Molecule.Structure.is(rootStructure.obj)) continue - if (!state.cellStates.get(s.transform.ref).isHidden) { + if (!s.state.isHidden) { rootStructures.add(rootStructure.obj) } } diff --git a/src/mol-plugin/behavior/dynamic/selection/structure-representation-interaction.ts b/src/mol-plugin/behavior/dynamic/selection/structure-representation-interaction.ts index 497d1420536422a93cff6f13aded50981b145507..7fc4487aa03f3610b41120f4ce75eb9bbf4d3609 100644 --- a/src/mol-plugin/behavior/dynamic/selection/structure-representation-interaction.ts +++ b/src/mol-plugin/behavior/dynamic/selection/structure-representation-interaction.ts @@ -56,30 +56,30 @@ export class StructureRepresentationInteractionBehavior extends PluginBehavior.W if (!refs['structure-interaction-group']) { refs['structure-interaction-group'] = builder.to(cell).group(StateTransforms.Misc.CreateGroup, - { label: 'Current Interaction' }, { props: { tag: Tags.Group } }).ref; + { label: 'Current Interaction' }, { tags: Tags.Group }).ref; } // Selections if (!refs[Tags.ResidueSel]) { refs[Tags.ResidueSel] = builder.to(refs['structure-interaction-group']).apply(StateTransforms.Model.StructureSelection, - { query: { } as any, label: 'Residue' }, { props: { tag: Tags.ResidueSel } }).ref; + { query: { } as any, label: 'Residue' }, { tags: Tags.ResidueSel }).ref; } if (!refs[Tags.SurrSel]) { refs[Tags.SurrSel] = builder.to(refs['structure-interaction-group']).apply(StateTransforms.Model.StructureSelection, - { query: { } as any, label: 'Surroundings' }, { props: { tag: Tags.SurrSel } }).ref; + { query: { } as any, label: 'Surroundings' }, { tags: Tags.SurrSel }).ref; } // Representations // TODO: ability to customize how it looks in the behavior params if (!refs[Tags.ResidueRepr]) { refs[Tags.ResidueRepr] = builder.to(refs['structure-interaction-residue-sel']!).apply(StateTransforms.Representation.StructureRepresentation3D, - this.createResVisualParams(cell.obj!.data), { props: { tag: Tags.ResidueRepr } }).ref; + this.createResVisualParams(cell.obj!.data), { tags: Tags.ResidueRepr }).ref; } if (!refs[Tags.SurrRepr]) { refs[Tags.SurrRepr] = builder.to(refs['structure-interaction-surr-sel']!).apply(StateTransforms.Representation.StructureRepresentation3D, - this.createSurVisualParams(cell.obj!.data), { props: { tag: Tags.SurrRepr } }).ref; + this.createSurVisualParams(cell.obj!.data), { tags: Tags.SurrRepr }).ref; } return { state, builder, refs }; @@ -87,7 +87,7 @@ export class StructureRepresentationInteractionBehavior extends PluginBehavior.W private clear(root: StateTransform.Ref) { const state = this.plugin.state.dataState; - const groups = state.select(StateSelection.Generators.byRef(root).subtree().filter(o => o.transform.props.tag === Tags.Group)); + const groups = state.select(StateSelection.Generators.byRef(root).subtree().withTag(Tags.Group)); if (groups.length === 0) return; const update = state.build(); diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts index 2ad87f7ff3b7a676dcd14d4d2298f06a267a7ecb..e7b357d9b7e3987edd704fcdd98559c4eed7bd73 100644 --- a/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts +++ b/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts @@ -22,6 +22,7 @@ import { VolumeServerHeader, VolumeServerInfo } from './model'; import { ButtonsType } from 'mol-util/input/input-observer'; import { PluginCommands } from 'mol-plugin/command'; import { StateSelection } from 'mol-state'; +import { Representation } from 'mol-repr/representation'; export class VolumeStreaming extends PluginStateObject.CreateBehavior<VolumeStreaming.Behavior>({ name: 'Volume Streaming' }) { } @@ -172,7 +173,21 @@ export namespace VolumeStreaming { } register(ref: string): void { - // this.ref = ref; + let lastLoci: Representation.Loci = Representation.Loci.Empty; + + this.subscribeObservable(this.plugin.events.state.object.removed, o => { + if (!PluginStateObject.Molecule.Structure.is(o.obj) || lastLoci.loci.kind !== 'element-loci') return; + if (lastLoci.loci.structure === o.obj.data) { + lastLoci = Representation.Loci.Empty; + } + }); + + this.subscribeObservable(this.plugin.events.state.object.updated, o => { + if (!PluginStateObject.Molecule.Structure.is(o.oldObj) || lastLoci.loci.kind !== 'element-loci') return; + if (lastLoci.loci.structure === o.oldObj.data) { + lastLoci = Representation.Loci.Empty; + } + }); this.subscribeObservable(this.plugin.behaviors.canvas3d.click, ({ current, buttons, modifiers }) => { if (buttons !== ButtonsType.Flag.Secondary || this.params.view.name !== 'selection-box') return; @@ -193,6 +208,13 @@ export namespace VolumeStreaming { const root = this.getStructureRoot(ref); if (!root || !root.obj || root.obj !== parent.obj) return; + if (Representation.Loci.areEqual(lastLoci, current)) { + lastLoci = Representation.Loci.Empty; + this.updateDynamicBox(ref, Box3D.empty()); + return; + } + lastLoci = current; + const loci = StructureElement.Loci.extendToWholeResidues(current.loci); const box = StructureElement.Loci.getBoundary(loci).box; this.updateDynamicBox(ref, box); diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts index 42e01f59695ca66d0bcf036babc2364823cd0036..aa98aeff147ac90f77973a1dab5f16b2bbb7d56a 100644 --- a/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts +++ b/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts @@ -59,11 +59,11 @@ export const InitVolumeStreaming = StateAction.build({ PD.getDefaultValues(VolumeStreaming.createParams(infoObj.data))); if (params.method === 'em') { - behTree.apply(VolumeStreamingVisual, { channel: 'em' }, { props: { isGhost: true } }); + behTree.apply(VolumeStreamingVisual, { channel: 'em' }, { state: { isGhost: true } }); } else { - behTree.apply(VolumeStreamingVisual, { channel: '2fo-fc' }, { props: { isGhost: true } }); - behTree.apply(VolumeStreamingVisual, { channel: 'fo-fc(+ve)' }, { props: { isGhost: true } }); - behTree.apply(VolumeStreamingVisual, { channel: 'fo-fc(-ve)' }, { props: { isGhost: true } }); + behTree.apply(VolumeStreamingVisual, { channel: '2fo-fc' }, { state: { isGhost: true } }); + behTree.apply(VolumeStreamingVisual, { channel: 'fo-fc(+ve)' }, { state: { isGhost: true } }); + behTree.apply(VolumeStreamingVisual, { channel: 'fo-fc(-ve)' }, { state: { isGhost: true } }); } await state.updateTree(behTree).runInContext(taskCtx); })); diff --git a/src/mol-plugin/behavior/static/representation.ts b/src/mol-plugin/behavior/static/representation.ts index b84697c9d7237d9ec824fba879929dfdc19f7eca..df0310e777b3f0af68d50952f13a2f4c1279a616 100644 --- a/src/mol-plugin/behavior/static/representation.ts +++ b/src/mol-plugin/behavior/static/representation.ts @@ -7,7 +7,7 @@ import { PluginStateObject as SO } from '../../state/objects'; import { PluginContext } from 'mol-plugin/context'; import { Representation } from 'mol-repr/representation'; -import { State } from 'mol-state'; +import { StateObjectCell } from 'mol-state'; export function registerDefault(ctx: PluginContext) { SyncRepresentationToCanvas(ctx); @@ -21,7 +21,7 @@ export function SyncRepresentationToCanvas(ctx: PluginContext) { const events = ctx.state.dataState.events; events.object.created.subscribe(e => { if (!SO.isRepresentation3D(e.obj)) return; - updateVisibility(e, e.obj.data.repr); + updateVisibility(e.state.cells.get(e.ref)!, e.obj.data.repr); e.obj.data.repr.setState({ syncManually: true }); ctx.canvas3d.add(e.obj.data.repr); @@ -39,7 +39,7 @@ export function SyncRepresentationToCanvas(ctx: PluginContext) { return; } - updateVisibility(e, e.obj.data.repr); + updateVisibility(e.state.cells.get(e.ref)!, e.obj.data.repr); if (e.action === 'recreate') { e.obj.data.repr.setState({ syncManually: true }); } @@ -86,11 +86,11 @@ export function UpdateRepresentationVisibility(ctx: PluginContext) { ctx.state.dataState.events.cell.stateUpdated.subscribe(e => { const cell = e.state.cells.get(e.ref)!; if (!SO.isRepresentation3D(cell.obj)) return; - updateVisibility(e, cell.obj.data.repr); + updateVisibility(cell, cell.obj.data.repr); ctx.canvas3d.requestDraw(true); }) } -function updateVisibility(e: State.ObjectEvent, r: Representation<any>) { - r.setState({ visible: !e.state.cellStates.get(e.ref).isHidden }); +function updateVisibility(cell: StateObjectCell, r: Representation<any>) { + r.setState({ visible: !cell.state.isHidden }); } \ 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 818a14252d4b8f0a553c8c73ed13f30b17bd71c1..c4111b37104383e19db7b1f1d602892404d82b3d 100644 --- a/src/mol-plugin/behavior/static/state.ts +++ b/src/mol-plugin/behavior/static/state.ts @@ -72,7 +72,8 @@ export function RemoveObject(ctx: PluginContext) { 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); + // TODO: should this use "cell state" instead? + if (!parent.state.isGhost) return remove(state, curr.ref); curr = parent; } } else { @@ -86,7 +87,7 @@ export function ToggleExpanded(ctx: PluginContext) { } export function ToggleVisibility(ctx: PluginContext) { - PluginCommands.State.ToggleVisibility.subscribe(ctx, ({ state, ref }) => setVisibility(state, ref, !state.cellStates.get(ref).isHidden)); + PluginCommands.State.ToggleVisibility.subscribe(ctx, ({ state, ref }) => setVisibility(state, ref, !state.cells.get(ref)!.state.isHidden)); } function setVisibility(state: State, root: StateTransform.Ref, value: boolean) { diff --git a/src/mol-plugin/command.ts b/src/mol-plugin/command.ts index 05a967dccdd398e0e8815993742a515da3a2a221..01e3375f9552d19827ba5c8c1efeb610f5b9e2b6 100644 --- a/src/mol-plugin/command.ts +++ b/src/mol-plugin/command.ts @@ -53,7 +53,7 @@ export const PluginCommands = { }, Camera: { Reset: PluginCommand<{}>(), - SetSnapshot: PluginCommand<{ snapshot: Camera.Snapshot, durationMs?: number }>(), + SetSnapshot: PluginCommand<{ snapshot: Partial<Camera.Snapshot>, durationMs?: number }>(), Snapshots: { Add: PluginCommand<{ name?: string, description?: string }>(), Remove: PluginCommand<{ id: string }>(), diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index 6b2e8e6e44db466294b3d80b08593d3ff86472b9..85da6b7514b8073897fb16a3c4f7cdf1eb5237ea 100644 --- a/src/mol-plugin/context.ts +++ b/src/mol-plugin/context.ts @@ -34,6 +34,7 @@ import { StructureElementSelectionManager } from './util/structure-element-selec import { SubstructureParentHelper } from './util/substructure-parent-helper'; import { Representation } from 'mol-repr/representation'; import { ModifiersKeys } from 'mol-util/input/input-observer'; +import { isProductionMode, isDebugMode } from 'mol-util/debug'; export class PluginContext { private disposed = false; @@ -111,7 +112,8 @@ export class PluginContext { this.layout.setRoot(container); if (this.spec.layout && this.spec.layout.initial) this.layout.setProps(this.spec.layout.initial); (this.canvas3d as Canvas3D) = Canvas3D.create(canvas, container); - PluginCommands.Canvas3D.SetSettings.dispatch(this, { settings: { backgroundColor: Color(0xFCFBF9) } }); + const renderer = this.canvas3d.props.renderer; + PluginCommands.Canvas3D.SetSettings.dispatch(this, { settings: { renderer: { ...renderer, backgroundColor: Color(0xFCFBF9) } } }); this.canvas3d.animate(); return true; } catch (e) { @@ -180,7 +182,7 @@ export class PluginContext { 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 } }); + tree.toRoot().apply(PluginBehavior.CreateCategory, { label: (PluginBehavior.Categories as any)[cat] }, { ref: cat, state: { isLocked: true } }); } for (const b of this.spec.behaviors) { @@ -225,5 +227,7 @@ export class PluginContext { this.lociLabels = new LociLabelManager(this); this.log.message(`Mol* Plugin ${PLUGIN_VERSION} [${PLUGIN_VERSION_DATE.toLocaleString()}]`); + if (!isProductionMode) this.log.message(`Development mode enabled`); + if (isDebugMode) this.log.message(`Debug mode enabled`); } } \ No newline at end of file diff --git a/src/mol-plugin/index.ts b/src/mol-plugin/index.ts index 3fce9342c9409b2ce783bb4559a0f3b8de0aa507..8b9610bba7b1695ba377016a29bc5a396fc25eec 100644 --- a/src/mol-plugin/index.ts +++ b/src/mol-plugin/index.ts @@ -12,10 +12,11 @@ import * as ReactDOM from 'react-dom'; import { PluginSpec } from './spec'; import { StateTransforms } from './state/transforms'; import { PluginBehaviors } from './behavior'; -import { AnimateModelIndex, AnimateAssemblyUnwind, AnimateUnitsExplode } from './state/animation/built-in'; +import { AnimateModelIndex, AnimateAssemblyUnwind, AnimateUnitsExplode, AnimateStateInterpolation } from './state/animation/built-in'; import { StateActions } from './state/actions'; import { InitVolumeStreaming, BoxifyVolumeStreaming, CreateVolumeStreamingBehavior } from './behavior/dynamic/volume-streaming/transformers'; import { StructureRepresentationInteraction } from './behavior/dynamic/selection/structure-representation-interaction'; +import { TransformStructureConformation } from './state/actions/structure'; export const DefaultPluginSpec: PluginSpec = { actions: [ @@ -38,6 +39,7 @@ export const DefaultPluginSpec: PluginSpec = { PluginSpec.Action(StateTransforms.Model.TrajectoryFromPDB), PluginSpec.Action(StateTransforms.Model.StructureAssemblyFromModel), PluginSpec.Action(StateTransforms.Model.StructureSymmetryFromModel), + PluginSpec.Action(TransformStructureConformation), PluginSpec.Action(StateTransforms.Model.StructureFromModel), PluginSpec.Action(StateTransforms.Model.ModelFromTrajectory), PluginSpec.Action(StateTransforms.Model.UserStructureSelection), @@ -46,7 +48,8 @@ export const DefaultPluginSpec: PluginSpec = { PluginSpec.Action(StateTransforms.Representation.StructureLabels3D), PluginSpec.Action(StateTransforms.Representation.ExplodeStructureRepresentation3D), PluginSpec.Action(StateTransforms.Representation.UnwindStructureAssemblyRepresentation3D), - PluginSpec.Action(StateTransforms.Representation.ColorStructureRepresentation3D), + PluginSpec.Action(StateTransforms.Representation.OverpaintStructureRepresentation3D), + PluginSpec.Action(StateTransforms.Representation.TransparencyStructureRepresentation3D), PluginSpec.Action(StateTransforms.Representation.VolumeRepresentation3D), PluginSpec.Action(StateActions.Structure.StructureFromSelection), @@ -65,6 +68,7 @@ export const DefaultPluginSpec: PluginSpec = { AnimateModelIndex, AnimateAssemblyUnwind, AnimateUnitsExplode, + AnimateStateInterpolation ] } diff --git a/src/mol-plugin/skin/base/components/temp.scss b/src/mol-plugin/skin/base/components/temp.scss index 8913fd5d31ccf465be9388fe3aa63820d001eac7..9d20d320fa4b0ca6cc344b6decb9b8b65a1ef6db 100644 --- a/src/mol-plugin/skin/base/components/temp.scss +++ b/src/mol-plugin/skin/base/components/temp.scss @@ -180,11 +180,15 @@ line-height: $row-height; float: left; margin-right: $control-spacing; - background-color: $msp-form-control-background; + + > button { + background-color: $msp-form-control-background; + } > select { display: inline-block; width: 200px; + margin-right: $control-spacing; } } diff --git a/src/mol-plugin/state.ts b/src/mol-plugin/state.ts index 0e2a82007cb13fd299de8f7e56cf8fdd302ee28d..25f6c1035fe071c91ca4c1f06bc0577e2717a863 100644 --- a/src/mol-plugin/state.ts +++ b/src/mol-plugin/state.ts @@ -99,7 +99,7 @@ class PluginState { constructor(private plugin: import('./context').PluginContext) { this.snapshots = new PluginStateSnapshotManager(plugin); this.dataState = State.create(new SO.Root({ }), { globalContext: plugin }); - this.behaviorState = State.create(new PluginBehavior.Root({ }), { globalContext: plugin, rootProps: { isLocked: true } }); + this.behaviorState = State.create(new PluginBehavior.Root({ }), { globalContext: plugin, rootState: { isLocked: true } }); this.dataState.behaviors.currentObject.subscribe(o => { if (this.behavior.kind.value === 'data') this.behavior.currentObject.next(o); diff --git a/src/mol-plugin/state/actions/data-format.ts b/src/mol-plugin/state/actions/data-format.ts index 7e4bbd656f0e3d47097f3c7d73f0960571a448b3..3cb8a05da5a2fae2722df766e895f691050b2d53 100644 --- a/src/mol-plugin/state/actions/data-format.ts +++ b/src/mol-plugin/state/actions/data-format.ts @@ -13,6 +13,8 @@ import { ParamDefinition as PD } from 'mol-util/param-definition'; import { Ccp4Provider, Dsn6Provider, DscifProvider } from './volume'; import { StateTransforms } from '../transforms'; import { MmcifProvider, PdbProvider, GroProvider } from './structure'; +import msgpackDecode from 'mol-io/common/msgpack/decode' +import { PlyProvider } from './shape'; export class DataFormatRegistry<D extends PluginStateObject.Data.Binary | PluginStateObject.Data.String> { private _list: { name: string, provider: DataFormatProvider<D> }[] = [] @@ -59,6 +61,7 @@ export class DataFormatRegistry<D extends PluginStateObject.Data.Binary | Plugin this.add('gro', GroProvider) this.add('mmcif', MmcifProvider) this.add('pdb', PdbProvider) + this.add('ply', PlyProvider) }; private _clear() { @@ -139,4 +142,18 @@ export const OpenFile = StateAction.build({ 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) -})); \ No newline at end of file +})); + +// + +type cifVariants = 'dscif' | -1 +export function guessCifVariant(info: FileInfo, data: Uint8Array | string): cifVariants { + if (info.ext === 'bcif') { + try { + if (msgpackDecode(data as Uint8Array).encoder.startsWith('VolumeServer')) return 'dscif' + } catch { } + } else if (info.ext === 'cif') { + if ((data as string).startsWith('data_SERVER\n#\n_density_server_result')) return 'dscif' + } + return -1 +} \ No newline at end of file diff --git a/src/mol-plugin/state/actions/shape.ts b/src/mol-plugin/state/actions/shape.ts new file mode 100644 index 0000000000000000000000000000000000000000..7a1311149ed894a5112b189a1ab0f92f323b79d8 --- /dev/null +++ b/src/mol-plugin/state/actions/shape.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { PluginContext } from 'mol-plugin/context'; +import { State, StateBuilder } from 'mol-state'; +import { Task } from 'mol-task'; +import { FileInfo } from 'mol-util/file-info'; +import { PluginStateObject } from '../objects'; +import { StateTransforms } from '../transforms'; +import { DataFormatProvider } from './data-format'; + +export const PlyProvider: DataFormatProvider<any> = { + label: 'PLY', + description: 'PLY', + stringExtensions: ['ply'], + binaryExtensions: [], + isApplicable: (info: FileInfo, data: string) => { + return info.ext === 'ply' + }, + getDefaultBuilder: (ctx: PluginContext, data: StateBuilder.To<PluginStateObject.Data.String>, state: State) => { + return Task.create('PLY default builder', async taskCtx => { + const tree = data.apply(StateTransforms.Data.ParsePly) + .apply(StateTransforms.Model.ShapeFromPly) + .apply(StateTransforms.Representation.ShapeRepresentation3D) + 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 index 714adecf217632e6c000e5e2344ff76866148005..c32e25c47e360c1e12bb71c36e849c9b50458df4 100644 --- a/src/mol-plugin/state/actions/structure.ts +++ b/src/mol-plugin/state/actions/structure.ts @@ -13,7 +13,7 @@ import { StateTransforms } from '../transforms'; import { Download } from '../transforms/data'; import { StructureRepresentation3DHelpers } from '../transforms/representation'; import { CustomModelProperties, StructureSelection } from '../transforms/model'; -import { DataFormatProvider } from './data-format'; +import { DataFormatProvider, guessCifVariant } from './data-format'; import { FileInfo } from 'mol-util/file-info'; import { Task } from 'mol-task'; import { StructureElement } from 'mol-model/structure'; @@ -24,7 +24,10 @@ export const MmcifProvider: DataFormatProvider<any> = { stringExtensions: ['cif', 'mmcif', 'mcif'], binaryExtensions: ['bcif'], isApplicable: (info: FileInfo, data: Uint8Array | string) => { - return info.ext === 'cif' || info.ext === 'mmcif' || info.ext === 'mcif' || info.ext === 'bcif' + if (info.ext === 'mmcif' || info.ext === 'mcif') return true + // assume cif/bcif files that are not DensityServer CIF are mmCIF + if (info.ext === 'cif' || info.ext === 'bcif') return guessCifVariant(info, data) !== 'dscif' + return false }, getDefaultBuilder: (ctx: PluginContext, data: StateBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, state: State) => { return Task.create('mmCIF default builder', async taskCtx => { @@ -143,7 +146,7 @@ const DownloadStructure = StateAction.build({ createStructureTree(ctx, traj, supportProps); } else { for (const download of downloadParams) { - const data = b.toRoot().apply(StateTransforms.Data.Download, download, { props: { isGhost: true } }); + const data = b.toRoot().apply(StateTransforms.Data.Download, download, { state: { isGhost: true } }); const traj = createModelTree(data, src.name === 'url' ? src.params.format : 'cif'); createStructureTree(ctx, traj, supportProps) } @@ -176,14 +179,14 @@ export function createModelTree(b: StateBuilder.To<PluginStateObject.Data.Binary let parsed: StateBuilder.To<PluginStateObject.Molecule.Trajectory> switch (format) { case 'cif': - parsed = b.apply(StateTransforms.Data.ParseCif, void 0, { props: { isGhost: true } }) - .apply(StateTransforms.Model.TrajectoryFromMmCif, void 0, { props: { isGhost: true } }) + parsed = b.apply(StateTransforms.Data.ParseCif, void 0, { state: { isGhost: true } }) + .apply(StateTransforms.Model.TrajectoryFromMmCif, void 0, { state: { isGhost: true } }) break case 'pdb': - parsed = b.apply(StateTransforms.Model.TrajectoryFromPDB, void 0, { props: { isGhost: true } }); + parsed = b.apply(StateTransforms.Model.TrajectoryFromPDB, void 0, { state: { isGhost: true } }); break case 'gro': - parsed = b.apply(StateTransforms.Model.TrajectoryFromGRO, void 0, { props: { isGhost: true } }); + parsed = b.apply(StateTransforms.Model.TrajectoryFromGRO, void 0, { state: { isGhost: true } }); break default: throw new Error('unsupported format') @@ -284,6 +287,15 @@ export const EnableModelCustomProps = StateAction.build({ return state.updateTree(root); }); +export const TransformStructureConformation = StateAction.build({ + display: { name: 'Transform Conformation' }, + from: PluginStateObject.Molecule.Structure, + params: StateTransforms.Model.TransformStructureConformation.definition.params, +})(({ ref, params, state }) => { + const root = state.build().to(ref).insert(StateTransforms.Model.TransformStructureConformation, params as any); + return state.updateTree(root); +}); + export const StructureFromSelection = StateAction.build({ display: { name: 'Selection Structure', description: 'Create a new Structure from the current selection.' }, from: PluginStateObject.Molecule.Structure, diff --git a/src/mol-plugin/state/actions/volume.ts b/src/mol-plugin/state/actions/volume.ts index baf6e0ded64d08c380cf907bd4a671302cbeac5a..727114bf9f8b56ec9b7c619130b28e208d79ab06 100644 --- a/src/mol-plugin/state/actions/volume.ts +++ b/src/mol-plugin/state/actions/volume.ts @@ -16,7 +16,7 @@ import { PluginStateObject } from '../objects'; import { StateTransforms } from '../transforms'; import { Download } from '../transforms/data'; import { VolumeRepresentation3DHelpers } from '../transforms/representation'; -import { DataFormatProvider } from './data-format'; +import { DataFormatProvider, guessCifVariant } from './data-format'; export const Ccp4Provider: DataFormatProvider<any> = { label: 'CCP4/MRC/BRIX', @@ -59,10 +59,10 @@ export const DscifProvider: DataFormatProvider<any> = { description: 'DensityServer CIF', stringExtensions: ['cif'], binaryExtensions: ['bcif'], - isApplicable: (info: FileInfo, data: Uint8Array) => { - return info.ext === 'cif' || info.ext === 'bcif' + isApplicable: (info: FileInfo, data: Uint8Array | string) => { + return guessCifVariant(info, data) === 'dscif' ? true : false }, - getDefaultBuilder: (ctx: PluginContext, data: StateBuilder.To<PluginStateObject.Data.Binary>, state: State) => { + getDefaultBuilder: (ctx: PluginContext, data: StateBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, 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) diff --git a/src/mol-plugin/state/animation/built-in.ts b/src/mol-plugin/state/animation/built-in.ts index 308bc1bd6d07f909c3977287cf1a825719cc4b2f..4bbab98fe88601f239b20f6a5b45812123ce7aec 100644 --- a/src/mol-plugin/state/animation/built-in.ts +++ b/src/mol-plugin/state/animation/built-in.ts @@ -121,7 +121,7 @@ export const AnimateAssemblyUnwind = PluginStateAnimation.create({ changed = true; update.to(r) - .apply(StateTransforms.Representation.UnwindStructureAssemblyRepresentation3D, { t: 0 }, { props: { tag: 'animate-assembly-unwind' } }); + .apply(StateTransforms.Representation.UnwindStructureAssemblyRepresentation3D, { t: 0 }, { tags: 'animate-assembly-unwind' }); } if (!changed) return; @@ -131,7 +131,7 @@ export const AnimateAssemblyUnwind = PluginStateAnimation.create({ async teardown(_, plugin) { const state = plugin.state.dataState; const reprs = state.select(StateSelection.Generators.ofType(PluginStateObject.Molecule.Structure.Representation3DState) - .filter(c => c.transform.props.tag === 'animate-assembly-unwind')); + .withTag('animate-assembly-unwind')); if (reprs.length === 0) return; const update = state.build(); @@ -191,7 +191,7 @@ export const AnimateUnitsExplode = PluginStateAnimation.create({ changed = true; update.to(r.transform.ref) - .apply(StateTransforms.Representation.ExplodeStructureRepresentation3D, { t: 0 }, { props: { tag: 'animate-units-explode' } }); + .apply(StateTransforms.Representation.ExplodeStructureRepresentation3D, { t: 0 }, { tags: 'animate-units-explode' }); } if (!changed) return; @@ -201,7 +201,7 @@ export const AnimateUnitsExplode = PluginStateAnimation.create({ async teardown(_, plugin) { const state = plugin.state.dataState; const reprs = state.select(StateSelection.Generators.ofType(PluginStateObject.Molecule.Structure.Representation3DState) - .filter(c => c.transform.props.tag === 'animate-units-explode')); + .withTag('animate-units-explode')); if (reprs.length === 0) return; const update = state.build(); @@ -229,4 +229,54 @@ export const AnimateUnitsExplode = PluginStateAnimation.create({ return { kind: 'next', state: { t: newTime } }; } +}) + +export const AnimateStateInterpolation = PluginStateAnimation.create({ + name: 'built-in.animate-state-interpolation', + display: { name: 'Animate State Interpolation' }, + params: () => ({ + transtionDurationInMs: PD.Numeric(2000, { min: 100, max: 30000, step: 10 }) + }), + initialState: () => ({ }), + async apply(animState, t, ctx) { + + const snapshots = ctx.plugin.state.snapshots.state.entries; + if (snapshots.size < 2) return { kind: 'finished' }; + + // const totalTime = (snapshots.size - 1) * ctx.params.transtionDurationInMs; + const currentT = (t.current % ctx.params.transtionDurationInMs) / ctx.params.transtionDurationInMs; + + let srcIndex = Math.floor(t.current / ctx.params.transtionDurationInMs) % snapshots.size; + let tarIndex = Math.ceil(t.current / ctx.params.transtionDurationInMs); + if (tarIndex === srcIndex) tarIndex++; + tarIndex = tarIndex % snapshots.size; + + const _src = snapshots.get(srcIndex)!.snapshot, _tar = snapshots.get(tarIndex)!.snapshot; + + if (!_src.data || !_tar.data) return { kind: 'skip' }; + + const src = _src.data.tree.transforms, tar = _tar.data.tree.transforms; + + const state = ctx.plugin.state.dataState; + const update = state.build(); + + for (const s of src) { + for (const t of tar) { + if (t.ref !== s.ref) continue; + if (t.version === s.version) continue; + + const e = StateTransform.fromJSON(s), f = StateTransform.fromJSON(t); + + if (!e.transformer.definition.interpolate) { + update.to(s.ref).update(currentT <= 0.5 ? e.params : f.params); + } else { + update.to(s.ref).update(e.transformer.definition.interpolate(e.params, f.params, currentT, ctx.plugin)); + } + } + } + + await PluginCommands.State.Update.dispatch(ctx.plugin, { state, tree: update, options: { doNotLogTiming: true } }); + + return { kind: 'next', state: { } }; + } }) \ No newline at end of file diff --git a/src/mol-plugin/state/animation/helpers.ts b/src/mol-plugin/state/animation/helpers.ts index df6bfb962bbc35bfe78088aed47fc39df1d854f2..d86448d5fedb44a6d39f2e87c4c3a895e112d238 100644 --- a/src/mol-plugin/state/animation/helpers.ts +++ b/src/mol-plugin/state/animation/helpers.ts @@ -4,16 +4,10 @@ * @author David Sehnal <david.sehnal@gmail.com> */ - import { SymmetryOperator } from 'mol-math/geometry'; import { Mat4, Vec3 } from 'mol-math/linear-algebra'; -import { Structure, StructureSelection, QueryContext } from 'mol-model/structure'; +import { Structure } from 'mol-model/structure'; import { StructureUnitTransforms } from 'mol-model/structure/structure/util/unit-transforms'; -import { Color } from 'mol-util/color'; -import { Overpaint } from 'mol-theme/overpaint'; -import { parseMolScript } from 'mol-script/language/parser'; -import { transpileMolScript } from 'mol-script/script/mol-script/symbols'; -import { compile } from 'mol-script/runtime/query/compiler'; const _unwindMatrix = Mat4.zero(); export function unwindStructureAssembly(structure: Structure, unitTransforms: StructureUnitTransforms, t: number) { @@ -38,22 +32,4 @@ export function explodeStructure(structure: Structure, unitTransforms: Structure unitTransforms.setTransform(_transMat, u); } -} - -type ScriptLayers = { script: { language: string, expression: string }, color: Color }[] -export function getStructureOverpaint(structure: Structure, scriptLayers: ScriptLayers, alpha: number): Overpaint { - const layers: Overpaint.Layer[] = [] - for (let i = 0, il = scriptLayers.length; i < il; ++i) { - const { script, color } = scriptLayers[i] - const parsed = parseMolScript(script.expression) - if (parsed.length === 0) throw new Error('No query') - const query = transpileMolScript(parsed[0]) - - const compiled = compile<StructureSelection>(query) - const result = compiled(new QueryContext(structure)) - const loci = StructureSelection.toLoci2(result) - - layers.push({ loci, color }) - } - return { layers, alpha } } \ No newline at end of file diff --git a/src/mol-plugin/state/objects.ts b/src/mol-plugin/state/objects.ts index c73a2d03df02236ad1204e229ae0bf8a65816ba5..c3a0a8c979c81d6c9fc04c2fc5c3830c571774dd 100644 --- a/src/mol-plugin/state/objects.ts +++ b/src/mol-plugin/state/objects.ts @@ -6,6 +6,7 @@ */ import { CifFile } from 'mol-io/reader/cif'; +import { PlyFile } from 'mol-io/reader/ply/schema'; import { Model as _Model, Structure as _Structure } from 'mol-model/structure'; import { VolumeData } from 'mol-model/volume'; import { PluginBehavior } from 'mol-plugin/behavior/behavior'; @@ -16,6 +17,8 @@ import { StateObject, StateTransformer } from 'mol-state'; import { Ccp4File } from 'mol-io/reader/ccp4/schema'; import { Dsn6File } from 'mol-io/reader/dsn6/schema'; import { ShapeRepresentation } from 'mol-repr/shape/representation'; +import { Shape as _Shape } from 'mol-model/shape'; +import { ShapeProvider } from 'mol-model/shape/provider'; export type TypeClass = 'root' | 'data' | 'prop' @@ -61,6 +64,7 @@ export namespace PluginStateObject { export namespace Format { export class Json extends Create<any>({ name: 'JSON Data', typeClass: 'Data' }) { } export class Cif extends Create<CifFile>({ name: 'CIF File', typeClass: 'Data' }) { } + export class Ply extends Create<PlyFile>({ name: 'PLY File', typeClass: 'Data' }) { } export class Ccp4 extends Create<Ccp4File>({ name: 'CCP4/MRC/MAP File', typeClass: 'Data' }) { } export class Dsn6 extends Create<Dsn6File>({ name: 'DSN6/BRIX File', typeClass: 'Data' }) { } @@ -71,6 +75,7 @@ export namespace PluginStateObject { | { kind: 'cif', data: CifFile } | { kind: 'ccp4', data: Ccp4File } | { kind: 'dsn6', data: Dsn6File } + | { kind: 'ply', data: PlyFile } // For non-build in extensions | { kind: 'custom', data: unknown, tag: string }) export type BlobData = BlobEntry[] @@ -100,6 +105,11 @@ export namespace PluginStateObject { export class Data extends Create<VolumeData>({ name: 'Volume Data', typeClass: 'Object' }) { } export class Representation3D extends CreateRepresentation3D<VolumeRepresentation<any>>({ name: 'Volume 3D' }) { } } + + export namespace Shape { + export class Provider extends Create<ShapeProvider<any, any, any>>({ name: 'Shape Provider', typeClass: 'Object' }) { } + export class Representation3D extends CreateRepresentation3D<ShapeRepresentation<any, any, any>>({ name: 'Shape 3D' }) { } + } } export namespace PluginStateTransform { diff --git a/src/mol-plugin/state/transforms/data.ts b/src/mol-plugin/state/transforms/data.ts index aa0554a568085ad5ac2cb6a93bc0259e33e39b4b..43c82b639de8388404961518f571027401a37003 100644 --- a/src/mol-plugin/state/transforms/data.ts +++ b/src/mol-plugin/state/transforms/data.ts @@ -15,6 +15,7 @@ import { StateTransformer } from 'mol-state'; import { readFromFile, ajaxGetMany } from 'mol-util/data-source'; import * as CCP4 from 'mol-io/reader/ccp4/parser' import * as DSN6 from 'mol-io/reader/dsn6/parser' +import * as PLY from 'mol-io/reader/ply/parser' export { Download } type Download = typeof Download @@ -185,6 +186,23 @@ const ParseCif = PluginStateTransform.BuiltIn({ } }); +export { ParsePly } +type ParsePly = typeof ParsePly +const ParsePly = PluginStateTransform.BuiltIn({ + name: 'parse-ply', + display: { name: 'Parse PLY', description: 'Parse PLY from String data' }, + from: [SO.Data.String], + to: SO.Format.Ply +})({ + apply({ a }) { + return Task.create('Parse PLY', async ctx => { + const parsed = await PLY.parse(a.data).runInContext(ctx); + if (parsed.isError) throw new Error(parsed.message); + return new SO.Format.Ply(parsed.result, { label: parsed.result.comments[0] || 'PLY Data' }); + }); + } +}); + export { ParseCcp4 } type ParseCcp4 = typeof ParseCcp4 const ParseCcp4 = PluginStateTransform.BuiltIn({ diff --git a/src/mol-plugin/state/transforms/helpers.ts b/src/mol-plugin/state/transforms/helpers.ts new file mode 100644 index 0000000000000000000000000000000000000000..6a58d384a376b62865174483050c237e715b82fe --- /dev/null +++ b/src/mol-plugin/state/transforms/helpers.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { Structure, StructureSelection, QueryContext } from 'mol-model/structure'; +import { Color } from 'mol-util/color'; +import { Overpaint } from 'mol-theme/overpaint'; +import { parseMolScript } from 'mol-script/language/parser'; +import { transpileMolScript } from 'mol-script/script/mol-script/symbols'; +import { compile } from 'mol-script/runtime/query/compiler'; +import { Transparency } from 'mol-theme/transparency'; + +type Script = { language: string, expression: string } + +function scriptToLoci(structure: Structure, script: Script) { + const parsed = parseMolScript(script.expression) + if (parsed.length === 0) throw new Error('No query') + const query = transpileMolScript(parsed[0]) + + const compiled = compile<StructureSelection>(query) + const result = compiled(new QueryContext(structure)) + return StructureSelection.toLoci2(result) +} + +export function getStructureOverpaint(structure: Structure, scriptLayers: { script: Script, color: Color }[], alpha: number): Overpaint { + const layers: Overpaint.Layer[] = [] + for (let i = 0, il = scriptLayers.length; i < il; ++i) { + const { script, color } = scriptLayers[i] + layers.push({ loci: scriptToLoci(structure, script), color }) + } + return { layers, alpha } +} + +export function getStructureTransparency(structure: Structure, script: Script, value: number, variant: Transparency.Variant): Transparency { + return { loci: scriptToLoci(structure, script), value, variant } +} \ No newline at end of file diff --git a/src/mol-plugin/state/transforms/model.ts b/src/mol-plugin/state/transforms/model.ts index dd54e9dc49b54ddf352dd5f4e6888dfe33615734..01ef8d7ddeb870f743cf4a733fe4efccc1ab3a63 100644 --- a/src/mol-plugin/state/transforms/model.ts +++ b/src/mol-plugin/state/transforms/model.ts @@ -6,7 +6,7 @@ */ import { parsePDB } from 'mol-io/reader/pdb/parser'; -import { Vec3 } from 'mol-math/linear-algebra'; +import { Vec3, Mat4, Quat } 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, QueryFn } from 'mol-model/structure'; @@ -24,6 +24,8 @@ import { trajectoryFromGRO } from 'mol-model-formats/structure/gro'; import { parseGRO } from 'mol-io/reader/gro/parser'; import { parseMolScript } from 'mol-script/language/parser'; import { transpileMolScript } from 'mol-script/script/mol-script/symbols'; +import { shapeFromPly } from 'mol-model-formats/shape/ply'; +import { SymmetryOperator } from 'mol-math/geometry'; export { TrajectoryFromBlob }; export { TrajectoryFromMmCif }; @@ -33,6 +35,7 @@ export { ModelFromTrajectory }; export { StructureFromModel }; export { StructureAssemblyFromModel }; export { StructureSymmetryFromModel }; +export { TransformStructureConformation } export { StructureSelection }; export { UserStructureSelection }; export { StructureComplexElement }; @@ -250,6 +253,52 @@ const StructureSymmetryFromModel = PluginStateTransform.BuiltIn({ } }); +const _translation = Vec3.zero(), _m = Mat4.zero(), _n = Mat4.zero(); +type TransformStructureConformation = typeof TransformStructureConformation +const TransformStructureConformation = PluginStateTransform.BuiltIn({ + name: 'transform-structure-conformation', + display: { name: 'Transform Conformation' }, + from: SO.Molecule.Structure, + to: SO.Molecule.Structure, + params: { + axis: PD.Vec3(Vec3.create(1, 0, 0)), + angle: PD.Numeric(0, { min: -180, max: 180, step: 0.1 }), + translation: PD.Vec3(Vec3.create(0, 0, 0)), + } +})({ + canAutoUpdate() { + return true; + }, + apply({ a, params }) { + // TODO: optimze + + const center = a.data.boundary.sphere.center; + Mat4.fromTranslation(_m, Vec3.negate(_translation, center)); + Mat4.fromTranslation(_n, Vec3.add(_translation, center, params.translation)); + const rot = Mat4.fromRotation(Mat4.zero(), Math.PI / 180 * params.angle, Vec3.normalize(Vec3.zero(), params.axis)); + + const m = Mat4.zero(); + Mat4.mul3(m, _n, rot, _m); + + const s = Structure.transform(a.data, m); + const props = { label: `${a.label}`, description: `Transformed` }; + return new SO.Molecule.Structure(s, props); + }, + interpolate(src, tar, t) { + // TODO: optimize + const u = Mat4.fromRotation(Mat4.zero(), Math.PI / 180 * src.angle, Vec3.normalize(Vec3.zero(), src.axis)); + Mat4.setTranslation(u, src.translation); + const v = Mat4.fromRotation(Mat4.zero(), Math.PI / 180 * tar.angle, Vec3.normalize(Vec3.zero(), tar.axis)); + Mat4.setTranslation(v, tar.translation); + const m = SymmetryOperator.slerp(Mat4.zero(), u, v, t); + const rot = Mat4.getRotation(Quat.zero(), m); + const axis = Vec3.zero(); + const angle = Quat.getAxisAngle(axis, rot); + const translation = Mat4.getTranslation(Vec3.zero(), m); + return { axis, angle, translation }; + } +}); + type StructureSelection = typeof StructureSelection const StructureSelection = PluginStateTransform.BuiltIn({ name: 'structure-selection', @@ -338,7 +387,6 @@ function updateStructureFromQuery(query: QueryFn<Sel>, src: Structure, obj: SO.M return true; } - namespace StructureComplexElement { export type Types = 'atomic-sequence' | 'water' | 'atomic-het' | 'spheres' } @@ -394,4 +442,24 @@ async function attachProps(model: Model, ctx: PluginContext, taskCtx: RuntimeCon const p = ctx.customModelProperties.get(name); await p.attach(model).runInContext(taskCtx); } -} \ No newline at end of file +} + +export { ShapeFromPly } +type ShapeFromPly = typeof ShapeFromPly +const ShapeFromPly = PluginStateTransform.BuiltIn({ + name: 'shape-from-ply', + display: { name: 'Shape from PLY', description: 'Create Shape from PLY data' }, + from: SO.Format.Ply, + to: SO.Shape.Provider, + params(a) { + return { }; + } +})({ + apply({ a, params }) { + return Task.create('Create shape from PLY', async ctx => { + const shape = await shapeFromPly(a.data, params).runInContext(ctx) + const props = { label: 'Shape' }; + return new SO.Shape.Provider(shape, props); + }); + } +}); \ 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 b5c9e1a8257281547faf30b096c49b42558d1816..f0873a69e5a3589938802a013ea1faaf89d58cba 100644 --- a/src/mol-plugin/state/transforms/representation.ts +++ b/src/mol-plugin/state/transforms/representation.ts @@ -15,7 +15,7 @@ import { BuiltInVolumeRepresentationsName } from 'mol-repr/volume/registry'; import { VolumeParams } from 'mol-repr/volume/representation'; import { StateTransformer } from 'mol-state'; import { Task } from 'mol-task'; -import { BuiltInColorThemeName, ColorTheme } from 'mol-theme/color'; +import { BuiltInColorThemeName, ColorTheme, BuiltInColorThemes } from 'mol-theme/color'; import { BuiltInSizeThemeName, SizeTheme } from 'mol-theme/size'; import { createTheme, ThemeRegistryContext } from 'mol-theme/theme'; import { ParamDefinition as PD } from 'mol-util/param-definition'; @@ -25,16 +25,20 @@ import { ColorNames } from 'mol-util/color/tables'; import { getLabelRepresentation } from 'mol-plugin/util/structure-labels'; import { ShapeRepresentation } from 'mol-repr/shape/representation'; import { StructureUnitTransforms } from 'mol-model/structure/structure/util/unit-transforms'; -import { unwindStructureAssembly, explodeStructure, getStructureOverpaint } from '../animation/helpers'; +import { unwindStructureAssembly, explodeStructure } from '../animation/helpers'; import { Color } from 'mol-util/color'; import { Overpaint } from 'mol-theme/overpaint'; +import { Transparency } from 'mol-theme/transparency'; +import { getStructureOverpaint, getStructureTransparency } from './helpers'; +import { BaseGeometry } from 'mol-geo/geometry/base'; export { StructureRepresentation3D } export { StructureRepresentation3DHelpers } export { StructureLabels3D} export { ExplodeStructureRepresentation3D } export { UnwindStructureAssemblyRepresentation3D } -export { OverpaintStructureRepresentation3D as ColorStructureRepresentation3D } +export { OverpaintStructureRepresentation3D } +export { TransparencyStructureRepresentation3D } export { VolumeRepresentation3D } namespace StructureRepresentation3DHelpers { @@ -187,10 +191,22 @@ const StructureRepresentation3D = PluginStateTransform.BuiltIn({ await b.data.repr.createOrUpdate(props, a.data).runInContext(ctx); return StateTransformer.UpdateResult.Updated; }); + }, + interpolate(src, tar, t) { + if (src.colorTheme.name !== 'uniform' || tar.colorTheme.name !== 'uniform') { + return t <= 0.5 ? src : tar; + } + BuiltInColorThemes + const from = src.colorTheme.params.value as Color, to = tar.colorTheme.params.value as Color; + const value = Color.interpolate(from, to, t); + return { + type: t <= 0.5 ? src.type : tar.type, + colorTheme: { name: 'uniform', params: { value } }, + sizeTheme: t <= 0.5 ? src.sizeTheme : tar.sizeTheme, + }; } }); - type StructureLabels3D = typeof StructureLabels3D const StructureLabels3D = PluginStateTransform.BuiltIn({ name: 'structure-labels-3d', @@ -366,6 +382,46 @@ const OverpaintStructureRepresentation3D = PluginStateTransform.BuiltIn({ } }); +type TransparencyStructureRepresentation3D = typeof TransparencyStructureRepresentation3D +const TransparencyStructureRepresentation3D = PluginStateTransform.BuiltIn({ + name: 'transparency-structure-representation-3d', + display: 'Transparency 3D Representation', + from: SO.Molecule.Structure.Representation3D, + to: SO.Molecule.Structure.Representation3DState, + params: { + script: PD.ScriptExpression({ language: 'mol-script', expression: '(sel.atom.atom-groups :chain-test (= atom.label_asym_id A))' }), + value: PD.Numeric(0.75, { min: 0, max: 1, step: 0.01 }, { label: 'Transparency' }), + variant: PD.Select('single', [['single', 'Single-layer'], ['multi', 'Multi-layer']]) + } +})({ + canAutoUpdate() { + return true; + }, + apply({ a, params }) { + const structure = a.data.source.data + const transparency = getStructureTransparency(structure, params.script, params.value, params.variant) + + return new SO.Molecule.Structure.Representation3DState({ + state: { transparency }, + initialState: { transparency: Transparency.Empty }, + info: structure, + source: a + }, { label: `Transparency (${transparency.value})` }) + }, + update({ a, b, newParams, oldParams }) { + const structure = b.data.info as Structure + if (a.data.source.data !== structure) return StateTransformer.UpdateResult.Recreate + const oldTransparency = b.data.state.transparency! + const newTransparency = getStructureTransparency(structure, newParams.script, newParams.value, newParams.variant) + if (Transparency.areEqual(oldTransparency, newTransparency)) return StateTransformer.UpdateResult.Unchanged + + b.data.state.transparency = newTransparency + b.data.source = a + b.label = `Transparency (${newTransparency.value})` + return StateTransformer.UpdateResult.Updated + } +}); + // export namespace VolumeRepresentation3DHelpers { @@ -471,4 +527,38 @@ const VolumeRepresentation3D = PluginStateTransform.BuiltIn({ return StateTransformer.UpdateResult.Updated; }); } +}); + +// + +export { ShapeRepresentation3D } +type ShapeRepresentation3D = typeof ShapeRepresentation3D +const ShapeRepresentation3D = PluginStateTransform.BuiltIn({ + name: 'shape-representation-3d', + display: '3D Representation', + from: SO.Shape.Provider, + to: SO.Shape.Representation3D, + params: (a, ctx: PluginContext) => { + return a ? a.data.params : BaseGeometry.Params + } +})({ + canAutoUpdate() { + return true; + }, + apply({ a, params }, plugin: PluginContext) { + return Task.create('Shape Representation', async ctx => { + const props = { ...PD.getDefaultValues(a.data.params), params } + const repr = ShapeRepresentation(a.data.getShape, a.data.geometryUtils) + // TODO set initial state, repr.setState({}) + await repr.createOrUpdate(props, a.data.data).runInContext(ctx); + return new SO.Shape.Representation3D({ repr, source: a }, { label: a.data.label }); + }); + }, + update({ a, b, oldParams, newParams }, plugin: PluginContext) { + return Task.create('Shape Representation', async ctx => { + const props = { ...b.data.repr.props, ...newParams } + await b.data.repr.createOrUpdate(props, a.data.data).runInContext(ctx); + return StateTransformer.UpdateResult.Updated; + }); + } }); \ No newline at end of file diff --git a/src/mol-plugin/ui/base.tsx b/src/mol-plugin/ui/base.tsx index d4ae0171d84882a5f0c9fb6e2152b4fbfd5e2d83..afa47c47d7cf31e9b27ba82b054fbbcb94ecebcb 100644 --- a/src/mol-plugin/ui/base.tsx +++ b/src/mol-plugin/ui/base.tsx @@ -58,4 +58,7 @@ export abstract class PurePluginUIComponent<P = {}, S = {}, SS = {}> extends Rea this.plugin = context; if (this.init) this.init(); } -} \ No newline at end of file +} + +export type _Props<C extends React.Component> = C extends React.Component<infer P> ? P : never +export type _State<C extends React.Component> = C extends React.Component<any, infer S> ? S : never \ No newline at end of file diff --git a/src/mol-plugin/ui/controls/parameters.tsx b/src/mol-plugin/ui/controls/parameters.tsx index 72135f24d2a4cca1b01a47a69b98830547f9e292..e179f8cfabfef097d13bee5106eaad2819ee8e15 100644 --- a/src/mol-plugin/ui/controls/parameters.tsx +++ b/src/mol-plugin/ui/controls/parameters.tsx @@ -16,6 +16,7 @@ import * as React from 'react'; import LineGraphComponent from './line-graph/line-graph-component'; import { Slider, Slider2 } from './slider'; import { NumericInput, IconButton } from './common'; +import { _Props, _State } from '../base'; export interface ParameterControlsProps<P extends PD.Params = PD.Params> { params: P, @@ -513,9 +514,6 @@ export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any> } } -type _Props<C extends React.Component> = C extends React.Component<infer P> ? P : never -type _State<C extends React.Component> = C extends React.Component<any, infer S> ? S : never - class ObjectListEditor extends React.PureComponent<{ params: PD.Params, value: object, isUpdate?: boolean, apply: (value: any) => void, isDisabled?: boolean }, { params: PD.Params, value: object, current: object }> { state = { params: {}, value: void 0 as any, current: void 0 as any }; diff --git a/src/mol-plugin/ui/state/tree.tsx b/src/mol-plugin/ui/state/tree.tsx index f2c4ac4f805505441a4f14a1b9e387b64cdf1a5a..16293756d3cb72c941337869271fc52a0a629e0c 100644 --- a/src/mol-plugin/ui/state/tree.tsx +++ b/src/mol-plugin/ui/state/tree.tsx @@ -6,9 +6,9 @@ import * as React from 'react'; import { PluginStateObject } from 'mol-plugin/state/objects'; -import { State, StateObject, StateTransform } from 'mol-state' +import { State, StateObject, StateTransform, StateObjectCell } from 'mol-state' import { PluginCommands } from 'mol-plugin/command'; -import { PluginUIComponent } from '../base'; +import { PluginUIComponent, _Props, _State } from '../base'; import { StateObjectActions } from './actions'; export class StateTree extends PluginUIComponent<{ state: State }, { showActions: boolean }> { @@ -37,74 +37,75 @@ export class StateTree extends PluginUIComponent<{ state: State }, { showActions if (this.state.showActions) { return <StateObjectActions state={this.props.state} nodeRef={ref} hideHeader={true} /> } - return <StateTreeNode state={this.props.state} nodeRef={ref} depth={0} />; + return <StateTreeNode cell={this.props.state.cells.get(ref)!} depth={0} />; } } -class StateTreeNode extends PluginUIComponent<{ nodeRef: string, state: State, depth: number }, { state: State, isCollapsed: boolean }> { +class StateTreeNode extends PluginUIComponent<{ cell: StateObjectCell, depth: number }, { isCollapsed: boolean }> { is(e: State.ObjectEvent) { - return e.ref === this.props.nodeRef && e.state === this.props.state; + return e.ref === this.ref && e.state === this.props.cell.parent; } - get cellState() { - return this.props.state.cellStates.get(this.props.nodeRef); + get ref() { + return this.props.cell.transform.ref; } componentDidMount() { this.subscribe(this.plugin.events.state.cell.stateUpdated, e => { - if (this.is(e) && e.state.transforms.has(this.props.nodeRef)) { - this.setState({ isCollapsed: e.cellState.isCollapsed }); + if (this.props.cell === e.cell && this.is(e) && e.state.cells.has(this.ref)) { + this.forceUpdate(); + // if (!!this.props.cell.state.isCollapsed !== this.state.isCollapsed) { + // this.setState({ isCollapsed: !!e.cell.state.isCollapsed }); + // } } }); this.subscribe(this.plugin.events.state.cell.created, e => { - if (this.props.state === e.state && this.props.nodeRef === e.cell.transform.parent) { + if (this.props.cell.parent === e.state && this.ref === e.cell.transform.parent) { this.forceUpdate(); } }); this.subscribe(this.plugin.events.state.cell.removed, e => { - if (this.props.state === e.state && this.props.nodeRef === e.parent) { + if (this.props.cell.parent === e.state && this.ref === e.parent) { this.forceUpdate(); } }); } state = { - isCollapsed: this.props.state.cellStates.get(this.props.nodeRef).isCollapsed, - state: this.props.state + isCollapsed: !!this.props.cell.state.isCollapsed } - static getDerivedStateFromProps(props: { nodeRef: string, state: State }, state: { state: State, isCollapsed: boolean }) { - if (props.state === state.state) return null; - return { - isCollapsed: props.state.cellStates.get(props.nodeRef).isCollapsed, - state: props.state - }; + static getDerivedStateFromProps(props: _Props<StateTreeNode>, state: _State<StateTreeNode>): _State<StateTreeNode> | null { + if (!!props.cell.state.isCollapsed === state.isCollapsed) return null; + return { isCollapsed: !!props.cell.state.isCollapsed }; } render() { - const cell = this.props.state.cells.get(this.props.nodeRef); - if (!cell || cell.obj === StateObject.Null) return null; + const cell = this.props.cell; + if (!cell || cell.obj === StateObject.Null || !cell.parent.tree.transforms.has(cell.transform.ref)) { + 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); + const cellState = cell.state; + const showLabel = cell.status !== 'ok' || !cell.state.isGhost; + const children = cell.parent.tree.children.get(this.ref); 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} />)} + {children.map(c => <StateTreeNode cell={cell.parent.cells.get(c!)!} key={c} depth={newDepth} />)} </div>; } return <> - <StateTreeNodeLabel nodeRef={this.props.nodeRef} state={this.props.state} depth={this.props.depth} /> + <StateTreeNodeLabel cell={cell} depth={this.props.depth} /> {children.size === 0 ? void 0 : <div style={{ display: cellState.isCollapsed ? 'none' : 'block' }}> - {children.map(c => <StateTreeNode state={this.props.state} nodeRef={c!} key={c} depth={newDepth} />)} + {children.map(c => <StateTreeNode cell={cell.parent.cells.get(c!)!} key={c} depth={newDepth} />)} </div> } </>; @@ -112,11 +113,15 @@ class StateTreeNode extends PluginUIComponent<{ nodeRef: string, state: State, d } class StateTreeNodeLabel extends PluginUIComponent< - { nodeRef: string, state: State, depth: number }, - { state: State, isCurrent: boolean, isCollapsed: boolean /*, updaterCollapsed: boolean */ }> { + { cell: StateObjectCell, depth: number }, + { isCurrent: boolean, isCollapsed: boolean }> { is(e: State.ObjectEvent) { - return e.ref === this.props.nodeRef && e.state === this.props.state; + return e.ref === this.ref && e.state === this.props.cell.parent; + } + + get ref() { + return this.props.cell.transform.ref; } componentDidMount() { @@ -126,70 +131,66 @@ class StateTreeNodeLabel extends PluginUIComponent< this.subscribe(this.plugin.state.behavior.currentObject, e => { if (!this.is(e)) { - if (this.state.isCurrent && e.state.transforms.has(this.props.nodeRef)) { - this.setState({ isCurrent: this.props.state.current === this.props.nodeRef }); + if (this.state.isCurrent && e.state.transforms.has(this.ref)) { + this.setState({ isCurrent: this.props.cell.parent.current === this.ref }); } return; } - if (e.state.transforms.has(this.props.nodeRef)) { + if (e.state.transforms.has(this.ref)) { this.setState({ - isCurrent: this.props.state.current === this.props.nodeRef, - isCollapsed: this.props.state.cellStates.get(this.props.nodeRef).isCollapsed + isCurrent: this.props.cell.parent.current === this.ref, + isCollapsed: !!this.props.cell.state.isCollapsed }); } }); } state = { - isCurrent: this.props.state.current === this.props.nodeRef, - isCollapsed: this.props.state.cellStates.get(this.props.nodeRef).isCollapsed, - state: this.props.state, - // updaterCollapsed: true + isCurrent: this.props.cell.parent.current === this.ref, + isCollapsed: !!this.props.cell.state.isCollapsed } - static getDerivedStateFromProps(props: { nodeRef: string, state: State }, state: { state: State, isCurrent: boolean, isCollapsed: boolean }) { - if (props.state === state.state) return null; - return { - isCurrent: props.state.current === props.nodeRef, - isCollapsed: props.state.cellStates.get(props.nodeRef).isCollapsed, - state: props.state, - updaterCollapsed: true - }; + static getDerivedStateFromProps(props: _Props<StateTreeNodeLabel>, state: _State<StateTreeNodeLabel>): _State<StateTreeNodeLabel> | null { + const isCurrent = props.cell.parent.current === props.cell.transform.ref; + const isCollapsed = !!props.cell.state.isCollapsed; + + if (state.isCollapsed === isCollapsed && state.isCurrent === isCurrent) return null; + return { isCurrent, isCollapsed }; } setCurrent = (e: React.MouseEvent<HTMLElement>) => { e.preventDefault(); e.currentTarget.blur(); - PluginCommands.State.SetCurrentObject.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef }); + PluginCommands.State.SetCurrentObject.dispatch(this.plugin, { state: this.props.cell.parent, ref: this.ref }); } remove = (e: React.MouseEvent<HTMLElement>) => { e.preventDefault(); - PluginCommands.State.RemoveObject.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef, removeParentGhosts: true }); + PluginCommands.State.RemoveObject.dispatch(this.plugin, { state: this.props.cell.parent, ref: this.ref, removeParentGhosts: true }); } toggleVisible = (e: React.MouseEvent<HTMLElement>) => { e.preventDefault(); - PluginCommands.State.ToggleVisibility.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef }); + PluginCommands.State.ToggleVisibility.dispatch(this.plugin, { state: this.props.cell.parent, ref: this.ref }); e.currentTarget.blur(); } toggleExpanded = (e: React.MouseEvent<HTMLElement>) => { e.preventDefault(); - PluginCommands.State.ToggleExpanded.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef }); + PluginCommands.State.ToggleExpanded.dispatch(this.plugin, { state: this.props.cell.parent, ref: this.ref }); e.currentTarget.blur(); } highlight = (e: React.MouseEvent<HTMLElement>) => { e.preventDefault(); - PluginCommands.State.Highlight.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef }); + PluginCommands.State.Highlight.dispatch(this.plugin, { state: this.props.cell.parent, ref: this.ref }); e.currentTarget.blur(); } clearHighlight = (e: React.MouseEvent<HTMLElement>) => { e.preventDefault(); - PluginCommands.State.ClearHighlight.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef }); + PluginCommands.State.ClearHighlight.dispatch(this.plugin, { state: this.props.cell.parent, ref: this.ref }); e.currentTarget.blur(); } @@ -201,12 +202,11 @@ class StateTreeNodeLabel extends PluginUIComponent< // } render() { - const n = this.props.state.transforms.get(this.props.nodeRef)!; - const cell = this.props.state.cells.get(this.props.nodeRef); + const cell = this.props.cell; + const n = cell.transform; if (!cell) return null; - const isCurrent = this.is(this.props.state.behaviors.currentObject.value); - + const isCurrent = this.state.isCurrent; // this.is(cell.parent.behaviors.currentObject.value); let label: any; if (cell.status === 'pending' || cell.status === 'processing') { @@ -226,8 +226,8 @@ class StateTreeNodeLabel extends PluginUIComponent< } } - const children = this.props.state.tree.children.get(this.props.nodeRef); - const cellState = this.props.state.cellStates.get(this.props.nodeRef); + const children = cell.parent.tree.children.get(this.ref); + const cellState = cell.state; 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' /> @@ -244,7 +244,7 @@ class StateTreeNodeLabel extends PluginUIComponent< {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>} - {!cell.transform.props.isLocked && <button onClick={this.remove} className='msp-btn msp-btn-link msp-tree-remove-button'> + {!cell.state.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>; diff --git a/src/mol-plugin/util/structure-labels.ts b/src/mol-plugin/util/structure-labels.ts index c6e944b29219ac96fed56f47556ad3885780ea36..932569da9046774aca3952c380c7174f7d9f31ca 100644 --- a/src/mol-plugin/util/structure-labels.ts +++ b/src/mol-plugin/util/structure-labels.ts @@ -44,7 +44,7 @@ export async function getLabelRepresentation(ctx: RuntimeContext, structure: Str function getLabelsShape(ctx: RuntimeContext, data: LabelsData, props: PD.Values<Text.Params>, shape?: Shape<Text>) { const geo = getLabelsText(data, props, shape && shape.geometry); - return Shape.create('Scene Labels', geo, () => ColorNames.dimgrey, g => data.sizes[g], () => '') + return Shape.create('Scene Labels', data, geo, () => ColorNames.dimgrey, g => data.sizes[g], () => '') } const boundaryHelper = new BoundaryHelper(); diff --git a/src/mol-repr/representation.ts b/src/mol-repr/representation.ts index 5bc851e2fdbac8163d79b00a4fdd8a5c18b319b9..812bc71d0ecbc3d35f9d0dc03200f76f2cfcd8d1 100644 --- a/src/mol-repr/representation.ts +++ b/src/mol-repr/representation.ts @@ -20,6 +20,7 @@ import { Mat4 } from 'mol-math/linear-algebra'; import { BaseGeometry } from 'mol-geo/geometry/base'; import { Visual } from './visual'; import { Overpaint } from 'mol-theme/overpaint'; +import { Transparency } from 'mol-theme/transparency'; // export interface RepresentationProps { // visuals?: string[] @@ -146,19 +147,22 @@ namespace Representation { pickable: boolean /** Overpaint applied to the representation's renderobjects */ overpaint: Overpaint + /** Per group transparency applied to the representation's renderobjects */ + transparency: Transparency /** Controls if the representation's renderobjects are synced automatically with GPU or not */ syncManually: boolean /** A transformation applied to the representation's renderobjects */ transform: Mat4 } export function createState(): State { - return { visible: false, alphaFactor: 0, pickable: false, syncManually: false, transform: Mat4.identity(), overpaint: Overpaint.Empty } + return { visible: false, alphaFactor: 0, pickable: false, syncManually: false, transform: Mat4.identity(), overpaint: Overpaint.Empty, transparency: Transparency.Empty } } export function updateState(state: State, update: Partial<State>) { if (update.visible !== undefined) state.visible = update.visible if (update.alphaFactor !== undefined) state.alphaFactor = update.alphaFactor if (update.pickable !== undefined) state.pickable = update.pickable if (update.overpaint !== undefined) state.overpaint = update.overpaint + if (update.transparency !== undefined) state.transparency = update.transparency if (update.syncManually !== undefined) state.syncManually = update.syncManually if (update.transform !== undefined) Mat4.copy(state.transform, update.transform) } @@ -322,6 +326,9 @@ namespace Representation { if (state.overpaint !== undefined) { // TODO } + if (state.transparency !== undefined) { + // TODO + } if (state.transform !== undefined) Visual.setTransform(renderObject, state.transform) Representation.updateState(currentState, state) diff --git a/src/mol-repr/shape/representation.ts b/src/mol-repr/shape/representation.ts index 83f1edb96e25a5ed817d4cba918fb1d2a9ec803b..b84a0bffc03e96a677e7fb103f10f52de4b011c3 100644 --- a/src/mol-repr/shape/representation.ts +++ b/src/mol-repr/shape/representation.ts @@ -5,11 +5,11 @@ */ import { Task, RuntimeContext } from 'mol-task' -import { createRenderObject, GraphicsRenderObject } from 'mol-gl/render-object'; +import { createRenderObject, GraphicsRenderObject, getNextMaterialId } from 'mol-gl/render-object'; import { Representation } from '../representation'; import { Loci, EmptyLoci, isEveryLoci } from 'mol-model/loci'; import { ValueCell } from 'mol-util'; -import { Shape } from 'mol-model/shape'; +import { Shape, ShapeGroup } from 'mol-model/shape'; import { OrderedSet, Interval } from 'mol-data/int'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import { createTransform, TransformData } from 'mol-geo/geometry/transform-data'; @@ -35,6 +35,7 @@ export function ShapeRepresentation<D, G extends Geometry, P extends Geometry.Pa let version = 0 const updated = new Subject<number>() const _state = Representation.createState() + const materialId = getNextMaterialId() const renderObjects: GraphicsRenderObject[] = [] let _renderObject: GraphicsRenderObject | undefined let _shape: Shape<G> @@ -56,9 +57,7 @@ export function ShapeRepresentation<D, G extends Geometry, P extends Geometry.Pa updateState.createNew = true } else if (shape && _shape && shape.id === _shape.id) { // console.log('same shape') - // trigger color update when shape has not changed - updateState.updateColor = true - updateState.updateTransform = true + // nothing to set } else if (shape && _shape && shape.id !== _shape.id) { // console.log('new shape') updateState.updateTransform = true @@ -101,7 +100,7 @@ export function ShapeRepresentation<D, G extends Geometry, P extends Geometry.Pa const values = geometryUtils.createValues(_shape.geometry, transform, locationIt, _theme, newProps) const state = geometryUtils.createRenderableState(newProps) - _renderObject = createRenderObject(_shape.geometry.kind, values, state) + _renderObject = createRenderObject(_shape.geometry.kind, values, state, materialId) if (_renderObject) renderObjects.push(_renderObject) // add new renderObject to list } else { if (!_renderObject) { @@ -170,7 +169,7 @@ export function ShapeRepresentation<D, G extends Geometry, P extends Geometry.Pa getLoci(pickingId: PickingId) { const { objectId, groupId, instanceId } = pickingId if (_renderObject && _renderObject.id === objectId) { - return Shape.Loci(_shape, [{ ids: OrderedSet.ofSingleton(groupId) }], instanceId) + return ShapeGroup.Loci(_shape, [{ ids: OrderedSet.ofSingleton(groupId) }], instanceId) } return EmptyLoci }, @@ -185,6 +184,9 @@ export function ShapeRepresentation<D, G extends Geometry, P extends Geometry.Pa if (state.overpaint !== undefined) { Visual.setOverpaint(_renderObject, state.overpaint, lociApply, true) } + if (state.transparency !== undefined) { + Visual.setTransparency(_renderObject, state.transparency, lociApply, true) + } if (state.transform !== undefined) Visual.setTransform(_renderObject, state.transform) } @@ -210,7 +212,7 @@ function createShapeTransform(transforms: Mat4[], transformData?: TransformData) } function eachShapeGroup(loci: Loci, shape: Shape, apply: (interval: Interval) => boolean) { - if (!Shape.isLoci(loci)) return false + if (!ShapeGroup.isLoci(loci)) return false if (loci.shape !== shape) return false let changed = false const { groupCount } = shape @@ -233,7 +235,7 @@ function eachShapeGroup(loci: Loci, shape: Shape, apply: (interval: Interval) => export namespace ShapeGroupIterator { export function fromShape(shape: Shape): LocationIterator { const instanceCount = shape.transforms.length - const location = Shape.Location(shape) + const location = ShapeGroup.Location(shape) const getLocation = (groupIndex: number, instanceIndex: number) => { location.group = groupIndex location.instance = instanceIndex diff --git a/src/mol-repr/structure/complex-representation.ts b/src/mol-repr/structure/complex-representation.ts index bbb8325859e481f190099c5e212bb79588ce0b95..e8ed4daf473e96be54a04d54cd67e357f1c6a329 100644 --- a/src/mol-repr/structure/complex-representation.ts +++ b/src/mol-repr/structure/complex-representation.ts @@ -16,11 +16,12 @@ import { RepresentationContext, RepresentationParamsGetter } from 'mol-repr/repr import { Theme, createEmptyTheme } from 'mol-theme/theme'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import { Subject } from 'rxjs'; -import { GraphicsRenderObject } from 'mol-gl/render-object'; +import { GraphicsRenderObject, getNextMaterialId } from 'mol-gl/render-object'; -export function ComplexRepresentation<P extends StructureParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: () => ComplexVisual<P>): StructureRepresentation<P> { +export function ComplexRepresentation<P extends StructureParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: (materialId: number) => ComplexVisual<P>): StructureRepresentation<P> { let version = 0 const updated = new Subject<number>() + const materialId = getNextMaterialId() const renderObjects: GraphicsRenderObject[] = [] const _state = StructureRepresentationStateBuilder.create() let visual: ComplexVisual<P> | undefined @@ -39,7 +40,7 @@ export function ComplexRepresentation<P extends StructureParams>(label: string, _props = Object.assign({}, _props, props) return Task.create('Creating or updating ComplexRepresentation', async runtime => { - if (!visual) visual = visualCtor() + if (!visual) visual = visualCtor(materialId) const promise = visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, structure) if (promise) await promise // update list of renderObjects @@ -68,6 +69,7 @@ export function ComplexRepresentation<P extends StructureParams>(label: string, if (state.alphaFactor !== undefined && visual) visual.setAlphaFactor(state.alphaFactor) if (state.pickable !== undefined && visual) visual.setPickable(state.pickable) if (state.overpaint !== undefined && visual) visual.setOverpaint(state.overpaint) + if (state.transparency !== undefined && visual) visual.setTransparency(state.transparency) if (state.transform !== undefined && visual) visual.setTransform(state.transform) if (state.unitTransforms !== undefined && visual) { // Since ComplexVisuals always renders geometries between units the application of `unitTransforms` diff --git a/src/mol-repr/structure/complex-visual.ts b/src/mol-repr/structure/complex-visual.ts index 7f5407789c126d221f45642205eab9576b0150df..0cc0bf9274c6ba4484e871bcc87300770119b993 100644 --- a/src/mol-repr/structure/complex-visual.ts +++ b/src/mol-repr/structure/complex-visual.ts @@ -29,15 +29,16 @@ import { DirectVolume } from 'mol-geo/geometry/direct-volume/direct-volume'; import { Mat4 } from 'mol-math/linear-algebra'; import { createIdentityTransform } from 'mol-geo/geometry/transform-data'; import { Overpaint } from 'mol-theme/overpaint'; +import { Transparency } from 'mol-theme/transparency'; export interface ComplexVisual<P extends StructureParams> extends Visual<Structure, P> { } -function createComplexRenderObject<G extends Geometry>(structure: Structure, geometry: G, locationIt: LocationIterator, theme: Theme, props: PD.Values<Geometry.Params<G>>) { +function createComplexRenderObject<G extends Geometry>(structure: Structure, geometry: G, locationIt: LocationIterator, theme: Theme, props: PD.Values<Geometry.Params<G>>, materialId: number) { const { createValues, createRenderableState } = Geometry.getUtils(geometry) const transform = createIdentityTransform() const values = createValues(geometry, transform, locationIt, theme, props) const state = createRenderableState(props) - return createRenderObject(geometry.kind, values, state) + return createRenderObject(geometry.kind, values, state, materialId) } const ComplexParams = { @@ -59,7 +60,7 @@ interface ComplexVisualGeometryBuilder<P extends UnitsParams, G extends Geometry geometryUtils: GeometryUtils<G> } -export function ComplexVisual<G extends Geometry, P extends ComplexParams & Geometry.Params<G>>(builder: ComplexVisualGeometryBuilder<P, G>): ComplexVisual<P> { +export function ComplexVisual<G extends Geometry, P extends ComplexParams & Geometry.Params<G>>(builder: ComplexVisualGeometryBuilder<P, G>, materialId: number): ComplexVisual<P> { const { defaultProps, createGeometry, createLocationIterator, getLoci, eachLocation, setUpdateState } = builder const { updateValues, updateBoundingSphere, updateRenderableState } = builder.geometryUtils const updateState = VisualUpdateState.create() @@ -117,7 +118,7 @@ export function ComplexVisual<G extends Geometry, P extends ComplexParams & Geom if (updateState.createNew) { locationIt = createLocationIterator(newStructure) if (newGeometry) { - renderObject = createComplexRenderObject(newStructure, newGeometry, locationIt, newTheme, newProps) + renderObject = createComplexRenderObject(newStructure, newGeometry, locationIt, newTheme, newProps, materialId) } else { throw new Error('expected geometry to be given') } @@ -196,9 +197,12 @@ export function ComplexVisual<G extends Geometry, P extends ComplexParams & Geom setTransform(matrix?: Mat4, instanceMatrices?: Float32Array | null) { Visual.setTransform(renderObject, matrix, instanceMatrices) }, - setOverpaint(overpaint: Overpaint, clear = false) { + setOverpaint(overpaint: Overpaint) { return Visual.setOverpaint(renderObject, overpaint, lociApply, true) }, + setTransparency(transparency: Transparency) { + return Visual.setTransparency(renderObject, transparency, lociApply, true) + }, destroy() { // TODO renderObject = undefined @@ -216,7 +220,7 @@ export type ComplexMeshParams = typeof ComplexMeshParams export interface ComplexMeshVisualBuilder<P extends ComplexMeshParams> extends ComplexVisualBuilder<P, Mesh> { } -export function ComplexMeshVisual<P extends ComplexMeshParams>(builder: ComplexMeshVisualBuilder<P>): ComplexVisual<P> { +export function ComplexMeshVisual<P extends ComplexMeshParams>(builder: ComplexMeshVisualBuilder<P>, materialId: number): ComplexVisual<P> { return ComplexVisual<Mesh, StructureMeshParams & UnitsParams>({ ...builder, setUpdateState: (state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme) => { @@ -224,7 +228,7 @@ export function ComplexMeshVisual<P extends ComplexMeshParams>(builder: ComplexM if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.createGeometry = true }, geometryUtils: Mesh.Utils - }) + }, materialId) } // direct-volume @@ -237,7 +241,7 @@ export type ComplexDirectVolumeParams = typeof ComplexDirectVolumeParams export interface ComplexDirectVolumeVisualBuilder<P extends ComplexDirectVolumeParams> extends ComplexVisualBuilder<P, DirectVolume> { } -export function ComplexDirectVolumeVisual<P extends ComplexDirectVolumeParams>(builder: ComplexDirectVolumeVisualBuilder<P>): ComplexVisual<P> { +export function ComplexDirectVolumeVisual<P extends ComplexDirectVolumeParams>(builder: ComplexDirectVolumeVisualBuilder<P>, materialId: number): ComplexVisual<P> { return ComplexVisual<DirectVolume, StructureDirectVolumeParams & UnitsParams>({ ...builder, setUpdateState: (state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme) => { @@ -245,5 +249,5 @@ export function ComplexDirectVolumeVisual<P extends ComplexDirectVolumeParams>(b if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.createGeometry = true }, geometryUtils: DirectVolume.Utils - }) + }, materialId) } \ No newline at end of file diff --git a/src/mol-repr/structure/registry.ts b/src/mol-repr/structure/registry.ts index 5cadc109a7933a576c8e291c75c97d052765a476..c83be7e241276e7a6b96e5f9b216f4e0d48b8731 100644 --- a/src/mol-repr/structure/registry.ts +++ b/src/mol-repr/structure/registry.ts @@ -8,14 +8,14 @@ import { Structure } from 'mol-model/structure'; import { RepresentationProvider, RepresentationRegistry } from '../representation'; import { CartoonRepresentationProvider } from './representation/cartoon'; import { BallAndStickRepresentationProvider } from './representation/ball-and-stick'; -import { MolecularSurfaceRepresentationProvider } from './representation/molecular-surface'; -import { MolecularVolumeRepresentationProvider } from './representation/molecular-volume'; +import { GaussianSurfaceRepresentationProvider } from './representation/gaussian-surface'; import { CarbohydrateRepresentationProvider } from './representation/carbohydrate'; import { SpacefillRepresentationProvider } from './representation/spacefill'; import { DistanceRestraintRepresentationProvider } from './representation/distance-restraint'; import { PointRepresentationProvider } from './representation/point'; import { StructureRepresentationState } from './representation'; import { PuttyRepresentationProvider } from './representation/putty'; +import { MolecularSurfaceRepresentationProvider } from './representation/molecular-surface'; export class StructureRepresentationRegistry extends RepresentationRegistry<Structure, StructureRepresentationState> { constructor() { @@ -32,8 +32,9 @@ export const BuiltInStructureRepresentations = { 'ball-and-stick': BallAndStickRepresentationProvider, 'carbohydrate': CarbohydrateRepresentationProvider, 'distance-restraint': DistanceRestraintRepresentationProvider, + 'gaussian-surface': GaussianSurfaceRepresentationProvider, + // 'gaussian-volume': GaussianVolumeRepresentationProvider, // TODO disabled for now, needs more work 'molecular-surface': MolecularSurfaceRepresentationProvider, - 'molecular-volume': MolecularVolumeRepresentationProvider, 'point': PointRepresentationProvider, 'putty': PuttyRepresentationProvider, 'spacefill': SpacefillRepresentationProvider, diff --git a/src/mol-repr/structure/representation.ts b/src/mol-repr/structure/representation.ts index 183cd297c381b744e1abfe7a073d28b9322fefb4..427ab0c0fc33c18e1ed27a24710423c2b30003f1 100644 --- a/src/mol-repr/structure/representation.ts +++ b/src/mol-repr/structure/representation.ts @@ -15,6 +15,7 @@ import { Lines } from 'mol-geo/geometry/lines/lines'; import { DirectVolume } from 'mol-geo/geometry/direct-volume/direct-volume'; import { Spheres } from 'mol-geo/geometry/spheres/spheres'; import { StructureUnitTransforms } from 'mol-model/structure/structure/util/unit-transforms'; +import { TextureMesh } from 'mol-geo/geometry/texture-mesh/texture-mesh'; export interface StructureRepresentationState extends Representation.State { unitTransforms: StructureUnitTransforms | null @@ -56,6 +57,9 @@ export type StructureLinesParams = typeof StructureLinesParams export const StructureDirectVolumeParams = { ...DirectVolume.Params, ...StructureParams } export type StructureDirectVolumeParams = typeof StructureDirectVolumeParams +export const StructureTextureMeshParams = { ...TextureMesh.Params, ...StructureParams } +export type StructureTextureMeshParams = typeof StructureTextureMeshParams + export { ComplexRepresentation } from './complex-representation' export { UnitsRepresentation } from './units-representation' export { ComplexVisual } from './complex-visual' diff --git a/src/mol-repr/structure/representation/gaussian-surface.ts b/src/mol-repr/structure/representation/gaussian-surface.ts new file mode 100644 index 0000000000000000000000000000000000000000..1e1e748f9fb67134062c9ac9200f9000f7f77662 --- /dev/null +++ b/src/mol-repr/structure/representation/gaussian-surface.ts @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { GaussianSurfaceMeshVisual, GaussianSurfaceTextureMeshVisual, GaussianSurfaceMeshParams } from '../visual/gaussian-surface-mesh'; +import { UnitsRepresentation } from '../units-representation'; +import { GaussianWireframeVisual, GaussianWireframeParams } from '../visual/gaussian-surface-wireframe'; +import { ParamDefinition as PD } from 'mol-util/param-definition'; +import { StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder } from '../representation'; +import { Representation, RepresentationParamsGetter, RepresentationContext } from 'mol-repr/representation'; +import { ThemeRegistryContext } from 'mol-theme/theme'; +import { Structure } from 'mol-model/structure'; + +const GaussianSurfaceVisuals = { + 'gaussian-surface-mesh': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, GaussianSurfaceMeshParams>) => UnitsRepresentation('Gaussian surface', ctx, getParams, GaussianSurfaceMeshVisual), + 'gaussian-surface-texture-mesh': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, GaussianSurfaceMeshParams>) => UnitsRepresentation('Gaussian surface', ctx, getParams, GaussianSurfaceTextureMeshVisual), + 'gaussian-wireframe': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, GaussianWireframeParams>) => UnitsRepresentation('Gaussian wireframe', ctx, getParams, GaussianWireframeVisual), +} +type GaussianSurfaceVisualName = keyof typeof GaussianSurfaceVisuals +const GaussianSurfaceVisualOptions = Object.keys(GaussianSurfaceVisuals).map(name => [name, name] as [GaussianSurfaceVisualName, string]) + +export const GaussianSurfaceParams = { + ...GaussianSurfaceMeshParams, + ...GaussianWireframeParams, + visuals: PD.MultiSelect<GaussianSurfaceVisualName>(['gaussian-surface-mesh'], GaussianSurfaceVisualOptions), +} +export type GaussianSurfaceParams = typeof GaussianSurfaceParams +export function getGaussianSurfaceParams(ctx: ThemeRegistryContext, structure: Structure) { + return PD.clone(GaussianSurfaceParams) +} + +export type GaussianSurfaceRepresentation = StructureRepresentation<GaussianSurfaceParams> +export function GaussianSurfaceRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, GaussianSurfaceParams>): GaussianSurfaceRepresentation { + return Representation.createMulti('Gaussian Surface', ctx, getParams, StructureRepresentationStateBuilder, GaussianSurfaceVisuals as unknown as Representation.Def<Structure, GaussianSurfaceParams>) +} + +export const GaussianSurfaceRepresentationProvider: StructureRepresentationProvider<GaussianSurfaceParams> = { + label: 'Gaussian Surface', + description: 'Displays a gaussian molecular surface.', + factory: GaussianSurfaceRepresentation, + getParams: getGaussianSurfaceParams, + defaultValues: PD.getDefaultValues(GaussianSurfaceParams), + defaultColorTheme: 'polymer-id', + defaultSizeTheme: 'uniform' +} \ No newline at end of file diff --git a/src/mol-repr/structure/representation/molecular-volume.ts b/src/mol-repr/structure/representation/gaussian-volume.ts similarity index 52% rename from src/mol-repr/structure/representation/molecular-volume.ts rename to src/mol-repr/structure/representation/gaussian-volume.ts index 4bf0a0141629d5912bd6cf5de50e001a4ed3a0da..df1e99b19484849f29fd78ae690f0600f17ce96c 100644 --- a/src/mol-repr/structure/representation/molecular-volume.ts +++ b/src/mol-repr/structure/representation/gaussian-volume.ts @@ -11,29 +11,29 @@ import { Representation, RepresentationParamsGetter, RepresentationContext } fro import { ThemeRegistryContext } from 'mol-theme/theme'; import { Structure } from 'mol-model/structure'; -const MolecularVolumeVisuals = { +const GaussianVolumeVisuals = { 'gaussian-volume': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, GaussianDensityVolumeParams>) => ComplexRepresentation('Gaussian volume', ctx, getParams, GaussianDensityVolumeVisual) } -export const MolecularVolumeParams = { +export const GaussianVolumeParams = { ...GaussianDensityVolumeParams, } -export type MolecularVolumeParams = typeof MolecularVolumeParams -export function getMolecularVolumeParams(ctx: ThemeRegistryContext, structure: Structure) { - return PD.clone(MolecularVolumeParams) +export type GaussianVolumeParams = typeof GaussianVolumeParams +export function getGaussianVolumeParams(ctx: ThemeRegistryContext, structure: Structure) { + return PD.clone(GaussianVolumeParams) } -export type MolecularVolumeRepresentation = StructureRepresentation<MolecularVolumeParams> -export function MolecularVolumeRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, MolecularVolumeParams>): MolecularVolumeRepresentation { - return Representation.createMulti('Molecular Volume', ctx, getParams, StructureRepresentationStateBuilder, MolecularVolumeVisuals as unknown as Representation.Def<Structure, MolecularVolumeParams>) +export type GaussianVolumeRepresentation = StructureRepresentation<GaussianVolumeParams> +export function GaussianVolumeRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, GaussianVolumeParams>): GaussianVolumeRepresentation { + return Representation.createMulti('Gaussian Volume', ctx, getParams, StructureRepresentationStateBuilder, GaussianVolumeVisuals as unknown as Representation.Def<Structure, GaussianVolumeParams>) } -export const MolecularVolumeRepresentationProvider: StructureRepresentationProvider<MolecularVolumeParams> = { - label: 'Molecular Volume', +export const GaussianVolumeRepresentationProvider: StructureRepresentationProvider<GaussianVolumeParams> = { + label: 'Gaussian Volume', description: 'Displays a gaussian molecular density using direct volume rendering.', - factory: MolecularVolumeRepresentation, - getParams: getMolecularVolumeParams, - defaultValues: PD.getDefaultValues(MolecularVolumeParams), + factory: GaussianVolumeRepresentation, + getParams: getGaussianVolumeParams, + defaultValues: PD.getDefaultValues(GaussianVolumeParams), defaultColorTheme: 'polymer-id', defaultSizeTheme: 'uniform' } \ No newline at end of file diff --git a/src/mol-repr/structure/representation/molecular-surface.ts b/src/mol-repr/structure/representation/molecular-surface.ts index 4cef6716dca74fcef9b2ec284b41b1b41eaee9f3..3a5f0c0510c8d7d3302c5c458d8f3d94a35d4b40 100644 --- a/src/mol-repr/structure/representation/molecular-surface.ts +++ b/src/mol-repr/structure/representation/molecular-surface.ts @@ -1,12 +1,11 @@ /** - * 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> */ -import { GaussianSurfaceVisual, GaussianSurfaceParams } from '../visual/gaussian-surface-mesh'; +import { MolecularSurfaceMeshVisual, MolecularSurfaceMeshParams } from '../visual/molecular-surface-mesh'; import { UnitsRepresentation } from '../units-representation'; -import { GaussianWireframeVisual, GaussianWireframeParams } from '../visual/gaussian-surface-wireframe'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import { StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder } from '../representation'; import { Representation, RepresentationParamsGetter, RepresentationContext } from 'mol-repr/representation'; @@ -14,16 +13,14 @@ import { ThemeRegistryContext } from 'mol-theme/theme'; import { Structure } from 'mol-model/structure'; const MolecularSurfaceVisuals = { - 'gaussian-surface': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, GaussianSurfaceParams>) => UnitsRepresentation('Gaussian surface', ctx, getParams, GaussianSurfaceVisual), - 'gaussian-wireframe': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, GaussianWireframeParams>) => UnitsRepresentation('Gaussian wireframe', ctx, getParams, GaussianWireframeVisual), + 'molecular-surface-mesh': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, MolecularSurfaceMeshParams>) => UnitsRepresentation('Molecular surface', ctx, getParams, MolecularSurfaceMeshVisual), } type MolecularSurfaceVisualName = keyof typeof MolecularSurfaceVisuals const MolecularSurfaceVisualOptions = Object.keys(MolecularSurfaceVisuals).map(name => [name, name] as [MolecularSurfaceVisualName, string]) export const MolecularSurfaceParams = { - ...GaussianSurfaceParams, - ...GaussianWireframeParams, - visuals: PD.MultiSelect<MolecularSurfaceVisualName>(['gaussian-surface'], MolecularSurfaceVisualOptions), + ...MolecularSurfaceMeshParams, + visuals: PD.MultiSelect<MolecularSurfaceVisualName>(['molecular-surface-mesh'], MolecularSurfaceVisualOptions), } export type MolecularSurfaceParams = typeof MolecularSurfaceParams export function getMolecularSurfaceParams(ctx: ThemeRegistryContext, structure: Structure) { @@ -37,7 +34,7 @@ export function MolecularSurfaceRepresentation(ctx: RepresentationContext, getPa export const MolecularSurfaceRepresentationProvider: StructureRepresentationProvider<MolecularSurfaceParams> = { label: 'Molecular Surface', - description: 'Displays a gaussian molecular surface.', + description: 'Displays a molecular surface.', factory: MolecularSurfaceRepresentation, getParams: getMolecularSurfaceParams, defaultValues: PD.getDefaultValues(MolecularSurfaceParams), diff --git a/src/mol-repr/structure/units-representation.ts b/src/mol-repr/structure/units-representation.ts index 92e0c6e28cb1581a2aa9843a2eb2921898ebaa19..89e7adc7b8f1fecb5075cc92a5e578e3957c5ee4 100644 --- a/src/mol-repr/structure/units-representation.ts +++ b/src/mol-repr/structure/units-representation.ts @@ -7,7 +7,7 @@ import { Structure, Unit } from 'mol-model/structure'; import { Task } from 'mol-task' -import { GraphicsRenderObject } from 'mol-gl/render-object'; +import { GraphicsRenderObject, getNextMaterialId } from 'mol-gl/render-object'; import { RepresentationContext, RepresentationParamsGetter } from '../representation'; import { Visual } from '../visual'; import { Loci, EmptyLoci, isEmptyLoci } from 'mol-model/loci'; @@ -28,9 +28,10 @@ export type UnitsParams = typeof UnitsParams export interface UnitsVisual<P extends UnitsParams> extends Visual<StructureGroup, P> { } -export function UnitsRepresentation<P extends UnitsParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: () => UnitsVisual<P>): StructureRepresentation<P> { +export function UnitsRepresentation<P extends UnitsParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: (materialId: number) => UnitsVisual<P>): StructureRepresentation<P> { let version = 0 const updated = new Subject<number>() + const materialId = getNextMaterialId() const renderObjects: GraphicsRenderObject[] = [] const _state = StructureRepresentationStateBuilder.create() let visuals = new Map<number, { group: Unit.SymmetryGroup, visual: UnitsVisual<P> }>() @@ -57,14 +58,13 @@ export function UnitsRepresentation<P extends UnitsParams>(label: string, ctx: R _groups = structure.unitSymmetryGroups; for (let i = 0; i < _groups.length; i++) { const group = _groups[i]; - const visual = visualCtor() + const visual = visualCtor(materialId) const promise = visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, { group, structure }) if (promise) await promise visuals.set(group.hashCode, { visual, group }) if (runtime.shouldUpdate) await runtime.update({ message: 'Creating or updating UnitsVisual', current: i, max: _groups.length }) } } else if (structure && !Structure.areEquivalent(structure, _structure)) { - // console.log(label, 'structure not equivalent') // Tries to re-use existing visuals for the groups of the new structure. // Creates additional visuals if needed, destroys left-over visuals. _groups = structure.unitSymmetryGroups; @@ -86,7 +86,7 @@ export function UnitsRepresentation<P extends UnitsParams>(label: string, ctx: R } else { // console.log(label, 'not found visualGroup to reuse, creating new') // newGroups.push(group) - const visual = visualCtor() + const visual = visualCtor(materialId) const promise = visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, { group, structure }) if (promise) await promise visuals.set(group.hashCode, { visual, group }) @@ -171,11 +171,12 @@ export function UnitsRepresentation<P extends UnitsParams>(label: string, ctx: R } function setState(state: Partial<StructureRepresentationState>) { - const { visible, alphaFactor, pickable, overpaint, transform, unitTransforms } = state + const { visible, alphaFactor, pickable, overpaint, transparency, transform, unitTransforms } = state if (visible !== undefined) visuals.forEach(({ visual }) => visual.setVisibility(visible)) if (alphaFactor !== undefined) visuals.forEach(({ visual }) => visual.setAlphaFactor(alphaFactor)) if (pickable !== undefined) visuals.forEach(({ visual }) => visual.setPickable(pickable)) if (overpaint !== undefined) visuals.forEach(({ visual }) => visual.setOverpaint(overpaint)) + if (transparency !== undefined) visuals.forEach(({ visual }) => visual.setTransparency(transparency)) if (transform !== undefined) visuals.forEach(({ visual }) => visual.setTransform(transform)) if (unitTransforms !== undefined) { visuals.forEach(({ visual, group }) => { diff --git a/src/mol-repr/structure/units-visual.ts b/src/mol-repr/structure/units-visual.ts index 4f3c5bf22635d6cd2a7c1a1f09d8d7e5059241f9..bad9e645c3e32c8137c6ef0438b9bdc2219b06ae 100644 --- a/src/mol-repr/structure/units-visual.ts +++ b/src/mol-repr/structure/units-visual.ts @@ -7,7 +7,7 @@ import { Unit, Structure } from 'mol-model/structure'; import { RepresentationProps } from '../representation'; import { Visual, VisualContext } from '../visual'; -import { StructureMeshParams, StructurePointsParams, StructureLinesParams, StructureDirectVolumeParams, StructureSpheresParams } from './representation'; +import { StructureMeshParams, StructurePointsParams, StructureLinesParams, StructureDirectVolumeParams, StructureSpheresParams, StructureTextureMeshParams } from './representation'; import { Loci, isEveryLoci, EmptyLoci } from 'mol-model/loci'; import { GraphicsRenderObject, createRenderObject } from 'mol-gl/render-object'; import { deepEqual, ValueCell } from 'mol-util'; @@ -32,17 +32,19 @@ import { Mat4 } from 'mol-math/linear-algebra'; import { Spheres } from 'mol-geo/geometry/spheres/spheres'; import { createUnitsTransform, includesUnitKind } from './visual/util/common'; import { Overpaint } from 'mol-theme/overpaint'; +import { Transparency } from 'mol-theme/transparency'; +import { TextureMesh } from 'mol-geo/geometry/texture-mesh/texture-mesh'; export type StructureGroup = { structure: Structure, group: Unit.SymmetryGroup } export interface UnitsVisual<P extends RepresentationProps = {}> extends Visual<StructureGroup, P> { } -function createUnitsRenderObject<G extends Geometry>(group: Unit.SymmetryGroup, geometry: G, locationIt: LocationIterator, theme: Theme, props: PD.Values<Geometry.Params<G>>) { +function createUnitsRenderObject<G extends Geometry>(group: Unit.SymmetryGroup, geometry: G, locationIt: LocationIterator, theme: Theme, props: PD.Values<Geometry.Params<G>>, materialId: number) { const { createValues, createRenderableState } = Geometry.getUtils(geometry) const transform = createUnitsTransform(group) const values = createValues(geometry, transform, locationIt, theme, props) const state = createRenderableState(props) - return createRenderObject(geometry.kind, values, state) + return createRenderObject(geometry.kind, values, state, materialId) } interface UnitsVisualBuilder<P extends UnitsParams, G extends Geometry> { @@ -58,7 +60,7 @@ interface UnitsVisualGeometryBuilder<P extends UnitsParams, G extends Geometry> geometryUtils: GeometryUtils<G> } -export function UnitsVisual<G extends Geometry, P extends UnitsParams & Geometry.Params<G>>(builder: UnitsVisualGeometryBuilder<P, G>): UnitsVisual<P> { +export function UnitsVisual<G extends Geometry, P extends UnitsParams & Geometry.Params<G>>(builder: UnitsVisualGeometryBuilder<P, G>, materialId: number): UnitsVisual<P> { const { defaultProps, createGeometry, createLocationIterator, getLoci, eachLocation, setUpdateState } = builder const { createEmpty: createEmptyGeometry, updateValues, updateBoundingSphere, updateRenderableState } = builder.geometryUtils const updateState = VisualUpdateState.create() @@ -119,8 +121,12 @@ export function UnitsVisual<G extends Geometry, P extends UnitsParams & Geometry } // check if the conformation of unit.model has changed - if (Unit.conformationId(newStructureGroup.group.units[0]) !== Unit.conformationId(currentStructureGroup.group.units[0])) { + // if (Unit.conformationId(newStructureGroup.group.units[0]) !== Unit.conformationId(currentStructureGroup.group.units[0])) { + if (Unit.conformationId(newStructureGroup.group.units[0]) !== Unit.conformationId(currentStructureGroup.group.units[0]) + // TODO: this needs more attention + || newStructureGroup.group.units[0].conformation !== currentStructureGroup.group.units[0].conformation) { // console.log('new conformation') + updateState.updateTransform = true; updateState.createGeometry = true } @@ -140,7 +146,7 @@ export function UnitsVisual<G extends Geometry, P extends UnitsParams & Geometry if (updateState.createNew) { locationIt = createLocationIterator(newStructureGroup.group) if (newGeometry) { - renderObject = createUnitsRenderObject(newStructureGroup.group, newGeometry, locationIt, newTheme, newProps) + renderObject = createUnitsRenderObject(newStructureGroup.group, newGeometry, locationIt, newTheme, newProps, materialId) } else { throw new Error('expected geometry to be given') } @@ -245,6 +251,9 @@ export function UnitsVisual<G extends Geometry, P extends UnitsParams & Geometry setOverpaint(overpaint: Overpaint) { return Visual.setOverpaint(renderObject, overpaint, lociApply, true) }, + setTransparency(transparency: Transparency) { + return Visual.setTransparency(renderObject, transparency, lociApply, true) + }, destroy() { // TODO renderObject = undefined @@ -258,7 +267,7 @@ export const UnitsMeshParams = { ...StructureMeshParams, ...UnitsParams } export type UnitsMeshParams = typeof UnitsMeshParams export interface UnitsMeshVisualBuilder<P extends UnitsMeshParams> extends UnitsVisualBuilder<P, Mesh> { } -export function UnitsMeshVisual<P extends UnitsMeshParams>(builder: UnitsMeshVisualBuilder<P>): UnitsVisual<P> { +export function UnitsMeshVisual<P extends UnitsMeshParams>(builder: UnitsMeshVisualBuilder<P>, materialId: number): UnitsVisual<P> { return UnitsVisual<Mesh, StructureMeshParams & UnitsParams>({ ...builder, setUpdateState: (state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme) => { @@ -266,7 +275,7 @@ export function UnitsMeshVisual<P extends UnitsMeshParams>(builder: UnitsMeshVis if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.createGeometry = true }, geometryUtils: Mesh.Utils - }) + }, materialId) } // spheres @@ -275,7 +284,7 @@ export const UnitsSpheresParams = { ...StructureSpheresParams, ...UnitsParams } export type UnitsSpheresParams = typeof UnitsSpheresParams export interface UnitsSpheresVisualBuilder<P extends UnitsSpheresParams> extends UnitsVisualBuilder<P, Spheres> { } -export function UnitsSpheresVisual<P extends UnitsSpheresParams>(builder: UnitsSpheresVisualBuilder<P>): UnitsVisual<P> { +export function UnitsSpheresVisual<P extends UnitsSpheresParams>(builder: UnitsSpheresVisualBuilder<P>, materialId: number): UnitsVisual<P> { return UnitsVisual<Spheres, StructureSpheresParams & UnitsParams>({ ...builder, setUpdateState: (state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme) => { @@ -283,7 +292,7 @@ export function UnitsSpheresVisual<P extends UnitsSpheresParams>(builder: UnitsS if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.updateSize = true }, geometryUtils: Spheres.Utils - }) + }, materialId) } // points @@ -292,7 +301,7 @@ export const UnitsPointsParams = { ...StructurePointsParams, ...UnitsParams } export type UnitsPointsParams = typeof UnitsPointsParams export interface UnitsPointVisualBuilder<P extends UnitsPointsParams> extends UnitsVisualBuilder<P, Points> { } -export function UnitsPointsVisual<P extends UnitsPointsParams>(builder: UnitsPointVisualBuilder<P>): UnitsVisual<P> { +export function UnitsPointsVisual<P extends UnitsPointsParams>(builder: UnitsPointVisualBuilder<P>, materialId: number): UnitsVisual<P> { return UnitsVisual<Points, StructurePointsParams & UnitsParams>({ ...builder, setUpdateState: (state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme) => { @@ -300,7 +309,7 @@ export function UnitsPointsVisual<P extends UnitsPointsParams>(builder: UnitsPoi if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.updateSize = true }, geometryUtils: Points.Utils - }) + }, materialId) } // lines @@ -309,7 +318,7 @@ export const UnitsLinesParams = { ...StructureLinesParams, ...UnitsParams } export type UnitsLinesParams = typeof UnitsLinesParams export interface UnitsLinesVisualBuilder<P extends UnitsLinesParams> extends UnitsVisualBuilder<P, Lines> { } -export function UnitsLinesVisual<P extends UnitsLinesParams>(builder: UnitsLinesVisualBuilder<P>): UnitsVisual<P> { +export function UnitsLinesVisual<P extends UnitsLinesParams>(builder: UnitsLinesVisualBuilder<P>, materialId: number): UnitsVisual<P> { return UnitsVisual<Lines, StructureLinesParams & UnitsParams>({ ...builder, setUpdateState: (state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme) => { @@ -317,16 +326,16 @@ export function UnitsLinesVisual<P extends UnitsLinesParams>(builder: UnitsLines if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.updateSize = true }, geometryUtils: Lines.Utils - }) + }, materialId) } // direct-volume export const UnitsDirectVolumeParams = { ...StructureDirectVolumeParams, ...UnitsParams } export type UnitsDirectVolumeParams = typeof UnitsDirectVolumeParams -export interface UnitsDirectVolumeVisualBuilder<P extends UnitsDirectVolumeParams> extends UnitsVisualGeometryBuilder<P, DirectVolume> { } +export interface UnitsDirectVolumeVisualBuilder<P extends UnitsDirectVolumeParams> extends UnitsVisualBuilder<P, DirectVolume> { } -export function UnitsDirectVolumeVisual<P extends UnitsDirectVolumeParams>(builder: UnitsDirectVolumeVisualBuilder<P>): UnitsVisual<P> { +export function UnitsDirectVolumeVisual<P extends UnitsDirectVolumeParams>(builder: UnitsDirectVolumeVisualBuilder<P>, materialId: number): UnitsVisual<P> { return UnitsVisual<DirectVolume, StructureDirectVolumeParams & UnitsParams>({ ...builder, setUpdateState: (state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme) => { @@ -334,5 +343,22 @@ export function UnitsDirectVolumeVisual<P extends UnitsDirectVolumeParams>(build if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.createGeometry = true }, geometryUtils: DirectVolume.Utils - }) + }, materialId) +} + +// texture-mesh + +export const UnitsTextureMeshParams = { ...StructureTextureMeshParams, ...UnitsParams } +export type UnitsTextureMeshParams = typeof UnitsTextureMeshParams +export interface UnitsTextureMeshVisualBuilder<P extends UnitsTextureMeshParams> extends UnitsVisualBuilder<P, TextureMesh> { } + +export function UnitsTextureMeshVisual<P extends UnitsTextureMeshParams>(builder: UnitsTextureMeshVisualBuilder<P>, materialId: number): UnitsVisual<P> { + return UnitsVisual<TextureMesh, StructureTextureMeshParams & UnitsParams>({ + ...builder, + setUpdateState: (state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme) => { + builder.setUpdateState(state, newProps, currentProps, newTheme, currentTheme) + if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.createGeometry = true + }, + geometryUtils: TextureMesh.Utils + }, materialId) } \ No newline at end of file diff --git a/src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts b/src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts index c699d7464aecdc28c8c48e764db74c516e48d872..73cd6fe643016d4532dae2938660a55d23956009 100644 --- a/src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts +++ b/src/mol-repr/structure/visual/carbohydrate-link-cylinder.ts @@ -55,7 +55,7 @@ export const CarbohydrateLinkParams = { } export type CarbohydrateLinkParams = typeof CarbohydrateLinkParams -export function CarbohydrateLinkVisual(): ComplexVisual<CarbohydrateLinkParams> { +export function CarbohydrateLinkVisual(materialId: number): ComplexVisual<CarbohydrateLinkParams> { return ComplexMeshVisual<CarbohydrateLinkParams>({ defaultProps: PD.getDefaultValues(CarbohydrateLinkParams), createGeometry: createCarbohydrateLinkCylinderMesh, @@ -68,7 +68,7 @@ export function CarbohydrateLinkVisual(): ComplexVisual<CarbohydrateLinkParams> newProps.radialSegments !== currentProps.radialSegments ) } - }) + }, materialId) } function CarbohydrateLinkIterator(structure: Structure): LocationIterator { diff --git a/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts b/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts index bc4e4ad6f71267d9e7599562be026c6324863535..c1c2b046f1d908e78c76dd2a27e102733a026ecd 100644 --- a/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts +++ b/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts @@ -148,7 +148,7 @@ export const CarbohydrateSymbolParams = { } export type CarbohydrateSymbolParams = typeof CarbohydrateSymbolParams -export function CarbohydrateSymbolVisual(): ComplexVisual<CarbohydrateSymbolParams> { +export function CarbohydrateSymbolVisual(materialId: number): ComplexVisual<CarbohydrateSymbolParams> { return ComplexMeshVisual<CarbohydrateSymbolParams>({ defaultProps: PD.getDefaultValues(CarbohydrateSymbolParams), createGeometry: createCarbohydrateSymbolMesh, @@ -161,7 +161,7 @@ export function CarbohydrateSymbolVisual(): ComplexVisual<CarbohydrateSymbolPara newProps.detail !== currentProps.detail ) } - }) + }, materialId) } function CarbohydrateElementIterator(structure: Structure): LocationIterator { diff --git a/src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts b/src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts index d78b03bd16a475cbfc7aa617f9d77a27104ef1d9..7a3d4e35a5274199d73aa36877da4172a214b866 100644 --- a/src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts +++ b/src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts @@ -65,7 +65,7 @@ export const CarbohydrateTerminalLinkParams = { } export type CarbohydrateTerminalLinkParams = typeof CarbohydrateTerminalLinkParams -export function CarbohydrateTerminalLinkVisual(): ComplexVisual<CarbohydrateTerminalLinkParams> { +export function CarbohydrateTerminalLinkVisual(materialId: number): ComplexVisual<CarbohydrateTerminalLinkParams> { return ComplexMeshVisual<CarbohydrateTerminalLinkParams>({ defaultProps: PD.getDefaultValues(CarbohydrateTerminalLinkParams), createGeometry: createCarbohydrateTerminalLinkCylinderMesh, @@ -78,7 +78,7 @@ export function CarbohydrateTerminalLinkVisual(): ComplexVisual<CarbohydrateTerm newProps.radialSegments !== currentProps.radialSegments ) } - }) + }, materialId) } function CarbohydrateTerminalLinkIterator(structure: Structure): LocationIterator { diff --git a/src/mol-repr/structure/visual/cross-link-restraint-cylinder.ts b/src/mol-repr/structure/visual/cross-link-restraint-cylinder.ts index 26de0839cfa9744b7ecd4e5e980ec98afc4da141..ccfb6c46ec9e170f9f4867156d1e017e3b7416aa 100644 --- a/src/mol-repr/structure/visual/cross-link-restraint-cylinder.ts +++ b/src/mol-repr/structure/visual/cross-link-restraint-cylinder.ts @@ -58,7 +58,7 @@ export const CrossLinkRestraintParams = { } export type CrossLinkRestraintParams = typeof CrossLinkRestraintParams -export function CrossLinkRestraintVisual(): ComplexVisual<CrossLinkRestraintParams> { +export function CrossLinkRestraintVisual(materialId: number): ComplexVisual<CrossLinkRestraintParams> { return ComplexMeshVisual<CrossLinkRestraintParams>({ defaultProps: PD.getDefaultValues(CrossLinkRestraintParams), createGeometry: createCrossLinkRestraintCylinderMesh, @@ -71,7 +71,7 @@ export function CrossLinkRestraintVisual(): ComplexVisual<CrossLinkRestraintPara newProps.radialSegments !== currentProps.radialSegments ) } - }) + }, materialId) } function CrossLinkRestraintIterator(structure: Structure): LocationIterator { diff --git a/src/mol-repr/structure/visual/element-point.ts b/src/mol-repr/structure/visual/element-point.ts index 64db953cbb4402571c10e3db99a2cf44d8109da8..5292dfa8fb9fd51f34b0cab92788c401fd0a778a 100644 --- a/src/mol-repr/structure/visual/element-point.ts +++ b/src/mol-repr/structure/visual/element-point.ts @@ -42,7 +42,7 @@ export function createElementPoint(ctx: VisualContext, unit: Unit, structure: St return builder.getPoints() } -export function ElementPointVisual(): UnitsVisual<ElementPointParams> { +export function ElementPointVisual(materialId: number): UnitsVisual<ElementPointParams> { return UnitsPointsVisual<ElementPointParams>({ defaultProps: PD.getDefaultValues(ElementPointParams), createGeometry: createElementPoint, @@ -52,5 +52,5 @@ export function ElementPointVisual(): UnitsVisual<ElementPointParams> { setUpdateState: (state: VisualUpdateState, newProps: PD.Values<ElementPointParams>, currentProps: PD.Values<ElementPointParams>) => { } - }) + }, materialId) } \ No newline at end of file diff --git a/src/mol-repr/structure/visual/element-sphere.ts b/src/mol-repr/structure/visual/element-sphere.ts index afbe90fbb144fe2a3dad2aff1817781fa19df137..6bff3a354f331036e868e6112026cc0b0cd4b616 100644 --- a/src/mol-repr/structure/visual/element-sphere.ts +++ b/src/mol-repr/structure/visual/element-sphere.ts @@ -24,7 +24,7 @@ export function getElementSphereVisual(webgl?: WebGLContext) { return webgl && webgl.extensions.fragDepth ? ElementSphereImpostorVisual : ElementSphereMeshVisual } -export function ElementSphereImpostorVisual(): UnitsVisual<ElementSphereParams> { +export function ElementSphereImpostorVisual(materialId: number): UnitsVisual<ElementSphereParams> { return UnitsSpheresVisual<ElementSphereParams>({ defaultProps: PD.getDefaultValues(ElementSphereParams), createGeometry: createElementSphereImpostor, @@ -34,10 +34,10 @@ export function ElementSphereImpostorVisual(): UnitsVisual<ElementSphereParams> setUpdateState: (state: VisualUpdateState, newProps: PD.Values<ElementSphereParams>, currentProps: PD.Values<ElementSphereParams>) => { } - }) + }, materialId) } -export function ElementSphereMeshVisual(): UnitsVisual<ElementSphereParams> { +export function ElementSphereMeshVisual(materialId: number): UnitsVisual<ElementSphereParams> { return UnitsMeshVisual<ElementSphereParams>({ defaultProps: PD.getDefaultValues(ElementSphereParams), createGeometry: createElementSphereMesh, @@ -50,5 +50,5 @@ export function ElementSphereMeshVisual(): UnitsVisual<ElementSphereParams> { newProps.detail !== currentProps.detail ) } - }) + }, materialId) } \ No newline at end of file diff --git a/src/mol-repr/structure/visual/gaussian-density-point.ts b/src/mol-repr/structure/visual/gaussian-density-point.ts deleted file mode 100644 index beef6aae62dbbf963c6d2b9460309e24664d48d4..0000000000000000000000000000000000000000 --- a/src/mol-repr/structure/visual/gaussian-density-point.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - */ - -import { Unit, Structure } from 'mol-model/structure'; -import { UnitsVisual } from '../representation'; -import { VisualUpdateState } from '../../util'; -import { StructureElementIterator } from './util/element'; -import { EmptyLoci } from 'mol-model/loci'; -import { Vec3 } from 'mol-math/linear-algebra'; -import { UnitsPointsVisual, UnitsPointsParams } from '../units-visual'; -import { ParamDefinition as PD } from 'mol-util/param-definition'; -import { Points } from 'mol-geo/geometry/points/points'; -import { PointsBuilder } from 'mol-geo/geometry/points/points-builder'; -import { VisualContext } from 'mol-repr/visual'; -import { Theme } from 'mol-theme/theme'; -import { computeUnitGaussianDensity, GaussianDensityParams, GaussianDensityProps } from './util/gaussian'; - -export const GaussianDensityPointParams = { - ...UnitsPointsParams, - ...GaussianDensityParams, - pointSizeAttenuation: PD.Boolean(false), -} -export type GaussianDensityPointParams = typeof GaussianDensityPointParams - -export async function createGaussianDensityPoint(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: GaussianDensityProps, points?: Points) { - const { transform, field: { space, data } } = await computeUnitGaussianDensity(unit, props, ctx.webgl).runInContext(ctx.runtime) - - const { dimensions, get } = space - const [ xn, yn, zn ] = dimensions - - const n = xn * yn * zn * 3 - const builder = PointsBuilder.create(n, n / 10, points) - - const p = Vec3.zero() - let i = 0 - - for (let x = 0; x < xn; ++x) { - for (let y = 0; y < yn; ++y) { - for (let z = 0; z < zn; ++z) { - if (get(data, x, y, z) > 0.001) { - Vec3.set(p, x, y, z) - Vec3.transformMat4(p, p, transform) - builder.add(p[0], p[1], p[2], i) - } - if (i % 100000 === 0 && ctx.runtime.shouldUpdate) { - await ctx.runtime.update({ message: 'Creating density points', current: i, max: n }); - } - ++i - } - } - } - return builder.getPoints() -} - -export function GaussianDensityPointVisual(): UnitsVisual<GaussianDensityPointParams> { - return UnitsPointsVisual<GaussianDensityPointParams>({ - defaultProps: PD.getDefaultValues(GaussianDensityPointParams), - createGeometry: createGaussianDensityPoint, - createLocationIterator: StructureElementIterator.fromGroup, - getLoci: () => EmptyLoci, - eachLocation: () => false, - setUpdateState: (state: VisualUpdateState, newProps: PD.Values<GaussianDensityPointParams>, currentProps: PD.Values<GaussianDensityPointParams>) => { - if (newProps.resolution !== currentProps.resolution) state.createGeometry = true - if (newProps.radiusOffset !== currentProps.radiusOffset) state.createGeometry = true - if (newProps.smoothness !== currentProps.smoothness) state.createGeometry = true - if (newProps.useGpu !== currentProps.useGpu) state.createGeometry = true - } - }) -} \ No newline at end of file diff --git a/src/mol-repr/structure/visual/gaussian-density-volume.ts b/src/mol-repr/structure/visual/gaussian-density-volume.ts index 7aac5a7b9aafbbe775f4769c0051f77c36e00152..40e2592a13516bdb707327dbb59381f7ae21b3bb 100644 --- a/src/mol-repr/structure/visual/gaussian-density-volume.ts +++ b/src/mol-repr/structure/visual/gaussian-density-volume.ts @@ -23,9 +23,9 @@ async function createGaussianDensityVolume(ctx: VisualContext, structure: Struct const p = { ...props, useGpu: true } const oldTexture = directVolume ? directVolume.gridTexture.ref.value : undefined const densityTextureData = await computeStructureGaussianDensityTexture(structure, p, webgl, oldTexture).runInContext(runtime) - const { transform, texture, bbox, gridDimension } = densityTextureData + const { transform, texture, bbox, gridDim } = densityTextureData - return DirectVolume.create(bbox, gridDimension, transform, texture, directVolume) + return DirectVolume.create(bbox, gridDim, transform, texture, directVolume) } export const GaussianDensityVolumeParams = { @@ -34,7 +34,7 @@ export const GaussianDensityVolumeParams = { } export type GaussianDensityVolumeParams = typeof GaussianDensityVolumeParams -export function GaussianDensityVolumeVisual(): ComplexVisual<GaussianDensityVolumeParams> { +export function GaussianDensityVolumeVisual(materialId: number): ComplexVisual<GaussianDensityVolumeParams> { return ComplexDirectVolumeVisual<GaussianDensityVolumeParams>({ defaultProps: PD.getDefaultValues(GaussianDensityVolumeParams), createGeometry: createGaussianDensityVolume, @@ -49,5 +49,5 @@ export function GaussianDensityVolumeVisual(): ComplexVisual<GaussianDensityVolu newProps.isoValueNorm = Math.exp(-newProps.smoothness) } } - }) + }, materialId) } \ No newline at end of file diff --git a/src/mol-repr/structure/visual/gaussian-surface-mesh.ts b/src/mol-repr/structure/visual/gaussian-surface-mesh.ts index 166252a81a076d7e1ccd496890c2c63496264b8b..ae26f57dd3fdb3fbada4a32de7d1b48f8778c371 100644 --- a/src/mol-repr/structure/visual/gaussian-surface-mesh.ts +++ b/src/mol-repr/structure/visual/gaussian-surface-mesh.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> */ @@ -7,14 +7,33 @@ import { Unit, Structure } from 'mol-model/structure'; import { UnitsVisual } from '../representation'; import { VisualUpdateState } from '../../util'; -import { UnitsMeshVisual, UnitsMeshParams } from '../units-visual'; +import { UnitsMeshVisual, UnitsMeshParams, UnitsTextureMeshParams, UnitsTextureMeshVisual } from '../units-visual'; import { StructureElementIterator, getElementLoci, eachElement } from './util/element'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import { Mesh } from 'mol-geo/geometry/mesh/mesh'; import { computeMarchingCubesMesh } from 'mol-geo/util/marching-cubes/algorithm'; import { VisualContext } from 'mol-repr/visual'; import { Theme } from 'mol-theme/theme'; -import { GaussianDensityProps, computeUnitGaussianDensity, GaussianDensityParams } from './util/gaussian'; +import { GaussianDensityProps, computeUnitGaussianDensity, GaussianDensityParams, computeUnitGaussianDensityTexture2d, GaussianDensityTextureProps } from './util/gaussian'; +import { WebGLContext } from 'mol-gl/webgl/context'; +import { TextureMesh } from 'mol-geo/geometry/texture-mesh/texture-mesh'; +import { calcActiveVoxels } from 'mol-gl/compute/marching-cubes/active-voxels'; +import { createHistogramPyramid } from 'mol-gl/compute/histogram-pyramid/reduction'; +import { createIsosurfaceBuffers } from 'mol-gl/compute/marching-cubes/isosurface'; +import { Sphere3D } from 'mol-math/geometry'; + +export const GaussianSurfaceMeshParams = { + ...UnitsMeshParams, + ...UnitsTextureMeshParams, + ...GaussianDensityParams, +} +export type GaussianSurfaceMeshParams = typeof GaussianSurfaceMeshParams + +export function getGaussianSurfaceVisual(webgl?: WebGLContext) { + return webgl && webgl.extensions.drawBuffers ? GaussianSurfaceTextureMeshVisual : GaussianSurfaceMeshVisual +} + +// async function createGaussianSurfaceMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: GaussianDensityProps, mesh?: Mesh): Promise<Mesh> { const { smoothness } = props @@ -28,30 +47,67 @@ async function createGaussianSurfaceMesh(ctx: VisualContext, unit: Unit, structu const surface = await computeMarchingCubesMesh(params, mesh).runAsChild(ctx.runtime) Mesh.transformImmediate(surface, transform) - Mesh.computeNormalsImmediate(surface) Mesh.uniformTriangleGroup(surface) - return surface; + return surface } -export const GaussianSurfaceParams = { - ...UnitsMeshParams, - ...GaussianDensityParams, -} -export type GaussianSurfaceParams = typeof GaussianSurfaceParams - -export function GaussianSurfaceVisual(): UnitsVisual<GaussianSurfaceParams> { - return UnitsMeshVisual<GaussianSurfaceParams>({ - defaultProps: PD.getDefaultValues(GaussianSurfaceParams), +export function GaussianSurfaceMeshVisual(materialId: number): UnitsVisual<GaussianSurfaceMeshParams> { + return UnitsMeshVisual<GaussianSurfaceMeshParams>({ + defaultProps: PD.getDefaultValues(GaussianSurfaceMeshParams), createGeometry: createGaussianSurfaceMesh, createLocationIterator: StructureElementIterator.fromGroup, getLoci: getElementLoci, eachLocation: eachElement, - setUpdateState: (state: VisualUpdateState, newProps: PD.Values<GaussianSurfaceParams>, currentProps: PD.Values<GaussianSurfaceParams>) => { + setUpdateState: (state: VisualUpdateState, newProps: PD.Values<GaussianSurfaceMeshParams>, currentProps: PD.Values<GaussianSurfaceMeshParams>) => { if (newProps.resolution !== currentProps.resolution) state.createGeometry = true if (newProps.radiusOffset !== currentProps.radiusOffset) state.createGeometry = true if (newProps.smoothness !== currentProps.smoothness) state.createGeometry = true if (newProps.useGpu !== currentProps.useGpu) state.createGeometry = true } - }) + }, materialId) +} + +// + +async function createGaussianSurfaceTextureMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: GaussianDensityTextureProps, textureMesh?: TextureMesh): Promise<TextureMesh> { + if (!ctx.webgl) throw new Error('webgl context required to create gaussian surface texture-mesh') + const isoLevel = Math.exp(-props.smoothness) + + const densityTextureData = await computeUnitGaussianDensityTexture2d(unit, props, ctx.webgl).runInContext(ctx.runtime) + // console.log(densityTextureData) + // console.log('vertexGroupTexture', readTexture(ctx.webgl, densityTextureData.texture)) + // ctx.webgl.waitForGpuCommandsCompleteSync() + + const activeVoxelsTex = calcActiveVoxels(ctx.webgl, densityTextureData.texture, densityTextureData.gridDim, densityTextureData.gridTexDim, isoLevel, densityTextureData.gridTexScale) + // ctx.webgl.waitForGpuCommandsCompleteSync() + + const compacted = createHistogramPyramid(ctx.webgl, activeVoxelsTex, densityTextureData.gridTexScale) + // ctx.webgl.waitForGpuCommandsCompleteSync() + + const gv = createIsosurfaceBuffers(ctx.webgl, activeVoxelsTex, densityTextureData.texture, compacted, densityTextureData.gridDim, densityTextureData.gridTexDim, densityTextureData.transform, isoLevel, textureMesh ? textureMesh.vertexGroupTexture.ref.value : undefined, textureMesh ? textureMesh.normalTexture.ref.value : undefined) + // ctx.webgl.waitForGpuCommandsCompleteSync() + + // const boundingSphere = Sphere3D.zero() + // Sphere3D.addVec3(boundingSphere, boundingSphere, densityTextureData.gridDimension) + const boundingSphere = Sphere3D.fromBox3D(Sphere3D(), densityTextureData.bbox) + const surface = TextureMesh.create(gv.vertexCount, 1, gv.vertexGroupTexture, gv.normalTexture, boundingSphere, textureMesh) + + // ctx.webgl.waitForGpuCommandsCompleteSync() + return surface +} + +export function GaussianSurfaceTextureMeshVisual(materialId: number): UnitsVisual<GaussianSurfaceMeshParams> { + return UnitsTextureMeshVisual<GaussianSurfaceMeshParams>({ + defaultProps: PD.getDefaultValues(GaussianSurfaceMeshParams), + createGeometry: createGaussianSurfaceTextureMesh, + createLocationIterator: StructureElementIterator.fromGroup, + getLoci: getElementLoci, + eachLocation: eachElement, + setUpdateState: (state: VisualUpdateState, newProps: PD.Values<GaussianSurfaceMeshParams>, currentProps: PD.Values<GaussianSurfaceMeshParams>) => { + if (newProps.resolution !== currentProps.resolution) state.createGeometry = true + if (newProps.radiusOffset !== currentProps.radiusOffset) state.createGeometry = true + if (newProps.smoothness !== currentProps.smoothness) state.createGeometry = true + } + }, materialId) } \ No newline at end of file diff --git a/src/mol-repr/structure/visual/gaussian-surface-wireframe.ts b/src/mol-repr/structure/visual/gaussian-surface-wireframe.ts index 5efe3f47bf6395431987a0717b27e6f7d4558b38..834b6fd41612761c1665a620955f1f224d038bca 100644 --- a/src/mol-repr/structure/visual/gaussian-surface-wireframe.ts +++ b/src/mol-repr/structure/visual/gaussian-surface-wireframe.ts @@ -39,7 +39,7 @@ export const GaussianWireframeParams = { } export type GaussianWireframeParams = typeof GaussianWireframeParams -export function GaussianWireframeVisual(): UnitsVisual<GaussianWireframeParams> { +export function GaussianWireframeVisual(materialId: number): UnitsVisual<GaussianWireframeParams> { return UnitsLinesVisual<GaussianWireframeParams>({ defaultProps: PD.getDefaultValues(GaussianWireframeParams), createGeometry: createGaussianWireframe, @@ -52,5 +52,5 @@ export function GaussianWireframeVisual(): UnitsVisual<GaussianWireframeParams> if (newProps.smoothness !== currentProps.smoothness) state.createGeometry = true if (newProps.useGpu !== currentProps.useGpu) state.createGeometry = true } - }) + }, materialId) } \ No newline at end of file diff --git a/src/mol-repr/structure/visual/inter-unit-link-cylinder.ts b/src/mol-repr/structure/visual/inter-unit-link-cylinder.ts index fa55745425b88e8b15149d9d2e83ee4af92e5377..90999a7f28ddfe8e4ff180e5f234ef69b30ee866 100644 --- a/src/mol-repr/structure/visual/inter-unit-link-cylinder.ts +++ b/src/mol-repr/structure/visual/inter-unit-link-cylinder.ts @@ -58,7 +58,7 @@ export const InterUnitLinkParams = { } export type InterUnitLinkParams = typeof InterUnitLinkParams -export function InterUnitLinkVisual(): ComplexVisual<InterUnitLinkParams> { +export function InterUnitLinkVisual(materialId: number): ComplexVisual<InterUnitLinkParams> { return ComplexMeshVisual<InterUnitLinkParams>({ defaultProps: PD.getDefaultValues(InterUnitLinkParams), createGeometry: createInterUnitLinkCylinderMesh, @@ -74,7 +74,7 @@ export function InterUnitLinkVisual(): ComplexVisual<InterUnitLinkParams> { newProps.linkSpacing !== currentProps.linkSpacing ) } - }) + }, materialId) } function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) { diff --git a/src/mol-repr/structure/visual/intra-unit-link-cylinder.ts b/src/mol-repr/structure/visual/intra-unit-link-cylinder.ts index 9f86c6700696e789bcfd4b4b4895b981facc26bc..7cbe60c189d4455ae3e134abbd682a02a1dd3adc 100644 --- a/src/mol-repr/structure/visual/intra-unit-link-cylinder.ts +++ b/src/mol-repr/structure/visual/intra-unit-link-cylinder.ts @@ -78,7 +78,7 @@ export const IntraUnitLinkParams = { } export type IntraUnitLinkParams = typeof IntraUnitLinkParams -export function IntraUnitLinkVisual(): UnitsVisual<IntraUnitLinkParams> { +export function IntraUnitLinkVisual(materialId: number): UnitsVisual<IntraUnitLinkParams> { return UnitsMeshVisual<IntraUnitLinkParams>({ defaultProps: PD.getDefaultValues(IntraUnitLinkParams), createGeometry: createIntraUnitLinkCylinderMesh, @@ -94,7 +94,7 @@ export function IntraUnitLinkVisual(): UnitsVisual<IntraUnitLinkParams> { newProps.linkSpacing !== currentProps.linkSpacing ) } - }) + }, materialId) } function getLinkLoci(pickingId: PickingId, structureGroup: StructureGroup, id: number) { diff --git a/src/mol-repr/structure/visual/molecular-surface-mesh.ts b/src/mol-repr/structure/visual/molecular-surface-mesh.ts new file mode 100644 index 0000000000000000000000000000000000000000..aea6b6de911d54e3b67996e1cb028cf1c9881db8 --- /dev/null +++ b/src/mol-repr/structure/visual/molecular-surface-mesh.ts @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { Unit, Structure } from 'mol-model/structure'; +import { UnitsVisual } from '../representation'; +import { VisualUpdateState } from '../../util'; +import { UnitsMeshVisual, UnitsMeshParams, UnitsTextureMeshParams } from '../units-visual'; +import { StructureElementIterator, getElementLoci, eachElement } from './util/element'; +import { ParamDefinition as PD } from 'mol-util/param-definition'; +import { Mesh } from 'mol-geo/geometry/mesh/mesh'; +import { computeMarchingCubesMesh } from 'mol-geo/util/marching-cubes/algorithm'; +import { VisualContext } from 'mol-repr/visual'; +import { Theme } from 'mol-theme/theme'; +import { computeUnitMolecularSurface } from './util/molecular-surface'; +import { MolecularSurfaceCalculationParams, MolecularSurfaceCalculationProps } from 'mol-math/geometry/molecular-surface'; + +export const MolecularSurfaceMeshParams = { + ...UnitsMeshParams, + ...UnitsTextureMeshParams, + ...MolecularSurfaceCalculationParams, +} +export type MolecularSurfaceMeshParams = typeof MolecularSurfaceMeshParams + +// + +async function createMolecularSurfaceMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: MolecularSurfaceCalculationProps, mesh?: Mesh): Promise<Mesh> { + + const { transform, field, idField } = await computeUnitMolecularSurface(unit, props).runInContext(ctx.runtime) + const params = { + isoLevel: props.probeRadius, + scalarField: field, + idField + } + const surface = await computeMarchingCubesMesh(params, mesh).runAsChild(ctx.runtime) + + Mesh.transformImmediate(surface, transform) + Mesh.uniformTriangleGroup(surface) + + return surface +} + +export function MolecularSurfaceMeshVisual(materialId: number): UnitsVisual<MolecularSurfaceMeshParams> { + return UnitsMeshVisual<MolecularSurfaceMeshParams>({ + defaultProps: PD.getDefaultValues(MolecularSurfaceMeshParams), + createGeometry: createMolecularSurfaceMesh, + createLocationIterator: StructureElementIterator.fromGroup, + getLoci: getElementLoci, + eachLocation: eachElement, + setUpdateState: (state: VisualUpdateState, newProps: PD.Values<MolecularSurfaceMeshParams>, currentProps: PD.Values<MolecularSurfaceMeshParams>) => { + if (newProps.resolution !== currentProps.resolution) state.createGeometry = true + if (newProps.probeRadius !== currentProps.probeRadius) state.createGeometry = true + if (newProps.probePositions !== currentProps.probePositions) state.createGeometry = true + } + }, materialId) +} \ No newline at end of file diff --git a/src/mol-repr/structure/visual/nucleotide-block-mesh.ts b/src/mol-repr/structure/visual/nucleotide-block-mesh.ts index 78fa5aa2f974b63e88cac56235da5aa3a09a66d4..f23f1424ef64516424b4be5826a2a8f8f0df7232 100644 --- a/src/mol-repr/structure/visual/nucleotide-block-mesh.ts +++ b/src/mol-repr/structure/visual/nucleotide-block-mesh.ts @@ -128,7 +128,7 @@ export const NucleotideBlockParams = { } export type NucleotideBlockParams = typeof NucleotideBlockParams -export function NucleotideBlockVisual(): UnitsVisual<NucleotideBlockParams> { +export function NucleotideBlockVisual(materialId: number): UnitsVisual<NucleotideBlockParams> { return UnitsMeshVisual<NucleotideBlockParams>({ defaultProps: PD.getDefaultValues(NucleotideBlockParams), createGeometry: createNucleotideBlockMesh, @@ -141,5 +141,5 @@ export function NucleotideBlockVisual(): UnitsVisual<NucleotideBlockParams> { newProps.radialSegments !== currentProps.radialSegments ) } - }) + }, materialId) } \ No newline at end of file diff --git a/src/mol-repr/structure/visual/nucleotide-ring-mesh.ts b/src/mol-repr/structure/visual/nucleotide-ring-mesh.ts index 6baef02237f105ec1392ceb42465fa160525e571..d3b246d5c68bea5bd3751d12a255f4e225a2616d 100644 --- a/src/mol-repr/structure/visual/nucleotide-ring-mesh.ts +++ b/src/mol-repr/structure/visual/nucleotide-ring-mesh.ts @@ -175,7 +175,7 @@ export const NucleotideRingParams = { } export type NucleotideRingParams = typeof NucleotideRingParams -export function NucleotideRingVisual(): UnitsVisual<NucleotideRingParams> { +export function NucleotideRingVisual(materialId: number): UnitsVisual<NucleotideRingParams> { return UnitsMeshVisual<NucleotideRingParams>({ defaultProps: PD.getDefaultValues(NucleotideRingParams), createGeometry: createNucleotideRingMesh, @@ -188,5 +188,5 @@ export function NucleotideRingVisual(): UnitsVisual<NucleotideRingParams> { newProps.radialSegments !== currentProps.radialSegments ) } - }) + }, materialId) } \ No newline at end of file diff --git a/src/mol-repr/structure/visual/polymer-backbone-cylinder.ts b/src/mol-repr/structure/visual/polymer-backbone-cylinder.ts index 8584245222c4ebd57889f01f13c254ab3790bf40..ce98d3d40174b412a73abff94212c3789c9c9634 100644 --- a/src/mol-repr/structure/visual/polymer-backbone-cylinder.ts +++ b/src/mol-repr/structure/visual/polymer-backbone-cylinder.ts @@ -67,7 +67,7 @@ export const PolymerBackboneParams = { } export type PolymerBackboneParams = typeof PolymerBackboneParams -export function PolymerBackboneVisual(): UnitsVisual<PolymerBackboneParams> { +export function PolymerBackboneVisual(materialId: number): UnitsVisual<PolymerBackboneParams> { return UnitsMeshVisual<PolymerBackboneParams>({ defaultProps: PD.getDefaultValues(PolymerBackboneParams), createGeometry: createPolymerBackboneCylinderMesh, @@ -81,5 +81,5 @@ export function PolymerBackboneVisual(): UnitsVisual<PolymerBackboneParams> { newProps.radialSegments !== currentProps.radialSegments ) } - }) + }, materialId) } \ No newline at end of file diff --git a/src/mol-repr/structure/visual/polymer-direction-wedge.ts b/src/mol-repr/structure/visual/polymer-direction-wedge.ts index ab3e037548ced5b766f2c8b630fd14b14697310d..43712a89d45b50cd00ffd9e810b4d11220c31d45 100644 --- a/src/mol-repr/structure/visual/polymer-direction-wedge.ts +++ b/src/mol-repr/structure/visual/polymer-direction-wedge.ts @@ -92,7 +92,7 @@ export const PolymerDirectionParams = { } export type PolymerDirectionParams = typeof PolymerDirectionParams -export function PolymerDirectionVisual(): UnitsVisual<PolymerDirectionParams> { +export function PolymerDirectionVisual(materialId: number): UnitsVisual<PolymerDirectionParams> { return UnitsMeshVisual<PolymerDirectionParams>({ defaultProps: PD.getDefaultValues(PolymerDirectionParams), createGeometry: createPolymerDirectionWedgeMesh, @@ -104,5 +104,5 @@ export function PolymerDirectionVisual(): UnitsVisual<PolymerDirectionParams> { newProps.sizeFactor !== currentProps.sizeFactor ) } - }) + }, materialId) } \ No newline at end of file diff --git a/src/mol-repr/structure/visual/polymer-gap-cylinder.ts b/src/mol-repr/structure/visual/polymer-gap-cylinder.ts index dc80fba747b4d4de619b47257899b0efc3ec3a14..28dddc9f1eefcede5512fae57dc399d14de9d660 100644 --- a/src/mol-repr/structure/visual/polymer-gap-cylinder.ts +++ b/src/mol-repr/structure/visual/polymer-gap-cylinder.ts @@ -85,7 +85,7 @@ export const PolymerGapParams = { } export type PolymerGapParams = typeof PolymerGapParams -export function PolymerGapVisual(): UnitsVisual<PolymerGapParams> { +export function PolymerGapVisual(materialId: number): UnitsVisual<PolymerGapParams> { return UnitsMeshVisual<PolymerGapParams>({ defaultProps: PD.getDefaultValues(PolymerGapParams), createGeometry: createPolymerGapCylinderMesh, @@ -98,5 +98,5 @@ export function PolymerGapVisual(): UnitsVisual<PolymerGapParams> { newProps.radialSegments !== currentProps.radialSegments ) } - }) + }, materialId) } \ No newline at end of file diff --git a/src/mol-repr/structure/visual/polymer-trace-mesh.ts b/src/mol-repr/structure/visual/polymer-trace-mesh.ts index dfbd3b6a363862434493f0977d83a5a362ab0b09..9fafb82b81d350f00d38afd0c6a80304d3608d19 100644 --- a/src/mol-repr/structure/visual/polymer-trace-mesh.ts +++ b/src/mol-repr/structure/visual/polymer-trace-mesh.ts @@ -104,7 +104,7 @@ export const PolymerTraceParams = { } export type PolymerTraceParams = typeof PolymerTraceParams -export function PolymerTraceVisual(): UnitsVisual<PolymerTraceParams> { +export function PolymerTraceVisual(materialId: number): UnitsVisual<PolymerTraceParams> { return UnitsMeshVisual<PolymerTraceParams>({ defaultProps: PD.getDefaultValues(PolymerTraceParams), createGeometry: createPolymerTraceMesh, @@ -120,5 +120,5 @@ export function PolymerTraceVisual(): UnitsVisual<PolymerTraceParams> { newProps.arrowFactor !== currentProps.arrowFactor ) } - }) + }, materialId) } \ No newline at end of file diff --git a/src/mol-repr/structure/visual/polymer-tube-mesh.ts b/src/mol-repr/structure/visual/polymer-tube-mesh.ts index 94accbff631fddd68ac6de7a8b8640d53f64a7d2..8f433bf01a8c4ba16704f1e13601db57148fd33e 100644 --- a/src/mol-repr/structure/visual/polymer-tube-mesh.ts +++ b/src/mol-repr/structure/visual/polymer-tube-mesh.ts @@ -70,7 +70,7 @@ export const PolymerTubeParams = { } export type PolymerTubeParams = typeof PolymerTubeParams -export function PolymerTubeVisual(): UnitsVisual<PolymerTubeParams> { +export function PolymerTubeVisual(materialId: number): UnitsVisual<PolymerTubeParams> { return UnitsMeshVisual<PolymerTubeParams>({ defaultProps: PD.getDefaultValues(PolymerTubeParams), createGeometry: createPolymerTubeMesh, @@ -84,5 +84,5 @@ export function PolymerTubeVisual(): UnitsVisual<PolymerTubeParams> { newProps.radialSegments !== currentProps.radialSegments ) } - }) + }, materialId) } \ No newline at end of file diff --git a/src/mol-repr/structure/visual/util/common.ts b/src/mol-repr/structure/visual/util/common.ts index 1e2f479d22e546a668aab603854caf7841db563f..f2f8faa6da167c06383549f84056fc1af8ee163a 100644 --- a/src/mol-repr/structure/visual/util/common.ts +++ b/src/mol-repr/structure/visual/util/common.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> */ @@ -9,9 +9,10 @@ import { Mat4 } from 'mol-math/linear-algebra'; import { TransformData, createTransform } from 'mol-geo/geometry/transform-data'; import { OrderedSet, SortedArray } from 'mol-data/int'; import { EmptyLoci, Loci } from 'mol-model/loci'; +import { PhysicalSizeTheme } from 'mol-theme/size/physical'; /** Return a Loci for the elements of a whole residue the elementIndex belongs to. */ -export function getResidueLoci(structure: Structure, unit: Unit, elementIndex: ElementIndex): Loci { +export function getResidueLoci(structure: Structure, unit: Unit.Atomic, elementIndex: ElementIndex): Loci { const { elements, model } = unit if (OrderedSet.indexOf(elements, elementIndex) !== -1) { const { index, offsets } = model.atomicHierarchy.residueAtomSegments @@ -55,4 +56,68 @@ export function includesUnitKind(unitKinds: UnitKind[], unit: Unit) { if (Unit.isGaussians(unit) && unitKinds[i] === 'gaussians') return true } return false +} + +// + +export function getConformation(unit: Unit) { + switch (unit.kind) { + case Unit.Kind.Atomic: return unit.model.atomicConformation + case Unit.Kind.Spheres: return unit.model.coarseConformation.spheres + case Unit.Kind.Gaussians: return unit.model.coarseConformation.gaussians + } +} + +export function getUnitConformationAndRadius(unit: Unit) { + const conformation = getConformation(unit) + const { elements } = unit + const position = { + indices: elements, + x: conformation.x, + y: conformation.y, + z: conformation.z + } + + const l = StructureElement.create(unit) + const sizeTheme = PhysicalSizeTheme({}, {}) + const radius = (index: number) => { + l.element = index as ElementIndex + return sizeTheme.size(l) + } + + return { position, radius } +} + +export function getStructureConformationAndRadius(structure: Structure) { + const n = structure.elementCount + + const xs = new Float32Array(n) + const ys = new Float32Array(n) + const zs = new Float32Array(n) + const rs = new Float32Array(n) + + const l = StructureElement.create() + const sizeTheme = PhysicalSizeTheme({}, {}) + + let m = 0 + for (let i = 0, il = structure.units.length; i < il; ++i) { + const unit = structure.units[i] + const { elements } = unit + const { x, y, z } = unit.conformation + l.unit = unit + for (let j = 0, jl = elements.length; j < jl; ++j) { + const eI = elements[j] + xs[m + j] = x(eI) + ys[m + j] = y(eI) + zs[m + j] = z(eI) + l.element = eI + rs[m + j] = sizeTheme.size(l) + } + m += elements.length + } + + const position = { indices: OrderedSet.ofRange(0, n), x: xs, y: ys, z: zs } + const radius = (index: number) => rs[index] + + return { position, radius } } \ No newline at end of file diff --git a/src/mol-repr/structure/visual/util/gaussian.ts b/src/mol-repr/structure/visual/util/gaussian.ts index 9cee5dcdddacc77754afb886ad4829538ea46779..2011ee1e1e7716c89e56ce08435cda4310b1d802 100644 --- a/src/mol-repr/structure/visual/util/gaussian.ts +++ b/src/mol-repr/structure/visual/util/gaussian.ts @@ -4,18 +4,17 @@ * @author Alexander Rose <alexander.rose@weirdbyte.de> */ -import { Unit, StructureElement, ElementIndex, Structure } from 'mol-model/structure'; +import { Unit, Structure } from 'mol-model/structure'; import { GaussianDensity } from 'mol-math/geometry/gaussian-density'; import { Task } from 'mol-task'; import { ParamDefinition as PD } from 'mol-util/param-definition'; -import { GaussianDensityTexture } from 'mol-math/geometry/gaussian-density/gpu'; +import { GaussianDensityTexture, GaussianDensityTexture2d } from 'mol-math/geometry/gaussian-density/gpu'; import { Texture } from 'mol-gl/webgl/texture'; import { WebGLContext } from 'mol-gl/webgl/context'; -import { PhysicalSizeTheme } from 'mol-theme/size/physical'; -import { OrderedSet } from 'mol-data/int'; +import { getUnitConformationAndRadius, getStructureConformationAndRadius } from './common'; export const GaussianDensityParams = { - resolution: PD.Numeric(1, { min: 0.1, max: 10, step: 0.1 }), + resolution: PD.Numeric(1, { min: 0.1, max: 20, step: 0.1 }), radiusOffset: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }), smoothness: PD.Numeric(1.5, { min: 0.5, max: 2.5, step: 0.1 }), useGpu: PD.Boolean(false), @@ -24,7 +23,7 @@ export const DefaultGaussianDensityProps = PD.getDefaultValues(GaussianDensityPa export type GaussianDensityProps = typeof DefaultGaussianDensityProps export const GaussianDensityTextureParams = { - resolution: PD.Numeric(1, { min: 0.1, max: 10, step: 0.1 }), + resolution: PD.Numeric(1, { min: 0.1, max: 20, step: 0.1 }), radiusOffset: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }), smoothness: PD.Numeric(1.5, { min: 0.5, max: 2.5, step: 0.1 }), } @@ -33,34 +32,6 @@ export type GaussianDensityTextureProps = typeof DefaultGaussianDensityTexturePr // -function getConformation(unit: Unit) { - switch (unit.kind) { - case Unit.Kind.Atomic: return unit.model.atomicConformation - case Unit.Kind.Spheres: return unit.model.coarseConformation.spheres - case Unit.Kind.Gaussians: return unit.model.coarseConformation.gaussians - } -} - -function getUnitConformationAndRadius(unit: Unit) { - const conformation = getConformation(unit) - const { elements } = unit - const position = { - indices: elements, - x: conformation.x, - y: conformation.y, - z: conformation.z - } - - const l = StructureElement.create(unit) - const sizeTheme = PhysicalSizeTheme({}, {}) - const radius = (index: number) => { - l.element = index as ElementIndex - return sizeTheme.size(l) - } - - return { position, radius } -} - export function computeUnitGaussianDensity(unit: Unit, props: GaussianDensityProps, webgl?: WebGLContext) { const { position, radius } = getUnitConformationAndRadius(unit) return Task.create('Gaussian Density', async ctx => { @@ -68,49 +39,22 @@ export function computeUnitGaussianDensity(unit: Unit, props: GaussianDensityPro }); } -export function computeUnitGaussianDensityTexture(unit: Unit, props: GaussianDensityProps, webgl: WebGLContext, texture?: Texture) { +export function computeUnitGaussianDensityTexture(unit: Unit, props: GaussianDensityTextureProps, webgl: WebGLContext, texture?: Texture) { const { position, radius } = getUnitConformationAndRadius(unit) return Task.create('Gaussian Density', async ctx => { - return await GaussianDensityTexture(ctx, webgl, position, unit.lookup3d.boundary.box, radius, props, texture); + return GaussianDensityTexture(webgl, position, unit.lookup3d.boundary.box, radius, props, texture); }); } -// - -function getStructureConformationAndRadius(structure: Structure) { - const n = structure.elementCount - - const xs = new Float32Array(n) - const ys = new Float32Array(n) - const zs = new Float32Array(n) - const rs = new Float32Array(n) - - const l = StructureElement.create() - const sizeTheme = PhysicalSizeTheme({}, {}) - - let m = 0 - for (let i = 0, il = structure.units.length; i < il; ++i) { - const unit = structure.units[i] - const { elements } = unit - const { x, y, z } = unit.conformation - l.unit = unit - for (let j = 0, jl = elements.length; j < jl; ++j) { - const eI = elements[j] - xs[m + j] = x(eI) - ys[m + j] = y(eI) - zs[m + j] = z(eI) - l.element = eI - rs[m + j] = sizeTheme.size(l) - } - m += elements.length - } - - const position = { indices: OrderedSet.ofRange(0, n), x: xs, y: ys, z: zs } - const radius = (index: number) => rs[index] - - return { position, radius } +export function computeUnitGaussianDensityTexture2d(unit: Unit, props: GaussianDensityTextureProps, webgl: WebGLContext, texture?: Texture) { + const { position, radius } = getUnitConformationAndRadius(unit) + return Task.create('Gaussian Density', async ctx => { + return GaussianDensityTexture2d(webgl, position, unit.lookup3d.boundary.box, radius, props, texture); + }); } +// + export function computeStructureGaussianDensity(structure: Structure, props: GaussianDensityProps, webgl?: WebGLContext) { const { position, radius } = getStructureConformationAndRadius(structure) return Task.create('Gaussian Density', async ctx => { @@ -121,6 +65,6 @@ export function computeStructureGaussianDensity(structure: Structure, props: Gau export function computeStructureGaussianDensityTexture(structure: Structure, props: GaussianDensityTextureProps, webgl: WebGLContext, texture?: Texture) { const { position, radius } = getStructureConformationAndRadius(structure) return Task.create('Gaussian Density', async ctx => { - return await GaussianDensityTexture(ctx, webgl, position, structure.lookup3d.boundary.box, radius, props, texture); + return GaussianDensityTexture(webgl, position, structure.lookup3d.boundary.box, radius, props, texture); }); } \ No newline at end of file diff --git a/src/mol-repr/structure/visual/util/molecular-surface.ts b/src/mol-repr/structure/visual/util/molecular-surface.ts new file mode 100644 index 0000000000000000000000000000000000000000..a1b54e9ff690de393d97b2ed9524861d04b427cb --- /dev/null +++ b/src/mol-repr/structure/visual/util/molecular-surface.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { Unit } from 'mol-model/structure'; +import { Task, RuntimeContext } from 'mol-task'; +import { getUnitConformationAndRadius } from './common'; +import { PositionData, DensityData } from 'mol-math/geometry'; +import { MolecularSurfaceCalculationProps, calcMolecularSurface } from 'mol-math/geometry/molecular-surface'; +import { OrderedSet } from 'mol-data/int'; + +function getPositionDataAndMaxRadius(unit: Unit, props: MolecularSurfaceCalculationProps) { + const { position, radius } = getUnitConformationAndRadius(unit) + const { indices } = position + const n = OrderedSet.size(indices) + const radii = new Float32Array(OrderedSet.end(indices)) + + let maxRadius = 0 + for (let i = 0; i < n; ++i) { + const j = OrderedSet.getAt(indices, i) + const r = radius(j) + if (maxRadius < r) maxRadius = r + radii[j] = r + props.probeRadius + } + + return { position: { ...position, radius: radii }, maxRadius } +} + +export function computeUnitMolecularSurface(unit: Unit, props: MolecularSurfaceCalculationProps) { + const { position, maxRadius } = getPositionDataAndMaxRadius(unit, props) + return Task.create('Molecular Surface', async ctx => { + return await MolecularSurface(ctx, position, maxRadius, props); + }); +} + +// + +async function MolecularSurface(ctx: RuntimeContext, position: Required<PositionData>, maxRadius: number, props: MolecularSurfaceCalculationProps): Promise<DensityData> { + return calcMolecularSurface(ctx, position, maxRadius, props) +} \ No newline at end of file diff --git a/src/mol-repr/structure/visual/util/nucleotide.ts b/src/mol-repr/structure/visual/util/nucleotide.ts index 22fe970f1692264dadea9354d0b454aa44fea7bc..8e8ed71d1e3ad7de88cede0d4e6d5000bb6e77ef 100644 --- a/src/mol-repr/structure/visual/util/nucleotide.ts +++ b/src/mol-repr/structure/visual/util/nucleotide.ts @@ -35,7 +35,9 @@ export function getNucleotideElementLoci(pickingId: PickingId, structureGroup: S if (id === objectId) { const { structure, group } = structureGroup const unit = group.units[instanceId] - return getResidueLoci(structure, unit, unit.polymerElements[groupId]) + if (Unit.isAtomic(unit)) { + return getResidueLoci(structure, unit, unit.polymerElements[groupId]) + } } return EmptyLoci } diff --git a/src/mol-repr/structure/visual/util/polymer.ts b/src/mol-repr/structure/visual/util/polymer.ts index 71b987a3ea8b5ac3954524d393e59875b55b5733..5734c8dee6464fd4c6ffe60c26f38129f139f9be 100644 --- a/src/mol-repr/structure/visual/util/polymer.ts +++ b/src/mol-repr/structure/visual/util/polymer.ts @@ -72,12 +72,25 @@ export function getPolymerElementLoci(pickingId: PickingId, structureGroup: Stru if (id === objectId) { const { structure, group } = structureGroup const unit = group.units[instanceId] - return getResidueLoci(structure, unit, unit.polymerElements[groupId]) + if (Unit.isAtomic(unit)) { + return getResidueLoci(structure, unit, unit.polymerElements[groupId]) + } else { + const { elements } = unit + const elementIndex = unit.polymerElements[groupId] + const unitIndex = OrderedSet.indexOf(elements, elementIndex) as StructureElement.UnitIndex | -1 + if (unitIndex !== -1) { + const indices = OrderedSet.ofSingleton(unitIndex) + return StructureElement.Loci(structure, [{ unit, indices }]) + } + } } return EmptyLoci } -/** Mark a polymer element (e.g. part of a cartoon trace) when all its residue's elements are in a loci. */ +/** + * Mark a polymer element (e.g. part of a cartoon trace) + * - for atomic units mark only when all its residue's elements are in a loci + */ export function eachPolymerElement(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) { let changed = false if (!StructureElement.isLoci(loci)) return false @@ -90,19 +103,32 @@ export function eachPolymerElement(loci: Loci, structureGroup: StructureGroup, a for (const e of loci.elements) { const unitIdx = group.unitIndexMap.get(e.unit.id) if (unitIdx !== undefined) { - // TODO optimized implementation for intervals - OrderedSet.forEach(e.indices, v => { - const rI = index[elements[v]] - const unitIndexMin = OrderedSet.findPredecessorIndex(elements, offsets[rI]) - const unitIndexMax = OrderedSet.findPredecessorIndex(elements, offsets[rI + 1] - 1) - const unitIndexInterval = Interval.ofRange(unitIndexMin, unitIndexMax) - if (!OrderedSet.isSubset(e.indices, unitIndexInterval)) return - const eI = traceElementIndex[rI] - const idx = OrderedSet.indexOf(e.unit.polymerElements, eI) - if (idx !== -1) { - if (apply(Interval.ofSingleton(unitIdx * groupCount + idx))) changed = true + if (Unit.isAtomic(e.unit)) { + // TODO optimized implementation for intervals + OrderedSet.forEach(e.indices, v => { + const rI = index[elements[v]] + const unitIndexMin = OrderedSet.findPredecessorIndex(elements, offsets[rI]) + const unitIndexMax = OrderedSet.findPredecessorIndex(elements, offsets[rI + 1] - 1) + const unitIndexInterval = Interval.ofRange(unitIndexMin, unitIndexMax) + if (!OrderedSet.isSubset(e.indices, unitIndexInterval)) return + const eI = traceElementIndex[rI] + const idx = OrderedSet.indexOf(e.unit.polymerElements, eI) + if (idx !== -1) { + if (apply(Interval.ofSingleton(unitIdx * groupCount + idx))) changed = true + } + }) + } else { + if (Interval.is(e.indices)) { + const start = unitIdx * groupCount + Interval.start(e.indices); + const end = unitIdx * groupCount + Interval.end(e.indices); + if (apply(Interval.ofBounds(start, end))) changed = true + } else { + for (let i = 0, _i = e.indices.length; i < _i; i++) { + const idx = unitIdx * groupCount + e.indices[i]; + if (apply(Interval.ofSingleton(idx))) changed = true + } } - }) + } } } return changed @@ -128,17 +154,33 @@ export function getPolymerGapElementLoci(pickingId: PickingId, structureGroup: S export function eachPolymerGapElement(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) { let changed = false - if (!Link.isLoci(loci)) return false - const { structure, group } = structureGroup - if (!Structure.areEquivalent(loci.structure, structure)) return false - const groupCount = group.units[0].gapElements.length - for (const b of loci.links) { - const unitIdx = group.unitIndexMap.get(b.aUnit.id) - if (unitIdx !== undefined) { - const idxA = OrderedSet.indexOf(b.aUnit.gapElements, b.aUnit.elements[b.aIndex]) - const idxB = OrderedSet.indexOf(b.bUnit.gapElements, b.bUnit.elements[b.bIndex]) - if (idxA !== -1 && idxB !== -1) { - if (apply(Interval.ofSingleton(unitIdx * groupCount + idxA))) changed = true + if (Link.isLoci(loci)) { + const { structure, group } = structureGroup + if (!Structure.areEquivalent(loci.structure, structure)) return false + const groupCount = group.units[0].gapElements.length + for (const b of loci.links) { + const unitIdx = group.unitIndexMap.get(b.aUnit.id) + if (unitIdx !== undefined) { + const idxA = OrderedSet.indexOf(b.aUnit.gapElements, b.aUnit.elements[b.aIndex]) + const idxB = OrderedSet.indexOf(b.bUnit.gapElements, b.bUnit.elements[b.bIndex]) + if (idxA !== -1 && idxB !== -1) { + if (apply(Interval.ofSingleton(unitIdx * groupCount + idxA))) changed = true + } + } + } + } else if (StructureElement.isLoci(loci)) { + const { structure, group } = structureGroup + if (!Structure.areEquivalent(loci.structure, structure)) return false + const groupCount = group.units[0].gapElements.length + for (const e of loci.elements) { + const unitIdx = group.unitIndexMap.get(e.unit.id) + if (unitIdx !== undefined) { + OrderedSet.forEach(e.indices, v => { + const idx = OrderedSet.indexOf(e.unit.gapElements, e.unit.elements[v]) + if (idx !== -1) { + if (apply(Interval.ofSingleton(unitIdx * groupCount + idx))) changed = true + } + }) } } } diff --git a/src/mol-repr/visual.ts b/src/mol-repr/visual.ts index 92072569ebe9dae974c23f7e6966aebefdafabfe..f0e6f182ab17fcd3423ff4b2211e5bca664ddfb8 100644 --- a/src/mol-repr/visual.ts +++ b/src/mol-repr/visual.ts @@ -7,7 +7,7 @@ import { RuntimeContext } from 'mol-task' import { GraphicsRenderObject } from 'mol-gl/render-object' import { PickingId } from '../mol-geo/geometry/picking'; -import { Loci } from 'mol-model/loci'; +import { Loci, isEmptyLoci } from 'mol-model/loci'; import { MarkerAction, applyMarkerAction } from '../mol-geo/geometry/marker-data'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import { WebGLContext } from 'mol-gl/webgl/context'; @@ -19,6 +19,8 @@ import { ValueCell } from 'mol-util'; import { Overpaint } from 'mol-theme/overpaint'; import { createOverpaint, clearOverpaint, applyOverpaintColor } from 'mol-geo/geometry/overpaint-data'; import { Interval } from 'mol-data/int'; +import { Transparency } from 'mol-theme/transparency'; +import { createTransparency, clearTransparency, applyTransparencyValue } from 'mol-geo/geometry/transparency-data'; export interface VisualContext { readonly runtime: RuntimeContext @@ -39,6 +41,7 @@ interface Visual<D, P extends PD.Params> { setPickable: (pickable: boolean) => void setTransform: (matrix?: Mat4, instanceMatrices?: Float32Array | null) => void setOverpaint: (overpaint: Overpaint) => void + setTransparency: (transparency: Transparency) => void destroy: () => void } namespace Visual { @@ -96,6 +99,30 @@ namespace Visual { ValueCell.update(tOverpaint, tOverpaint.ref.value) } + export function setTransparency(renderObject: GraphicsRenderObject | undefined, transparency: Transparency, lociApply: LociApply, clear: boolean) { + if (!renderObject) return + + const { tTransparency, uGroupCount, instanceCount } = renderObject.values + const count = uGroupCount.ref.value * instanceCount.ref.value + + const { loci, value, variant } = transparency + + // ensure texture has right size and variant + createTransparency(value && !isEmptyLoci(loci) ? count : 0, variant, renderObject.values) + + // clear if requested + if (clear) clearTransparency(tTransparency.ref.value.array, 0, count) + + const apply = (interval: Interval) => { + const start = Interval.start(interval) + const end = Interval.end(interval) + return applyTransparencyValue(tTransparency.ref.value.array, start, end, value) + } + lociApply(loci, apply) + + ValueCell.update(tTransparency, tTransparency.ref.value) + } + export function setTransform(renderObject: GraphicsRenderObject | undefined, transform?: Mat4, instanceTransforms?: Float32Array | null) { if (!renderObject || (!transform && !instanceTransforms)) return diff --git a/src/mol-repr/volume/direct-volume.ts b/src/mol-repr/volume/direct-volume.ts index 805045ae744120de65151dbd556ee97ad3d20f04..813a030f9899169011e9fa674f953800afa93abb 100644 --- a/src/mol-repr/volume/direct-volume.ts +++ b/src/mol-repr/volume/direct-volume.ts @@ -163,7 +163,7 @@ export function getDirectVolumeParams(ctx: ThemeRegistryContext, volume: VolumeD return PD.clone(DirectVolumeParams) } -export function DirectVolumeVisual(): VolumeVisual<DirectVolumeParams> { +export function DirectVolumeVisual(materialId: number): VolumeVisual<DirectVolumeParams> { return VolumeVisual<DirectVolume, DirectVolumeParams>({ defaultProps: PD.getDefaultValues(DirectVolumeParams), createGeometry: createDirectVolume, @@ -173,7 +173,7 @@ export function DirectVolumeVisual(): VolumeVisual<DirectVolumeParams> { setUpdateState: (state: VisualUpdateState, volume: VolumeData, newProps: PD.Values<DirectVolumeParams>, currentProps: PD.Values<DirectVolumeParams>) => { }, geometryUtils: DirectVolume.Utils - }) + }, materialId) } export function DirectVolumeRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, DirectVolumeParams>): VolumeRepresentation<DirectVolumeParams> { diff --git a/src/mol-repr/volume/isosurface.ts b/src/mol-repr/volume/isosurface.ts index b29c7afe5970b0898958696bda5c23590a15f924..224afe6779457f11d03b73c9a9e4806d41d0e752 100644 --- a/src/mol-repr/volume/isosurface.ts +++ b/src/mol-repr/volume/isosurface.ts @@ -78,7 +78,6 @@ export async function createVolumeIsosurfaceMesh(ctx: VisualContext, volume: Vol const transform = VolumeData.getGridToCartesianTransform(volume); ctx.runtime.update({ message: 'Transforming mesh...' }); Mesh.transformImmediate(surface, transform); - Mesh.computeNormalsImmediate(surface) return surface; } @@ -89,7 +88,7 @@ export const IsosurfaceMeshParams = { } export type IsosurfaceMeshParams = typeof IsosurfaceMeshParams -export function IsosurfaceMeshVisual(): VolumeVisual<IsosurfaceMeshParams> { +export function IsosurfaceMeshVisual(materialId: number): VolumeVisual<IsosurfaceMeshParams> { return VolumeVisual<Mesh, IsosurfaceMeshParams>({ defaultProps: PD.getDefaultValues(IsosurfaceMeshParams), createGeometry: createVolumeIsosurfaceMesh, @@ -100,7 +99,7 @@ export function IsosurfaceMeshVisual(): VolumeVisual<IsosurfaceMeshParams> { if (!VolumeIsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.dataStats)) state.createGeometry = true }, geometryUtils: Mesh.Utils - }) + }, materialId) } // @@ -125,7 +124,7 @@ export const IsosurfaceWireframeParams = { } export type IsosurfaceWireframeParams = typeof IsosurfaceWireframeParams -export function IsosurfaceWireframeVisual(): VolumeVisual<IsosurfaceWireframeParams> { +export function IsosurfaceWireframeVisual(materialId: number): VolumeVisual<IsosurfaceWireframeParams> { return VolumeVisual<Lines, IsosurfaceWireframeParams>({ defaultProps: PD.getDefaultValues(IsosurfaceWireframeParams), createGeometry: createVolumeIsosurfaceWireframe, @@ -136,7 +135,7 @@ export function IsosurfaceWireframeVisual(): VolumeVisual<IsosurfaceWireframePar if (!VolumeIsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.dataStats)) state.createGeometry = true }, geometryUtils: Lines.Utils - }) + }, materialId) } // diff --git a/src/mol-repr/volume/registry.ts b/src/mol-repr/volume/registry.ts index 51507b46e7188e41f305255fcf422d6e4a780d3d..fde44525b8afd8496cb737c17c03cacfae3163dc 100644 --- a/src/mol-repr/volume/registry.ts +++ b/src/mol-repr/volume/registry.ts @@ -7,7 +7,6 @@ import { RepresentationProvider, RepresentationRegistry, Representation } from '../representation'; import { VolumeData } from 'mol-model/volume'; import { IsosurfaceRepresentationProvider } from './isosurface'; -import { DirectVolumeRepresentationProvider } from './direct-volume'; export class VolumeRepresentationRegistry extends RepresentationRegistry<VolumeData, Representation.State> { constructor() { @@ -21,7 +20,7 @@ export class VolumeRepresentationRegistry extends RepresentationRegistry<VolumeD export const BuiltInVolumeRepresentations = { 'isosurface': IsosurfaceRepresentationProvider, - 'direct-volume': DirectVolumeRepresentationProvider, + // 'direct-volume': DirectVolumeRepresentationProvider, // TODO disabled for now, needs more work } export type BuiltInVolumeRepresentationsName = keyof typeof BuiltInVolumeRepresentations export const BuiltInVolumeRepresentationsNames = Object.keys(BuiltInVolumeRepresentations) diff --git a/src/mol-repr/volume/representation.ts b/src/mol-repr/volume/representation.ts index 9b2472a4cd1a29540f927f8127ba97ffac0db454..621c17ebcf8fb4e07924573f8b544e7e2fa28291 100644 --- a/src/mol-repr/volume/representation.ts +++ b/src/mol-repr/volume/representation.ts @@ -13,7 +13,7 @@ import { Geometry, GeometryUtils } from 'mol-geo/geometry/geometry'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import { PickingId } from 'mol-geo/geometry/picking'; import { MarkerAction } from 'mol-geo/geometry/marker-data'; -import { GraphicsRenderObject, createRenderObject } from 'mol-gl/render-object'; +import { GraphicsRenderObject, createRenderObject, getNextMaterialId } from 'mol-gl/render-object'; import { Interval } from 'mol-data/int'; import { LocationIterator } from 'mol-geo/util/location-iterator'; import { VisualUpdateState } from 'mol-repr/util'; @@ -27,15 +27,16 @@ import { ColorTheme } from 'mol-theme/color'; import { createColors } from 'mol-geo/geometry/color-data'; import { createSizes } from 'mol-geo/geometry/size-data'; import { Overpaint } from 'mol-theme/overpaint'; +import { Transparency } from 'mol-theme/transparency'; export interface VolumeVisual<P extends VolumeParams> extends Visual<VolumeData, P> { } -function createVolumeRenderObject<G extends Geometry>(volume: VolumeData, geometry: G, locationIt: LocationIterator, theme: Theme, props: PD.Values<Geometry.Params<G>>) { +function createVolumeRenderObject<G extends Geometry>(volume: VolumeData, geometry: G, locationIt: LocationIterator, theme: Theme, props: PD.Values<Geometry.Params<G>>, materialId: number) { const { createValues, createRenderableState } = Geometry.getUtils(geometry) const transform = createIdentityTransform() const values = createValues(geometry, transform, locationIt, theme, props) const state = createRenderableState(props) - return createRenderObject(geometry.kind, values, state) + return createRenderObject(geometry.kind, values, state, materialId) } interface VolumeVisualBuilder<P extends VolumeParams, G extends Geometry> { @@ -51,7 +52,7 @@ interface VolumeVisualGeometryBuilder<P extends VolumeParams, G extends Geometry geometryUtils: GeometryUtils<G> } -export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geometry.Params<G>>(builder: VolumeVisualGeometryBuilder<P, G>): VolumeVisual<P> { +export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geometry.Params<G>>(builder: VolumeVisualGeometryBuilder<P, G>, materialId: number): VolumeVisual<P> { const { defaultProps, createGeometry, createLocationIterator, getLoci, eachLocation, setUpdateState } = builder const { updateValues, updateBoundingSphere, updateRenderableState } = builder.geometryUtils const updateState = VisualUpdateState.create() @@ -104,7 +105,7 @@ export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geomet if (updateState.createNew) { locationIt = createLocationIterator(newVolume) if (newGeometry) { - renderObject = createVolumeRenderObject(newVolume, newGeometry, locationIt, newTheme, newProps) + renderObject = createVolumeRenderObject(newVolume, newGeometry, locationIt, newTheme, newProps, materialId) } else { throw new Error('expected geometry to be given') } @@ -186,6 +187,9 @@ export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geomet setOverpaint(overpaint: Overpaint) { return Visual.setOverpaint(renderObject, overpaint, lociApply, true) }, + setTransparency(transparency: Transparency) { + return Visual.setTransparency(renderObject, transparency, lociApply, true) + }, destroy() { // TODO renderObject = undefined @@ -204,9 +208,10 @@ export const VolumeParams = { } export type VolumeParams = typeof VolumeParams -export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, P>, visualCtor: () => VolumeVisual<P>): VolumeRepresentation<P> { +export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, P>, visualCtor: (materialId: number) => VolumeVisual<P>): VolumeRepresentation<P> { let version = 0 const updated = new Subject<number>() + const materialId = getNextMaterialId() const renderObjects: GraphicsRenderObject[] = [] const _state = Representation.createState() let visual: VolumeVisual<P> | undefined @@ -225,7 +230,7 @@ export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx: _props = Object.assign({}, _props, props) return Task.create('Creating or updating VolumeRepresentation', async runtime => { - if (!visual) visual = visualCtor() + if (!visual) visual = visualCtor(materialId) const promise = visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, volume) if (promise) await promise // update list of renderObjects @@ -249,6 +254,7 @@ export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx: if (state.alphaFactor !== undefined && visual) visual.setAlphaFactor(state.alphaFactor) if (state.pickable !== undefined && visual) visual.setPickable(state.pickable) if (state.overpaint !== undefined && visual) visual.setOverpaint(state.overpaint) + if (state.transparency !== undefined && visual) visual.setTransparency(state.transparency) if (state.transform !== undefined && visual) visual.setTransform(state.transform) Representation.updateState(_state, state) diff --git a/src/mol-script/language/symbol-table/structure-query.ts b/src/mol-script/language/symbol-table/structure-query.ts index 4f9f7377efb220f0dbb65717677675540095a9d1..f7d81aa44419493271ac210e149e5b3fb28f2e0e 100644 --- a/src/mol-script/language/symbol-table/structure-query.ts +++ b/src/mol-script/language/symbol-table/structure-query.ts @@ -167,6 +167,10 @@ const filter = { test: Argument(Type.Bool) }), Types.ElementSelectionQuery, 'Pick all atom sets that satisfy the test.'), + first: symbol(Arguments.Dictionary({ + 0: Argument(Types.ElementSelectionQuery) + }), Types.ElementSelectionQuery, 'Take the 1st atom set in the sequence.'), + withSameAtomProperties: symbol(Arguments.Dictionary({ 0: Argument(Types.ElementSelectionQuery), source: Argument(Types.ElementSelectionQuery), diff --git a/src/mol-script/runtime/query/compiler.ts b/src/mol-script/runtime/query/compiler.ts index ce49a29c3a640db42c317b06f302f52d339dbd34..28b96cb5275dda8bb12fa12f8b03eaf49d09af5e 100644 --- a/src/mol-script/runtime/query/compiler.ts +++ b/src/mol-script/runtime/query/compiler.ts @@ -5,7 +5,7 @@ */ import Expression from '../../language/expression'; -import { QueryContext, QueryFn, Structure, ModelPropertyDescriptor } from 'mol-model/structure'; +import { QueryContext, QueryFn, Structure, CustomPropertyDescriptor } from 'mol-model/structure'; import { MSymbol } from '../../language/symbol'; export class QueryRuntimeTable { @@ -18,7 +18,7 @@ export class QueryRuntimeTable { this.map.set(runtime.symbol.id, runtime); } - addCustomProp(desc: ModelPropertyDescriptor<any>) { + addCustomProp(desc: CustomPropertyDescriptor<any>) { if (!desc.symbols) return; for (const k of Object.keys(desc.symbols)) { diff --git a/src/mol-script/runtime/query/table.ts b/src/mol-script/runtime/query/table.ts index ba1072a587beb9c0c8ed2bf3b83dac130894b7f8..4ddbdb8b1945619c723c187001858a952a489ad8 100644 --- a/src/mol-script/runtime/query/table.ts +++ b/src/mol-script/runtime/query/table.ts @@ -186,6 +186,9 @@ const symbols = [ C(MolScript.structureQuery.slot.element, (ctx, _) => ctx.element), // C(MolScript.structureQuery.slot.elementSetReduce, (ctx, _) => ctx.element), + // ============= FILTERS ================ + D(MolScript.structureQuery.filter.first, (ctx, xs) => Queries.filters.first(xs[0] as any)(ctx)), + // ============= GENERATORS ================ D(MolScript.structureQuery.generator.atomGroups, (ctx, xs) => Queries.generators.atoms({ entityTest: xs['entity-test'], diff --git a/src/mol-script/script/mol-script/symbols.ts b/src/mol-script/script/mol-script/symbols.ts index 0c4826f7ffc685e85a84b9617b83188def6a6e06..9bd1119533c764200a1ab49b9e0f2e13faed0738 100644 --- a/src/mol-script/script/mol-script/symbols.ts +++ b/src/mol-script/script/mol-script/symbols.ts @@ -148,6 +148,7 @@ export const SymbolTable = [ [ 'Filters', Alias(MolScript.structureQuery.filter.pick, 'sel.atom.pick'), + Alias(MolScript.structureQuery.filter.first, 'sel.atom.first'), Alias(MolScript.structureQuery.filter.withSameAtomProperties, 'sel.atom.with-same-atom-properties'), Alias(MolScript.structureQuery.filter.intersectedBy, 'sel.atom.intersected-by'), Alias(MolScript.structureQuery.filter.within, 'sel.atom.within'), diff --git a/src/mol-state/manager.ts b/src/mol-state/manager.ts deleted file mode 100644 index 0042b15a93f4184628d575672d2570f63ac64dc6..0000000000000000000000000000000000000000 --- a/src/mol-state/manager.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author David Sehnal <david.sehnal@gmail.com> - */ - -// TODO manage snapshots etc \ No newline at end of file diff --git a/src/mol-state/object.ts b/src/mol-state/object.ts index b74efcf5ee78f98572acf08c549929d32282932d..dd4c5c1d34261e53c36614f9ffca1c8c86543c44 100644 --- a/src/mol-state/object.ts +++ b/src/mol-state/object.ts @@ -19,7 +19,7 @@ interface StateObject<D = any, T extends StateObject.Type = StateObject.Type<any readonly label: string, readonly description?: string, // assigned by reconciler to be StateTransform.props.tag - readonly tag?: string + readonly tags?: string[] } namespace StateObject { @@ -56,12 +56,15 @@ namespace StateObject { } interface StateObjectCell<T extends StateObject = StateObject, F extends StateTransform<StateTransformer<any, T, any>> = StateTransform<StateTransformer<any, T, any>>> { + parent: State, + transform: F, // Which object was used as a parent to create data in this cell sourceRef: StateTransform.Ref | undefined, status: StateObjectCell.Status, + state: StateTransform.State, params: { definition: ParamDefinition.Params, @@ -79,24 +82,6 @@ namespace StateObjectCell { export type Obj<C extends StateObjectCell> = C extends StateObjectCell<infer T> ? T : never export type Transform<C extends StateObjectCell> = C extends StateObjectCell<any, infer T> ? T : never - - export interface State { - isHidden: boolean, - isCollapsed: boolean - } - - export const DefaultState: State = { isHidden: false, isCollapsed: false }; - - export function areStatesEqual(a: State, b: State) { - return a.isHidden !== b.isHidden || a.isCollapsed !== b.isCollapsed; - } - - export function isStateChange(a: State, b?: Partial<State>) { - if (!b) return false; - if (typeof b.isCollapsed !== 'undefined' && a.isCollapsed !== b.isCollapsed) return true; - if (typeof b.isHidden !== 'undefined' && a.isHidden !== b.isHidden) return true; - return false; - } } // TODO: improve the API? diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts index 66563660ade1dd87bd95a428fd867f2d20618baa..6ffb8ff6480353dec56a2d1a6692e573a3314f71 100644 --- a/src/mol-state/state.ts +++ b/src/mol-state/state.ts @@ -33,7 +33,7 @@ class State { readonly globalContext: unknown = void 0; readonly events = { cell: { - stateUpdated: this.ev<State.ObjectEvent & { cellState: StateObjectCell.State }>(), + stateUpdated: this.ev<State.ObjectEvent & { cell: StateObjectCell }>(), created: this.ev<State.ObjectEvent & { cell: StateObjectCell }>(), removed: this.ev<State.ObjectEvent & { parent: StateTransform.Ref }>(), }, @@ -55,7 +55,6 @@ class State { get tree(): StateTree { return this._tree; } get transforms() { return (this._tree as StateTree).transforms; } - get cellStates() { return (this._tree as StateTree).cellStates; } get current() { return this.behaviors.currentObject.value.ref; } build() { return new StateBuilder.Root(this.tree, this); } @@ -76,13 +75,15 @@ class State { this.behaviors.currentObject.next({ state: this, ref }); } - 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; + updateCellState(ref: StateTransform.Ref, stateOrProvider: ((old: StateTransform.State) => Partial<StateTransform.State>) | Partial<StateTransform.State>) { + const cell = this.cells.get(ref); + if (!cell) return; - if (this._tree.updateCellState(ref, update)) { - this.events.cell.stateUpdated.next({ state: this, ref, cellState: this.tree.cellStates.get(ref) }); + const update = typeof stateOrProvider === 'function' ? stateOrProvider(cell.state) : stateOrProvider; + + if (StateTransform.assignState(cell.state, update)) { + cell.transform = this._tree.assignState(cell.transform.ref, update); + this.events.cell.stateUpdated.next({ state: this, ref, cell }); } } @@ -165,10 +166,6 @@ class State { if (updated) this.events.changed.next(); this.events.isUpdating.next(false); - - for (const ref of ctx.stateChanges) { - this.events.cell.stateUpdated.next({ state: this, ref, cellState: this.tree.cellStates.get(ref) }); - } } } @@ -189,7 +186,6 @@ class State { spine: this.spine, results: [], - stateChanges: [], options: { ...StateUpdateDefaultOptions, ...options }, @@ -203,16 +199,18 @@ class State { return ctx; } - constructor(rootObject: StateObject, params?: { globalContext?: unknown, rootProps?: StateTransform.Props }) { - this._tree = StateTree.createEmpty(StateTransform.createRoot(params && params.rootProps)).asTransient(); + constructor(rootObject: StateObject, params?: { globalContext?: unknown, rootState?: StateTransform.State }) { + this._tree = StateTree.createEmpty(StateTransform.createRoot(params && params.rootState)).asTransient(); const tree = this._tree; const root = tree.root; (this.cells as Map<StateTransform.Ref, StateObjectCell>).set(root.ref, { + parent: this, transform: root, sourceRef: void 0, obj: rootObject, status: 'ok', + state: { ...root.state }, errorText: void 0, params: { definition: {}, @@ -245,7 +243,7 @@ namespace State { doNotUpdateCurrent: boolean } - export function create(rootObject: StateObject, params?: { globalContext?: unknown, rootProps?: StateTransform.Props }) { + export function create(rootObject: StateObject, params?: { globalContext?: unknown, rootState?: StateTransform.State }) { return new State(rootObject, params); } } @@ -271,7 +269,6 @@ interface UpdateContext { spine: StateTreeSpine.Impl, results: UpdateNodeResult[], - stateChanges: StateTransform.Ref[], // suppress timing messages options: State.UpdateOptions, @@ -319,12 +316,6 @@ async function update(ctx: UpdateContext) { roots = findUpdateRoots(ctx.cells, ctx.tree); } - let newCellStates: StateTree.CellStates; - if (!ctx.editInfo) { - newCellStates = ctx.tree.cellStatesSnapshot(); - syncOldStates(ctx); - } - // Init empty cells where not present // this is done in "pre order", meaning that "parents" will be created 1st. const addedCells = initCells(ctx, roots); @@ -353,7 +344,7 @@ async function update(ctx: UpdateContext) { // Sync cell states if (!ctx.editInfo) { - syncNewStates(ctx, newCellStates!); + syncNewStates(ctx); } let newCurrent: StateTransform.Ref | undefined = ctx.newCurrent; @@ -363,7 +354,7 @@ async function update(ctx: UpdateContext) { ctx.parent.events.object.created.next({ state: ctx.parent, ref: update.ref, obj: update.obj! }); if (!ctx.newCurrent) { const transform = ctx.tree.transforms.get(update.ref); - if (!(transform.props && transform.props.isGhost) && update.obj !== StateObject.Null) newCurrent = update.ref; + if (!transform.state.isGhost && update.obj !== StateObject.Null) newCurrent = update.ref; } } else if (update.action === 'updated') { ctx.parent.events.object.updated.next({ state: ctx.parent, ref: update.ref, action: 'in-place', obj: update.obj }); @@ -415,25 +406,14 @@ function findDeletes(ctx: UpdateContext): Ref[] { return deleteCtx.deletes; } -function syncOldStatesVisitor(n: StateTransform, tree: StateTree, oldState: StateTree.CellStates) { - if (oldState.has(n.ref)) { - (tree as TransientTree).updateCellState(n.ref, oldState.get(n.ref)); - } -} -function syncOldStates(ctx: UpdateContext) { - StateTree.doPreOrder(ctx.tree, ctx.tree.root, ctx.oldTree.cellStates, syncOldStatesVisitor); +function syncNewStatesVisitor(n: StateTransform, tree: StateTree, ctx: UpdateContext) { + const cell = ctx.cells.get(n.ref); + if (!cell || !StateTransform.syncState(cell.state, n.state)) return; + ctx.parent.events.cell.stateUpdated.next({ state: ctx.parent, ref: n.ref, cell }); } -function syncNewStatesVisitor(n: StateTransform, tree: StateTree, ctx: { newState: StateTree.CellStates, changes: StateTransform.Ref[] }) { - if (ctx.newState.has(n.ref)) { - const changed = (tree as TransientTree).updateCellState(n.ref, ctx.newState.get(n.ref)); - if (changed) { - ctx.changes.push(n.ref); - } - } -} -function syncNewStates(ctx: UpdateContext, newState: StateTree.CellStates) { - StateTree.doPreOrder(ctx.tree, ctx.tree.root, { newState, changes: ctx.stateChanges }, syncNewStatesVisitor); +function syncNewStates(ctx: UpdateContext) { + StateTree.doPreOrder(ctx.tree, ctx.tree.root, ctx, syncNewStatesVisitor); } function setCellStatus(ctx: UpdateContext, ref: Ref, status: StateObjectCell.Status, errorText?: string) { @@ -441,7 +421,7 @@ function setCellStatus(ctx: UpdateContext, ref: Ref, status: StateObjectCell.Sta const changed = cell.status !== status; cell.status = status; cell.errorText = errorText; - if (changed) ctx.parent.events.cell.stateUpdated.next({ state: ctx.parent, ref, cellState: ctx.tree.cellStates.get(ref) }); + if (changed) ctx.parent.events.cell.stateUpdated.next({ state: ctx.parent, ref, cell }); } function initCellStatusVisitor(t: StateTransform, _: any, ctx: UpdateContext) { @@ -462,9 +442,11 @@ function initCellsVisitor(transform: StateTransform, _: any, { ctx, added }: Ini } const cell: StateObjectCell = { + parent: ctx.parent, transform, sourceRef: void 0, status: 'pending', + state: { ...transform.state }, errorText: void 0, params: void 0, cache: void 0 @@ -505,7 +487,7 @@ function _findNewCurrent(tree: StateTree, ref: Ref, deletes: Set<Ref>, cells: Ma } const t = tree.transforms.get(s.value); - if (t.props && t.props.isGhost) continue; + if (t.state.isGhost) continue; if (s.value === ref) { seenRef = true; if (!deletes.has(ref)) prevCandidate = ref; @@ -671,7 +653,7 @@ async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNo function updateTag(obj: StateObject | undefined, transform: StateTransform) { if (!obj || obj === StateObject.Null) return; - (obj.tag as string | undefined) = transform.props.tag; + (obj.tags as string[] | undefined) = transform.tags; } function runTask<T>(t: T | Task<T>, ctx: RuntimeContext) { diff --git a/src/mol-state/state/builder.ts b/src/mol-state/state/builder.ts index 6e9e48a10a680457d27f4efa86e602a0450f032f..ec9fd4d9217a875edaeb6fc9c2cfab05a998152e 100644 --- a/src/mol-state/state/builder.ts +++ b/src/mol-state/state/builder.ts @@ -36,7 +36,7 @@ namespace StateBuilder { | { kind: 'add', transform: StateTransform } | { kind: 'update', ref: string, params: any } | { kind: 'delete', ref: string } - | { kind: 'insert', ref: string, transform: StateTransform, initialCellState?: Partial<StateObjectCell.State> } + | { kind: 'insert', ref: string, transform: StateTransform } function buildTree(state: BuildState) { if (!state.state || state.state.tree === state.editInfo.sourceTree) { @@ -52,7 +52,7 @@ namespace StateBuilder { case 'delete': tree.remove(a.ref); break; case 'insert': { const children = tree.children.get(a.ref).toArray(); - tree.add(a.transform, a.initialCellState); + tree.add(a.transform); for (const c of children) { tree.changeParent(c, a.transform.ref); } @@ -84,12 +84,13 @@ namespace StateBuilder { } toRoot<A extends StateObject>() { return new To<A>(this.state, this.state.tree.root.ref, this); } delete(ref: StateTransform.Ref) { + if (!this.state.tree.transforms.has(ref)) return this; this.editInfo.count++; this.state.tree.remove(ref); this.state.actions.push({ kind: 'delete', ref }); return this; } - getTree(): StateTree { return buildTree(this.state); } //this.state.tree.asImmutable(); } + getTree(): StateTree { return buildTree(this.state); } constructor(tree: StateTree, state?: State) { this.state = { state, tree: tree.asTransient(), actions: [], editInfo: { sourceTree: tree, count: 0, lastUpdate: void 0 } } } } @@ -102,9 +103,9 @@ namespace StateBuilder { * 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>> { + apply<T extends StateTransformer<A, any, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>): To<StateTransformer.To<T>> { const t = tr.apply(this.ref, params, options); - this.state.tree.add(t, initialCellState); + this.state.tree.add(t); this.editInfo.count++; this.editInfo.lastUpdate = t.ref; @@ -113,23 +114,36 @@ namespace StateBuilder { return new To(this.state, t.ref, this.root); } + /** + * If the ref is present, the transform is applied. + * Otherwise a transform with the specifed ref is created. + */ + applyOrUpdate<T extends StateTransformer<A, any, any>>(ref: StateTransform.Ref, tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>): To<StateTransformer.To<T>> { + if (this.state.tree.transforms.has(ref)) { + this.to(ref).update(params); + return this.to(ref) as To<StateTransformer.To<T>>; + } else { + return this.apply(tr, params, { ...options, ref }); + } + } + /** * A helper to greate a group-like state object and keep the current type. */ - group<T extends StateTransformer<A, any, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>, initialCellState?: Partial<StateObjectCell.State>): To<A> { - return this.apply(tr, params, options, initialCellState) as To<A>; + group<T extends StateTransformer<A, any, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>): To<A> { + return this.apply(tr, params, options) as To<A>; } /** * 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>> { + insert<T extends StateTransformer<A, A, any>>(tr: T, params?: StateTransformer.Params<T>, options?: Partial<StateTransform.Options>): 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); + this.state.tree.add(t); // move the original children to the new node for (const c of children) { @@ -139,7 +153,7 @@ namespace StateBuilder { this.editInfo.count++; this.editInfo.lastUpdate = t.ref; - this.state.actions.push({ kind: 'insert', ref: this.ref, transform: t, initialCellState }); + this.state.actions.push({ kind: 'insert', ref: this.ref, transform: t }); return new To(this.state, t.ref, this.root); } @@ -193,7 +207,7 @@ namespace StateBuilder { toRoot<A extends StateObject>() { return this.root.toRoot<A>(); } delete(ref: StateTransform.Ref) { return this.root.delete(ref); } - getTree(): StateTree { return buildTree(this.state); } //this.state.tree.asImmutable(); } + getTree(): StateTree { return buildTree(this.state); } constructor(private state: BuildState, ref: StateTransform.Ref, private root: Root) { this.ref = ref; diff --git a/src/mol-state/state/selection.ts b/src/mol-state/state/selection.ts index 12a31907342c71fbfcdb7dc27ac750e255034e7f..2a47745f5c84e51b9ec4116a91a10b88e6033461 100644 --- a/src/mol-state/state/selection.ts +++ b/src/mol-state/state/selection.ts @@ -49,6 +49,7 @@ namespace StateSelection { parent(): Builder<C>; first(): Builder<C>; filter(p: (n: C) => boolean): Builder<C>; + withTag(tag: string): Builder<C>; withTransformer<T extends StateTransformer<any, StateObjectCell.Obj<C>, any>>(t: T): Builder<StateObjectCell<StateObjectCell.Obj<C>, StateTransform<T>>>; withStatus(s: StateObjectCell.Status): Builder<C>; subtree(): Builder; @@ -200,6 +201,9 @@ namespace StateSelection { registerModifier('withStatus', withStatus); export function withStatus(b: Selector, s: StateObjectCell.Status) { return filter(b, n => n.status === s); } + registerModifier('withTag', withTag); + export function withTag(b: Selector, tag: string) { return filter(b, n => !!n.transform.tags && n.transform.tags.indexOf(tag) >= 0); } + registerModifier('subtree', subtree); export function subtree(b: Selector) { return flatMap(b, (n, s) => { @@ -268,8 +272,12 @@ namespace StateSelection { } function _findUniqueTagsInSubtree(n: StateTransform, _: any, s: { refs: { [name: string]: StateTransform.Ref }, tags: Set<string> }) { - if (n.props.tag && s.tags.has(n.props.tag)) { - s.refs[n.props.tag] = n.ref; + if (n.tags) { + for (const t of n.tags) { + if (!s.tags.has(t)) continue; + s.refs[t] = n.ref; + break; + } } return true; } @@ -279,7 +287,7 @@ namespace StateSelection { } function _findTagInSubtree(n: StateTransform, _: any, s: { ref: string | undefined, tag: string }) { - if (n.props.tag === s.tag) { + if (n.tags && n.tags.indexOf(s.tag) >= 0) { s.ref = n.ref; return false; } diff --git a/src/mol-state/transform.ts b/src/mol-state/transform.ts index 9703a2f53254c6dbfbcf2e5449b8a0775679cc05..fe4b251bf76b60ec6e6cea23d6b903f99b6586cf 100644 --- a/src/mol-state/transform.ts +++ b/src/mol-state/transform.ts @@ -12,7 +12,8 @@ export { Transform as StateTransform } interface Transform<T extends StateTransformer = StateTransformer> { readonly parent: Transform.Ref, readonly transformer: T, - readonly props: Transform.Props, + readonly state: Transform.State, + readonly tags?: string[], readonly ref: Transform.Ref, readonly params?: StateTransformer.Params<T>, readonly version: string @@ -24,24 +25,80 @@ namespace Transform { export const RootRef = '-=root=-' as Ref; - export interface Props { - tag?: string + export interface State { + // is the cell shown in the UI isGhost?: boolean, - // determine if the corresponding cell can be deleted by the user. - isLocked?: boolean + // can the corresponding be deleted by the user. + isLocked?: boolean, + // is the representation associated with the cell hidden + isHidden?: boolean, + // is the tree node collapsed? + isCollapsed?: boolean + } + + export function areStatesEqual(a: State, b: State) { + return !!a.isHidden !== !!b.isHidden || !!a.isCollapsed !== !!b.isCollapsed + || !!a.isGhost !== !!b.isGhost || !!a.isLocked !== !!b.isLocked; + } + + export function isStateChange(a: State, b?: Partial<State>) { + if (!b) return false; + if (typeof b.isCollapsed !== 'undefined' && a.isCollapsed !== b.isCollapsed) return true; + if (typeof b.isHidden !== 'undefined' && a.isHidden !== b.isHidden) return true; + if (typeof b.isGhost !== 'undefined' && a.isGhost !== b.isGhost) return true; + if (typeof b.isLocked !== 'undefined' && a.isLocked !== b.isLocked) return true; + return false; + } + + export function assignState(a: State, b?: Partial<State>): boolean { + if (!b) return false; + + let changed = false; + for (const k of Object.keys(b)) { + const s = (b as any)[k], t = (a as any)[k]; + if (!!s === !!t) continue; + changed = true; + (a as any)[k] = s; + } + return changed; + } + + export function syncState(a: State, b?: Partial<State>): boolean { + if (!b) return false; + + let changed = false; + for (const k of Object.keys(b)) { + const s = (b as any)[k], t = (a as any)[k]; + if (!!s === !!t) continue; + changed = true; + (a as any)[k] = s; + } + for (const k of Object.keys(a)) { + const s = (b as any)[k], t = (a as any)[k]; + if (!!s === !!t) continue; + changed = true; + (a as any)[k] = s; + } + return changed; } export interface Options { ref?: string, - props?: Props + tags?: string | string[], + state?: State } export function create<T extends StateTransformer>(parent: Ref, transformer: T, params?: StateTransformer.Params<T>, options?: Options): Transform<T> { const ref = options && options.ref ? options.ref : UUID.create22() as string as Ref; + let tags: string[] | undefined = void 0; + if (options && options.tags) { + tags = typeof options.tags === 'string' ? [options.tags] : options.tags; + } return { parent, transformer, - props: (options && options.props) || { }, + state: (options && options.state) || { }, + tags, ref, params, version: UUID.create22() @@ -52,23 +109,30 @@ namespace Transform { return { ...t, params, version: UUID.create22() }; } + export function withState(t: Transform, state?: Partial<State>): Transform { + if (!state) return t; + return { ...t, state: { ...t.state, ...state } }; + } + 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(state?: State): Transform { + return create(RootRef, StateTransformer.ROOT, {}, { ref: RootRef, state }); } - export function createRoot(props?: Props): Transform { - return create(RootRef, StateTransformer.ROOT, {}, { ref: RootRef, props }); + export function hasTag(t: Transform, tag: string) { + if (!t.tags) return false; + return t.tags.indexOf(tag) >= 0; } export interface Serialized { parent: string, transformer: string, params: any, - props: Props, + state?: State, + tags?: string[], ref: string, version: string } @@ -78,11 +142,19 @@ namespace Transform { const pToJson = t.transformer.definition.customSerialization ? t.transformer.definition.customSerialization.toJSON : _id; + let state: any = void 0; + for (const k of Object.keys(t.state)) { + const s = (t.state as any)[k]; + if (!s) continue; + if (!state) state = { }; + state[k] = true; + } return { parent: t.parent, transformer: t.transformer.id, params: t.params ? pToJson(t.params) : void 0, - props: t.props, + state, + tags: t.tags, ref: t.ref, version: t.version }; @@ -97,7 +169,8 @@ namespace Transform { parent: t.parent as Ref, transformer, params: t.params ? pFromJson(t.params) : void 0, - props: t.props, + state: t.state || { }, + tags: t.tags, ref: t.ref as Ref, version: t.version }; diff --git a/src/mol-state/transformer.ts b/src/mol-state/transformer.ts index 01759e7382efe88f25f3311f1509300c06290ced..4e60189b745c7bbd21e844f0a28f230c736cd1cc 100644 --- a/src/mol-state/transformer.ts +++ b/src/mol-state/transformer.ts @@ -86,6 +86,9 @@ namespace Transformer { /** By default, returns true */ isSerializable?(params: P): { isSerializable: true } | { isSerializable: false; reason: string }, + /** Parameter interpolation */ + interpolate?(src: P, target: P, t: number, globalCtx: unknown): P + /** Custom conversion to and from JSON */ readonly customSerialization?: { toJSON(params: P, obj?: B): any, fromJSON(data: any): P } } @@ -95,7 +98,7 @@ namespace Transformer { readonly from: StateObject.Ctor[], readonly to: StateObject.Ctor[], readonly display: { readonly name: string, readonly description?: string }, - params?(a: A | undefined, globalCtx: unknown): { [K in keyof P]: PD.Any }, + params?(a: A | undefined, globalCtx: unknown): { [K in keyof P]: PD.Any } } const registry = new Map<Id, Transformer<any, any>>(); diff --git a/src/mol-state/tree/immutable.ts b/src/mol-state/tree/immutable.ts index ab3d8f60301414dc4deb67d41348a393eea6e2f5..fba1811e7e3108decbb0b36023e791ec8de11a51 100644 --- a/src/mol-state/tree/immutable.ts +++ b/src/mol-state/tree/immutable.ts @@ -7,7 +7,6 @@ import { Map as ImmutableMap, OrderedSet } from 'immutable'; import { StateTransform } from '../transform'; import { TransientTree } from './transient'; -import { StateObjectCell } from 'mol-state/object'; export { StateTree } @@ -19,7 +18,6 @@ interface StateTree { readonly root: StateTransform, readonly transforms: StateTree.Transforms, readonly children: StateTree.Children, - readonly cellStates: StateTree.CellStates, asTransient(): TransientTree } @@ -43,7 +41,6 @@ namespace StateTree { 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(StateTransform.RootRef)! } @@ -52,7 +49,7 @@ namespace StateTree { return new TransientTree(this); } - constructor(public transforms: StateTree.Transforms, public children: Children, public cellStates: CellStates) { + constructor(public transforms: StateTree.Transforms, public children: Children) { } } @@ -61,11 +58,11 @@ namespace StateTree { */ 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]])); + return create(ImmutableMap([[root.ref, root]]), ImmutableMap([[root.ref, OrderedSet()]])); } - export function create(nodes: Transforms, children: Children, cellStates: CellStates): StateTree { - return new Impl(nodes, children, cellStates); + export function create(nodes: Transforms, children: Children): StateTree { + return new Impl(nodes, children); } type VisitorCtx = { tree: StateTree, state: any, f: (node: StateTransform, tree: StateTree, state: any) => boolean | undefined | void }; @@ -116,19 +113,19 @@ namespace StateTree { return doPostOrder<StateTransform[]>(tree, root, [], _subtree); } - function _visitNodeToJson(node: StateTransform, tree: StateTree, ctx: [StateTransform.Serialized, StateObjectCell.State][]) { + function _visitNodeToJson(node: StateTransform, tree: StateTree, ctx: StateTransform.Serialized[]) { // const children: Ref[] = []; // tree.children.get(node.ref).forEach(_visitChildToJson as any, children); - ctx.push([StateTransform.toJSON(node), tree.cellStates.get(node.ref)]); + ctx.push(StateTransform.toJSON(node)); } export interface Serialized { /** Transforms serialized in pre-order */ - transforms: [StateTransform.Serialized, StateObjectCell.State][] + transforms: StateTransform.Serialized[] } export function toJSON(tree: StateTree): Serialized { - const transforms: [StateTransform.Serialized, StateObjectCell.State][] = []; + const transforms: StateTransform.Serialized[] = []; doPreOrder(tree, tree.root, transforms, _visitNodeToJson); return { transforms }; } @@ -136,12 +133,10 @@ namespace StateTree { 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 = StateTransform.fromJSON(t[0]); + const transform = StateTransform.fromJSON(t); nodes.set(transform.ref, transform); - cellStates.set(transform.ref, t[1]); if (!children.has(transform.ref)) { children.set(transform.ref, OrderedSet<Ref>().asMutable()); @@ -151,19 +146,18 @@ namespace StateTree { } for (const t of data.transforms) { - const ref = t[0].ref; + const ref = t.ref; children.set(ref, children.get(ref).asImmutable()); } - return create(nodes.asImmutable(), children.asImmutable(), cellStates.asImmutable()); + return create(nodes.asImmutable(), children.asImmutable()); } export function dump(tree: StateTree) { console.log({ tr: (tree.transforms as ImmutableMap<any, any>).keySeq().toArray(), tr1: (tree.transforms as ImmutableMap<any, any>).valueSeq().toArray().map(t => t.ref), - ch: (tree.children as ImmutableMap<any, any>).keySeq().toArray(), - cs: (tree.cellStates as ImmutableMap<any, any>).keySeq().toArray() + ch: (tree.children as ImmutableMap<any, any>).keySeq().toArray() }); } } \ No newline at end of file diff --git a/src/mol-state/tree/transient.ts b/src/mol-state/tree/transient.ts index a48e8e6635778e3037298890dedf69f5f892f1b8..c9a5e8d8402a08a0c2115b2caca26af4bb424105 100644 --- a/src/mol-state/tree/transient.ts +++ b/src/mol-state/tree/transient.ts @@ -7,7 +7,6 @@ import { Map as ImmutableMap, OrderedSet } from 'immutable'; import { StateTransform } from '../transform'; import { StateTree } from './immutable'; -import { StateObjectCell } from 'mol-state/object'; import { shallowEqual } from 'mol-util/object'; export { TransientTree } @@ -15,13 +14,12 @@ export { TransientTree } class TransientTree implements StateTree { 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<StateTransform.Ref, OrderedSet<StateTransform.Ref>> | undefined = void 0; + private _stateUpdates: Set<StateTransform.Ref> | undefined = void 0; private get childMutations() { if (this._childMutations) return this._childMutations; @@ -29,12 +27,6 @@ class TransientTree implements StateTree { return this._childMutations; } - private changeStates() { - if (this.changedStates) return; - this.changedStates = true; - this.cellStates = this.cellStates.asMutable(); - } - private changeNodes() { if (this.changedNodes) return; this.changedNodes = true; @@ -49,10 +41,6 @@ class TransientTree implements StateTree { get root() { return this.transforms.get(StateTransform.RootRef)! } - cellStatesSnapshot() { - return this.cellStates.asImmutable(); - } - asTransient() { return this.asImmutable().asTransient(); } @@ -104,15 +92,7 @@ class TransientTree implements StateTree { 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>) { + add(transform: StateTransform) { const ref = transform.ref; if (this.transforms.has(transform.ref)) { @@ -138,15 +118,6 @@ class TransientTree implements StateTree { this.changeNodes(); this.transforms.set(ref, transform); - if (!this.cellStates.has(ref)) { - this.changeStates(); - if (StateObjectCell.isStateChange(StateObjectCell.DefaultState, initialState)) { - this.cellStates.set(ref, { ...StateObjectCell.DefaultState, ...initialState }); - } else { - this.cellStates.set(ref, StateObjectCell.DefaultState); - } - } - return this; } @@ -169,16 +140,21 @@ class TransientTree implements StateTree { return true; } - updateCellState(ref: StateTransform.Ref, state: Partial<StateObjectCell.State>) { + assignState(ref: StateTransform.Ref, state?: Partial<StateTransform.State>) { ensurePresent(this.transforms, ref); - const old = this.cellStates.get(ref); - if (!StateObjectCell.isStateChange(old, state)) return false; - - this.changeStates(); - this.cellStates.set(ref, { ...old, ...state }); - - return true; + const old = this.transforms.get(ref); + if (this._stateUpdates && this._stateUpdates.has(ref)) { + StateTransform.assignState(old.state, state); + return old; + } else { + if (!this._stateUpdates) this._stateUpdates = new Set(); + this._stateUpdates.add(old.ref); + this.changeNodes(); + const updated = StateTransform.withState(old, state); + this.transforms.set(ref, updated); + return updated; + } } remove(ref: StateTransform.Ref): StateTransform[] { @@ -197,12 +173,10 @@ class TransientTree implements StateTree { this.changeNodes(); this.changeChildren(); - this.changeStates(); for (const n of st) { this.transforms.delete(n.ref); this.children.delete(n.ref); - this.cellStates.delete(n.ref); if (this._childMutations) this._childMutations.delete(n.ref); } @@ -210,12 +184,11 @@ class TransientTree implements StateTree { } asImmutable() { - if (!this.changedNodes && !this.changedChildren && !this.changedStates && !this._childMutations) return this.tree; + if (!this.changedNodes && !this.changedChildren && !this._childMutations) return this.tree; if (this._childMutations) this._childMutations.forEach(fixChildMutations, this.children); return StateTree.create( this.changedNodes ? this.transforms.asImmutable() : this.transforms, - this.changedChildren ? this.children.asImmutable() : this.children, - this.changedStates ? this.cellStates.asImmutable() : this.cellStates); + this.changedChildren ? this.children.asImmutable() : this.children); } constructor(private tree: StateTree) { diff --git a/src/mol-task/util/user-timing.ts b/src/mol-task/util/user-timing.ts index a4e3b7e33808f2dad86b1c59c929a85dda08a550..54456d65fa3b1cecce102d16b8ac492f297d15b7 100644 --- a/src/mol-task/util/user-timing.ts +++ b/src/mol-task/util/user-timing.ts @@ -5,14 +5,10 @@ */ import { Task } from '../task' +import { isProductionMode } from 'mol-util/debug'; const hasPerformance = typeof performance !== 'undefined' -/** - * on node `process.env.NODE_ENV` is available, in webpack build it is automatically set - * by the DefinePlugin to the webpack `mode` value - */ -const productionMode = process.env.NODE_ENV === 'production' -const timingEnabled = hasPerformance && !productionMode +const timingEnabled = hasPerformance && !isProductionMode export namespace UserTiming { function startMarkName(task: Task<any>) { return `startTask${task.id}` } diff --git a/src/mol-theme/color/secondary-structure.ts b/src/mol-theme/color/secondary-structure.ts index 384c5fb0b7d3c37031ef670b0d17a9e7b9f79ade..cb8878f28b77889d830573b5609a3b1c32feed6e 100644 --- a/src/mol-theme/color/secondary-structure.ts +++ b/src/mol-theme/color/secondary-structure.ts @@ -22,6 +22,8 @@ const SecondaryStructureColors = ColorMap({ 'betaTurn': 0x6080FF, 'betaStrand': 0xFFC800, 'coil': 0xFFFFFF, + 'bend': 0x66D8C9 /* biting original color used 0x00FF00 */, + 'turn': 0x00B266, 'dna': 0xAE00FE, 'rna': 0xFD0162, @@ -53,8 +55,10 @@ export function secondaryStructureColor(unit: Unit, element: ElementIndex): Colo return SecondaryStructureColors.alphaHelix } else if (SecondaryStructureType.is(secStrucType, SecondaryStructureType.Flag.Beta)) { return SecondaryStructureColors.betaStrand + } else if (SecondaryStructureType.is(secStrucType, SecondaryStructureType.Flag.Bend)) { + return SecondaryStructureColors.bend } else if (SecondaryStructureType.is(secStrucType, SecondaryStructureType.Flag.Turn)) { - return SecondaryStructureColors.coil + return SecondaryStructureColors.turn } else { const moleculeType = getElementMoleculeType(unit, element) if (moleculeType === MoleculeType.DNA) { diff --git a/src/mol-theme/color/shape-group.ts b/src/mol-theme/color/shape-group.ts index b352ecd50e0581296b9cd59a3ac9669214a670ac..905cce02c84062540b7465a00d98868e5ffb946a 100644 --- a/src/mol-theme/color/shape-group.ts +++ b/src/mol-theme/color/shape-group.ts @@ -7,7 +7,7 @@ import { ColorTheme } from '../color'; import { Color } from 'mol-util/color'; import { Location } from 'mol-model/location'; -import { Shape } from 'mol-model/shape'; +import { ShapeGroup } from 'mol-model/shape'; import { ParamDefinition as PD } from 'mol-util/param-definition' import { ThemeDataContext } from 'mol-theme/theme'; @@ -25,7 +25,7 @@ export function ShapeGroupColorTheme(ctx: ThemeDataContext, props: PD.Values<Sha factory: ShapeGroupColorTheme, granularity: 'groupInstance', color: (location: Location): Color => { - if (Shape.isLocation(location)) { + if (ShapeGroup.isLocation(location)) { return location.shape.getColor(location.group, location.instance) } return DefaultColor diff --git a/src/mol-theme/label.ts b/src/mol-theme/label.ts index 8aac229a59d8db1ca68eeab84eb03b921ec55f0c..69b67a6b9b4559b4e12c9e1bc5bd8c52313d6bb8 100644 --- a/src/mol-theme/label.ts +++ b/src/mol-theme/label.ts @@ -33,6 +33,8 @@ export function labelFirst(loci: Loci): string { case 'link-loci': const link = loci.links[0] return link ? linkLabel(link) : 'Unknown' + case 'shape-loci': + return loci.shape.name case 'group-loci': const g = loci.groups[0] if (g) { diff --git a/src/mol-theme/size/shape-group.ts b/src/mol-theme/size/shape-group.ts index 60fa329f3aacd2ee175aedbc8122e9d6398dc505..8bfcdf4f715519e1e91b88c2914795671857c955 100644 --- a/src/mol-theme/size/shape-group.ts +++ b/src/mol-theme/size/shape-group.ts @@ -5,7 +5,7 @@ */ import { Location } from 'mol-model/location'; -import { Shape } from 'mol-model/shape'; +import { ShapeGroup } from 'mol-model/shape'; import { ParamDefinition as PD } from 'mol-util/param-definition' import { ThemeDataContext } from 'mol-theme/theme'; import { SizeTheme } from 'mol-theme/size'; @@ -24,7 +24,7 @@ export function ShapeGroupSizeTheme(ctx: ThemeDataContext, props: PD.Values<Shap factory: ShapeGroupSizeTheme, granularity: 'groupInstance', size: (location: Location): number => { - if (Shape.isLocation(location)) { + if (ShapeGroup.isLocation(location)) { return location.shape.getSize(location.group, location.instance) } return DefaultSize diff --git a/src/mol-theme/transparency.ts b/src/mol-theme/transparency.ts new file mode 100644 index 0000000000000000000000000000000000000000..404a55b220aa6992dc2282fb9dfd16b408476a1a --- /dev/null +++ b/src/mol-theme/transparency.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { Loci, EmptyLoci } from 'mol-model/loci'; + +export { Transparency } + +interface Transparency { + readonly loci: Loci + readonly value: number + readonly variant: Transparency.Variant +} + +namespace Transparency { + export type Variant = 'single' | 'multi' + export const Empty: Transparency = { loci: EmptyLoci, value: 0, variant: 'single' } + + export function areEqual(tA: Transparency, tB: Transparency) { + if (tA.value !== tB.value) return false + if (tA.variant !== tB.variant) return false + if (!Loci.areEqual(tA.loci, tB.loci)) return false + return true + } +} \ No newline at end of file diff --git a/src/mol-util/_spec/reference-cache.spec.ts b/src/mol-util/_spec/reference-cache.spec.ts index 544b642ecfb686d638c8f718f890f5271a552007..0dc3bf4b03d870ca162a5773a12e5ce438efdbd5 100644 --- a/src/mol-util/_spec/reference-cache.spec.ts +++ b/src/mol-util/_spec/reference-cache.spec.ts @@ -10,20 +10,19 @@ describe('reference-cache', () => { it('basic', () => { const refCache = createReferenceCache( (x: number) => x.toString(), - (ctx: {}, x) => x, + (x) => x, () => {} ) expect(refCache.count).toBe(0) - const ctx = {} - const ref2a = refCache.get(ctx, 2) + const ref2a = refCache.get(2) expect(refCache.count).toBe(1) - const ref2b = refCache.get(ctx, 2) + const ref2b = refCache.get(2) expect(refCache.count).toBe(1) expect(ref2b.value).toBe(2) - const ref3 = refCache.get(ctx, 3) + const ref3 = refCache.get(3) expect(refCache.count).toBe(2) expect(ref3.value).toBe(3) diff --git a/src/mol-util/array.ts b/src/mol-util/array.ts index 078ab5319b4db6bdd612c7655390a8e4a0e2225f..fae588111095541e1614aca6a6b40b0010602c7a 100644 --- a/src/mol-util/array.ts +++ b/src/mol-util/array.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> */ @@ -9,7 +9,7 @@ import { NumberArray } from './type-helpers'; // TODO move to mol-math as Vector??? /** Get the maximum value in an array */ -export function arrayMax(array: NumberArray) { +export function arrayMax(array: ArrayLike<number>) { let max = -Infinity for (let i = 0, il = array.length; i < il; ++i) { if (array[i] > max) max = array[i] @@ -18,7 +18,7 @@ export function arrayMax(array: NumberArray) { } /** Get the minimum value in an array */ -export function arrayMin(array: NumberArray) { +export function arrayMin(array: ArrayLike<number>) { let min = Infinity for (let i = 0, il = array.length; i < il; ++i) { if (array[i] < min) min = array[i] @@ -27,7 +27,7 @@ export function arrayMin(array: NumberArray) { } /** Get the sum of values in an array */ -export function arraySum(array: NumberArray, stride = 1, offset = 0) { +export function arraySum(array: ArrayLike<number>, stride = 1, offset = 0) { const n = array.length let sum = 0 for (let i = offset; i < n; i += stride) { @@ -37,12 +37,12 @@ export function arraySum(array: NumberArray, stride = 1, offset = 0) { } /** Get the mean of values in an array */ -export function arrayMean(array: NumberArray, stride = 1, offset = 0) { +export function arrayMean(array: ArrayLike<number>, stride = 1, offset = 0) { return arraySum(array, stride, offset) / (array.length / stride) } /** Get the root mean square of values in an array */ -export function arrayRms(array: NumberArray) { +export function arrayRms(array: ArrayLike<number>) { const n = array.length let sumSq = 0 for (let i = 0; i < n; ++i) { @@ -54,7 +54,13 @@ export function arrayRms(array: NumberArray) { /** Fill an array with serial numbers starting from 0 until n - 1 (defaults to array.length) */ export function fillSerial<T extends NumberArray> (array: T, n?: number) { - for (let i = 0, il = n ? Math.min(n, array.length) : array.length; i < il; ++i) array[ i ] = i + for (let i = 0, il = n ? Math.min(n, array.length) : array.length; i < il; ++i) array[i] = i + return array +} + +/** Fill an array with value starting from index 0 until n - 1 (defaults to array.length) */ +export function fillUniform<T extends NumberArray> (array: T, v: number, n?: number) { + for (let i = 0, il = n ? Math.min(n, array.length) : array.length; i < il; ++i) array[i] = v return array } diff --git a/src/mol-util/color/color.ts b/src/mol-util/color/color.ts index 150417a6820ebfb44ac7fe6ff46884b0c1e926fa..60260c745788673d11c3e6fcd8f588321b542584 100644 --- a/src/mol-util/color/color.ts +++ b/src/mol-util/color/color.ts @@ -5,6 +5,7 @@ */ import { NumberArray } from 'mol-util/type-helpers'; +import { Vec3 } from 'mol-math/linear-algebra'; /** RGB color triplet expressed as a single number */ export type Color = { readonly '@type': 'color' } & number @@ -24,11 +25,11 @@ export namespace Color { return `RGB: ${Color.toRgb(hexColor).join(', ')}` } - export function toRgb(hexColor: Color) { + export function toRgb(hexColor: Color): [number, number, number] { return [ hexColor >> 16 & 255, hexColor >> 8 & 255, hexColor & 255 ] } - export function toRgbNormalized(hexColor: Color) { + export function toRgbNormalized(hexColor: Color): [number, number, number] { return [ (hexColor >> 16 & 255) / 255, (hexColor >> 8 & 255) / 255, (hexColor & 255) / 255 ] } @@ -64,6 +65,22 @@ export namespace Color { return array } + /** Copies hex color to rgb vec3 */ + export function toVec3(out: Vec3, hexColor: Color) { + out[0] = (hexColor >> 16 & 255) + out[1] = (hexColor >> 8 & 255) + out[2] = (hexColor & 255) + return out + } + + /** Copies normalized (0 to 1) hex color to rgb vec3 */ + export function toVec3Normalized(out: Vec3, hexColor: Color) { + out[0] = (hexColor >> 16 & 255) / 255 + out[1] = (hexColor >> 8 & 255) / 255 + out[2] = (hexColor & 255) / 255 + return out + } + /** Linear interpolation between two colors */ export function interpolate(c1: Color, c2: Color, t: number): Color { const r1 = c1 >> 16 & 255 diff --git a/src/mol-util/debug.ts b/src/mol-util/debug.ts new file mode 100644 index 0000000000000000000000000000000000000000..5dc93664ac37fbb9825811dc1bd6e38e421d3d56 --- /dev/null +++ b/src/mol-util/debug.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +/** + * on node `process.env.NODE_ENV` is available, in webpack build it is automatically set + * by the DefinePlugin to the webpack `mode` value + */ +const isProductionMode = process.env.NODE_ENV === 'production' + +/** + * set to true to enable more comprehensive checks and assertions, + * mostly used in `mol-gl` + */ +const isDebugMode = process.env.DEBUG === '*' || process.env.DEBUG === 'molstar' + +export { isProductionMode, isDebugMode } \ No newline at end of file diff --git a/src/mol-util/float-packing.ts b/src/mol-util/float-packing.ts index 42087b10738d71adb4e1913c187cf5fd460b38cf..e75d31a6bc815612f561069aae7bffbf1c9998fb 100644 --- a/src/mol-util/float-packing.ts +++ b/src/mol-util/float-packing.ts @@ -15,7 +15,7 @@ export function encodeFloatLog(value: number) { return Math.log(value + 1.0) / f /** decode logarithmically encoded float */ export function decodeFloatLog(value: number) { return Math.exp(value * floatLogFactor) - 1.0 } -/** encode float as rgb triplet */ +/** encode float as normalized rgb triplet */ export function encodeFloatRGB(value: number) { value = clamp(value, 0.0, 16777216.0 - 1.0) + 1.0 const b = (value % 256) / 255.0 diff --git a/src/mol-util/param-definition.ts b/src/mol-util/param-definition.ts index a7f08f3cf1a3d8227703292d4c4078aeb862e499..67c6b8d06879da6e2dbd7dbe67e52ce27bc89e12 100644 --- a/src/mol-util/param-definition.ts +++ b/src/mol-util/param-definition.ts @@ -168,7 +168,7 @@ export namespace ParamDefinition { } export interface NamedParams<T = any, K = string> { name: K, params: T } - export type NamedParamUnion<P extends Params, K = keyof P> = K extends any ? NamedParams<P[K]['defaultValue'], K> : never + export type NamedParamUnion<P extends Params, K extends keyof P = keyof P> = K extends any ? NamedParams<P[K]['defaultValue'], K> : never export interface Mapped<T extends NamedParams<any, any>> extends Base<T> { type: 'mapped', select: Select<string>, @@ -284,7 +284,7 @@ export namespace ParamDefinition { return true; } - function isParamEqual(p: Any, a: any, b: any): boolean { + export function isParamEqual(p: Any, a: any, b: any): boolean { if (a === b) return true; if (!a) return !b; if (!b) return !a; diff --git a/src/mol-util/reference-cache.ts b/src/mol-util/reference-cache.ts index aa5f341c1349a1e656b5b11ffdcdbbc389604cbd..e383416cf487b5fdfd493418c1f76515984161e7 100644 --- a/src/mol-util/reference-cache.ts +++ b/src/mol-util/reference-cache.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> */ @@ -24,22 +24,22 @@ export function createReferenceItem<T>(ref: Reference<T>) { } } -export interface ReferenceCache<T, P, C> { - get: (ctx: C, props: P) => ReferenceItem<T> +export interface ReferenceCache<T, P> { + get: (props: P) => ReferenceItem<T> clear: () => void readonly count: number dispose: () => void } -export function createReferenceCache<T, P, C>(hashFn: (props: P) => string, ctor: (ctx: C, props: P) => T, deleteFn: (v: T) => void): ReferenceCache<T, P, C> { +export function createReferenceCache<T, P, C>(hashFn: (props: P) => string, ctor: (props: P) => T, deleteFn: (v: T) => void): ReferenceCache<T, P> { const map: Map<string, Reference<T>> = new Map() return { - get: (ctx: C, props: P) => { + get: (props: P) => { const id = hashFn(props) let ref = map.get(id) if (!ref) { - ref = createReference<T>(ctor(ctx, props)) + ref = createReference<T>(ctor(props)) map.set(id, ref) } ref.usageCount += 1 diff --git a/src/perf-tests/mol-script.ts b/src/perf-tests/mol-script.ts index 3895e0f8269a018c9bd9dae8ceba58744bb842ce..f459e3a203a5c48a0df98d490066240b8c9dcab3 100644 --- a/src/perf-tests/mol-script.ts +++ b/src/perf-tests/mol-script.ts @@ -1,6 +1,6 @@ import { MolScriptBuilder } from 'mol-script/language/builder'; import { compile, QuerySymbolRuntime, DefaultQueryRuntimeTable } from 'mol-script/runtime/query/compiler'; -import { QueryContext, Structure, StructureQuery, ModelPropertyDescriptor } from 'mol-model/structure'; +import { QueryContext, Structure, StructureQuery, CustomPropertyDescriptor } from 'mol-model/structure'; import { readCifFile, getModelsAndStructure } from '../apps/structure-info/model'; import { CustomPropSymbol } from 'mol-script/language/symbol'; import Type from 'mol-script/language/type'; @@ -46,7 +46,7 @@ const compiled = compile<number>(expr); const result = compiled(new QueryContext(Structure.Empty)); console.log(result); -const CustomProp = ModelPropertyDescriptor({ +const CustomProp = CustomPropertyDescriptor({ name: 'test_prop', isStatic: true, cifExport: { prefix: '', categories: [ ]}, diff --git a/src/tests/browser/index.html b/src/tests/browser/index.html index f28af95b2858709af424bf4ff1cbf49396329846..62d647dc8a15e3801ee7119a313c2b86927d1cc7 100644 --- a/src/tests/browser/index.html +++ b/src/tests/browser/index.html @@ -1,38 +1,37 @@ <!DOCTYPE html> <html lang="en"> - <head> - <meta charset="utf-8" /> - <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"> - <title>Mol* Browser Test</title> - <style> - * { - margin: 0; - padding: 0; - box-sizing: border-box; - } - html, body { - width: 100%; - height: 100%; - overflow: hidden; - } - </style> - </head> - <body> - <div id="app"></div> - <script type="text/javascript"> - function urlQueryParameter (id) { - if (typeof window === 'undefined') return undefined - const a = new RegExp(`${id}=([^&#=]*)`) - const m = a.exec(window.location.search) - return m ? decodeURIComponent(m[1]) : undefined - } - - const name = urlQueryParameter('name') - if (name) { - const script = document.createElement('script') - script.src = name + '.js' - document.body.appendChild(script) - } - </script> - </body> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"> + <title>Mol* Browser Test</title> + <style> + * { + margin: 0; + padding: 0; + box-sizing: border-box; + } + html, body { + width: 100%; + height: 100%; + overflow: hidden; + } + </style> + </head> + <body> + <div id="app"></div> + <script type="text/javascript"> + function urlQueryParameter (id) { + if (typeof window === 'undefined') return undefined + const a = new RegExp(`${id}=([^&#=]*)`) + const m = a.exec(window.location.search) + return m ? decodeURIComponent(m[1]) : undefined + } + const name = urlQueryParameter('name') + if (name) { + const script = document.createElement('script') + script.src = name + '.js' + document.body.appendChild(script) + } + </script> + </body> </html> \ No newline at end of file diff --git a/src/tests/browser/marching-cubes.ts b/src/tests/browser/marching-cubes.ts new file mode 100644 index 0000000000000000000000000000000000000000..2aa507cc08ade1e0745f08befbfd7fa8dd1103e9 --- /dev/null +++ b/src/tests/browser/marching-cubes.ts @@ -0,0 +1,147 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import './index.html' +import { Canvas3D } from 'mol-canvas3d/canvas3d'; +import { Representation } from 'mol-repr/representation'; +import { Color } from 'mol-util/color'; +import { createRenderObject } from 'mol-gl/render-object'; +import { computeGaussianDensity, computeGaussianDensityTexture2d } from 'mol-math/geometry/gaussian-density'; +import { PositionData, Box3D, Sphere3D } from 'mol-math/geometry'; +import { OrderedSet } from 'mol-data/int'; +import { Vec3 } from 'mol-math/linear-algebra'; +import { computeMarchingCubesMesh } from 'mol-geo/util/marching-cubes/algorithm'; +import { Mesh } from 'mol-geo/geometry/mesh/mesh'; +import { ColorNames } from 'mol-util/color/tables'; +import { TextureMesh } from 'mol-geo/geometry/texture-mesh/texture-mesh'; +import { calcActiveVoxels } from 'mol-gl/compute/marching-cubes/active-voxels'; +import { createHistogramPyramid } from 'mol-gl/compute/histogram-pyramid/reduction'; +import { createIsosurfaceBuffers } from 'mol-gl/compute/marching-cubes/isosurface'; +import { RendererParams } from 'mol-gl/renderer'; +import { ParamDefinition as PD } from 'mol-util/param-definition'; + +const parent = document.getElementById('app')! +parent.style.width = '100%' +parent.style.height = '100%' + +const canvas = document.createElement('canvas') +canvas.style.width = '100%' +canvas.style.height = '100%' +parent.appendChild(canvas) + +const canvas3d = Canvas3D.create(canvas, parent, { + renderer: { ...PD.getDefaultValues(RendererParams), backgroundColor: ColorNames.white }, + cameraMode: 'orthographic' +}) +canvas3d.animate() + +async function init() { + const { webgl } = canvas3d + + const position: PositionData = { + x: [0, 2], + y: [0, 2], + z: [0, 2], + indices: OrderedSet.ofSortedArray([0, 1]), + } + const box = Box3D.create(Vec3.create(0, 0, 0), Vec3.create(2, 2, 2)) + const radius = () => 1.8 + const props = { + resolution: 0.1, + radiusOffset: 0, + smoothness: 1.5 + } + const isoValue = Math.exp(-props.smoothness) + + if (true) { + console.time('gpu gaussian2') + const densityTextureData2 = await computeGaussianDensityTexture2d(position, box, radius, props, webgl).run() + webgl.waitForGpuCommandsCompleteSync() + console.timeEnd('gpu gaussian2') + + console.time('gpu mc2') + console.time('gpu mc active2') + const activeVoxelsTex2 = calcActiveVoxels(webgl, densityTextureData2.texture, densityTextureData2.gridDim, densityTextureData2.gridTexDim, isoValue, densityTextureData2.gridTexScale) + webgl.waitForGpuCommandsCompleteSync() + console.timeEnd('gpu mc active2') + + console.time('gpu mc pyramid2') + const compacted2 = createHistogramPyramid(webgl, activeVoxelsTex2, densityTextureData2.gridTexScale) + webgl.waitForGpuCommandsCompleteSync() + console.timeEnd('gpu mc pyramid2') + + console.time('gpu mc vert2') + createIsosurfaceBuffers(webgl, activeVoxelsTex2, densityTextureData2.texture, compacted2, densityTextureData2.gridDim, densityTextureData2.gridTexDim, densityTextureData2.transform, isoValue) + webgl.waitForGpuCommandsCompleteSync() + console.timeEnd('gpu mc vert2') + console.timeEnd('gpu mc2') + } + + console.time('gpu gaussian') + const densityTextureData = await computeGaussianDensityTexture2d(position, box, radius, props, webgl).run() + webgl.waitForGpuCommandsCompleteSync() + console.timeEnd('gpu gaussian') + + console.time('gpu mc') + console.time('gpu mc active') + const activeVoxelsTex = calcActiveVoxels(webgl, densityTextureData.texture, densityTextureData.gridDim, densityTextureData.gridTexDim, isoValue, densityTextureData.gridTexScale) + webgl.waitForGpuCommandsCompleteSync() + console.timeEnd('gpu mc active') + + console.time('gpu mc pyramid') + const compacted = createHistogramPyramid(webgl, activeVoxelsTex, densityTextureData.gridTexScale) + webgl.waitForGpuCommandsCompleteSync() + console.timeEnd('gpu mc pyramid') + + console.time('gpu mc vert') + const gv = createIsosurfaceBuffers(webgl, activeVoxelsTex, densityTextureData.texture, compacted, densityTextureData.gridDim, densityTextureData.gridTexDim, densityTextureData.transform, isoValue) + webgl.waitForGpuCommandsCompleteSync() + console.timeEnd('gpu mc vert') + console.timeEnd('gpu mc') + + console.log({ ...webgl.stats, programCount: webgl.programCache.count, shaderCount: webgl.shaderCache.count }) + + const mcBoundingSphere = Sphere3D.fromBox3D(Sphere3D(), densityTextureData.bbox) + const mcIsosurface = TextureMesh.create(gv.vertexCount, 1, gv.vertexGroupTexture, gv.normalTexture, mcBoundingSphere) + const mcIsoSurfaceProps = { doubleSided: true, flatShaded: true, alpha: 1.0 } + const mcIsoSurfaceValues = TextureMesh.Utils.createValuesSimple(mcIsosurface, mcIsoSurfaceProps, Color(0x112299), 1) + // console.log('mcIsoSurfaceValues', mcIsoSurfaceValues) + const mcIsoSurfaceState = TextureMesh.Utils.createRenderableState(mcIsoSurfaceProps) + const mcIsoSurfaceRenderObject = createRenderObject('texture-mesh', mcIsoSurfaceValues, mcIsoSurfaceState, -1) + const mcIsoSurfaceRepr = Representation.fromRenderObject('texture-mesh', mcIsoSurfaceRenderObject) + + canvas3d.add(mcIsoSurfaceRepr) + canvas3d.resetCamera() + + // + + console.time('cpu gaussian') + const densityData = await computeGaussianDensity(position, box, radius, { ...props, useGpu: false }, webgl).run() + console.timeEnd('cpu gaussian') + console.log({ densityData }) + + const params = { + isoLevel: isoValue, + scalarField: densityData.field, + idField: densityData.idField + } + + console.time('cpu mc') + const surface = await computeMarchingCubesMesh(params).run() + console.timeEnd('cpu mc') + console.log('surface', surface) + Mesh.transformImmediate(surface, densityData.transform) + const meshProps = { doubleSided: true, flatShaded: false, alpha: 1.0 } + const meshValues = Mesh.Utils.createValuesSimple(surface, meshProps, Color(0x995511), 1) + const meshState = Mesh.Utils.createRenderableState(meshProps) + const meshRenderObject = createRenderObject('mesh', meshValues, meshState, -1) + const meshRepr = Representation.fromRenderObject('mesh', meshRenderObject) + + canvas3d.add(meshRepr) + canvas3d.resetCamera() +} + +init() \ No newline at end of file diff --git a/src/tests/browser/render-lines.ts b/src/tests/browser/render-lines.ts index 9eeecd9a090d762b12fb75c563488aad41d40da6..40962cea9511bd2940f28dbf733a856028cd1f81 100644 --- a/src/tests/browser/render-lines.ts +++ b/src/tests/browser/render-lines.ts @@ -35,7 +35,7 @@ function linesRepr() { const values = Lines.Utils.createValuesSimple(lines, {}, Color(0xFF0000), 3) const state = Lines.Utils.createRenderableState({}) - const renderObject = createRenderObject('lines', values, state) + const renderObject = createRenderObject('lines', values, state, -1) const repr = Representation.fromRenderObject('cage-lines', renderObject) return repr } diff --git a/src/tests/browser/render-mesh.ts b/src/tests/browser/render-mesh.ts index 4c63a2eb90fbef2500adcdcfea766fc5d045e153..13d752b7d66c0c6fd45fa42cba6be670c3c57646 100644 --- a/src/tests/browser/render-mesh.ts +++ b/src/tests/browser/render-mesh.ts @@ -29,7 +29,7 @@ canvas3d.animate() function meshRepr() { const builderState = MeshBuilder.createState() - + const t = Mat4.identity() MeshBuilder.addCage(builderState, t, HexagonalPrismCage(), 0.005, 2) @@ -41,7 +41,7 @@ function meshRepr() { const values = Mesh.Utils.createValuesSimple(mesh, {}, Color(0xFF0000), 1) const state = Mesh.Utils.createRenderableState({}) - const renderObject = createRenderObject('mesh', values, state) + const renderObject = createRenderObject('mesh', values, state, -1) const repr = Representation.fromRenderObject('mesh', renderObject) return repr } diff --git a/src/tests/browser/render-shape.ts b/src/tests/browser/render-shape.ts index 943a50476785bf19aedb3bd7b959cc210ce54c75..adc6c9eae42b7c5b3c54ff6a68db6007d7ca8871 100644 --- a/src/tests/browser/render-shape.ts +++ b/src/tests/browser/render-shape.ts @@ -15,6 +15,9 @@ import { ColorNames } from 'mol-util/color/tables'; import { Mesh } from 'mol-geo/geometry/mesh/mesh'; import { labelFirst } from 'mol-theme/label'; import { RuntimeContext, Progress } from 'mol-task'; +import { Representation } from 'mol-repr/representation'; +import { MarkerAction } from 'mol-geo/geometry/marker-data'; +import { EveryLoci } from 'mol-model/loci'; const parent = document.getElementById('app')! parent.style.width = '100%' @@ -34,14 +37,23 @@ info.style.right = '20px' info.style.color = 'white' parent.appendChild(info) +let prevReprLoci = Representation.Loci.Empty const canvas3d = Canvas3D.create(canvas, parent) canvas3d.animate() canvas3d.input.move.subscribe(async ({x, y}) => { const pickingId = await canvas3d.identify(x, y) let label = '' if (pickingId) { - const { loci } = canvas3d.getLoci(pickingId) - label = labelFirst(loci) + const reprLoci = canvas3d.getLoci(pickingId) + label = labelFirst(reprLoci.loci) + if (!Representation.Loci.areEqual(prevReprLoci, reprLoci)) { + canvas3d.mark(prevReprLoci, MarkerAction.RemoveHighlight) + canvas3d.mark(reprLoci, MarkerAction.Highlight) + prevReprLoci = reprLoci + } + } else { + canvas3d.mark({ loci: EveryLoci }, MarkerAction.RemoveHighlight) + prevReprLoci = Representation.Loci.Empty } info.innerText = label }) @@ -56,7 +68,7 @@ async function getSphereMesh(ctx: RuntimeContext, centers: number[], mesh?: Mesh const builderState = MeshBuilder.createState(centers.length * 128, centers.length * 128 / 2, mesh) const t = Mat4.identity() const v = Vec3.zero() - const sphere = Sphere(2) + const sphere = Sphere(3) builderState.currentGroup = 0 for (let i = 0, il = centers.length / 3; i < il; ++i) { // for production, calls to update should be guarded by `if (ctx.shouldUpdate)` @@ -69,8 +81,8 @@ async function getSphereMesh(ctx: RuntimeContext, centers: number[], mesh?: Mesh } const myData = { - centers: [0, 0, 0, 0, 3, 0], - colors: [ColorNames.tomato, ColorNames.springgreen], + centers: [0, 0, 0, 0, 3, 0, 1, 0 , 4], + colors: [ColorNames.tomato, ColorNames.springgreen, ColorNames.springgreen], labels: ['Sphere 0, Instance A', 'Sphere 1, Instance A', 'Sphere 0, Instance B', 'Sphere 1, Instance B'], transforms: [Mat4.identity(), Mat4.fromTranslation(Mat4.zero(), Vec3.create(3, 0, 0))] } @@ -84,8 +96,8 @@ async function getShape(ctx: RuntimeContext, data: MyData, props: {}, shape?: Sh const { centers, colors, labels, transforms } = data const mesh = await getSphereMesh(ctx, centers, shape && shape.geometry) const groupCount = centers.length / 3 - return shape || Shape.create( - 'test', mesh, + return Shape.create( + 'test', data, mesh, (groupId: number) => colors[groupId], // color: per group, same for instances () => 1, // size: constant (groupId: number, instanceId: number) => labels[instanceId * groupCount + groupId], // label: per group and instance @@ -96,10 +108,9 @@ async function getShape(ctx: RuntimeContext, data: MyData, props: {}, shape?: Sh // Init ShapeRepresentation container const repr = ShapeRepresentation(getShape, Mesh.Utils) -async function init() { +export async function init() { // Create shape from myData and add to canvas3d await repr.createOrUpdate({}, myData).run((p: Progress) => console.log(Progress.format(p))) - console.log(repr) canvas3d.add(repr) canvas3d.resetCamera() diff --git a/src/tests/browser/render-spheres.ts b/src/tests/browser/render-spheres.ts index d17e32eb5250a57ee4376f759b57c7d22a96af77..c933d27819eb94058201acaee3b175be2f4f1a84 100644 --- a/src/tests/browser/render-spheres.ts +++ b/src/tests/browser/render-spheres.ts @@ -33,7 +33,7 @@ function spheresRepr() { const values = Spheres.Utils.createValuesSimple(spheres, {}, Color(0xFF0000), 1) const state = Spheres.Utils.createRenderableState({}) - const renderObject = createRenderObject('spheres', values, state) + const renderObject = createRenderObject('spheres', values, state, -1) console.log(renderObject) const repr = Representation.fromRenderObject('spheres', renderObject) return repr diff --git a/src/tests/browser/render-structure.ts b/src/tests/browser/render-structure.ts index 9dae8a78c7380888fce1cf4db1a64259745cba21..290afa43d6b56c3b607aafcfdf98f8a7ac46724d 100644 --- a/src/tests/browser/render-structure.ts +++ b/src/tests/browser/render-structure.ts @@ -13,6 +13,9 @@ import { SizeTheme } from 'mol-theme/size'; import { CartoonRepresentationProvider } from 'mol-repr/structure/representation/cartoon'; import { trajectoryFromMmCIF } from 'mol-model-formats/structure/mmcif'; import { computeModelDSSP } from 'mol-model/structure/model/properties/utils/secondary-structure'; +import { MolecularSurfaceRepresentationProvider } from 'mol-repr/structure/representation/molecular-surface'; +import { BallAndStickRepresentationProvider } from 'mol-repr/structure/representation/ball-and-stick'; +import { GaussianSurfaceRepresentationProvider } from 'mol-repr/structure/representation/gaussian-surface'; const parent = document.getElementById('app')! parent.style.width = '100%' @@ -61,24 +64,81 @@ function getCartoonRepr() { return CartoonRepresentationProvider.factory(reprCtx, CartoonRepresentationProvider.getParams) } +function getBallAndStickRepr() { + return BallAndStickRepresentationProvider.factory(reprCtx, BallAndStickRepresentationProvider.getParams) +} + +function getMolecularSurfaceRepr() { + return MolecularSurfaceRepresentationProvider.factory(reprCtx, MolecularSurfaceRepresentationProvider.getParams) +} + +function getGaussianSurfaceRepr() { + return GaussianSurfaceRepresentationProvider.factory(reprCtx, GaussianSurfaceRepresentationProvider.getParams) +} + async function init() { - const cif = await downloadFromPdb('3j3q') + const cif = await downloadFromPdb('1crn') const models = await getModels(cif) console.time('computeModelDSSP') const secondaryStructure = computeModelDSSP(models[0].atomicHierarchy, models[0].atomicConformation) - console.timeEnd('computeModelDSSP') - ;(models[0].properties as any).secondaryStructure = secondaryStructure + console.timeEnd('computeModelDSSP'); + (models[0].properties as any).secondaryStructure = secondaryStructure const structure = await getStructure(models[0]) + + const show = { + cartoon: false, + ballAndStick: true, + molecularSurface: true, + gaussianSurface: false, + } + const cartoonRepr = getCartoonRepr() + const ballAndStickRepr = getBallAndStickRepr() + const molecularSurfaceRepr = getMolecularSurfaceRepr() + const gaussianSurfaceRepr = getGaussianSurfaceRepr() + + if (show.cartoon) { + cartoonRepr.setTheme({ + color: reprCtx.colorThemeRegistry.create('secondary-structure', { structure }), + size: reprCtx.sizeThemeRegistry.create('uniform', { structure }) + }) + await cartoonRepr.createOrUpdate({ ...CartoonRepresentationProvider.defaultValues, quality: 'auto' }, structure).run() + } + + if (show.ballAndStick) { + ballAndStickRepr.setTheme({ + color: reprCtx.colorThemeRegistry.create('secondary-structure', { structure }), + size: reprCtx.sizeThemeRegistry.create('uniform', { structure }) + }) + await ballAndStickRepr.createOrUpdate({ ...BallAndStickRepresentationProvider.defaultValues, quality: 'auto' }, structure).run() + } + + if (show.molecularSurface) { + molecularSurfaceRepr.setTheme({ + color: reprCtx.colorThemeRegistry.create('secondary-structure', { structure }), + size: reprCtx.sizeThemeRegistry.create('physical', { structure }) + }) + console.time('molecular surface') + await molecularSurfaceRepr.createOrUpdate({ ...MolecularSurfaceRepresentationProvider.defaultValues, quality: 'custom', alpha: 0.5, flatShaded: true, doubleSided: true, resolution: 0.3 }, structure).run() + console.timeEnd('molecular surface') + } + + if (show.gaussianSurface) { + gaussianSurfaceRepr.setTheme({ + color: reprCtx.colorThemeRegistry.create('secondary-structure', { structure }), + size: reprCtx.sizeThemeRegistry.create('physical', { structure }) + }) + console.time('gaussian surface') + await gaussianSurfaceRepr.createOrUpdate({ ...GaussianSurfaceRepresentationProvider.defaultValues, quality: 'custom', alpha: 1.0, flatShaded: true, doubleSided: true, resolution: 0.3 }, structure).run() + console.timeEnd('gaussian surface') + } - cartoonRepr.setTheme({ - color: reprCtx.colorThemeRegistry.create('secondary-structure', { structure }), - size: reprCtx.sizeThemeRegistry.create('uniform', { structure }) - }) - await cartoonRepr.createOrUpdate({ ...CartoonRepresentationProvider.defaultValues, quality: 'auto' }, structure).run() - - canvas3d.add(cartoonRepr) + if (show.cartoon) canvas3d.add(cartoonRepr) + if (show.ballAndStick) canvas3d.add(ballAndStickRepr) + if (show.molecularSurface) canvas3d.add(molecularSurfaceRepr) + if (show.gaussianSurface) canvas3d.add(gaussianSurfaceRepr) canvas3d.resetCamera() + // canvas3d.setProps({ trackball: { ...canvas3d.props.trackball, spin: true } }) } init() \ No newline at end of file diff --git a/src/tests/browser/render-text.ts b/src/tests/browser/render-text.ts index 4c453479b34dcd3041d72219f577b1b7e143bbb8..c8f55c2fb9659ecf1883bac6a07b129ac3e148f5 100644 --- a/src/tests/browser/render-text.ts +++ b/src/tests/browser/render-text.ts @@ -51,7 +51,7 @@ function textRepr() { const values = Text.Utils.createValuesSimple(text, props, Color(0xFFDD00), 1) const state = Text.Utils.createRenderableState(props) - const renderObject = createRenderObject('text', values, state) + const renderObject = createRenderObject('text', values, state, -1) console.log('text', renderObject, props) const repr = Representation.fromRenderObject('text', renderObject) return repr @@ -66,7 +66,7 @@ function spheresRepr() { const values = Spheres.Utils.createValuesSimple(spheres, {}, Color(0xFF0000), 0.2) const state = Spheres.Utils.createRenderableState({}) - const renderObject = createRenderObject('spheres', values, state) + const renderObject = createRenderObject('spheres', values, state, -1) console.log('spheres', renderObject) const repr = Representation.fromRenderObject('spheres', renderObject) return repr diff --git a/webpack.config.js b/webpack.config.js index 59557a3d688f91d2d87b2543bdf469f2495ffc0e..28c3be13aab7da7eb0e18b7b6dfeacd9edfa8b97 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), + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), + 'process.env.DEBUG': JSON.stringify(process.env.DEBUG) }), new MiniCssExtractPlugin({ filename: 'app.css' }) ], @@ -103,6 +105,7 @@ module.exports = [ createBrowserTest('font-atlas'), createBrowserTest('render-asa'), + createBrowserTest('marching-cubes'), createBrowserTest('render-lines'), createBrowserTest('render-mesh'), createBrowserTest('render-shape'),