diff --git a/package-lock.json b/package-lock.json index 4d46805a5edccbc2fb5c26a992a06e77ac016ed1..63ac0178bd15b50ad3c918ff905ffecf351dbaf9 100644 Binary files a/package-lock.json and b/package-lock.json differ diff --git a/package.json b/package.json index 31377fcba688342aebbb61ffdffcbdbf544d1ce1..8d149012463de799160e12c086b3f8724f97eb26 100644 --- a/package.json +++ b/package.json @@ -76,9 +76,9 @@ "@types/compression": "0.0.36", "@types/express": "^4.16.0", "@types/jest": "^23.3.1", - "@types/node": "^10.7.1", + "@types/node": "^10.9.4", "@types/node-fetch": "^2.1.2", - "@types/react": "^16.4.11", + "@types/react": "^16.4.13", "@types/react-dom": "^16.0.7", "benchmark": "^2.1.4", "cpx": "^1.5.0", @@ -97,11 +97,11 @@ "raw-loader": "^0.5.1", "resolve-url-loader": "^2.3.0", "sass-loader": "^7.1.0", - "style-loader": "^0.22.1", + "style-loader": "^0.23.0", "ts-jest": "^23.1.4", "tslint": "^5.11.0", - "typescript": "^3.0.1", - "uglify-js": "^3.4.7", + "typescript": "^3.0.3", + "uglify-js": "^3.4.9", "util.promisify": "^1.0.0", "webpack": "^4.17.1", "webpack-cli": "^3.1.0" @@ -116,6 +116,6 @@ "node-fetch": "^2.2.0", "react": "^16.4.2", "react-dom": "^16.4.2", - "rxjs": "^6.2.2" + "rxjs": "^6.3.1" } } diff --git a/src/apps/canvas/app.ts b/src/apps/canvas/app.ts new file mode 100644 index 0000000000000000000000000000000000000000..9aeeccf00a3c9a622834b2372c37c854d62eef24 --- /dev/null +++ b/src/apps/canvas/app.ts @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import Viewer from 'mol-view/viewer'; +import { getCifFromUrl, getModelsFromMmcif, getCifFromFile } from './util'; +import { StructureView } from './structure-view'; +import { BehaviorSubject } from 'rxjs'; +import { CifBlock } from 'mol-io/reader/cif'; + +export class App { + viewer: Viewer + container: HTMLDivElement | null = null; + canvas: HTMLCanvasElement | null = null; + structureView: StructureView | null = null; + + pdbIdLoaded: BehaviorSubject<StructureView | null> = new BehaviorSubject<StructureView | null>(null) + + initViewer(_canvas: HTMLCanvasElement, _container: HTMLDivElement) { + this.canvas = _canvas + this.container = _container + + try { + this.viewer = Viewer.create(this.canvas, this.container) + this.viewer.animate() + return true + } catch (e) { + console.error(e) + return false + } + } + + async loadCif(cif: CifBlock, assemblyId?: string) { + const models = await getModelsFromMmcif(cif) + this.structureView = await StructureView(this.viewer, models, { assemblyId }) + this.pdbIdLoaded.next(this.structureView) + } + + async loadPdbId(id: string, assemblyId?: string) { + if (this.structureView) this.structureView.destroy() + const cif = await getCifFromUrl(`https://files.rcsb.org/download/${id}.cif`) + this.loadCif(cif, assemblyId) + } + + async loadCifFile(file: File) { + if (this.structureView) this.structureView.destroy() + const cif = await getCifFromFile(file) + this.loadCif(cif) + } +} \ No newline at end of file diff --git a/src/apps/canvas/assembly-symmetry.ts b/src/apps/canvas/assembly-symmetry.ts new file mode 100644 index 0000000000000000000000000000000000000000..69fee7dd07fd6acfb60c87a354f719dd556a2493 --- /dev/null +++ b/src/apps/canvas/assembly-symmetry.ts @@ -0,0 +1,99 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { AssemblySymmetry } from 'mol-model-props/rcsb/symmetry'; +import { Table } from 'mol-data/db'; +import { Color, ColorScale } from 'mol-util/color'; +import { MeshBuilder } from 'mol-geo/mesh/mesh-builder'; +import { Tensor } from 'mol-math/linear-algebra'; +import { addSphere } from 'mol-geo/mesh/builder/sphere'; +import { addCylinder } from 'mol-geo/mesh/builder/cylinder'; +import { Shape } from 'mol-model/shape'; +import { ColorTheme } from 'mol-view/theme/color'; +import { Location } from 'mol-model/location'; +import { StructureElement, Unit, StructureProperties } from 'mol-model/structure'; + +export function getAxesShape(featureId: number, assemblySymmetry: AssemblySymmetry) { + const f = assemblySymmetry.db.rcsb_assembly_symmetry_feature + const feature = Table.pickRow(f, i => f.id.value(i) === featureId) + if (!feature) return + + const axes = assemblySymmetry.getAxes(featureId) + if (!axes._rowCount) return + + const vectorSpace = AssemblySymmetry.Schema.rcsb_assembly_symmetry_axis.start.space; + + const colors: Color[] = [] + const labels: string[] = [] + + const radius = 0.4 + const cylinderProps = { radiusTop: radius, radiusBottom: radius } + const meshBuilder = MeshBuilder.create(256, 128) + + for (let i = 0, il = axes._rowCount; i < il; ++i) { + const start = Tensor.toVec3(vectorSpace, axes.start.value(i)) + const end = Tensor.toVec3(vectorSpace, axes.end.value(i)) + meshBuilder.setGroup(i) + addSphere(meshBuilder, start, radius, 2) + addSphere(meshBuilder, end, radius, 2) + addCylinder(meshBuilder, start, end, 1, cylinderProps) + colors.push(Color(0xCCEE11)) + labels.push(`Axis ${i + 1} for ${feature.symmetry_value} ${feature.type.toLowerCase()} symmetry`) + } + const mesh = meshBuilder.getMesh() + const shape = Shape.create('Axes', mesh, colors, labels) + return shape +} + +function getAsymId(unit: Unit): StructureElement.Property<string> { + switch (unit.kind) { + case Unit.Kind.Atomic: + return StructureProperties.chain.auth_asym_id // TODO + case Unit.Kind.Spheres: + case Unit.Kind.Gaussians: + return StructureProperties.coarse.asym_id + } +} + +function memberKey (asym_id: string, oper_list_id?: number) { + return `${asym_id}|${oper_list_id}` +} + +export function getClusterColorTheme(featureId: number, assemblySymmetry: AssemblySymmetry): ColorTheme { + const DefaultColor = Color(0xCCCCCC) + const f = assemblySymmetry.db.rcsb_assembly_symmetry_feature + const feature = Table.pickRow(f, i => f.id.value(i) === featureId) + if (!feature) return { granularity: 'uniform', color: () => DefaultColor } + + const clusters = assemblySymmetry.getClusters(featureId) + if (!clusters._rowCount) return { granularity: 'uniform', color: () => DefaultColor } + + const clusterByMember = new Map<string, number>() + for (let i = 0, il = clusters._rowCount; i < il; ++i) { + clusters.members.value(i).forEach(m => { + const ms = m.split('_') + const asym_id = ms[0] + const oper_list_id = ms.length === 2 ? parseInt(ms[1]) : undefined + clusterByMember.set(memberKey(asym_id, oper_list_id), i) + }) + } + + const scale = ColorScale.create({ domain: [ 0, clusters._rowCount - 1 ] }) + + return { + granularity: 'instance', + color: (location: Location): Color => { + if (StructureElement.isLocation(location)) { + const ns = location.unit.conformation.operator.name.split('-') + const asym_id = getAsymId(location.unit) + const oper_list_id = ns.length === 2 ? parseInt(ns[1]) : undefined + const cluster = clusterByMember.get(memberKey(asym_id(location), oper_list_id)) + return cluster !== undefined ? scale.color(cluster) : DefaultColor + } + return DefaultColor + } + } +} \ No newline at end of file diff --git a/src/apps/canvas/component/app.tsx b/src/apps/canvas/component/app.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6fe5f0872a62a8a5d552767872e1798c8c845426 --- /dev/null +++ b/src/apps/canvas/component/app.tsx @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import * as React from 'react' +import { StructureView } from '../structure-view'; +import { App } from '../app'; +import { Viewport } from './viewport'; +import { StructureViewComponent } from './structure-view'; + +// export function FileInput (props: { +// accept: string +// onChange: (v: FileList | null) => void, +// }) { +// return <input +// accept={props.accept || '*.*'} +// type='file' +// onChange={e => props.onChange.call(null, e.target.files)} +// /> +// } + +export interface AppProps { + app: App +} + +export interface AppState { + structureView: StructureView | null +} + +export class AppComponent extends React.Component<AppProps, AppState> { + state = { + structureView: this.props.app.structureView, + } + + componentDidMount() { + this.props.app.pdbIdLoaded.subscribe((structureView) => { + this.setState({ + structureView: this.props.app.structureView + }) + }) + } + + render() { + const { structureView } = this.state + + return <div style={{width: '100%', height: '100%'}}> + <div style={{left: '0px', right: '350px', height: '100%', position: 'absolute'}}> + <Viewport app={this.props.app} /> + </div> + + <div style={{width: '330px', paddingLeft: '10px', paddingRight: '10px', right: '0px', height: '100%', position: 'absolute', overflow: 'auto'}}> + <div style={{marginTop: '10px'}}> + <span>Load PDB ID </span> + <input + type='text' + onKeyDown={e => { + if (e.keyCode === 13) { + const value = e.currentTarget.value.trim() + if (value) { + this.props.app.loadPdbId(value) + } + } + }} + /> + </div> + <div> + <span>Load CIF file </span> + <input + accept='*.cif' + type='file' + onChange={e => { + if (e.target.files) this.props.app.loadCifFile(e.target.files[0]) + }} + /> + </div> + <hr/> + <div style={{marginBottom: '10px'}}> + {structureView ? <StructureViewComponent structureView={structureView} /> : ''} + </div> + </div> + </div>; + } +} \ No newline at end of file diff --git a/src/apps/canvas/component/structure-representation.tsx b/src/apps/canvas/component/structure-representation.tsx new file mode 100644 index 0000000000000000000000000000000000000000..25eb022ae279bbae55b8af05fc50cfc8a369d901 --- /dev/null +++ b/src/apps/canvas/component/structure-representation.tsx @@ -0,0 +1,128 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import * as React from 'react' +import { StructureRepresentation, StructureProps } from 'mol-geo/representation/structure'; +import Viewer from 'mol-view/viewer'; +import { VisualQuality, VisualQualityNames } from 'mol-geo/representation/util'; +import { ColorThemeProps, ColorThemeName, ColorThemeNames, ColorTheme } from 'mol-view/theme/color'; +import { Color } from 'mol-util/color'; + +export interface StructureRepresentationComponentProps { + viewer: Viewer + representation: StructureRepresentation<StructureProps> +} + +export interface StructureRepresentationComponentState { + label: string + visible: boolean + quality: VisualQuality + colorTheme: ColorThemeProps +} + +export class StructureRepresentationComponent extends React.Component<StructureRepresentationComponentProps, StructureRepresentationComponentState> { + state = { + label: this.props.representation.label, + visible: this.props.representation.props.visible, + quality: this.props.representation.props.quality, + colorTheme: this.props.representation.props.colorTheme, + } + + componentWillMount() { + const repr = this.props.representation + + this.setState({ + ...this.state, + label: repr.label, + visible: repr.props.visible, + quality: repr.props.quality, + colorTheme: repr.props.colorTheme, + }) + } + + async update(state: Partial<StructureRepresentationComponentState>) { + const repr = this.props.representation + const props: Partial<StructureProps> = {} + + if (state.visible !== undefined) props.visible = state.visible + if (state.quality !== undefined) props.quality = state.quality + if (state.colorTheme !== undefined) props.colorTheme = state.colorTheme + + await repr.createOrUpdate(props).run() + this.props.viewer.add(repr) + this.props.viewer.requestDraw(true) + console.log(this.props.viewer.stats) + + const newState = { + ...this.state, + visible: repr.props.visible, + quality: repr.props.quality, + colorTheme: repr.props.colorTheme, + } + this.setState(newState) + } + + render() { + const { label, visible, quality, colorTheme } = this.state + + const ct = ColorTheme(colorTheme) + + if (ct.legend && ct.legend.kind === 'scale-legend') { + // console.log(`linear-gradient(to right, ${ct.legend.colors.map(c => Color.toStyle(c)).join(', ')})`) + } + + return <div> + <div> + <h4>{label}</h4> + </div> + <div> + <div> + <span>Visible </span> + <button onClick={(e) => this.update({ visible: !visible }) }> + {visible ? 'Hide' : 'Show'} + </button> + </div> + <div> + <span>Quality </span> + <select value={quality} onChange={(e) => this.update({ quality: e.target.value as VisualQuality }) }> + {VisualQualityNames.map(name => <option key={name} value={name}>{name}</option>)} + </select> + </div> + <div> + <span>Color Theme </span> + <select value={colorTheme.name} onChange={(e) => this.update({ colorTheme: { name: e.target.value as ColorThemeName } }) }> + {ColorThemeNames.map(name => <option key={name} value={name}>{name}</option>)} + </select> + {ct.description ? <div><i>{ct.description}</i></div> : ''} + { + ct.legend && ct.legend.kind === 'scale-legend' + ? <div + style={{ + width: '100%', + height: '30px', + background: `linear-gradient(to right, ${ct.legend.colors.map(c => Color.toStyle(c)).join(', ')})` + }} + > + <span style={{float: 'left', padding: '6px', color: 'white', fontWeight: 'bold', backgroundColor: 'rgba(0, 0, 0, 0.2)'}}>{ct.legend.min}</span> + <span style={{float: 'right', padding: '6px', color: 'white', fontWeight: 'bold', backgroundColor: 'rgba(0, 0, 0, 0.2)'}}>{ct.legend.max}</span> + </div> + : ct.legend && ct.legend.kind === 'table-legend' + ? <div> + {ct.legend.table.map((value, i) => { + const [name, color] = value + return <div key={i} style={{minWidth: '60px', marginRight: '5px', display: 'inline-block'}}> + <div style={{width: '30px', height: '20px', backgroundColor: Color.toStyle(color), display: 'inline-block'}}></div> + {name} + </div> + })} + </div> + : '' + } + </div> + </div> + </div>; + } +} \ No newline at end of file diff --git a/src/apps/canvas/component/structure-view.tsx b/src/apps/canvas/component/structure-view.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6f05f7403c1ed359535db6d9707f79aec47ec424 --- /dev/null +++ b/src/apps/canvas/component/structure-view.tsx @@ -0,0 +1,198 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import * as React from 'react' +import { StructureView } from '../structure-view'; +import { StructureRepresentation } from 'mol-geo/representation/structure'; +import { StructureRepresentationComponent } from './structure-representation'; + +// export function FileInput (props: { +// accept: string +// onChange: (v: FileList | null) => void, +// }) { +// return <input +// accept={props.accept || '*.*'} +// type='file' +// onChange={e => props.onChange.call(null, e.target.files)} +// /> +// } + +export interface StructureViewComponentProps { + structureView: StructureView +} + +export interface StructureViewComponentState { + structureView: StructureView + + label: string + modelId: number + modelIds: { id: number, label: string }[] + assemblyId: string + assemblyIds: { id: string, label: string }[] + symmetryFeatureId: number + symmetryFeatureIds: { id: number, label: string }[] + + active: { [k: string]: boolean } + structureRepresentations: { [k: string]: StructureRepresentation<any> } +} + +export class StructureViewComponent extends React.Component<StructureViewComponentProps, StructureViewComponentState> { + state = this.stateFromStructureView(this.props.structureView) + + private stateFromStructureView(sv: StructureView) { + return { + structureView: sv, + + label: sv.label, + modelId: sv.modelId, + modelIds: sv.getModelIds(), + assemblyId: sv.assemblyId, + assemblyIds: sv.getAssemblyIds(), + symmetryFeatureId: sv.symmetryFeatureId, + symmetryFeatureIds: sv.getSymmetryFeatureIds(), + + active: sv.active, + structureRepresentations: sv.structureRepresentations + } + } + + componentWillMount() { + this.setState(this.stateFromStructureView(this.props.structureView)) + } + + componentDidMount() { + const sv = this.props.structureView + + this.props.structureView.updated.subscribe(() => this.setState({ + symmetryFeatureIds: sv.getSymmetryFeatureIds(), + structureRepresentations: sv.structureRepresentations + })) + } + + componentWillReceiveProps(nextProps: StructureViewComponentProps) { + if (nextProps.structureView !== this.props.structureView) { + this.setState(this.stateFromStructureView(nextProps.structureView)) + + nextProps.structureView.updated.subscribe(() => this.setState({ + symmetryFeatureIds: nextProps.structureView.getSymmetryFeatureIds(), + structureRepresentations: nextProps.structureView.structureRepresentations + })) + } + } + + async update(state: Partial<StructureViewComponentState>) { + const sv = this.state.structureView + + if (state.modelId !== undefined) await sv.setModel(state.modelId) + if (state.assemblyId !== undefined) await sv.setAssembly(state.assemblyId) + if (state.symmetryFeatureId !== undefined) await sv.setSymmetryFeature(state.symmetryFeatureId) + + this.setState(this.stateFromStructureView(sv)) + } + + render() { + const { structureView, label, modelIds, assemblyIds, symmetryFeatureIds, active, structureRepresentations } = this.state + + const modelIdOptions = modelIds.map(m => { + return <option key={m.id} value={m.id}>{m.label}</option> + }) + const assemblyIdOptions = assemblyIds.map(a => { + return <option key={a.id} value={a.id}>{a.label}</option> + }) + const symmetryFeatureIdOptions = symmetryFeatureIds.map(f => { + return <option key={f.id} value={f.id}>{f.label}</option> + }) + + return <div> + <div> + <h2>{label}</h2> + </div> + <div> + <div> + <span>Model </span> + <select + style={{width: '100px'}} + value={this.state.modelId} + onChange={(e) => { + this.update({ modelId: parseInt(e.target.value) }) + }} + > + {modelIdOptions} + </select> + <span> </span> + <input type='range' + value={this.state.modelId} + min={Math.min(...modelIds.map(m => m.id))} + max={Math.max(...modelIds.map(m => m.id))} + step='1' + onInput={(e) => { + this.update({ modelId: parseInt(e.currentTarget.value) }) + }} + > + </input> + </div> + <div> + <span>Assembly </span> + <select + style={{width: '150px'}} + value={this.state.assemblyId} + onChange={(e) => { + this.update({ assemblyId: e.target.value }) + }} + > + {assemblyIdOptions} + </select> + </div> + <div> + <span>Symmetry Feature </span> + <select + style={{width: '150px'}} + value={this.state.symmetryFeatureId} + onChange={(e) => { + this.update({ symmetryFeatureId: parseInt(e.target.value) }) + }} + > + {symmetryFeatureIdOptions} + </select> + </div> + <div> + <h4>Active</h4> + { Object.keys(active).map((k, i) => { + return <div key={i}> + <input + type='checkbox' + checked={active[k]} + onChange={(e) => { + const sv = structureView + if (k === 'symmetryAxes') { + sv.setSymmetryAxes(e.target.checked) + } else if (Object.keys(sv.structureRepresentations).includes(k)) { + sv.setStructureRepresentation(k, e.target.checked) + } + }} + /> {k} + </div> + } ) } + </div> + <div> + <h3>Structure Representations</h3> + { Object.keys(structureRepresentations).map((k, i) => { + if (active[k]) { + return <div key={i}> + <StructureRepresentationComponent + representation={structureRepresentations[k]} + viewer={structureView.viewer} + /> + </div> + } else { + return '' + } + } ) } + </div> + </div> + </div>; + } +} \ No newline at end of file diff --git a/src/apps/canvas/component/viewport.tsx b/src/apps/canvas/component/viewport.tsx new file mode 100644 index 0000000000000000000000000000000000000000..90ec862422d69ad86960d61fb6ebf8bce59fc44b --- /dev/null +++ b/src/apps/canvas/component/viewport.tsx @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import * as React from 'react' +import { App } from '../app'; +import { MarkerAction } from 'mol-geo/util/marker-data'; +import { EmptyLoci, Loci, areLociEqual } from 'mol-model/loci'; +import { labelFirst } from 'mol-view/label'; + +interface ViewportProps { + app: App +} + +interface ViewportState { + noWebGl: boolean, + info: string +} + +export class Viewport extends React.Component<ViewportProps, ViewportState> { + private container: HTMLDivElement | null = null; + private canvas: HTMLCanvasElement | null = null; + + state: ViewportState = { + noWebGl: false, + info: '' + }; + + handleResize() { + this.props.app.viewer.handleResize() + } + + componentDidMount() { + if (!this.canvas || !this.container || !this.props.app.initViewer(this.canvas, this.container)) { + this.setState({ noWebGl: true }); + } + this.handleResize() + + const viewer = this.props.app.viewer + + viewer.input.resize.subscribe(() => this.handleResize()) + + let prevLoci: Loci = EmptyLoci + viewer.input.move.subscribe(({x, y, inside, buttons}) => { + if (!inside || buttons) return + const p = viewer.identify(x, y) + if (p) { + const loci = viewer.getLoci(p) + + if (!areLociEqual(loci, prevLoci)) { + viewer.mark(prevLoci, MarkerAction.RemoveHighlight) + viewer.mark(loci, MarkerAction.Highlight) + prevLoci = loci + + const label = labelFirst(loci) + const info = `${label}` + this.setState({ info }) + } + } + }) + } + + componentWillUnmount() { + if (super.componentWillUnmount) super.componentWillUnmount(); + // TODO viewer cleanup + } + + renderMissing() { + return <div> + <div> + <p><b>WebGL does not seem to be available.</b></p> + <p>This can be caused by an outdated browser, graphics card driver issue, or bad weather. Sometimes, just restarting the browser helps.</p> + <p>For a list of supported browsers, refer to <a href='http://caniuse.com/#feat=webgl' target='_blank'>http://caniuse.com/#feat=webgl</a>.</p> + </div> + </div> + } + + render() { + if (this.state.noWebGl) return this.renderMissing(); + + return <div style={{ backgroundColor: 'rgb(0, 0, 0)', width: '100%', height: '100%'}}> + <div ref={elm => this.container = elm} style={{width: '100%', height: '100%'}}> + <canvas ref={elm => this.canvas = elm}></canvas> + </div> + <div + style={{ + position: 'absolute', + top: 10, + left: 10, + padding: 10, + color: 'lightgrey', + background: 'rgba(0, 0, 0, 0.2)' + }} + > + {this.state.info} + </div> + </div>; + } +} \ No newline at end of file diff --git a/src/apps/canvas/index.html b/src/apps/canvas/index.html index 3aa50333eec315dc64f79dce7366971dda63cc71..bc9f73aa6dfcb30b9a82e79ed5fa3ce5cdf07e24 100644 --- a/src/apps/canvas/index.html +++ b/src/apps/canvas/index.html @@ -4,12 +4,30 @@ <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"> <title>Mol* Canvas</title> + <style> + * { + margin: 0; + padding: 0; + } + html, body { + width: 100%; + height: 100%; + overflow: hidden; + } + hr { + margin: 10px; + } + h1, h2, h3, h4, h5 { + margin-top: 5px; + margin-bottom: 3px; + } + button { + padding: 2px; + } + </style> </head> <body> - <div id="container" style="width:800px; height: 600px;"> - <canvas id="canvas"></canvas> - </div> - <span id="info"></span> + <div id="app" style="width: 100%; height: 100%"></div> <script type="text/javascript" src="./index.js"></script> </body> </html> \ No newline at end of file diff --git a/src/apps/canvas/index.ts b/src/apps/canvas/index.ts index 5166630cac8f8059edde039026cabcaaf9bade2c..0c110a8856d8a28fa1a44b277c49c3727cab9222 100644 --- a/src/apps/canvas/index.ts +++ b/src/apps/canvas/index.ts @@ -4,151 +4,21 @@ * @author Alexander Rose <alexander.rose@weirdbyte.de> */ -import './index.html' - -import Viewer from 'mol-view/viewer'; -import CIF, { CifBlock } from 'mol-io/reader/cif' -// import { parse as parseObj } from 'mol-io/reader/obj/parser' -import { readUrlAs } from 'mol-util/read' -import { Model, Format, Structure, StructureSymmetry } from 'mol-model/structure'; -import { CartoonRepresentation } from 'mol-geo/representation/structure/representation/cartoon'; -import { BallAndStickRepresentation } from 'mol-geo/representation/structure/representation/ball-and-stick'; -import { EveryLoci } from 'mol-model/loci'; -import { MarkerAction } from 'mol-geo/util/marker-data'; -import { labelFirst } from 'mol-view/label'; -import { Queries as Q, StructureProperties as SP, StructureSelection, StructureQuery } from 'mol-model/structure'; -import { MeshBuilder } from 'mol-geo/mesh/mesh-builder'; -import { ShapeRepresentation } from 'mol-geo/representation/shape'; -import { Vec3, Mat4 } from 'mol-math/linear-algebra'; -import { Shape } from 'mol-model/shape'; -import { Color } from 'mol-util/color'; -import { addSphere } from 'mol-geo/mesh/builder/sphere'; -import { Box } from 'mol-geo/primitive/box'; - -const container = document.getElementById('container') -if (!container) throw new Error('Can not find element with id "container".') - -const canvas = document.getElementById('canvas') as HTMLCanvasElement -if (!canvas) throw new Error('Can not find element with id "canvas".') - -const info = document.getElementById('info') as HTMLCanvasElement -if (!info) throw new Error('Can not find element with id "info".') - -const viewer = Viewer.create(canvas, container) -viewer.animate() - -viewer.input.resize.subscribe(() => { - // do whatever appropriate -}) - -viewer.input.move.subscribe(({x, y, inside, buttons}) => { - if (!inside || buttons) return - const p = viewer.identify(x, y) - const loci = viewer.getLoci(p) - - viewer.mark(EveryLoci, MarkerAction.RemoveHighlight) - viewer.mark(loci, MarkerAction.Highlight) - - const label = labelFirst(loci) - info.innerText = `${label}` -}) - +import * as React from 'react' +import * as ReactDOM from 'react-dom' -// async function getObjFromUrl(url: string) { -// const data = await readUrlAs(url, false) as string -// const comp = parseObj(data) -// const parsed = await comp.run() -// if (parsed.isError) throw parsed -// return parsed.result -// } - -async function getCifFromUrl(url: string) { - const data = await readUrlAs(url, false) - const comp = CIF.parse(data) - const parsed = await comp.run() - if (parsed.isError) throw parsed - return parsed.result.blocks[0] -} - -async function getModelFromMmcif(cif: CifBlock) { - const models = await Model.create(Format.mmCIF(cif)).run() - return models[0] -} - -async function getStructureFromModel(model: Model, assembly = '1') { - const assemblies = model.symmetry.assemblies - if (assemblies.length) { - return await StructureSymmetry.buildAssembly(Structure.ofModel(model), assembly).run() - } else { - return Structure.ofModel(model) - } -} - -async function init() { - const cif = await getCifFromUrl('https://files.rcsb.org/download/1crn.cif') - const model = await getModelFromMmcif(cif) - const structure = await getStructureFromModel(model) - - viewer.center(structure.boundary.sphere.center) - - // cartoon for whole structure - const cartoonRepr = CartoonRepresentation() - await cartoonRepr.create(structure, { - colorTheme: { name: 'chain-id' }, - sizeTheme: { name: 'uniform', value: 0.2 }, - useFog: false // TODO fog not working properly - }).run() - viewer.add(cartoonRepr) - - // create new structure via query - const q1 = Q.generators.atoms({ - residueTest: qtx => SP.residue.label_seq_id(qtx.element) < 7 - }); - const newStructure = StructureSelection.unionStructure(await StructureQuery.run(q1, structure)); - - // ball+stick for new structure - const ballStickRepr = BallAndStickRepresentation() - await ballStickRepr.create(newStructure, { - colorTheme: { name: 'element-symbol' }, - sizeTheme: { name: 'uniform', value: 0.1 }, - useFog: false // TODO fog not working properly - }).run() - viewer.add(ballStickRepr) - - // create a mesh - const meshBuilder = MeshBuilder.create(256, 128) - const colors: Color[] = [] - const labels: string[] = [] - // red sphere - meshBuilder.setGroup(0) - colors[0] = Color(0xFF2233) - labels[0] = 'red sphere' - addSphere(meshBuilder, Vec3.create(0, 0, 0), 4, 2) - // green cube - meshBuilder.setGroup(1) - colors[1] = Color(0x2233FF) - labels[1] = 'blue cube' - const t = Mat4.identity() - Mat4.fromTranslation(t, Vec3.create(10, 0, 0)) - Mat4.scale(t, t, Vec3.create(3, 3, 3)) - meshBuilder.add(t, Box()) - const mesh = meshBuilder.getMesh() - // const mesh = getObjFromUrl('mesh.obj') +import './index.html' - // create shape from mesh - const shape = Shape.create('myShape', mesh, colors, labels) +import { App } from './app'; +import { AppComponent } from './component/app'; +import { urlQueryParameter } from 'mol-util/url-query'; - // add representation from shape - const customRepr = ShapeRepresentation() - await customRepr.create(shape, { - colorTheme: { name: 'shape-group' }, - // colorTheme: { name: 'uniform', value: Color(0xFFCC22) }, - useFog: false // TODO fog not working properly - }).run() - viewer.add(customRepr) +const elm = document.getElementById('app') as HTMLElement +if (!elm) throw new Error('Can not find element with id "app".') - // ensure the added representations get rendered, i.e. without mouse input - viewer.requestDraw() -} +const app = new App() +ReactDOM.render(React.createElement(AppComponent, { app }), elm); -init() \ No newline at end of file +const assemblyId = urlQueryParameter('assembly') +const pdbId = urlQueryParameter('pdb') +if (pdbId) app.loadPdbId(pdbId, assemblyId) \ No newline at end of file diff --git a/src/apps/canvas/structure-view.ts b/src/apps/canvas/structure-view.ts new file mode 100644 index 0000000000000000000000000000000000000000..b381ab2f082e2918092e7e695c1de8b82a8ffd45 --- /dev/null +++ b/src/apps/canvas/structure-view.ts @@ -0,0 +1,371 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { Model, Structure } from 'mol-model/structure'; +import { CartoonRepresentation } from 'mol-geo/representation/structure/representation/cartoon'; +import { BallAndStickRepresentation } from 'mol-geo/representation/structure/representation/ball-and-stick'; +import { getStructureFromModel } from './util'; +import { AssemblySymmetry } from 'mol-model-props/rcsb/symmetry'; +import { ShapeRepresentation, ShapeProps } from 'mol-geo/representation/shape'; +import { getAxesShape } from './assembly-symmetry'; +import Viewer from 'mol-view/viewer'; +import { CarbohydrateRepresentation } from 'mol-geo/representation/structure/representation/carbohydrate'; +// import { MeshBuilder } from 'mol-geo/mesh/mesh-builder'; +// import { addSphere } from 'mol-geo/mesh/builder/sphere'; +// import { Shape } from 'mol-model/shape'; +// import { Color } from 'mol-util/color'; +// import { computeUnitBoundary } from 'mol-model/structure/structure/util/boundary'; +// import { addBoundingBox } from 'mol-geo/mesh/builder/bounding-box'; +import { PointRepresentation } from 'mol-geo/representation/structure/representation/point'; +import { StructureRepresentation } from 'mol-geo/representation/structure'; +import { BehaviorSubject } from 'rxjs'; +import { SpacefillRepresentation } from 'mol-geo/representation/structure/representation/spacefill'; +import { DistanceRestraintRepresentation } from 'mol-geo/representation/structure/representation/distance-restraint'; + +export interface StructureView { + readonly viewer: Viewer + + readonly label: string + readonly models: ReadonlyArray<Model> + readonly structure: Structure | undefined + readonly assemblySymmetry: AssemblySymmetry | undefined + + readonly active: { [k: string]: boolean } + readonly structureRepresentations: { [k: string]: StructureRepresentation<any> } + readonly updated: BehaviorSubject<null> + readonly symmetryAxes: ShapeRepresentation<ShapeProps> + + setSymmetryAxes(value: boolean): void + setStructureRepresentation(name: string, value: boolean): void + + readonly modelId: number + readonly assemblyId: string + readonly symmetryFeatureId: number + + setModel(modelId: number): Promise<void> + getModelIds(): { id: number, label: string }[] + setAssembly(assemblyId: string): Promise<void> + getAssemblyIds(): { id: string, label: string }[] + setSymmetryFeature(symmetryFeatureId: number): Promise<void> + getSymmetryFeatureIds(): { id: number, label: string }[] + + destroy: () => void +} + +interface StructureViewProps { + assemblyId?: string + symmetryFeatureId?: number +} + + + +export async function StructureView(viewer: Viewer, models: ReadonlyArray<Model>, props: StructureViewProps = {}): Promise<StructureView> { + const active: { [k: string]: boolean } = { + cartoon: true, + point: false, + ballAndStick: false, + carbohydrate: false, + spacefill: false, + distanceRestraint: false, + symmetryAxes: false, + // polymerSphere: false, + } + + const structureRepresentations: { [k: string]: StructureRepresentation<any> } = { + cartoon: CartoonRepresentation(), + point: PointRepresentation(), + ballAndStick: BallAndStickRepresentation(), + carbohydrate: CarbohydrateRepresentation(), + spacefill: SpacefillRepresentation(), + distanceRestraint: DistanceRestraintRepresentation(), + } + + const symmetryAxes = ShapeRepresentation() + const polymerSphere = ShapeRepresentation() + + const updated: BehaviorSubject<null> = new BehaviorSubject<null>(null) + + let label: string + let model: Model | undefined + let assemblySymmetry: AssemblySymmetry | undefined + let structure: Structure | undefined + + let modelId: number + let assemblyId: string + let symmetryFeatureId: number + + async function setSymmetryAxes(value: boolean) { + if (!value) { + assemblySymmetry = undefined + } else { + await AssemblySymmetry.attachFromCifOrAPI(models[modelId]) + assemblySymmetry = AssemblySymmetry.get(models[modelId]) + } + active.symmetryAxes = value + await setSymmetryFeature() + } + + async function setStructureRepresentation(k: string, value: boolean) { + active[k] = value + await createStructureRepr() + } + + async function setModel(newModelId: number, newAssemblyId?: string, newSymmetryFeatureId?: number) { + console.log('setModel', newModelId) + modelId = newModelId + model = models[modelId] + if (active.symmetryAxes) { + await AssemblySymmetry.attachFromCifOrAPI(model) + assemblySymmetry = AssemblySymmetry.get(model) + } + await setAssembly(newAssemblyId, newSymmetryFeatureId) + } + + function getModelIds() { + const modelIds: { id: number, label: string }[] = [] + models.forEach((m, i) => { + modelIds.push({ id: i, label: `${i}: ${m.label} #${m.modelNum}` }) + }) + return modelIds + } + + async function setAssembly(newAssemblyId?: string, newSymmetryFeatureId?: number) { + console.log('setAssembly', newAssemblyId) + if (newAssemblyId !== undefined) { + assemblyId = newAssemblyId + } else if (model && model.symmetry.assemblies.length) { + assemblyId = model.symmetry.assemblies[0].id + } else if (model) { + assemblyId = '0' + } else { + assemblyId = '-1' + } + await getStructure() + await setSymmetryFeature(newSymmetryFeatureId) + } + + function getAssemblyIds() { + const assemblyIds: { id: string, label: string }[] = [ + { id: '0', label: '0: model' } + ] + if (model) model.symmetry.assemblies.forEach(a => { + assemblyIds.push({ id: a.id, label: `${a.id}: ${a.details}` }) + }) + return assemblyIds + } + + async function setSymmetryFeature(newSymmetryFeatureId?: number) { + console.log('setSymmetryFeature', newSymmetryFeatureId) + if (newSymmetryFeatureId !== undefined) { + symmetryFeatureId = newSymmetryFeatureId + } else if (assemblySymmetry) { + const f = assemblySymmetry.getFeatures(assemblyId) + if (f._rowCount) { + symmetryFeatureId = f.id.value(0) + } else { + symmetryFeatureId = -1 + } + } else { + symmetryFeatureId = -1 + } + await createSymmetryRepr() + } + + function getSymmetryFeatureIds() { + const symmetryFeatureIds: { id: number, label: string }[] = [] + if (assemblySymmetry) { + const symmetryFeatures = assemblySymmetry.getFeatures(assemblyId) + for (let i = 0, il = symmetryFeatures._rowCount; i < il; ++i) { + const id = symmetryFeatures.id.value(i) + const symmetry = symmetryFeatures.symmetry_value.value(i) + const type = symmetryFeatures.type.value(i) + const stoichiometry = symmetryFeatures.stoichiometry_value.value(i) + const label = `${id}: ${symmetry} ${type} ${stoichiometry}` + symmetryFeatureIds.push({ id, label }) + } + } + return symmetryFeatureIds + } + + async function getStructure() { + if (model) structure = await getStructureFromModel(model, assemblyId) + if (model && structure) { + label = `${model.label} - Assembly ${assemblyId}` + } else { + label = '' + } + await createStructureRepr() + } + + async function createStructureRepr() { + if (structure) { + console.log('createStructureRepr') + for (const k in structureRepresentations) { + if (active[k]) { + await structureRepresentations[k].createOrUpdate({}, structure).run() + viewer.add(structureRepresentations[k]) + } else { + viewer.remove(structureRepresentations[k]) + } + } + + viewer.center(structure.boundary.sphere.center) + + // const mb = MeshBuilder.create() + // mb.setGroup(0) + // addSphere(mb, structure.boundary.sphere.center, structure.boundary.sphere.radius, 3) + // addBoundingBox(mb, structure.boundary.box, 1, 2, 8) + // for (let i = 0, il = structure.units.length; i < il; ++i) { + // mb.setGroup(1) + // const u = structure.units[i] + // const ci = u.model.atomicHierarchy.chainAtomSegments.index[u.elements[0]] + // const ek = u.model.atomicHierarchy.getEntityKey(ci) + // if (u.model.entities.data.type.value(ek) === 'water') continue + // const boundary = computeUnitBoundary(u) + // addSphere(mb, boundary.sphere.center, boundary.sphere.radius, 3) + // addBoundingBox(mb, boundary.box, 0.5, 2, 8) + // } + // const shape = Shape.create('boundary', mb.getMesh(), [Color(0xCC6633), Color(0x3366CC)], ['sphere boundary']) + // await polymerSphere.createOrUpdate({ + // alpha: 0.5, + // doubleSided: false, + // depthMask: false, + // useFog: false // TODO fog not working properly + // }, shape).run() + } else { + for (const k in structureRepresentations) structureRepresentations[k].destroy() + polymerSphere.destroy() + } + + viewer.add(polymerSphere) + + updated.next(null) + viewer.requestDraw(true) + console.log(viewer.stats) + } + + async function createSymmetryRepr() { + if (assemblySymmetry) { + const features = assemblySymmetry.getFeatures(assemblyId) + if (features._rowCount) { + const axesShape = getAxesShape(symmetryFeatureId, assemblySymmetry) + if (axesShape) { + // const colorTheme = getClusterColorTheme(symmetryFeatureId, assemblySymmetry) + // await cartoon.createOrUpdate({ + // colorTheme: { name: 'custom', color: colorTheme.color, granularity: colorTheme.granularity }, + // sizeTheme: { name: 'uniform', value: 0.2 }, + // useFog: false // TODO fog not working properly + // }).run() + // await ballAndStick.createOrUpdate({ + // colorTheme: { name: 'custom', color: colorTheme.color, granularity: colorTheme.granularity }, + // sizeTheme: { name: 'uniform', value: 0.1 }, + // useFog: false // TODO fog not working properly + // }).run() + await symmetryAxes.createOrUpdate({}, axesShape).run() + viewer.add(symmetryAxes) + } else { + viewer.remove(symmetryAxes) + } + } else { + viewer.remove(symmetryAxes) + } + } else { + viewer.remove(symmetryAxes) + } + updated.next(null) + viewer.requestDraw(true) + } + + await setModel(0, props.assemblyId, props.symmetryFeatureId) + + return { + viewer, + + get label() { return label }, + models, + get structure() { return structure }, + get assemblySymmetry() { return assemblySymmetry }, + + active, + structureRepresentations, + updated, + symmetryAxes, + + setSymmetryAxes, + setStructureRepresentation, + + get modelId() { return modelId }, + get assemblyId() { return assemblyId }, + get symmetryFeatureId() { return symmetryFeatureId }, + + setModel, + getModelIds, + setAssembly, + getAssemblyIds, + setSymmetryFeature, + getSymmetryFeatureIds, + + destroy: () => { + for (const k in structureRepresentations) { + viewer.remove(structureRepresentations[k]) + structureRepresentations[k].destroy() + } + viewer.remove(polymerSphere) + viewer.remove(symmetryAxes) + viewer.requestDraw(true) + + polymerSphere.destroy() + symmetryAxes.destroy() + } + } +} + +// // create new structure via query +// const q1 = Q.generators.atoms({ +// residueTest: qtx => SP.residue.label_seq_id(qtx.element) < 7 +// }); +// const newStructure = StructureSelection.unionStructure(await StructureQuery.run(q1, structure)); + +// // ball+stick for new structure +// const newBallStickRepr = BallAndStickRepresentation() +// await newBallStickRepr.create(newStructure, { +// colorTheme: { name: 'element-symbol' }, +// sizeTheme: { name: 'uniform', value: 0.1 }, +// useFog: false // TODO fog not working properly +// }).run() +// viewer.add(newBallStickRepr) + +// // create a mesh +// const meshBuilder = MeshBuilder.create(256, 128) +// const colors: Color[] = [] +// const labels: string[] = [] +// // red sphere +// meshBuilder.setGroup(0) +// colors[0] = Color(0xFF2233) +// labels[0] = 'red sphere' +// addSphere(meshBuilder, Vec3.create(0, 0, 0), 4, 2) +// // green cube +// meshBuilder.setGroup(1) +// colors[1] = Color(0x2233FF) +// labels[1] = 'blue cube' +// const t = Mat4.identity() +// Mat4.fromTranslation(t, Vec3.create(10, 0, 0)) +// Mat4.scale(t, t, Vec3.create(3, 3, 3)) +// meshBuilder.add(t, Box()) +// const mesh = meshBuilder.getMesh() +// const mesh = getObjFromUrl('mesh.obj') + +// // create shape from mesh +// const shape = Shape.create('myShape', mesh, colors, labels) + +// // add representation from shape +// const customRepr = ShapeRepresentation() +// await customRepr.create(shape, { +// colorTheme: { name: 'shape-group' }, +// // colorTheme: { name: 'uniform', value: Color(0xFFCC22) }, +// useFog: false // TODO fog not working properly +// }).run() +// viewer.add(customRepr) \ No newline at end of file diff --git a/src/apps/canvas/util.ts b/src/apps/canvas/util.ts new file mode 100644 index 0000000000000000000000000000000000000000..9a2c8f7fa9f6d9b27d5f4973450962c741a7a7ba --- /dev/null +++ b/src/apps/canvas/util.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import CIF, { CifBlock } from 'mol-io/reader/cif' +import { readUrlAs, readFileAs } from 'mol-util/read'; +import { Model, Format, StructureSymmetry, Structure } from 'mol-model/structure'; +// import { parse as parseObj } from 'mol-io/reader/obj/parser' + +// export async function getObjFromUrl(url: string) { +// const data = await readUrlAs(url, false) as string +// const comp = parseObj(data) +// const parsed = await comp.run() +// if (parsed.isError) throw parsed +// return parsed.result +// } + +export async function getCifFromData(data: string | Uint8Array) { + const comp = CIF.parse(data) + const parsed = await comp.run() + if (parsed.isError) throw parsed + return parsed.result.blocks[0] +} + +export async function getCifFromUrl(url: string) { + return getCifFromData(await readUrlAs(url, false)) +} + +export async function getCifFromFile(file: File, binary = false) { + return getCifFromData(await readFileAs(file, binary)) +} + +export async function getModelsFromMmcif(cif: CifBlock) { + return await Model.create(Format.mmCIF(cif)).run() +} + +export async function getStructureFromModel(model: Model, assembly: string) { + const assemblies = model.symmetry.assemblies + if (assembly === '0') { + return Structure.ofModel(model) + } else if (assemblies.find(a => a.id === assembly)) { + return await StructureSymmetry.buildAssembly(Structure.ofModel(model), assembly).run() + } +} \ No newline at end of file diff --git a/src/apps/viewer/index.tsx b/src/apps/viewer/index.tsx index 940efc7d074991890d466e3ec52e10da2fa25c69..f1c68970f4779c1fd3a8740d974f277b3c464298 100644 --- a/src/apps/viewer/index.tsx +++ b/src/apps/viewer/index.tsx @@ -104,10 +104,4 @@ ctx.dispatcher.getStream(InteractivityEvents.HighlightLoci).subscribe(event => { } }) -ctx.dispatcher.getStream(InteractivityEvents.SelectLoci).subscribe(event => { - if (event && event.data) { - ctx.stage.viewer.mark(event.data, MarkerAction.ToggleSelect) - } -}) - ReactDOM.render(React.createElement(Layout, { controller: ctx.layout }), elm); diff --git a/src/mol-app/ui/visualization/viewport.tsx b/src/mol-app/ui/visualization/viewport.tsx index d23f182e1b5bbfa3731d1f98edc7fa2cb9243692..ed6c563a40673701268de1330556a64680dc23b9 100644 --- a/src/mol-app/ui/visualization/viewport.tsx +++ b/src/mol-app/ui/visualization/viewport.tsx @@ -163,19 +163,24 @@ export class Viewport extends View<ViewportController, ViewportState, { noWebGl? viewer.input.move.subscribe(({x, y, inside, buttons}) => { if (!inside || buttons) return const p = viewer.identify(x, y) - const loci = viewer.getLoci(p) - InteractivityEvents.HighlightLoci.dispatch(this.controller.context, loci); - - // TODO use LabelLoci event and make configurable - const label = labelFirst(loci) - const info = `Object: ${p.objectId}, Instance: ${p.instanceId}, Group: ${p.groupId}, Label: ${label}` - this.setState({ info }) + if (p) { + const loci = viewer.getLoci(p) + InteractivityEvents.HighlightLoci.dispatch(this.controller.context, loci); + + // TODO use LabelLoci event and make configurable + const label = labelFirst(loci) + const info = `Object: ${p.objectId}, Instance: ${p.instanceId}, Group: ${p.groupId}, Label: ${label}` + this.setState({ info }) + } }) // TODO filter only for left button/single finger touch? viewer.input.click.subscribe(({x, y}) => { - const loci = viewer.getLoci(viewer.identify(x, y)) - InteractivityEvents.SelectLoci.dispatch(this.controller.context, loci); + const p = viewer.identify(x, y) + if (p) { + const loci = viewer.getLoci(p) + InteractivityEvents.SelectLoci.dispatch(this.controller.context, loci); + } }) } diff --git a/src/mol-data/db/table.ts b/src/mol-data/db/table.ts index 15fd6aef229002f97e13fde5f609131cb19fa245..3573c7bd604a7ce5bf5877efbd1a53a89d38bcec 100644 --- a/src/mol-data/db/table.ts +++ b/src/mol-data/db/table.ts @@ -101,6 +101,14 @@ namespace Table { return ret as Table<R>; } + export function pick<S extends R, R extends Schema>(table: Table<S>, schema: R, test: (i: number) => boolean) { + const _view: number[] = [] + for (let i = 0, il = table._rowCount; i < il; ++i) { + if (test(i)) _view.push(i) + } + return view(table, schema, _view) + } + export function window<S extends R, R extends Schema>(table: Table<S>, schema: R, start: number, end: number) { if (start === 0 && end === table._rowCount) return table; const ret = Object.create(null); @@ -194,6 +202,13 @@ namespace Table { return row; } + /** Pick the first row for which `test` evaluates to true */ + export function pickRow<S extends Schema>(table: Table<S>, test: (i: number) => boolean) { + for (let i = 0, il = table._rowCount; i < il; ++i) { + if (test(i)) return getRow(table, i) + } + } + export function getRows<S extends Schema>(table: Table<S>) { const ret: Row<S>[] = []; const { _rowCount: c } = table; diff --git a/src/mol-data/int/impl/interval.ts b/src/mol-data/int/impl/interval.ts index f7568480900db112b77c934e7e4d856a923062b8..fe293e0a0c013897a68b07321349a5e8277ee6e9 100644 --- a/src/mol-data/int/impl/interval.ts +++ b/src/mol-data/int/impl/interval.ts @@ -19,6 +19,7 @@ export function size(i: Tuple) { return Tuple.snd(i) - Tuple.fst(i); } export const hashCode = Tuple.hashCode; export function has(int: Tuple, v: number) { return Tuple.fst(int) <= v && v < Tuple.snd(int); } +/** Returns the index of `x` in `set` or -1 if not found. */ export function indexOf(int: Tuple, x: number) { const m = start(int); return x >= m && x < end(int) ? x - m : -1; } export function getAt(int: Tuple, i: number) { return Tuple.fst(int) + i; } diff --git a/src/mol-data/int/impl/ordered-set.ts b/src/mol-data/int/impl/ordered-set.ts index 27ee1f1db798f09ebc789a2043520656c63ebec3..33794046c652e26bba6469695c510d928ad46977 100644 --- a/src/mol-data/int/impl/ordered-set.ts +++ b/src/mol-data/int/impl/ordered-set.ts @@ -25,6 +25,7 @@ export function ofSortedArray(xs: Nums): OrderedSetImpl { export function size(set: OrderedSetImpl) { return I.is(set) ? I.size(set) : S.size(set); } export function has(set: OrderedSetImpl, x: number) { return I.is(set) ? I.has(set, x) : S.has(set, x); } +/** Returns the index of `x` in `set` or -1 if not found. */ export function indexOf(set: OrderedSetImpl, x: number) { return I.is(set) ? I.indexOf(set, x) : S.indexOf(set, x); } export function getAt(set: OrderedSetImpl, i: number) { return I.is(set) ? I.getAt(set, i) : set[i]; } export function min(set: OrderedSetImpl) { return I.is(set) ? I.min(set) : S.min(set); } diff --git a/src/mol-data/int/impl/sorted-array.ts b/src/mol-data/int/impl/sorted-array.ts index 2d441bc3fdf4304e7808e9a417d64a98060441a6..6fff1aa007c47f26cc99d19e8ed790e0e57c4398 100644 --- a/src/mol-data/int/impl/sorted-array.ts +++ b/src/mol-data/int/impl/sorted-array.ts @@ -36,6 +36,7 @@ export function hashCode(xs: Nums) { return hash3(s, xs[0], xs[s - 1]); } +/** Returns the index of `x` in `set` or -1 if not found. */ export function indexOf(xs: Nums, v: number) { const l = xs.length; return l === 0 ? -1 : xs[0] <= v && v <= xs[l - 1] ? binarySearchRange(xs, v, 0, l) : -1; diff --git a/src/mol-data/int/interval.ts b/src/mol-data/int/interval.ts index b2ab7fb8350624c57d88f4d39960e6f41465abe7..ef72450b05865a03ecbffc4562e06c81ada34f49 100644 --- a/src/mol-data/int/interval.ts +++ b/src/mol-data/int/interval.ts @@ -18,6 +18,7 @@ namespace Interval { /** Test if a value is within the bounds of the interval */ export const has: <T extends number = number>(interval: Interval<T>, x: T) => boolean = Impl.has as any; + /** Returns the index of `x` in `set` or -1 if not found. */ export const indexOf: <T extends number = number>(interval: Interval<T>, x: T) => number = Impl.indexOf as any; export const getAt: <T extends number = number>(interval: Interval<T>, i: number) => T = Impl.getAt as any; diff --git a/src/mol-data/int/ordered-set.ts b/src/mol-data/int/ordered-set.ts index d18e61446d77582a7c5e8d6af78c586db838f875..046c1b6ab94b3acf93b5b5d232eb711c8b054402 100644 --- a/src/mol-data/int/ordered-set.ts +++ b/src/mol-data/int/ordered-set.ts @@ -18,6 +18,7 @@ namespace OrderedSet { export const ofSortedArray: <T extends number = number>(xs: ArrayLike<T>) => OrderedSet<T> = Base.ofSortedArray as any; export const has: <T extends number = number>(set: OrderedSet<T>, x: T) => boolean = Base.has as any; + /** Returns the index of `x` in `set` or -1 if not found. */ export const indexOf: <T extends number = number>(set: OrderedSet<T>, x: T) => number = Base.indexOf as any; export const getAt: <T extends number = number>(set: OrderedSet<T>, i: number) => T = Base.getAt as any; diff --git a/src/mol-data/int/sorted-array.ts b/src/mol-data/int/sorted-array.ts index c0f1248db3f25e3c7764c67164412d1baafb0c7a..cf0f5d11e09470a4b520cb00707faf4e7f034ea3 100644 --- a/src/mol-data/int/sorted-array.ts +++ b/src/mol-data/int/sorted-array.ts @@ -19,6 +19,7 @@ namespace SortedArray { export const is: <T extends number = number>(v: any) => v is SortedArray<T> = Impl.is as any; export const has: <T extends number = number>(array: SortedArray<T>, x: T) => boolean = Impl.has as any; + /** Returns the index of `x` in `set` or -1 if not found. */ export const indexOf: <T extends number = number>(array: SortedArray<T>, x: T) => number = Impl.indexOf as any; export const indexOfInInterval: <T extends number = number>(array: SortedArray<T>, x: number, bounds: Interval) => number = Impl.indexOfInInterval as any; diff --git a/src/mol-data/int/sorted-ranges.ts b/src/mol-data/int/sorted-ranges.ts index d51ba633178667806c81f5e775878acbfa588e03..967595e0f670ba6840665ca03ebce519fcdde4bb 100644 --- a/src/mol-data/int/sorted-ranges.ts +++ b/src/mol-data/int/sorted-ranges.ts @@ -18,7 +18,7 @@ namespace SortedRanges { export function max<T extends number = number>(ranges: SortedRanges<T>) { return ranges[ranges.length - 1] } export function size<T extends number = number>(ranges: SortedRanges<T>) { let size = 0 - for(let i = 0, il = ranges.length; i < il; i += 2) { + for (let i = 0, il = ranges.length; i < il; i += 2) { size += ranges[i + 1] - ranges[i] + 1 } return size diff --git a/src/mol-data/util/hash-functions.ts b/src/mol-data/util/hash-functions.ts index d822d8d9c4c1f60d77106d1d2b00526d146fc75b..0f672694237f5d4c7c6a6d7c1df15527582ece27 100644 --- a/src/mol-data/util/hash-functions.ts +++ b/src/mol-data/util/hash-functions.ts @@ -1,7 +1,8 @@ /** - * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2017-2018 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> */ // from http://burtleburtle.net/bob/hash/integer.html @@ -67,4 +68,16 @@ export function cantorPairing(a: number, b: number) { */ export function sortedCantorPairing(a: number, b: number) { return a < b ? cantorPairing(a, b) : cantorPairing(b, a); +} + +/** + * 32 bit FNV-1a hash, see http://isthe.com/chongo/tech/comp/fnv/ + */ +export function hashFnv32a(array: number[]) { + let hval = 0x811c9dc5; + for (let i = 0, il = array.length; i < il; ++i) { + hval ^= array[i]; + hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24); + } + return hval >>> 0; } \ No newline at end of file diff --git a/src/mol-geo/mesh/builder/bounding-box.ts b/src/mol-geo/mesh/builder/bounding-box.ts new file mode 100644 index 0000000000000000000000000000000000000000..ba36360454d6d689d8b392631543a2be861ff9cb --- /dev/null +++ b/src/mol-geo/mesh/builder/bounding-box.ts @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { Vec3 } from 'mol-math/linear-algebra'; +import { Box3D } from 'mol-math/geometry'; +import { MeshBuilder } from '../mesh-builder'; +import { CylinderProps } from '../../primitive/cylinder'; +import { addCylinder } from './cylinder'; +import { addSphere } from './sphere'; + +const tmpStart = Vec3.zero() +const tmpEnd = Vec3.zero() +const cylinderProps: CylinderProps = {} + +export function addBoundingBox(builder: MeshBuilder, box: Box3D, radius: number, detail: number, radialSegments: number) { + const { min, max } = box + + cylinderProps.radiusTop = radius + cylinderProps.radiusBottom = radius + cylinderProps.radialSegments = radialSegments + + Vec3.set(tmpStart, max[0], max[1], max[2]) + addSphere(builder, tmpStart, radius, detail) + Vec3.set(tmpEnd, max[0], max[1], min[2]) + addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps) + Vec3.set(tmpEnd, max[0], min[1], max[2]) + addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps) + Vec3.set(tmpEnd, min[0], max[1], max[2]) + addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps) + + Vec3.set(tmpStart, min[0], min[1], min[2]) + addSphere(builder, tmpStart, radius, detail) + Vec3.set(tmpEnd, min[0], min[1], max[2]) + addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps) + Vec3.set(tmpEnd, min[0], max[1], min[2]) + addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps) + Vec3.set(tmpEnd, max[0], min[1], min[2]) + addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps) + + Vec3.set(tmpStart, max[0], min[1], min[2]) + addSphere(builder, tmpStart, radius, detail) + Vec3.set(tmpEnd, max[0], min[1], max[2]) + addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps) + Vec3.set(tmpEnd, max[0], max[1], min[2]) + addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps) + + Vec3.set(tmpStart, min[0], min[1], max[2]) + addSphere(builder, tmpStart, radius, detail) + Vec3.set(tmpEnd, min[0], max[1], max[2]) + addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps) + Vec3.set(tmpEnd, max[0], min[1], max[2]) + addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps) + + Vec3.set(tmpStart, min[0], max[1], min[2]) + addSphere(builder, tmpStart, radius, detail) + Vec3.set(tmpEnd, max[0], max[1], min[2]) + addSphere(builder, tmpEnd, radius, detail) + addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps) + Vec3.set(tmpEnd, min[0], max[1], max[2]) + addSphere(builder, tmpEnd, radius, detail) + addCylinder(builder, tmpStart, tmpEnd, 1, cylinderProps) +} \ No newline at end of file diff --git a/src/mol-geo/mesh/builder/tube.ts b/src/mol-geo/mesh/builder/tube.ts index 944429468a81cbf653b99bc69295b8c5ca012c9a..eb3de091cec07adeed2b354b661be988ff42f32e 100644 --- a/src/mol-geo/mesh/builder/tube.ts +++ b/src/mol-geo/mesh/builder/tube.ts @@ -84,7 +84,7 @@ export function addTube(builder: MeshBuilder, controlPoints: ArrayLike<number>, Vec3.fromArray(u, normalVectors, offset) Vec3.fromArray(v, binormalVectors, offset) Vec3.fromArray(controlPoint, controlPoints, offset) - Vec3.cross(normalVector, u, v) + Vec3.cross(normalVector, v, u) ChunkedArray.add3(vertices, controlPoint[0], controlPoint[1], controlPoint[2]); ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]); @@ -107,9 +107,9 @@ export function addTube(builder: MeshBuilder, controlPoints: ArrayLike<number>, ChunkedArray.add3( indices, - centerVertex, + vertexCount + (i + 1) % radialSegments, vertexCount + i, - vertexCount + (i + 1) % radialSegments + centerVertex ); } } diff --git a/src/mol-geo/representation/index.ts b/src/mol-geo/representation/index.ts index a444545fe64d4823516e40b01a07e054b43239e9..ce1425040fd53f1705d7e241c155ddb6a2db4d5e 100644 --- a/src/mol-geo/representation/index.ts +++ b/src/mol-geo/representation/index.ts @@ -13,20 +13,19 @@ import { MarkerAction } from '../util/marker-data'; export interface RepresentationProps {} export interface Representation<D, P extends RepresentationProps = {}> { + readonly label: string readonly renderObjects: ReadonlyArray<RenderObject> readonly props: Readonly<P> - create: (data: D, props?: Partial<P>) => Task<void> - update: (props: Partial<P>) => Task<void> + createOrUpdate: (props?: Partial<P>, data?: D) => Task<void> getLoci: (pickingId: PickingId) => Loci - mark: (loci: Loci, action: MarkerAction) => void + mark: (loci: Loci, action: MarkerAction) => boolean destroy: () => void } export interface Visual<D, P extends RepresentationProps = {}> { - readonly renderObject: RenderObject - create: (ctx: RuntimeContext, data: D, props?: Partial<P>) => Promise<void> - update: (ctx: RuntimeContext, props: Partial<P>) => Promise<boolean> + readonly renderObject: RenderObject | undefined + createOrUpdate: (ctx: RuntimeContext, props?: Partial<P>, data?: D) => Promise<void> getLoci: (pickingId: PickingId) => Loci - mark: (loci: Loci, action: MarkerAction) => void + mark: (loci: Loci, action: MarkerAction) => boolean destroy: () => void } \ No newline at end of file diff --git a/src/mol-geo/representation/shape/index.ts b/src/mol-geo/representation/shape/index.ts index c9272af8df70789e128b6dc0be8d095f46504462..c5b86178a700bd021cbd759e1dcfe36ee43c5df2 100644 --- a/src/mol-geo/representation/shape/index.ts +++ b/src/mol-geo/representation/shape/index.ts @@ -10,7 +10,7 @@ import { RepresentationProps, Representation } from '..'; import { PickingId } from '../../util/picking'; import { Loci, EmptyLoci, isEveryLoci } from 'mol-model/loci'; import { MarkerAction, applyMarkerAction, createMarkers } from '../../util/marker-data'; -import { createRenderableState, createMeshValues, createIdentityTransform, DefaultMeshProps } from '../util'; +import { createRenderableState, createMeshValues, DefaultMeshProps } from '../util'; import { getMeshData } from '../../util/mesh-data'; import { MeshValues } from 'mol-gl/renderable'; import { ValueCell } from 'mol-util'; @@ -19,40 +19,48 @@ import { Shape } from 'mol-model/shape'; import { LocationIterator } from '../../util/location-iterator'; import { createColors } from '../structure/visual/util/common'; import { OrderedSet, Interval } from 'mol-data/int'; +import { createIdentityTransform } from '../../util/transform-data'; export interface ShapeRepresentation<P extends RepresentationProps = {}> extends Representation<Shape, P> { } export const DefaultShapeProps = { ...DefaultMeshProps, + colorTheme: { name: 'shape-group' } as ColorThemeProps } export type ShapeProps = typeof DefaultShapeProps +// TODO +// export type ShapeRepresentation = ShapeRepresentation<ShapeProps> + export function ShapeRepresentation<P extends ShapeProps>(): ShapeRepresentation<P> { const renderObjects: RenderObject[] = [] - let _renderObject: MeshRenderObject + let _renderObject: MeshRenderObject | undefined let _shape: Shape let _props: P - function create(shape: Shape, props: Partial<P> = {}) { + function createOrUpdate(props: Partial<P> = {}, shape?: Shape) { _props = Object.assign({}, DefaultShapeProps, _props, props) - _shape = shape + if (shape) _shape = shape return Task.create('ShapeRepresentation.create', async ctx => { renderObjects.length = 0 - const mesh = shape.mesh - const locationIt = ShapeGroupIterator.fromShape(shape) + if (!_shape) return + + const mesh = _shape.mesh + const locationIt = ShapeGroupIterator.fromShape(_shape) const { groupCount, instanceCount } = locationIt - const color = createColors(locationIt, _props.colorTheme) + const transform = createIdentityTransform() + const color = await createColors(ctx, locationIt, _props.colorTheme) const marker = createMarkers(instanceCount * groupCount) const counts = { drawCount: mesh.triangleCount * 3, groupCount, instanceCount } const values: MeshValues = { ...getMeshData(mesh), ...createMeshValues(_props, counts), - aTransform: createIdentityTransform(), + ...transform, ...color, ...marker, @@ -61,31 +69,24 @@ export function ShapeRepresentation<P extends ShapeProps>(): ShapeRepresentation const state = createRenderableState(_props) _renderObject = createMeshRenderObject(values, state) - console.log(_renderObject) renderObjects.push(_renderObject) }); } - function update(props: Partial<P>) { - return Task.create('ShapeRepresentation.update', async ctx => { - // TODO handle general update - // TODO check shape.colors.ref.version - }) - } - return { + label: 'Shape mesh', get renderObjects () { return renderObjects }, get props () { return _props }, - create, - update, + createOrUpdate, getLoci(pickingId: PickingId) { const { objectId, groupId } = pickingId - if (_renderObject.id === objectId) { + if (_renderObject && _renderObject.id === objectId) { return Shape.Loci([ { shape: _shape, ids: OrderedSet.ofSingleton(groupId) } ]) } return EmptyLoci }, mark(loci: Loci, action: MarkerAction) { + if (!_renderObject) return false const { tMarker } = _renderObject.values let changed = false if (isEveryLoci(loci)) { @@ -107,9 +108,12 @@ export function ShapeRepresentation<P extends ShapeProps>(): ShapeRepresentation if (changed) { ValueCell.update(tMarker, tMarker.ref.value) } + return changed }, destroy() { // TODO + renderObjects.length = 0 + _renderObject = undefined } } } diff --git a/src/mol-geo/representation/structure/complex-representation.ts b/src/mol-geo/representation/structure/complex-representation.ts index 6a258b24ca99cf9ccc9edc5d1297b306ac483051..3f624d4af188435dccb9238decbf223f9de2665c 100644 --- a/src/mol-geo/representation/structure/complex-representation.ts +++ b/src/mol-geo/representation/structure/complex-representation.ts @@ -8,70 +8,43 @@ import { Structure } from 'mol-model/structure'; import { Task } from 'mol-task' import { PickingId } from '../../util/picking'; -import { Loci, EmptyLoci, isEmptyLoci } from 'mol-model/loci'; +import { Loci, EmptyLoci } from 'mol-model/loci'; import { MarkerAction } from '../../util/marker-data'; -import { getQualityProps } from '../util'; -import { StructureProps, DefaultStructureProps, StructureRepresentation } from '.'; +import { StructureProps, StructureRepresentation } from '.'; import { ComplexVisual } from './complex-visual'; -export function ComplexRepresentation<P extends StructureProps>(visualCtor: () => ComplexVisual<P>): StructureRepresentation<P> { - let visual: ComplexVisual<P> - +export function ComplexRepresentation<P extends StructureProps>(label: string, visualCtor: () => ComplexVisual<P>): StructureRepresentation<P> { + let visual: ComplexVisual<P> | undefined let _props: P - let _structure: Structure - function create(structure: Structure, props: Partial<P> = {}) { - _props = Object.assign({}, DefaultStructureProps, _props, props, getQualityProps(props, structure)) - _props.colorTheme.structure = structure + function createOrUpdate(props: Partial<P> = {}, structure?: Structure) { + _props = Object.assign({}, _props, props) return Task.create('Creating StructureRepresentation', async ctx => { - if (!_structure) { - visual = visualCtor() - await visual.create(ctx, structure, _props) - } else { - if (_structure.hashCode === structure.hashCode) { - await update(_props) - } else { - if (!await visual.update(ctx, _props)) { - await visual.create(ctx, structure, _props) - } - } - } - _structure = structure + if (!visual) visual = visualCtor() + await visual.createOrUpdate(ctx, _props, structure) }); } - function update(props: Partial<P>) { - return Task.create('Updating StructureRepresentation', async ctx => { - _props = Object.assign({}, DefaultStructureProps, _props, props, getQualityProps(props, _structure)) - _props.colorTheme.structure = _structure - - if (!await visual.update(ctx, _props)) { - await visual.create(ctx, _structure, _props) - } - }) - } - function getLoci(pickingId: PickingId) { - let loci: Loci = EmptyLoci - const _loci = visual.getLoci(pickingId) - if (!isEmptyLoci(_loci)) loci = _loci - return loci + return visual ? visual.getLoci(pickingId) : EmptyLoci } function mark(loci: Loci, action: MarkerAction) { - visual.mark(loci, action) + return visual ? visual.mark(loci, action) : false } function destroy() { - visual.destroy() + if (visual) visual.destroy() } return { - get renderObjects() { return [ visual.renderObject ] }, + label, + get renderObjects() { + return visual && visual.renderObject ? [ visual.renderObject ] : [] + }, get props() { return _props }, - create, - update, + createOrUpdate, getLoci, mark, destroy diff --git a/src/mol-geo/representation/structure/complex-visual.ts b/src/mol-geo/representation/structure/complex-visual.ts index 063ecf36dc20499485644fb736b9f853f2b1b4bf..53a59c6d4ed75890f3a3463971c138f14445192b 100644 --- a/src/mol-geo/representation/structure/complex-visual.ts +++ b/src/mol-geo/representation/structure/complex-visual.ts @@ -15,7 +15,7 @@ import { StructureProps, DefaultStructureMeshProps, MeshUpdateState } from '.'; import { deepEqual, ValueCell } from 'mol-util'; import { updateMeshValues, updateRenderableState } from '../util'; import { PickingId } from '../../util/picking'; -import { Loci, isEveryLoci } from 'mol-model/loci'; +import { Loci, isEveryLoci, EmptyLoci } from 'mol-model/loci'; import { MarkerAction, applyMarkerAction } from '../../util/marker-data'; import { Interval } from 'mol-data/int'; @@ -39,62 +39,85 @@ export function ComplexMeshVisual<P extends ComplexMeshProps>(builder: ComplexMe const { defaultProps, createMesh, createLocationIterator, getLoci, mark, setUpdateState } = builder const updateState = MeshUpdateState.create() - let renderObject: MeshRenderObject + let renderObject: MeshRenderObject | undefined let currentProps: P let mesh: Mesh let currentStructure: Structure let locationIt: LocationIterator + let conformationHash: number - return { - get renderObject () { return renderObject }, - async create(ctx: RuntimeContext, structure: Structure, props: Partial<P> = {}) { - currentProps = Object.assign({}, defaultProps, props) - currentStructure = structure + async function create(ctx: RuntimeContext, structure: Structure, props: Partial<P> = {}) { + currentProps = Object.assign({}, defaultProps, props) + currentProps.colorTheme.structure = structure + currentStructure = structure - mesh = await createMesh(ctx, currentStructure, currentProps, mesh) + conformationHash = Structure.conformationHash(currentStructure) + mesh = await createMesh(ctx, currentStructure, currentProps, mesh) - locationIt = createLocationIterator(structure) - renderObject = createComplexMeshRenderObject(structure, mesh, locationIt, currentProps) - }, - async update(ctx: RuntimeContext, props: Partial<P>) { - const newProps = Object.assign({}, currentProps, props) + locationIt = createLocationIterator(structure) + renderObject = await createComplexMeshRenderObject(ctx, structure, mesh, locationIt, currentProps) + } - if (!renderObject) return false + async function update(ctx: RuntimeContext, props: Partial<P>) { + const newProps = Object.assign({}, currentProps, props) + newProps.colorTheme.structure = currentStructure - locationIt.reset() - MeshUpdateState.reset(updateState) - setUpdateState(updateState, newProps, currentProps) + if (!renderObject) return false - if (!deepEqual(newProps.sizeTheme, currentProps.sizeTheme)) { - updateState.createMesh = true - } + locationIt.reset() + MeshUpdateState.reset(updateState) + setUpdateState(updateState, newProps, currentProps) - if (!deepEqual(newProps.colorTheme, currentProps.colorTheme)) { - updateState.updateColor = true - } + const newConformationHash = Structure.conformationHash(currentStructure) + if (newConformationHash !== conformationHash) { + conformationHash = newConformationHash + updateState.createMesh = true + } - // + if (!deepEqual(newProps.sizeTheme, currentProps.sizeTheme)) updateState.createMesh = true + if (!deepEqual(newProps.colorTheme, currentProps.colorTheme)) updateState.updateColor = true + // if (!deepEqual(newProps.unitKinds, currentProps.unitKinds)) updateState.createMesh = true // TODO - if (updateState.createMesh) { - mesh = await createMesh(ctx, currentStructure, newProps, mesh) - ValueCell.update(renderObject.values.drawCount, mesh.triangleCount * 3) - updateState.updateColor = true - } + // - if (updateState.updateColor) { - createColors(locationIt, newProps.colorTheme, renderObject.values) - } + if (updateState.createMesh) { + mesh = await createMesh(ctx, currentStructure, newProps, mesh) + ValueCell.update(renderObject.values.drawCount, mesh.triangleCount * 3) + updateState.updateColor = true + } + + if (updateState.updateColor) { + await createColors(ctx, locationIt, newProps.colorTheme, renderObject.values) + } - updateMeshValues(renderObject.values, newProps) - updateRenderableState(renderObject.state, newProps) + updateMeshValues(renderObject.values, newProps) + updateRenderableState(renderObject.state, newProps) + + currentProps = newProps + return true + } - currentProps = newProps - return true + return { + get renderObject () { return renderObject }, + async createOrUpdate(ctx: RuntimeContext, props: Partial<P> = {}, structure?: Structure) { + if (!structure && !currentStructure) { + throw new Error('missing structure') + } else if (structure && (!currentStructure || !renderObject)) { + await create(ctx, structure, props) + } else if (structure && structure.hashCode !== currentStructure.hashCode) { + await create(ctx, structure, props) + } else { + if (structure && Structure.conformationHash(structure) !== Structure.conformationHash(currentStructure)) { + currentStructure = structure + } + await update(ctx, props) + } }, getLoci(pickingId: PickingId) { - return getLoci(pickingId, currentStructure, renderObject.id) + return renderObject ? getLoci(pickingId, currentStructure, renderObject.id) : EmptyLoci }, mark(loci: Loci, action: MarkerAction) { + if (!renderObject) return false const { tMarker } = renderObject.values const { groupCount, instanceCount } = locationIt @@ -106,17 +129,18 @@ export function ComplexMeshVisual<P extends ComplexMeshProps>(builder: ComplexMe let changed = false if (isEveryLoci(loci)) { - apply(Interval.ofBounds(0, groupCount * instanceCount)) - changed = true + changed = apply(Interval.ofBounds(0, groupCount * instanceCount)) } else { changed = mark(loci, currentStructure, apply) } if (changed) { ValueCell.update(tMarker, tMarker.ref.value) } + return changed }, destroy() { // TODO + renderObject = undefined } } } \ No newline at end of file diff --git a/src/mol-geo/representation/structure/index.ts b/src/mol-geo/representation/structure/index.ts index 06a4cba0e2a23ef2b0b742256298f705a1675a3b..7628d9e084ba81ff6ef7e9705638532e73ee2c77 100644 --- a/src/mol-geo/representation/structure/index.ts +++ b/src/mol-geo/representation/structure/index.ts @@ -27,17 +27,20 @@ export const DefaultStructureMeshProps = { export type StructureMeshProps = typeof DefaultStructureMeshProps export interface MeshUpdateState { + updateTransform: boolean updateColor: boolean createMesh: boolean } export namespace MeshUpdateState { export function create(): MeshUpdateState { return { + updateTransform: false, updateColor: false, createMesh: false } } export function reset(state: MeshUpdateState) { + state.updateTransform = false state.updateColor = false state.createMesh = false } diff --git a/src/mol-geo/representation/structure/representation/backbone.ts b/src/mol-geo/representation/structure/representation/backbone.ts index 2afb0c8cfde3121f807a4ba2e83c998f5af7bbcc..2d618326baff0370501be0557e13a74a6bce2a71 100644 --- a/src/mol-geo/representation/structure/representation/backbone.ts +++ b/src/mol-geo/representation/structure/representation/backbone.ts @@ -11,40 +11,39 @@ import { Task } from 'mol-task'; import { Loci } from 'mol-model/loci'; import { MarkerAction } from '../../../util/marker-data'; import { PolymerBackboneVisual, DefaultPolymerBackboneProps } from '../visual/polymer-backbone-cylinder'; +import { getQualityProps } from '../../util'; export const DefaultBackboneProps = { ...DefaultPolymerBackboneProps } export type BackboneProps = typeof DefaultBackboneProps -export function BackboneRepresentation(): StructureRepresentation<BackboneProps> { - const traceRepr = UnitsRepresentation(PolymerBackboneVisual) +export type BackboneRepresentation = StructureRepresentation<BackboneProps> + +export function BackboneRepresentation(): BackboneRepresentation { + const traceRepr = UnitsRepresentation('Polymer backbone cylinder', PolymerBackboneVisual) let currentProps: BackboneProps return { + label: 'Backbone', get renderObjects() { return [ ...traceRepr.renderObjects ] }, get props() { return { ...traceRepr.props } }, - create: (structure: Structure, props: Partial<BackboneProps> = {}) => { - currentProps = Object.assign({}, DefaultBackboneProps, props) + createOrUpdate: (props: Partial<BackboneProps> = {}, structure?: Structure) => { + const qualityProps = getQualityProps(Object.assign({}, currentProps, props), structure) + currentProps = Object.assign({}, DefaultBackboneProps, currentProps, props, qualityProps) return Task.create('BackboneRepresentation', async ctx => { - await traceRepr.create(structure, currentProps).runInContext(ctx) - }) - }, - update: (props: Partial<BackboneProps>) => { - currentProps = Object.assign(currentProps, props) - return Task.create('Updating BackboneRepresentation', async ctx => { - await traceRepr.update(currentProps).runInContext(ctx) + await traceRepr.createOrUpdate(currentProps, structure).runInContext(ctx) }) }, getLoci: (pickingId: PickingId) => { return traceRepr.getLoci(pickingId) }, mark: (loci: Loci, action: MarkerAction) => { - traceRepr.mark(loci, action) + return traceRepr.mark(loci, action) }, destroy() { traceRepr.destroy() diff --git a/src/mol-geo/representation/structure/representation/ball-and-stick.ts b/src/mol-geo/representation/structure/representation/ball-and-stick.ts index d26ab096839de45dba420f8cd18c14da374f3c1f..c031185cf1fcd557487dd50550fd6da3757c4453 100644 --- a/src/mol-geo/representation/structure/representation/ball-and-stick.ts +++ b/src/mol-geo/representation/structure/representation/ball-and-stick.ts @@ -14,43 +14,40 @@ import { Loci, isEmptyLoci } from 'mol-model/loci'; import { MarkerAction } from '../../../util/marker-data'; import { InterUnitLinkVisual } from '../visual/inter-unit-link-cylinder'; import { SizeThemeProps } from 'mol-view/theme/size'; +import { getQualityProps } from '../../util'; export const DefaultBallAndStickProps = { ...DefaultElementSphereProps, ...DefaultIntraUnitLinkProps, - sizeTheme: { name: 'uniform', value: 0.25 } as SizeThemeProps, + sizeTheme: { name: 'uniform', value: 0.2 } as SizeThemeProps, unitKinds: [ Unit.Kind.Atomic ] as Unit.Kind[] } export type BallAndStickProps = typeof DefaultBallAndStickProps -export function BallAndStickRepresentation(): StructureRepresentation<BallAndStickProps> { - const elmementRepr = UnitsRepresentation(ElementSphereVisual) - const intraLinkRepr = UnitsRepresentation(IntraUnitLinkVisual) - const interLinkRepr = ComplexRepresentation(InterUnitLinkVisual) +export type BallAndStickRepresentation = StructureRepresentation<BallAndStickProps> + +export function BallAndStickRepresentation(): BallAndStickRepresentation { + const elmementRepr = UnitsRepresentation('Element sphere mesh', ElementSphereVisual) + const intraLinkRepr = UnitsRepresentation('Intra-unit link cylinder', IntraUnitLinkVisual) + const interLinkRepr = ComplexRepresentation('Inter-unit link cylinder', InterUnitLinkVisual) let currentProps: BallAndStickProps return { + label: 'Ball & Stick', get renderObjects() { return [ ...elmementRepr.renderObjects, ...intraLinkRepr.renderObjects, ...interLinkRepr.renderObjects ] }, get props() { return { ...elmementRepr.props, ...intraLinkRepr.props, ...interLinkRepr.props } }, - create: (structure: Structure, props: Partial<BallAndStickProps> = {}) => { - currentProps = Object.assign({}, DefaultBallAndStickProps, props) - return Task.create('DistanceRestraintRepresentation', async ctx => { - await elmementRepr.create(structure, currentProps).runInContext(ctx) - await intraLinkRepr.create(structure, currentProps).runInContext(ctx) - await interLinkRepr.create(structure, currentProps).runInContext(ctx) - }) - }, - update: (props: Partial<BallAndStickProps>) => { - currentProps = Object.assign(currentProps, props) - return Task.create('Updating BallAndStickRepresentation', async ctx => { - await elmementRepr.update(currentProps).runInContext(ctx) - await intraLinkRepr.update(currentProps).runInContext(ctx) - await interLinkRepr.update(currentProps).runInContext(ctx) + createOrUpdate: (props: Partial<BallAndStickProps> = {}, structure?: Structure) => { + const qualityProps = getQualityProps(Object.assign({}, currentProps, props), structure) + currentProps = Object.assign({}, DefaultBallAndStickProps, currentProps, props, qualityProps) + return Task.create('BallAndStickRepresentation', async ctx => { + await elmementRepr.createOrUpdate(currentProps, structure).runInContext(ctx) + await intraLinkRepr.createOrUpdate(currentProps, structure).runInContext(ctx) + await interLinkRepr.createOrUpdate(currentProps, structure).runInContext(ctx) }) }, getLoci: (pickingId: PickingId) => { @@ -68,9 +65,10 @@ export function BallAndStickRepresentation(): StructureRepresentation<BallAndSti } }, mark: (loci: Loci, action: MarkerAction) => { - elmementRepr.mark(loci, action) - intraLinkRepr.mark(loci, action) - interLinkRepr.mark(loci, action) + const markElement = elmementRepr.mark(loci, action) + const markIntraLink = intraLinkRepr.mark(loci, action) + const markInterLink = interLinkRepr.mark(loci, action) + return markElement || markIntraLink || markInterLink }, destroy() { elmementRepr.destroy() diff --git a/src/mol-geo/representation/structure/representation/carbohydrate.ts b/src/mol-geo/representation/structure/representation/carbohydrate.ts index bc877da0f8223269ce86771643282d18dda58a0e..46220707b4a823d84dc9c6d3777f18b8a348bb89 100644 --- a/src/mol-geo/representation/structure/representation/carbohydrate.ts +++ b/src/mol-geo/representation/structure/representation/carbohydrate.ts @@ -12,37 +12,38 @@ import { Loci, isEmptyLoci } from 'mol-model/loci'; import { MarkerAction } from '../../../util/marker-data'; import { CarbohydrateSymbolVisual, DefaultCarbohydrateSymbolProps } from '../visual/carbohydrate-symbol-mesh'; import { CarbohydrateLinkVisual, DefaultCarbohydrateLinkProps } from '../visual/carbohydrate-link-cylinder'; +import { SizeThemeProps } from 'mol-view/theme/size'; +import { getQualityProps } from '../../util'; -export const DefaultCartoonProps = { +export const DefaultCarbohydrateProps = { ...DefaultCarbohydrateSymbolProps, - ...DefaultCarbohydrateLinkProps + ...DefaultCarbohydrateLinkProps, + + sizeTheme: { name: 'uniform', value: 1, factor: 1 } as SizeThemeProps, } -export type CarbohydrateProps = typeof DefaultCartoonProps +export type CarbohydrateProps = typeof DefaultCarbohydrateProps + +export type CarbohydrateRepresentation = StructureRepresentation<CarbohydrateProps> -export function CarbohydrateRepresentation(): StructureRepresentation<CarbohydrateProps> { - const carbohydrateSymbolRepr = ComplexRepresentation(CarbohydrateSymbolVisual) - const carbohydrateLinkRepr = ComplexRepresentation(CarbohydrateLinkVisual) +export function CarbohydrateRepresentation(): CarbohydrateRepresentation { + const carbohydrateSymbolRepr = ComplexRepresentation('Carbohydrate symbol mesh', CarbohydrateSymbolVisual) + const carbohydrateLinkRepr = ComplexRepresentation('Carbohydrate link cylinder', CarbohydrateLinkVisual) let currentProps: CarbohydrateProps return { + label: 'Carbohydrate', get renderObjects() { return [ ...carbohydrateSymbolRepr.renderObjects, ...carbohydrateLinkRepr.renderObjects ] }, get props() { return { ...carbohydrateSymbolRepr.props, ...carbohydrateLinkRepr.props } }, - create: (structure: Structure, props: Partial<CarbohydrateProps> = {} as CarbohydrateProps) => { - currentProps = Object.assign({}, DefaultCartoonProps, props) + createOrUpdate: (props: Partial<CarbohydrateProps> = {}, structure?: Structure) => { + const qualityProps = getQualityProps(Object.assign({}, currentProps, props), structure) + currentProps = Object.assign({}, DefaultCarbohydrateProps, currentProps, props, qualityProps) return Task.create('Creating CarbohydrateRepresentation', async ctx => { - await carbohydrateSymbolRepr.create(structure, currentProps).runInContext(ctx) - await carbohydrateLinkRepr.create(structure, currentProps).runInContext(ctx) - }) - }, - update: (props: Partial<CarbohydrateProps>) => { - currentProps = Object.assign(currentProps, props) - return Task.create('Updating CarbohydrateRepresentation', async ctx => { - await carbohydrateSymbolRepr.update(currentProps).runInContext(ctx) - await carbohydrateLinkRepr.update(currentProps).runInContext(ctx) + await carbohydrateSymbolRepr.createOrUpdate(currentProps, structure).runInContext(ctx) + await carbohydrateLinkRepr.createOrUpdate(currentProps, structure).runInContext(ctx) }) }, getLoci: (pickingId: PickingId) => { @@ -52,8 +53,9 @@ export function CarbohydrateRepresentation(): StructureRepresentation<Carbohydra : carbohydrateLinkLoci }, mark: (loci: Loci, action: MarkerAction) => { - carbohydrateSymbolRepr.mark(loci, action) - carbohydrateLinkRepr.mark(loci, action) + const markSymbol = carbohydrateSymbolRepr.mark(loci, action) + const markLink = carbohydrateLinkRepr.mark(loci, action) + return markSymbol || markLink }, destroy() { carbohydrateSymbolRepr.destroy() diff --git a/src/mol-geo/representation/structure/representation/cartoon.ts b/src/mol-geo/representation/structure/representation/cartoon.ts index bc9b43e09a2c19f225b88a7ca122b4cbf362d807..2bafe115fc5788ef70a58f9e5b075ec515126d01 100644 --- a/src/mol-geo/representation/structure/representation/cartoon.ts +++ b/src/mol-geo/representation/structure/representation/cartoon.ts @@ -13,24 +13,31 @@ import { MarkerAction } from '../../../util/marker-data'; import { PolymerTraceVisual, DefaultPolymerTraceProps } from '../visual/polymer-trace-mesh'; import { PolymerGapVisual, DefaultPolymerGapProps } from '../visual/polymer-gap-cylinder'; import { NucleotideBlockVisual, DefaultNucleotideBlockProps } from '../visual/nucleotide-block-mesh'; -import { PolymerDirectionVisual, DefaultPolymerDirectionProps } from '../visual/polymer-direction-wedge'; +import { SizeThemeProps } from 'mol-view/theme/size'; +import { getQualityProps } from '../../util'; +// import { PolymerDirectionVisual, DefaultPolymerDirectionProps } from '../visual/polymer-direction-wedge'; export const DefaultCartoonProps = { ...DefaultPolymerTraceProps, ...DefaultPolymerGapProps, ...DefaultNucleotideBlockProps, - ...DefaultPolymerDirectionProps + // ...DefaultPolymerDirectionProps, + + sizeTheme: { name: 'uniform', value: 0.2 } as SizeThemeProps, } export type CartoonProps = typeof DefaultCartoonProps -export function CartoonRepresentation(): StructureRepresentation<CartoonProps> { - const traceRepr = UnitsRepresentation(PolymerTraceVisual) - const gapRepr = UnitsRepresentation(PolymerGapVisual) - const blockRepr = UnitsRepresentation(NucleotideBlockVisual) - const directionRepr = UnitsRepresentation(PolymerDirectionVisual) +export type CartoonRepresentation = StructureRepresentation<CartoonProps> + +export function CartoonRepresentation(): CartoonRepresentation { + const traceRepr = UnitsRepresentation('Polymer trace mesh', PolymerTraceVisual) + const gapRepr = UnitsRepresentation('Polymer gap cylinder', PolymerGapVisual) + const blockRepr = UnitsRepresentation('Nucleotide block mesh', NucleotideBlockVisual) + // const directionRepr = UnitsRepresentation('Polymer direction wedge', PolymerDirectionVisual) let currentProps: CartoonProps return { + label: 'Cartoon', get renderObjects() { return [ ...traceRepr.renderObjects, ...gapRepr.renderObjects, ...blockRepr.renderObjects // , ...directionRepr.renderObjects @@ -39,45 +46,39 @@ export function CartoonRepresentation(): StructureRepresentation<CartoonProps> { get props() { return { ...traceRepr.props, ...gapRepr.props, ...blockRepr.props } }, - create: (structure: Structure, props: Partial<CartoonProps> = {}) => { - currentProps = Object.assign({}, DefaultCartoonProps, props) + createOrUpdate: (props: Partial<CartoonProps> = {}, structure?: Structure) => { + const qualityProps = getQualityProps(Object.assign({}, currentProps, props), structure) + currentProps = Object.assign({}, DefaultCartoonProps, currentProps, props, qualityProps) return Task.create('Creating CartoonRepresentation', async ctx => { - await traceRepr.create(structure, currentProps).runInContext(ctx) - await gapRepr.create(structure, currentProps).runInContext(ctx) - await blockRepr.create(structure, currentProps).runInContext(ctx) - await directionRepr.create(structure, currentProps).runInContext(ctx) - }) - }, - update: (props: Partial<CartoonProps>) => { - currentProps = Object.assign(currentProps, props) - return Task.create('Updating CartoonRepresentation', async ctx => { - await traceRepr.update(currentProps).runInContext(ctx) - await gapRepr.update(currentProps).runInContext(ctx) - await blockRepr.update(currentProps).runInContext(ctx) - await directionRepr.update(currentProps).runInContext(ctx) + await traceRepr.createOrUpdate(currentProps, structure).runInContext(ctx) + await gapRepr.createOrUpdate(currentProps, structure).runInContext(ctx) + await blockRepr.createOrUpdate(currentProps, structure).runInContext(ctx) + // await directionRepr.createOrUpdate(currentProps, structure).runInContext(ctx) }) }, getLoci: (pickingId: PickingId) => { const traceLoci = traceRepr.getLoci(pickingId) const gapLoci = gapRepr.getLoci(pickingId) const blockLoci = blockRepr.getLoci(pickingId) - const directionLoci = directionRepr.getLoci(pickingId) + // const directionLoci = directionRepr.getLoci(pickingId) return !isEmptyLoci(traceLoci) ? traceLoci : !isEmptyLoci(gapLoci) ? gapLoci - : !isEmptyLoci(blockLoci) ? blockLoci - : directionLoci + : blockLoci + // : !isEmptyLoci(blockLoci) ? blockLoci + // : directionLoci }, mark: (loci: Loci, action: MarkerAction) => { - traceRepr.mark(loci, action) - gapRepr.mark(loci, action) - blockRepr.mark(loci, action) - directionRepr.mark(loci, action) + const markTrace = traceRepr.mark(loci, action) + const markGap = gapRepr.mark(loci, action) + const markBlock = blockRepr.mark(loci, action) + // const markDirection = directionRepr.mark(loci, action) + return markTrace || markGap || markBlock // \\ markDirection }, destroy() { traceRepr.destroy() gapRepr.destroy() blockRepr.destroy() - directionRepr.destroy() + // directionRepr.destroy() } } } \ No newline at end of file diff --git a/src/mol-geo/representation/structure/representation/distance-restraint.ts b/src/mol-geo/representation/structure/representation/distance-restraint.ts index bf16518eb73e61c16ad4a8b1395a02eca2c4ac38..d1ed2524ee9dcd68226d126442ca945d9f374fa7 100644 --- a/src/mol-geo/representation/structure/representation/distance-restraint.ts +++ b/src/mol-geo/representation/structure/representation/distance-restraint.ts @@ -12,6 +12,7 @@ import { Loci } from 'mol-model/loci'; import { MarkerAction } from '../../../util/marker-data'; import { CrossLinkRestraintVisual, DefaultCrossLinkRestraintProps } from '../visual/cross-link-restraint-cylinder'; import { SizeThemeProps } from 'mol-view/theme/size'; +import { getQualityProps } from '../../util'; export const DefaultDistanceRestraintProps = { ...DefaultCrossLinkRestraintProps, @@ -19,34 +20,32 @@ export const DefaultDistanceRestraintProps = { } export type DistanceRestraintProps = typeof DefaultDistanceRestraintProps -export function DistanceRestraintRepresentation(): StructureRepresentation<DistanceRestraintProps> { - const crossLinkRepr = ComplexRepresentation(CrossLinkRestraintVisual) +export type DistanceRestraintRepresentation = StructureRepresentation<DistanceRestraintProps> + +export function DistanceRestraintRepresentation(): DistanceRestraintRepresentation { + const crossLinkRepr = ComplexRepresentation('Cross-link restraint', CrossLinkRestraintVisual) let currentProps: DistanceRestraintProps return { + label: 'Distance restraint', get renderObjects() { return [ ...crossLinkRepr.renderObjects ] }, get props() { return { ...crossLinkRepr.props } }, - create: (structure: Structure, props: Partial<DistanceRestraintProps> = {}) => { - currentProps = Object.assign({}, DefaultDistanceRestraintProps, props) + createOrUpdate: (props: Partial<DistanceRestraintProps> = {}, structure?: Structure) => { + const qualityProps = getQualityProps(Object.assign({}, currentProps, props), structure) + currentProps = Object.assign({}, DefaultDistanceRestraintProps, currentProps, props, qualityProps) return Task.create('DistanceRestraintRepresentation', async ctx => { - await crossLinkRepr.create(structure, currentProps).runInContext(ctx) - }) - }, - update: (props: Partial<DistanceRestraintProps>) => { - currentProps = Object.assign(currentProps, props) - return Task.create('Updating DistanceRestraintRepresentation', async ctx => { - await crossLinkRepr.update(currentProps).runInContext(ctx) + await crossLinkRepr.createOrUpdate(currentProps, structure).runInContext(ctx) }) }, getLoci: (pickingId: PickingId) => { return crossLinkRepr.getLoci(pickingId) }, mark: (loci: Loci, action: MarkerAction) => { - crossLinkRepr.mark(loci, action) + return crossLinkRepr.mark(loci, action) }, destroy() { crossLinkRepr.destroy() diff --git a/src/mol-geo/representation/structure/representation/point.ts b/src/mol-geo/representation/structure/representation/point.ts new file mode 100644 index 0000000000000000000000000000000000000000..9c388317cd9badd0c37899a7833201cb20831d9b --- /dev/null +++ b/src/mol-geo/representation/structure/representation/point.ts @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { UnitsRepresentation } from '..'; +import { ElementPointVisual, DefaultElementPointProps } from '../visual/element-point'; +import { StructureRepresentation } from '../units-representation'; +import { Structure } from 'mol-model/structure'; +import { MarkerAction } from '../../../util/marker-data'; +import { Loci } from 'mol-model/loci'; +import { PickingId } from '../../../util/picking'; + +export const DefaultPointProps = { + ...DefaultElementPointProps, +} +export type PointProps = typeof DefaultPointProps + +export type PointRepresentation = StructureRepresentation<PointProps> + +export function PointRepresentation(): PointRepresentation { + let currentProps: PointProps + const pointRepr = UnitsRepresentation('Point', ElementPointVisual) + return { + label: 'Point', + get renderObjects() { + return [ ...pointRepr.renderObjects ] + }, + get props() { + return { ...pointRepr.props } + }, + createOrUpdate: (props: Partial<PointProps> = {}, structure?: Structure) => { + currentProps = Object.assign({}, DefaultPointProps, currentProps, props) + return pointRepr.createOrUpdate(currentProps, structure) + }, + getLoci: (pickingId: PickingId) => { + return pointRepr.getLoci(pickingId) + }, + mark: (loci: Loci, action: MarkerAction) => { + return pointRepr.mark(loci, action) + }, + destroy() { + pointRepr.destroy() + } + } +} \ No newline at end of file diff --git a/src/mol-geo/representation/structure/representation/spacefill.ts b/src/mol-geo/representation/structure/representation/spacefill.ts index d2062b2975e6e3efe568520b26c439447dec7870..5de4260277c617a03b0012cab38c7de44aeb5686 100644 --- a/src/mol-geo/representation/structure/representation/spacefill.ts +++ b/src/mol-geo/representation/structure/representation/spacefill.ts @@ -6,12 +6,44 @@ import { UnitsRepresentation } from '..'; import { ElementSphereVisual, DefaultElementSphereProps } from '../visual/element-sphere'; +import { StructureRepresentation } from '../units-representation'; +import { Structure } from 'mol-model/structure'; +import { PickingId } from '../../../util/picking'; +import { MarkerAction } from '../../../util/marker-data'; +import { Loci } from 'mol-model/loci'; +import { getQualityProps } from '../../util'; export const DefaultSpacefillProps = { ...DefaultElementSphereProps } export type SpacefillProps = typeof DefaultSpacefillProps -export function SpacefillRepresentation() { - return UnitsRepresentation(ElementSphereVisual) +export type SpacefillRepresentation = StructureRepresentation<SpacefillProps> + +export function SpacefillRepresentation(): SpacefillRepresentation { + let currentProps: SpacefillProps + const sphereRepr = UnitsRepresentation('Sphere mesh', ElementSphereVisual) + return { + label: 'Spacefill', + get renderObjects() { + return [ ...sphereRepr.renderObjects ] + }, + get props() { + return { ...sphereRepr.props } + }, + createOrUpdate: (props: Partial<SpacefillProps> = {}, structure?: Structure) => { + const qualityProps = getQualityProps(Object.assign({}, currentProps, props), structure) + currentProps = Object.assign({}, DefaultSpacefillProps, currentProps, props, qualityProps) + return sphereRepr.createOrUpdate(currentProps, structure) + }, + getLoci: (pickingId: PickingId) => { + return sphereRepr.getLoci(pickingId) + }, + mark: (loci: Loci, action: MarkerAction) => { + return sphereRepr.mark(loci, action) + }, + destroy() { + sphereRepr.destroy() + } + } } \ No newline at end of file diff --git a/src/mol-geo/representation/structure/units-representation.ts b/src/mol-geo/representation/structure/units-representation.ts index f2ed113e7b621b7db6d0ef4827f9f4c9d019a9be..8b7e9ec5e2c22db5460a928eec7ecf2d254d35ce 100644 --- a/src/mol-geo/representation/structure/units-representation.ts +++ b/src/mol-geo/representation/structure/units-representation.ts @@ -12,87 +12,99 @@ import { Representation, RepresentationProps, Visual } from '..'; import { PickingId } from '../../util/picking'; import { Loci, EmptyLoci, isEmptyLoci } from 'mol-model/loci'; import { MarkerAction } from '../../util/marker-data'; -import { getQualityProps } from '../util'; -import { DefaultStructureProps, StructureProps } from '.'; +import { StructureProps } from '.'; +import { StructureGroup } from './units-visual'; -export interface UnitsVisual<P extends RepresentationProps = {}> extends Visual<Unit.SymmetryGroup, P> { } +export interface UnitsVisual<P extends RepresentationProps = {}> extends Visual<StructureGroup, P> { } export interface StructureVisual<P extends RepresentationProps = {}> extends Visual<Structure, P> { } export interface StructureRepresentation<P extends RepresentationProps = {}> extends Representation<Structure, P> { } -export function UnitsRepresentation<P extends StructureProps>(visualCtor: () => UnitsVisual<P>): StructureRepresentation<P> { +export function UnitsRepresentation<P extends StructureProps>(label: string, visualCtor: () => UnitsVisual<P>): StructureRepresentation<P> { let visuals = new Map<number, { group: Unit.SymmetryGroup, visual: UnitsVisual<P> }>() let _props: P let _structure: Structure let _groups: ReadonlyArray<Unit.SymmetryGroup> - function create(structure: Structure, props: Partial<P> = {}) { - _props = Object.assign({}, DefaultStructureProps, _props, props, getQualityProps(props, structure)) - _props.colorTheme!.structure = structure + function createOrUpdate(props: Partial<P> = {}, structure?: Structure) { + _props = Object.assign({}, _props, props) - return Task.create('Creating StructureRepresentation', async ctx => { - if (!_structure) { + return Task.create('Creating or updating StructureRepresentation', async ctx => { + if (!_structure && !structure) { + throw new Error('missing structure') + } else if (structure && !_structure) { + // console.log('initial structure') + // First call with a structure, create visuals for each group. _groups = structure.unitSymmetryGroups; for (let i = 0; i < _groups.length; i++) { const group = _groups[i]; const visual = visualCtor() - await visual.create(ctx, group, _props) + await visual.createOrUpdate(ctx, _props, { group, structure }) visuals.set(group.hashCode, { visual, group }) } - } else { - if (_structure.hashCode === structure.hashCode) { - await update(_props) - } else { - _groups = structure.unitSymmetryGroups; - const newGroups: Unit.SymmetryGroup[] = [] - const oldUnitsVisuals = visuals - visuals = new Map() - for (let i = 0; i < _groups.length; i++) { - const group = _groups[i]; - const visualGroup = oldUnitsVisuals.get(group.hashCode) - if (visualGroup) { - const { visual, group } = visualGroup - if (!await visual.update(ctx, _props)) { - await visual.create(ctx, group, _props) - } - oldUnitsVisuals.delete(group.hashCode) - } else { - newGroups.push(group) - const visual = visualCtor() - await visual.create(ctx, group, _props) - visuals.set(group.hashCode, { visual, group }) - } + } else if (structure && _structure.hashCode !== structure.hashCode) { + // console.log('_structure.hashCode !== structure.hashCode') + // 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; + // const newGroups: Unit.SymmetryGroup[] = [] + const oldVisuals = visuals + visuals = new Map() + for (let i = 0; i < _groups.length; i++) { + const group = _groups[i]; + const visualGroup = oldVisuals.get(group.hashCode) + if (visualGroup) { + const { visual } = visualGroup + await visual.createOrUpdate(ctx, _props, { group, structure }) + visuals.set(group.hashCode, { visual, group }) + oldVisuals.delete(group.hashCode) + } else { + // newGroups.push(group) + const visual = visualCtor() + await visual.createOrUpdate(ctx, _props, { group, structure }) + visuals.set(group.hashCode, { visual, group }) } + } + oldVisuals.forEach(({ visual }) => visual.destroy()) - // for new groups, re-use leftover visuals - const unusedVisuals: UnitsVisual<P>[] = [] - oldUnitsVisuals.forEach(({ visual }) => unusedVisuals.push(visual)) - newGroups.forEach(async group => { - const visual = unusedVisuals.pop() || visualCtor() - await visual.create(ctx, group, _props) - visuals.set(group.hashCode, { visual, group }) - }) - unusedVisuals.forEach(visual => visual.destroy()) + // TODO review logic + // For new groups, re-use left-over visuals + // const unusedVisuals: UnitsVisual<P>[] = [] + // oldVisuals.forEach(({ visual }) => unusedVisuals.push(visual)) + // newGroups.forEach(async group => { + // const visual = unusedVisuals.pop() || visualCtor() + // await visual.createOrUpdate(ctx, _props, group) + // visuals.set(group.hashCode, { visual, group }) + // }) + // unusedVisuals.forEach(visual => visual.destroy()) + } else if (structure && _structure.hashCode === structure.hashCode) { + // console.log('_structure.hashCode === structure.hashCode') + // Expects that for structures with the same hashCode, + // the unitSymmetryGroups are the same as well. + // Re-uses existing visuals for the groups of the new structure. + _groups = structure.unitSymmetryGroups; + for (let i = 0; i < _groups.length; i++) { + const group = _groups[i]; + const visualGroup = visuals.get(group.hashCode) + if (visualGroup) { + await visualGroup.visual.createOrUpdate(ctx, _props, { group, structure }) + visualGroup.group = group + } else { + throw new Error(`expected to find visual for hashCode ${group.hashCode}`) + } } + } else { + // console.log('no new structure') + // No new structure given, just update all visuals with new props. + visuals.forEach(async ({ visual, group }) => { + await visual.createOrUpdate(ctx, _props, { group, structure: _structure }) + }) } - _structure = structure + if (structure) _structure = structure }); } - function update(props: Partial<P>) { - return Task.create('Updating StructureRepresentation', async ctx => { - _props = Object.assign({}, DefaultStructureProps, _props, props, getQualityProps(props, _structure)) - _props.colorTheme!.structure = _structure - - visuals.forEach(async ({ visual, group }) => { - if (!await visual.update(ctx, _props)) { - await visual.create(ctx, group, _props) - } - }) - }) - } - function getLoci(pickingId: PickingId) { let loci: Loci = EmptyLoci visuals.forEach(({ visual }) => { @@ -103,7 +115,11 @@ export function UnitsRepresentation<P extends StructureProps>(visualCtor: () => } function mark(loci: Loci, action: MarkerAction) { - visuals.forEach(({ visual }) => visual.mark(loci, action)) + let changed = false + visuals.forEach(({ visual }) => { + changed = visual.mark(loci, action) || changed + }) + return changed } function destroy() { @@ -112,16 +128,18 @@ export function UnitsRepresentation<P extends StructureProps>(visualCtor: () => } return { + label, get renderObjects() { const renderObjects: RenderObject[] = [] - visuals.forEach(({ visual }) => renderObjects.push(visual.renderObject)) + visuals.forEach(({ visual }) => { + if (visual.renderObject) renderObjects.push(visual.renderObject) + }) return renderObjects }, get props() { return _props }, - create, - update, + createOrUpdate, getLoci, mark, destroy diff --git a/src/mol-geo/representation/structure/units-visual.ts b/src/mol-geo/representation/structure/units-visual.ts index d7ebb4b9bc5ccc140c3091671ba836e8fbc87f6a..534764f3d57028e1ad33103d4e90125d3c69d4fe 100644 --- a/src/mol-geo/representation/structure/units-visual.ts +++ b/src/mol-geo/representation/structure/units-visual.ts @@ -4,22 +4,25 @@ * @author Alexander Rose <alexander.rose@weirdbyte.de> */ -import { Unit } from 'mol-model/structure'; +import { Unit, Structure } from 'mol-model/structure'; import { RepresentationProps, Visual } from '..'; import { DefaultStructureMeshProps, MeshUpdateState } from '.'; import { RuntimeContext } from 'mol-task'; import { PickingId } from '../../util/picking'; import { LocationIterator } from '../../util/location-iterator'; import { Mesh } from '../../mesh/mesh'; -import { MarkerAction, applyMarkerAction } from '../../util/marker-data'; -import { Loci, isEveryLoci } from 'mol-model/loci'; +import { MarkerAction, applyMarkerAction, createMarkers } from '../../util/marker-data'; +import { Loci, isEveryLoci, EmptyLoci } from 'mol-model/loci'; import { MeshRenderObject } from 'mol-gl/render-object'; import { createUnitsMeshRenderObject, createColors } from './visual/util/common'; -import { deepEqual, ValueCell } from 'mol-util'; +import { deepEqual, ValueCell, UUID } from 'mol-util'; import { updateMeshValues, updateRenderableState } from '../util'; import { Interval } from 'mol-data/int'; +import { createTransforms } from '../../util/transform-data'; -export interface UnitsVisual<P extends RepresentationProps = {}> extends Visual<Unit.SymmetryGroup, P> { } +export type StructureGroup = { structure: Structure, group: Unit.SymmetryGroup } + +export interface UnitsVisual<P extends RepresentationProps = {}> extends Visual<StructureGroup, P> { } export const DefaultUnitsMeshProps = { ...DefaultStructureMeshProps, @@ -40,66 +43,104 @@ export function UnitsMeshVisual<P extends UnitsMeshProps>(builder: UnitsMeshVisu const { defaultProps, createMesh, createLocationIterator, getLoci, mark, setUpdateState } = builder const updateState = MeshUpdateState.create() - let renderObject: MeshRenderObject + let renderObject: MeshRenderObject | undefined let currentProps: P let mesh: Mesh let currentGroup: Unit.SymmetryGroup + let currentStructure: Structure let locationIt: LocationIterator + let currentConformationId: UUID + + async function create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: Partial<P> = {}) { + currentProps = Object.assign({}, defaultProps, props) + currentProps.colorTheme.structure = currentStructure + currentGroup = group + + const unit = group.units[0] + currentConformationId = Unit.conformationId(unit) + mesh = currentProps.unitKinds.includes(unit.kind) + ? await createMesh(ctx, unit, currentProps, mesh) + : Mesh.createEmpty(mesh) + + // TODO create empty location iterator when not in unitKinds + locationIt = createLocationIterator(group) + renderObject = await createUnitsMeshRenderObject(ctx, group, mesh, locationIt, currentProps) + } - return { - get renderObject () { return renderObject }, - async create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: Partial<P> = {}) { - currentProps = Object.assign({}, defaultProps, props) - currentGroup = group + async function update(ctx: RuntimeContext, props: Partial<P> = {}) { + if (!renderObject) return - const unit = group.units[0] - mesh = currentProps.unitKinds.includes(unit.kind) - ? await createMesh(ctx, unit, currentProps, mesh) - : Mesh.createEmpty(mesh) + const newProps = Object.assign({}, currentProps, props) + newProps.colorTheme.structure = currentStructure + const unit = currentGroup.units[0] - locationIt = createLocationIterator(group) - renderObject = createUnitsMeshRenderObject(group, mesh, locationIt, currentProps) - }, - async update(ctx: RuntimeContext, props: Partial<P>) { - const newProps = Object.assign({}, currentProps, props) - const unit = currentGroup.units[0] + locationIt.reset() + MeshUpdateState.reset(updateState) + setUpdateState(updateState, newProps, currentProps) - if (!renderObject) return false + const newConformationId = Unit.conformationId(unit) + if (newConformationId !== currentConformationId) { + currentConformationId = newConformationId + updateState.createMesh = true + } - locationIt.reset() - MeshUpdateState.reset(updateState) - setUpdateState(updateState, newProps, currentProps) + if (currentGroup.units.length !== locationIt.instanceCount) updateState.updateTransform = true - if (!deepEqual(newProps.sizeTheme, currentProps.sizeTheme)) { - updateState.createMesh = true - } + if (!deepEqual(newProps.sizeTheme, currentProps.sizeTheme)) updateState.createMesh = true + if (!deepEqual(newProps.colorTheme, currentProps.colorTheme)) updateState.updateColor = true + if (!deepEqual(newProps.unitKinds, currentProps.unitKinds)) updateState.createMesh = true - if (!deepEqual(newProps.colorTheme, currentProps.colorTheme)) { - updateState.updateColor = true - } + // - // + if (updateState.updateTransform) { + locationIt = createLocationIterator(currentGroup) + const { instanceCount, groupCount } = locationIt + createTransforms(currentGroup, renderObject.values) + createMarkers(instanceCount * groupCount, renderObject.values) + updateState.updateColor = true + } - if (updateState.createMesh) { - mesh = await createMesh(ctx, unit, newProps, mesh) - ValueCell.update(renderObject.values.drawCount, mesh.triangleCount * 3) - updateState.updateColor = true - } + if (updateState.createMesh) { + mesh = newProps.unitKinds.includes(unit.kind) + ? await createMesh(ctx, unit, newProps, mesh) + : Mesh.createEmpty(mesh) + ValueCell.update(renderObject.values.drawCount, mesh.triangleCount * 3) + updateState.updateColor = true + } - if (updateState.updateColor) { - createColors(locationIt, newProps.colorTheme, renderObject.values) - } + if (updateState.updateColor) { + await createColors(ctx, locationIt, newProps.colorTheme, renderObject.values) + } - updateMeshValues(renderObject.values, newProps) - updateRenderableState(renderObject.state, newProps) + updateMeshValues(renderObject.values, newProps) + updateRenderableState(renderObject.state, newProps) - currentProps = newProps - return true + currentProps = newProps + } + + return { + get renderObject () { return renderObject }, + async createOrUpdate(ctx: RuntimeContext, props: Partial<P> = {}, structureGroup?: StructureGroup) { + if (structureGroup) currentStructure = structureGroup.structure + const group = structureGroup ? structureGroup.group : undefined + if (!group && !currentGroup) { + throw new Error('missing group') + } else if (group && (!currentGroup || !renderObject)) { + await create(ctx, group, props) + } else if (group && group.hashCode !== currentGroup.hashCode) { + await create(ctx, group, props) + } else { + if (group && !areGroupsIdentical(group, currentGroup)) { + currentGroup = group + } + await update(ctx, props) + } }, getLoci(pickingId: PickingId) { - return getLoci(pickingId, currentGroup, renderObject.id) + return renderObject ? getLoci(pickingId, currentGroup, renderObject.id) : EmptyLoci }, mark(loci: Loci, action: MarkerAction) { + if (!renderObject) return false const { tMarker } = renderObject.values const { groupCount, instanceCount } = locationIt @@ -111,17 +152,25 @@ export function UnitsMeshVisual<P extends UnitsMeshProps>(builder: UnitsMeshVisu let changed = false if (isEveryLoci(loci)) { - apply(Interval.ofBounds(0, groupCount * instanceCount)) - changed = true + changed = apply(Interval.ofBounds(0, groupCount * instanceCount)) } else { changed = mark(loci, currentGroup, apply) } if (changed) { ValueCell.update(tMarker, tMarker.ref.value) } + return changed }, destroy() { // TODO + renderObject = undefined } } +} + +function areGroupsIdentical(groupA: Unit.SymmetryGroup, groupB: Unit.SymmetryGroup) { + return ( + groupA.units.length === groupB.units.length && + Unit.conformationId(groupA.units[0]) === Unit.conformationId(groupB.units[0]) + ) } \ No newline at end of file diff --git a/src/mol-geo/representation/structure/visual/carbohydrate-link-cylinder.ts b/src/mol-geo/representation/structure/visual/carbohydrate-link-cylinder.ts index 6526391f04326d93df99dd68f5c35c69de5d16c0..185835c629e4bcca92a85f02008992f9466199df 100644 --- a/src/mol-geo/representation/structure/visual/carbohydrate-link-cylinder.ts +++ b/src/mol-geo/representation/structure/visual/carbohydrate-link-cylinder.ts @@ -92,15 +92,15 @@ function CarbohydrateLinkIterator(structure: Structure): LocationIterator { const link = links[groupIndex] const carbA = elements[link.carbohydrateIndexA] const carbB = elements[link.carbohydrateIndexB] - const indexA = OrderedSet.findPredecessorIndex(carbA.unit.elements, carbA.anomericCarbon) - const indexB = OrderedSet.findPredecessorIndex(carbB.unit.elements, carbB.anomericCarbon) + const indexA = OrderedSet.indexOf(carbA.unit.elements, carbA.anomericCarbon) + const indexB = OrderedSet.indexOf(carbB.unit.elements, carbB.anomericCarbon) location.aUnit = carbA.unit location.aIndex = indexA as StructureElement.UnitIndex location.bUnit = carbB.unit location.bIndex = indexB as StructureElement.UnitIndex return location } - return LocationIterator(groupCount, instanceCount, getLocation) + return LocationIterator(groupCount, instanceCount, getLocation, true) } function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) { @@ -110,14 +110,16 @@ function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) { const l = links[groupId] const carbA = elements[l.carbohydrateIndexA] const carbB = elements[l.carbohydrateIndexB] - const indexA = OrderedSet.findPredecessorIndex(carbA.unit.elements, carbA.anomericCarbon) - const indexB = OrderedSet.findPredecessorIndex(carbB.unit.elements, carbB.anomericCarbon) - return Link.Loci([ - Link.Location( - carbA.unit, indexA as StructureElement.UnitIndex, - carbB.unit, indexB as StructureElement.UnitIndex - ) - ]) + const indexA = OrderedSet.indexOf(carbA.unit.elements, carbA.anomericCarbon) + const indexB = OrderedSet.indexOf(carbB.unit.elements, carbB.anomericCarbon) + if (indexA !== -1 && indexB !== -1) { + return Link.Loci([ + Link.Location( + carbA.unit, indexA as StructureElement.UnitIndex, + carbB.unit, indexB as StructureElement.UnitIndex + ) + ]) + } } return EmptyLoci } diff --git a/src/mol-geo/representation/structure/visual/carbohydrate-symbol-mesh.ts b/src/mol-geo/representation/structure/visual/carbohydrate-symbol-mesh.ts index 6546a2b2ac14106953307cabfb2655f3db9b7311..13d568ccb73cd097c659a36a8281c9e600b4aa85 100644 --- a/src/mol-geo/representation/structure/visual/carbohydrate-symbol-mesh.ts +++ b/src/mol-geo/representation/structure/visual/carbohydrate-symbol-mesh.ts @@ -5,7 +5,7 @@ */ import { Unit, Structure, StructureElement } from 'mol-model/structure'; -import { ComplexVisual } from '..'; +import { ComplexVisual, MeshUpdateState } from '..'; import { RuntimeContext } from 'mol-task' import { Mesh } from '../../../mesh/mesh'; import { PickingId } from '../../../util/picking'; @@ -154,7 +154,9 @@ export function CarbohydrateSymbolVisual(): ComplexVisual<CarbohydrateSymbolProp createLocationIterator: CarbohydrateElementIterator, getLoci: getCarbohydrateLoci, mark: markCarbohydrate, - setUpdateState: () => {} + setUpdateState: (state: MeshUpdateState, newProps: CarbohydrateSymbolProps, currentProps: CarbohydrateSymbolProps) => { + state.createMesh = newProps.detail !== currentProps.detail + } }) } @@ -172,7 +174,7 @@ function CarbohydrateElementIterator(structure: Structure): LocationIterator { function isSecondary (elementIndex: number, instanceIndex: number) { return (elementIndex % 2) === 1 } - return LocationIterator(groupCount, instanceCount, getLocation, isSecondary) + return LocationIterator(groupCount, instanceCount, getLocation, true, isSecondary) } function getCarbohydrateLoci(pickingId: PickingId, structure: Structure, id: number) { @@ -180,9 +182,11 @@ function getCarbohydrateLoci(pickingId: PickingId, structure: Structure, id: num if (id === objectId) { const carb = structure.carbohydrates.elements[Math.floor(groupId / 2)] const { unit } = carb - const index = OrderedSet.findPredecessorIndex(unit.elements, carb.anomericCarbon) - const indices = OrderedSet.ofSingleton(index as StructureElement.UnitIndex) - return StructureElement.Loci([{ unit, indices }]) + const index = OrderedSet.indexOf(unit.elements, carb.anomericCarbon) + if (index !== -1) { + const indices = OrderedSet.ofSingleton(index as StructureElement.UnitIndex) + return StructureElement.Loci([{ unit, indices }]) + } } return EmptyLoci } diff --git a/src/mol-geo/representation/structure/visual/cross-link-restraint-cylinder.ts b/src/mol-geo/representation/structure/visual/cross-link-restraint-cylinder.ts index 6bd885217c6132fbbdce6ed6ed481aeb68afda33..735499fd290cee6d58f5213079613c20aed9d75b 100644 --- a/src/mol-geo/representation/structure/visual/cross-link-restraint-cylinder.ts +++ b/src/mol-geo/representation/structure/visual/cross-link-restraint-cylinder.ts @@ -84,7 +84,7 @@ function CrossLinkRestraintIterator(structure: Structure): LocationIterator { location.bIndex = pair.indexB return location } - return LocationIterator(groupCount, instanceCount, getLocation) + return LocationIterator(groupCount, instanceCount, getLocation, true) } function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) { diff --git a/src/mol-geo/representation/structure/visual/element-point.ts b/src/mol-geo/representation/structure/visual/element-point.ts index e58f1bee17d71ac6a7605317fb6000a6ce448ca4..d0b74c089e9f37c81a817f653f54702e4574fabf 100644 --- a/src/mol-geo/representation/structure/visual/element-point.ts +++ b/src/mol-geo/representation/structure/visual/element-point.ts @@ -6,131 +6,164 @@ */ import { ValueCell } from 'mol-util/value-cell' -import { createPointRenderObject, PointRenderObject } from 'mol-gl/render-object' -import { Unit } from 'mol-model/structure'; +import { PointRenderObject } from 'mol-gl/render-object' +import { Unit, Structure } from 'mol-model/structure'; import { RuntimeContext } from 'mol-task' - import { UnitsVisual, DefaultStructureProps } from '..'; -import { getElementLoci, StructureElementIterator } from './util/element'; -import { createTransforms, createColors, createSizes } from './util/common'; -import { deepEqual, defaults } from 'mol-util'; -import { SortedArray } from 'mol-data/int'; -import { RenderableState, PointValues } from 'mol-gl/renderable'; +import { getElementLoci, StructureElementIterator, markElement } from './util/element'; +import { createColors, createSizes, createUnitsPointRenderObject } from './util/common'; +import { deepEqual, UUID } from 'mol-util'; +import { Interval } from 'mol-data/int'; import { PickingId } from '../../../util/picking'; -import { Loci } from 'mol-model/loci'; -import { MarkerAction, createMarkers } from '../../../util/marker-data'; +import { Loci, EmptyLoci, isEveryLoci } from 'mol-model/loci'; +import { MarkerAction, createMarkers, applyMarkerAction } from '../../../util/marker-data'; import { Vec3 } from 'mol-math/linear-algebra'; import { fillSerial } from 'mol-util/array'; import { SizeThemeProps } from 'mol-view/theme/size'; +import { LocationIterator } from '../../../util/location-iterator'; +import { createTransforms } from '../../../util/transform-data'; +import { StructureGroup } from '../units-visual'; +import { updateRenderableState } from '../../util'; -export const DefaultPointProps = { +export const DefaultElementPointProps = { ...DefaultStructureProps, - sizeTheme: { name: 'physical' } as SizeThemeProps + + sizeTheme: { name: 'uniform', value: 0.2 } as SizeThemeProps, + pointSizeAttenuation: true, } -export type PointProps = Partial<typeof DefaultPointProps> +export type ElementPointProps = Partial<typeof DefaultElementPointProps> -export function createPointVertices(unit: Unit) { +export async function createElementPointVertices(ctx: RuntimeContext, unit: Unit, vertices?: ValueCell<Float32Array>) { const elements = unit.elements - const elementCount = elements.length - const vertices = new Float32Array(elementCount * 3) + const n = elements.length * 3 + const array = vertices && vertices.ref.value.length >= n ? vertices.ref.value : new Float32Array(n) const pos = unit.conformation.invariantPosition const p = Vec3.zero() - for (let i = 0; i < elementCount; i++) { - const i3 = i * 3 - pos(elements[i], p) - vertices[i3] = p[0] - vertices[i3 + 1] = p[1] - vertices[i3 + 2] = p[2] + for (let i = 0; i < n; i += 3) { + pos(elements[i / 3], p) + array[i] = p[0] + array[i + 1] = p[1] + array[i + 2] = p[2] + + if (i % 10000 === 0 && ctx.shouldUpdate) { + await ctx.update({ message: 'Creating points', current: i / 3, max: elements.length }); + } + ++i } - return vertices + return vertices ? ValueCell.update(vertices, array) : ValueCell.create(array) } -export default function PointVisual(): UnitsVisual<PointProps> { - let renderObject: PointRenderObject - let currentProps = DefaultPointProps +export function ElementPointVisual(): UnitsVisual<ElementPointProps> { + let renderObject: PointRenderObject | undefined + let currentProps = DefaultElementPointProps let currentGroup: Unit.SymmetryGroup - - let _units: ReadonlyArray<Unit> - let _elements: SortedArray + let currentStructure: Structure + let locationIt: LocationIterator + let vertices: ValueCell<Float32Array> + let currentConformationId: UUID return { get renderObject () { return renderObject }, - async create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: PointProps = {}) { - currentProps = Object.assign({}, DefaultPointProps, props) - currentGroup = group - - _units = group.units - _elements = group.elements; - - const { colorTheme, sizeTheme } = currentProps - const elementCount = _elements.length - const instanceCount = group.units.length - - const locationIt = StructureElementIterator.fromGroup(group) - - const vertices = createPointVertices(_units[0]) - const transforms = createTransforms(group) - const color = createColors(locationIt, colorTheme) - const size = createSizes(locationIt, sizeTheme) - const marker = createMarkers(instanceCount * elementCount) - - const values: PointValues = { - aPosition: ValueCell.create(vertices), - aGroup: ValueCell.create(fillSerial(new Float32Array(elementCount))), - aTransform: transforms, - aInstance: ValueCell.create(fillSerial(new Float32Array(instanceCount))), - ...color, - ...marker, - ...size, - - uAlpha: ValueCell.create(defaults(props.alpha, 1.0)), - uInstanceCount: ValueCell.create(instanceCount), - uGroupCount: ValueCell.create(group.elements.length), - - drawCount: ValueCell.create(vertices.length / 3), - instanceCount: ValueCell.create(instanceCount), - - dPointSizeAttenuation: ValueCell.create(true), - dUseFog: ValueCell.create(defaults(props.useFog, true)), - } - const state: RenderableState = { - depthMask: defaults(props.depthMask, true), - visible: defaults(props.visible, true) + async createOrUpdate(ctx: RuntimeContext, props: ElementPointProps = {}, structureGroup?: StructureGroup) { + if (structureGroup) currentStructure = structureGroup.structure + const group = structureGroup ? structureGroup.group : undefined + if (!group && !currentGroup) { + throw new Error('missing group') + } else if (group && !currentGroup) { + currentProps = Object.assign({}, DefaultElementPointProps, props) + currentProps.colorTheme.structure = currentStructure + currentGroup = group + locationIt = StructureElementIterator.fromGroup(group) + + const unit = group.units[0] + currentConformationId = Unit.conformationId(unit) + vertices = await createElementPointVertices(ctx, unit, vertices) + + renderObject = await createUnitsPointRenderObject(ctx, group, vertices, locationIt, currentProps) + } else if (renderObject) { + if (group) currentGroup = group + + const newProps = { ...currentProps, ...props } + const unit = currentGroup.units[0] + + let updateTransform = false + let createVertices = false + let updateColor = false + let updateSize = false + + const newConformationId = Unit.conformationId(unit) + if (newConformationId !== currentConformationId) { + currentConformationId = newConformationId + createVertices = true + } + + if (currentGroup.units.length !== locationIt.instanceCount) updateTransform = true + + if (!deepEqual(newProps.sizeTheme, currentProps.sizeTheme)) createVertices = true + if (!deepEqual(newProps.colorTheme, currentProps.colorTheme)) updateColor = true + if (!deepEqual(newProps.sizeTheme, currentProps.sizeTheme)) updateSize = true + + if (updateTransform) { + locationIt = StructureElementIterator.fromGroup(currentGroup) + const { instanceCount, groupCount } = locationIt + createTransforms(currentGroup, renderObject.values) + createMarkers(instanceCount * groupCount, renderObject.values) + updateColor = true + updateSize = true + } + + if (createVertices) { + await createElementPointVertices(ctx, unit, vertices) + ValueCell.update(renderObject.values.aGroup, fillSerial(new Float32Array(locationIt.groupCount))) // TODO reuse array + ValueCell.update(renderObject.values.drawCount, locationIt.groupCount) + updateColor = true + updateSize = true + } + + if (updateColor) { + await createColors(ctx, locationIt, newProps.colorTheme, renderObject.values) + } + + if (updateSize) { + await createSizes(ctx, locationIt, newProps.sizeTheme, renderObject.values) + } + + updateRenderableState(renderObject.state, newProps) + + currentProps = newProps } - - renderObject = createPointRenderObject(values, state) }, - async update(ctx: RuntimeContext, props: PointProps) { - if (!renderObject || !_units || !_elements) return false - - const newProps = { ...currentProps, ...props } - if (deepEqual(currentProps, newProps)) { - console.log('props identical, nothing to change') - return true + getLoci(pickingId: PickingId) { + return renderObject ? getElementLoci(pickingId, currentGroup, renderObject.id) : EmptyLoci + }, + mark(loci: Loci, action: MarkerAction) { + if (!renderObject) return false + const { tMarker } = renderObject.values + const { groupCount, instanceCount } = locationIt + + function apply(interval: Interval) { + const start = Interval.start(interval) + const end = Interval.end(interval) + return applyMarkerAction(tMarker.ref.value.array, start, end, action) } - if (!deepEqual(currentProps.colorTheme, newProps.colorTheme)) { - console.log('colorTheme changed', currentProps.colorTheme, newProps.colorTheme) + let changed = false + if (isEveryLoci(loci)) { + apply(Interval.ofBounds(0, groupCount * instanceCount)) + changed = true + } else { + changed = markElement(loci, currentGroup, apply) } - - if (!deepEqual(currentProps.sizeTheme, newProps.sizeTheme)) { - console.log('sizeTheme changed', currentProps.sizeTheme, newProps.sizeTheme) + if (changed) { + ValueCell.update(tMarker, tMarker.ref.value) } - - currentProps = newProps - return false - }, - getLoci(pickingId: PickingId) { - return getElementLoci(pickingId, currentGroup, renderObject.id) - }, - mark(loci: Loci, action: MarkerAction) { - // TODO - // markElement(loci, action, currentGroup, renderObject.values) + return changed }, destroy() { // TODO + renderObject = undefined } } } diff --git a/src/mol-geo/representation/structure/visual/inter-unit-link-cylinder.ts b/src/mol-geo/representation/structure/visual/inter-unit-link-cylinder.ts index 813da2a3b9ce6864298fe0e8424972e05ab97b7a..44469dacff2b8bc4587e8cdb89ce9da3a15b4bff 100644 --- a/src/mol-geo/representation/structure/visual/inter-unit-link-cylinder.ts +++ b/src/mol-geo/representation/structure/visual/inter-unit-link-cylinder.ts @@ -51,6 +51,7 @@ async function createInterUnitLinkCylinderMesh(ctx: RuntimeContext, structure: S export const DefaultInterUnitLinkProps = { ...DefaultComplexMeshProps, ...DefaultLinkCylinderProps, + sizeTheme: { name: 'physical', factor: 0.3 } as SizeThemeProps, } export type InterUnitLinkProps = typeof DefaultInterUnitLinkProps diff --git a/src/mol-geo/representation/structure/visual/intra-unit-link-cylinder.ts b/src/mol-geo/representation/structure/visual/intra-unit-link-cylinder.ts index bf3e964bc6788f9b2f36e47324de31e147e6eb40..9b9452a4191d579a07837137b34ccb22082261ae 100644 --- a/src/mol-geo/representation/structure/visual/intra-unit-link-cylinder.ts +++ b/src/mol-geo/representation/structure/visual/intra-unit-link-cylinder.ts @@ -6,7 +6,7 @@ */ import { Unit, Link, StructureElement } from 'mol-model/structure'; -import { UnitsVisual } from '..'; +import { UnitsVisual, MeshUpdateState } from '..'; import { RuntimeContext } from 'mol-task' import { DefaultLinkCylinderProps, LinkCylinderProps, createLinkCylinderMesh, LinkIterator } from './util/link'; import { Mesh } from '../../../mesh/mesh'; @@ -65,6 +65,7 @@ async function createIntraUnitLinkCylinderMesh(ctx: RuntimeContext, unit: Unit, export const DefaultIntraUnitLinkProps = { ...DefaultUnitsMeshProps, ...DefaultLinkCylinderProps, + sizeTheme: { name: 'physical', factor: 0.3 } as SizeThemeProps, } export type IntraUnitLinkProps = typeof DefaultIntraUnitLinkProps @@ -76,7 +77,9 @@ export function IntraUnitLinkVisual(): UnitsVisual<IntraUnitLinkProps> { createLocationIterator: LinkIterator.fromGroup, getLoci: getLinkLoci, mark: markLink, - setUpdateState: () => {} + setUpdateState: (state: MeshUpdateState, newProps: LinkCylinderProps, currentProps: LinkCylinderProps) => { + state.createMesh = newProps.radialSegments !== currentProps.radialSegments + } }) } @@ -99,12 +102,13 @@ function markLink(loci: Loci, group: Unit.SymmetryGroup, apply: (interval: Inter let changed = false if (Unit.isAtomic(unit) && Link.isLoci(loci)) { + const groupCount = unit.links.edgeCount * 2 for (const b of loci.links) { - const unitIdx = Unit.findUnitById(b.aUnit.id, group.units) - if (unitIdx !== -1) { + const unitIdx = group.unitIndexMap.get(b.aUnit.id) + if (unitIdx !== undefined) { const idx = unit.links.getDirectedEdgeIndex(b.aIndex, b.bIndex) if (idx !== -1) { - if (apply(Interval.ofSingleton(idx))) changed = true + if (apply(Interval.ofSingleton(unitIdx * groupCount + idx))) changed = true } } } diff --git a/src/mol-geo/representation/structure/visual/nucleotide-block-mesh.ts b/src/mol-geo/representation/structure/visual/nucleotide-block-mesh.ts index 0ace3c5262d2d6ad41fe037b03a982400857c89f..8b5d893ef85dc1b6ae8b18ea8a651c3427e8872d 100644 --- a/src/mol-geo/representation/structure/visual/nucleotide-block-mesh.ts +++ b/src/mol-geo/representation/structure/visual/nucleotide-block-mesh.ts @@ -9,14 +9,14 @@ import { UnitsVisual } from '..'; import { RuntimeContext } from 'mol-task' import { Mesh } from '../../../mesh/mesh'; import { MeshBuilder } from '../../../mesh/mesh-builder'; -import { getElementLoci, markElement, StructureElementIterator } from './util/element'; import { Vec3, Mat4 } from 'mol-math/linear-algebra'; -import { Segmentation, SortedArray } from 'mol-data/int'; +import { Segmentation } from 'mol-data/int'; import { MoleculeType, isNucleic, isPurinBase, isPyrimidineBase } from 'mol-model/structure/model/types'; import { getElementIndexForAtomId, getElementIndexForAtomRole } from 'mol-model/structure/util'; import { DefaultUnitsMeshProps, UnitsMeshVisual } from '../units-visual'; import { addCylinder } from '../../../mesh/builder/cylinder'; import { Box } from '../../../primitive/box'; +import { NucleotideLocationIterator, markNucleotideElement, getNucleotideElementLoci } from './util/nucleotide'; const p1 = Vec3.zero() const p2 = Vec3.zero() @@ -82,25 +82,28 @@ async function createNucleotideBlockMesh(ctx: RuntimeContext, unit: Unit, props: idx6 = getElementIndexForAtomRole(model, residueIndex, 'trace') } - if (idx1 !== -1 && idx2 !== -1 && idx3 !== -1 && idx4 !== -1 && idx5 !== -1 && idx6 !== -1) { - pos(idx1, p1); pos(idx2, p2); pos(idx3, p3); pos(idx4, p4); pos(idx5, p5); pos(idx6, p6) - Vec3.normalize(v12, Vec3.sub(v12, p2, p1)) - Vec3.normalize(v34, Vec3.sub(v34, p4, p3)) - Vec3.normalize(vC, Vec3.cross(vC, v12, v34)) - Mat4.targetTo(t, p1, p2, vC) - Vec3.scaleAndAdd(center, p1, v12, height / 2 - 0.2) - Mat4.scale(t, t, Vec3.set(sVec, width, depth, height)) - Mat4.setTranslation(t, center) - builder.setGroup(SortedArray.findPredecessorIndex(elements, idx6)) - builder.add(t, box) + if (idx5 !== -1 && idx6 !== -1) { + pos(idx5, p5); pos(idx6, p6) + builder.setGroup(i) addCylinder(builder, p5, p6, 1, { radiusTop: 0.2, radiusBottom: 0.2 }) + if (idx1 !== -1 && idx2 !== -1 && idx3 !== -1 && idx4 !== -1) { + pos(idx1, p1); pos(idx2, p2); pos(idx3, p3); pos(idx4, p4); + Vec3.normalize(v12, Vec3.sub(v12, p2, p1)) + Vec3.normalize(v34, Vec3.sub(v34, p4, p3)) + Vec3.normalize(vC, Vec3.cross(vC, v12, v34)) + Mat4.targetTo(t, p1, p2, vC) + Vec3.scaleAndAdd(center, p1, v12, height / 2 - 0.2) + Mat4.scale(t, t, Vec3.set(sVec, width, depth, height)) + Mat4.setTranslation(t, center) + builder.add(t, box) + } } - } - if (i % 10000 === 0 && ctx.shouldUpdate) { - await ctx.update({ message: 'Nucleotide block mesh', current: i }); + if (i % 10000 === 0 && ctx.shouldUpdate) { + await ctx.update({ message: 'Nucleotide block mesh', current: i }); + } + ++i } - ++i } } @@ -116,9 +119,9 @@ export function NucleotideBlockVisual(): UnitsVisual<NucleotideBlockProps> { return UnitsMeshVisual<NucleotideBlockProps>({ defaultProps: DefaultNucleotideBlockProps, createMesh: createNucleotideBlockMesh, - createLocationIterator: StructureElementIterator.fromGroup, - getLoci: getElementLoci, - mark: markElement, + createLocationIterator: NucleotideLocationIterator.fromGroup, + getLoci: getNucleotideElementLoci, + mark: markNucleotideElement, setUpdateState: () => {} }) } \ No newline at end of file diff --git a/src/mol-geo/representation/structure/visual/polymer-backbone-cylinder.ts b/src/mol-geo/representation/structure/visual/polymer-backbone-cylinder.ts index e3b1b00ae8c4923e157908bd9ecd404e10285b79..d975ae940228601a3fd1a833979d28dc2c0dc96e 100644 --- a/src/mol-geo/representation/structure/visual/polymer-backbone-cylinder.ts +++ b/src/mol-geo/representation/structure/visual/polymer-backbone-cylinder.ts @@ -5,11 +5,11 @@ */ import { Unit } from 'mol-model/structure'; -import { UnitsVisual } from '..'; +import { UnitsVisual, MeshUpdateState } from '..'; import { RuntimeContext } from 'mol-task' import { Mesh } from '../../../mesh/mesh'; import { MeshBuilder } from '../../../mesh/mesh-builder'; -import { getPolymerElementCount, PolymerBackboneIterator } from './util/polymer'; +import { PolymerBackboneIterator } from './util/polymer'; import { getElementLoci, markElement, StructureElementIterator } from './util/element'; import { Vec3 } from 'mol-math/linear-algebra'; import { DefaultUnitsMeshProps, UnitsMeshVisual } from '../units-visual'; @@ -24,7 +24,7 @@ export interface PolymerBackboneCylinderProps { } async function createPolymerBackboneCylinderMesh(ctx: RuntimeContext, unit: Unit, props: PolymerBackboneCylinderProps, mesh?: Mesh) { - const polymerElementCount = getPolymerElementCount(unit) + const polymerElementCount = unit.polymerElements.length if (!polymerElementCount) return Mesh.createEmpty(mesh) const sizeTheme = SizeTheme(props.sizeTheme) @@ -47,11 +47,11 @@ async function createPolymerBackboneCylinderMesh(ctx: RuntimeContext, unit: Unit pos(centerB.element, pB) cylinderProps.radiusTop = cylinderProps.radiusBottom = sizeTheme.size(centerA) - builder.setGroup(OrderedSet.findPredecessorIndex(elements, centerA.element)) + builder.setGroup(OrderedSet.indexOf(elements, centerA.element)) addCylinder(builder, pA, pB, 0.5, cylinderProps) cylinderProps.radiusTop = cylinderProps.radiusBottom = sizeTheme.size(centerB) - builder.setGroup(OrderedSet.findPredecessorIndex(elements, centerB.element)) + builder.setGroup(OrderedSet.indexOf(elements, centerB.element)) addCylinder(builder, pB, pA, 0.5, cylinderProps) if (i % 10000 === 0 && ctx.shouldUpdate) { @@ -73,9 +73,12 @@ export function PolymerBackboneVisual(): UnitsVisual<PolymerBackboneProps> { return UnitsMeshVisual<PolymerBackboneProps>({ defaultProps: DefaultPolymerBackboneProps, createMesh: createPolymerBackboneCylinderMesh, + // TODO create a specialized location iterator createLocationIterator: StructureElementIterator.fromGroup, getLoci: getElementLoci, mark: markElement, - setUpdateState: () => {} + setUpdateState: (state: MeshUpdateState, newProps: PolymerBackboneProps, currentProps: PolymerBackboneProps) => { + state.createMesh = newProps.radialSegments !== currentProps.radialSegments + } }) } \ No newline at end of file diff --git a/src/mol-geo/representation/structure/visual/polymer-direction-wedge.ts b/src/mol-geo/representation/structure/visual/polymer-direction-wedge.ts index 6ad043eba547f99896cfbc6e620c709b76dfcc7d..1e6b8e42a198d66249becde9ff3219b723a659ee 100644 --- a/src/mol-geo/representation/structure/visual/polymer-direction-wedge.ts +++ b/src/mol-geo/representation/structure/visual/polymer-direction-wedge.ts @@ -7,15 +7,13 @@ import { Unit } from 'mol-model/structure'; import { UnitsVisual } from '..'; import { RuntimeContext } from 'mol-task' -import { markElement, getElementLoci, StructureElementIterator } from './util/element'; import { Mesh } from '../../../mesh/mesh'; import { MeshBuilder } from '../../../mesh/mesh-builder'; -import { getPolymerElementCount, PolymerTraceIterator, createCurveSegmentState, interpolateCurveSegment } from './util/polymer'; +import { PolymerTraceIterator, createCurveSegmentState, interpolateCurveSegment, PolymerLocationIterator, getPolymerElementLoci, markPolymerElement } from './util/polymer'; import { Vec3, Mat4 } from 'mol-math/linear-algebra'; import { SecondaryStructureType, MoleculeType } from 'mol-model/structure/model/types'; import { DefaultUnitsMeshProps, UnitsMeshVisual } from '../units-visual'; import { SizeThemeProps, SizeTheme } from 'mol-view/theme/size'; -import { OrderedSet } from 'mol-data/int'; import { Wedge } from '../../../primitive/wedge'; const t = Mat4.identity() @@ -35,7 +33,7 @@ export interface PolymerDirectionWedgeProps { } async function createPolymerDirectionWedgeMesh(ctx: RuntimeContext, unit: Unit, props: PolymerDirectionWedgeProps, mesh?: Mesh) { - const polymerElementCount = getPolymerElementCount(unit) + const polymerElementCount = unit.polymerElements.length if (!polymerElementCount) return Mesh.createEmpty(mesh) const sizeTheme = SizeTheme(props.sizeTheme) @@ -51,7 +49,7 @@ async function createPolymerDirectionWedgeMesh(ctx: RuntimeContext, unit: Unit, const polymerTraceIt = PolymerTraceIterator(unit) while (polymerTraceIt.hasNext) { const v = polymerTraceIt.move() - builder.setGroup(OrderedSet.findPredecessorIndex(unit.elements, v.center.element)) + builder.setGroup(i) const isNucleic = v.moleculeType === MoleculeType.DNA || v.moleculeType === MoleculeType.RNA const isSheet = SecondaryStructureType.is(v.secStrucType, SecondaryStructureType.Flag.Beta) @@ -95,9 +93,9 @@ export function PolymerDirectionVisual(): UnitsVisual<PolymerDirectionProps> { return UnitsMeshVisual<PolymerDirectionProps>({ defaultProps: DefaultPolymerDirectionProps, createMesh: createPolymerDirectionWedgeMesh, - createLocationIterator: StructureElementIterator.fromGroup, - getLoci: getElementLoci, - mark: markElement, + createLocationIterator: PolymerLocationIterator.fromGroup, + getLoci: getPolymerElementLoci, + mark: markPolymerElement, setUpdateState: () => {} }) } \ No newline at end of file diff --git a/src/mol-geo/representation/structure/visual/polymer-gap-cylinder.ts b/src/mol-geo/representation/structure/visual/polymer-gap-cylinder.ts index 047850c67aae193be0cb0035aed18925e054bfcd..24038dfc3182746c74d2ae0336a5556ff00cfcde 100644 --- a/src/mol-geo/representation/structure/visual/polymer-gap-cylinder.ts +++ b/src/mol-geo/representation/structure/visual/polymer-gap-cylinder.ts @@ -4,13 +4,12 @@ * @author Alexander Rose <alexander.rose@weirdbyte.de> */ -import { Unit, StructureElement } from 'mol-model/structure'; -import { UnitsVisual } from '..'; +import { Unit } from 'mol-model/structure'; +import { UnitsVisual, MeshUpdateState } from '..'; import { RuntimeContext } from 'mol-task' import { Mesh } from '../../../mesh/mesh'; import { MeshBuilder } from '../../../mesh/mesh-builder'; -import { getPolymerGapCount, PolymerGapIterator } from './util/polymer'; -import { getElementLoci, markElement, StructureElementIterator } from './util/element'; +import { PolymerGapIterator, PolymerGapLocationIterator, markPolymerGapElement, getPolymerGapElementLoci } from './util/polymer'; import { Vec3 } from 'mol-math/linear-algebra'; import { UnitsMeshVisual, DefaultUnitsMeshProps } from '../units-visual'; import { SizeThemeProps, SizeTheme } from 'mol-view/theme/size'; @@ -26,7 +25,7 @@ export interface PolymerGapCylinderProps { } async function createPolymerGapCylinderMesh(ctx: RuntimeContext, unit: Unit, props: PolymerGapCylinderProps, mesh?: Mesh) { - const polymerGapCount = getPolymerGapCount(unit) + const polymerGapCount = unit.gapElements.length if (!polymerGapCount) return Mesh.createEmpty(mesh) const sizeTheme = SizeTheme(props.sizeTheme) @@ -35,11 +34,9 @@ async function createPolymerGapCylinderMesh(ctx: RuntimeContext, unit: Unit, pro const vertexCountEstimate = segmentCount * radialSegments * 2 * polymerGapCount * 2 const builder = MeshBuilder.create(vertexCountEstimate, vertexCountEstimate / 10, mesh) - const { elements } = unit const pos = unit.conformation.invariantPosition const pA = Vec3.zero() const pB = Vec3.zero() - const l = StructureElement.create(unit) const cylinderProps: CylinderProps = { radiusTop: 1, radiusBottom: 1, topCap: true, bottomCap: true, radialSegments } @@ -49,30 +46,26 @@ async function createPolymerGapCylinderMesh(ctx: RuntimeContext, unit: Unit, pro while (polymerGapIt.hasNext) { const { centerA, centerB } = polymerGapIt.move() if (centerA.element === centerB.element) { - builder.setGroup(centerA.element) - pos(elements[centerA.element], pA) + builder.setGroup(i) + pos(centerA.element, pA) addSphere(builder, pA, 0.6, 0) } else { - const elmA = elements[centerA.element] - const elmB = elements[centerB.element] - pos(elmA, pA) - pos(elmB, pB) + pos(centerA.element, pA) + pos(centerB.element, pB) - l.element = elmA - cylinderProps.radiusTop = cylinderProps.radiusBottom = sizeTheme.size(l) - builder.setGroup(centerA.element) + cylinderProps.radiusTop = cylinderProps.radiusBottom = sizeTheme.size(centerA) + builder.setGroup(i) addFixedCountDashedCylinder(builder, pA, pB, 0.5, segmentCount, cylinderProps) - l.element = elmB - cylinderProps.radiusTop = cylinderProps.radiusBottom = sizeTheme.size(l) - builder.setGroup(centerB.element) + cylinderProps.radiusTop = cylinderProps.radiusBottom = sizeTheme.size(centerB) + builder.setGroup(i + 1) addFixedCountDashedCylinder(builder, pB, pA, 0.5, segmentCount, cylinderProps) } if (i % 10000 === 0 && ctx.shouldUpdate) { await ctx.update({ message: 'Gap mesh', current: i, max: polymerGapCount }); } - ++i + i += 2 } return builder.getMesh() @@ -88,9 +81,11 @@ export function PolymerGapVisual(): UnitsVisual<PolymerGapProps> { return UnitsMeshVisual<PolymerGapProps>({ defaultProps: DefaultPolymerGapProps, createMesh: createPolymerGapCylinderMesh, - createLocationIterator: StructureElementIterator.fromGroup, - getLoci: getElementLoci, - mark: markElement, - setUpdateState: () => {} + createLocationIterator: PolymerGapLocationIterator.fromGroup, + getLoci: getPolymerGapElementLoci, + mark: markPolymerGapElement, + setUpdateState: (state: MeshUpdateState, newProps: PolymerGapProps, currentProps: PolymerGapProps) => { + state.createMesh = newProps.radialSegments !== currentProps.radialSegments + } }) } \ No newline at end of file diff --git a/src/mol-geo/representation/structure/visual/polymer-trace-mesh.ts b/src/mol-geo/representation/structure/visual/polymer-trace-mesh.ts index f65c7fb9cb36488df559bbe8a412d1ee03bd81f6..1a37e4d9809fa189a779c2dcb2b969ccc382dcba 100644 --- a/src/mol-geo/representation/structure/visual/polymer-trace-mesh.ts +++ b/src/mol-geo/representation/structure/visual/polymer-trace-mesh.ts @@ -7,14 +7,12 @@ import { Unit } from 'mol-model/structure'; import { UnitsVisual, MeshUpdateState } from '..'; import { RuntimeContext } from 'mol-task' -import { markElement, getElementLoci, StructureElementIterator } from './util/element'; import { Mesh } from '../../../mesh/mesh'; import { MeshBuilder } from '../../../mesh/mesh-builder'; -import { getPolymerElementCount, PolymerTraceIterator, createCurveSegmentState, interpolateCurveSegment } from './util/polymer'; +import { PolymerTraceIterator, createCurveSegmentState, interpolateCurveSegment, PolymerLocationIterator, getPolymerElementLoci, markPolymerElement } from './util/polymer'; import { SecondaryStructureType, isNucleic } from 'mol-model/structure/model/types'; import { UnitsMeshVisual, DefaultUnitsMeshProps } from '../units-visual'; import { SizeThemeProps, SizeTheme } from 'mol-view/theme/size'; -import { OrderedSet } from 'mol-data/int'; import { addSheet } from '../../../mesh/builder/sheet'; import { addTube } from '../../../mesh/builder/tube'; @@ -29,7 +27,7 @@ export interface PolymerTraceMeshProps { // TODO handle polymer ends properly async function createPolymerTraceMesh(ctx: RuntimeContext, unit: Unit, props: PolymerTraceMeshProps, mesh?: Mesh) { - const polymerElementCount = getPolymerElementCount(unit) + const polymerElementCount = unit.polymerElements.length if (!polymerElementCount) return Mesh.createEmpty(mesh) const sizeTheme = SizeTheme(props.sizeTheme) @@ -45,7 +43,7 @@ async function createPolymerTraceMesh(ctx: RuntimeContext, unit: Unit, props: Po const polymerTraceIt = PolymerTraceIterator(unit) while (polymerTraceIt.hasNext) { const v = polymerTraceIt.move() - builder.setGroup(OrderedSet.findPredecessorIndex(unit.elements, v.center.element)) + builder.setGroup(i) const isNucleicType = isNucleic(v.moleculeType) const isSheet = SecondaryStructureType.is(v.secStrucType, SecondaryStructureType.Flag.Beta) @@ -95,9 +93,9 @@ export function PolymerTraceVisual(): UnitsVisual<PolymerTraceProps> { return UnitsMeshVisual<PolymerTraceProps>({ defaultProps: DefaultPolymerTraceProps, createMesh: createPolymerTraceMesh, - createLocationIterator: StructureElementIterator.fromGroup, - getLoci: getElementLoci, - mark: markElement, + createLocationIterator: PolymerLocationIterator.fromGroup, + getLoci: getPolymerElementLoci, + mark: markPolymerElement, setUpdateState: (state: MeshUpdateState, newProps: PolymerTraceProps, currentProps: PolymerTraceProps) => { state.createMesh = ( newProps.linearSegments !== currentProps.linearSegments || diff --git a/src/mol-geo/representation/structure/visual/util/common.ts b/src/mol-geo/representation/structure/visual/util/common.ts index e069563c8771d3a7d6281a4f91720be24a3b6c27..0dfb1317bd369f48ba0ec9f8590f0811f3cdba6b 100644 --- a/src/mol-geo/representation/structure/visual/util/common.ts +++ b/src/mol-geo/representation/structure/visual/util/common.ts @@ -6,57 +6,57 @@ */ import { Unit, Structure } from 'mol-model/structure'; -import { Mat4 } from 'mol-math/linear-algebra' - -import { createUniformColor, ColorData, createGroupColor, createGroupInstanceColor, createInstanceColor } from '../../../../util/color-data'; -import { createUniformSize, SizeData, createGroupSize, createGroupInstanceSize, createInstanceSize } from '../../../../util/size-data'; +import { createUniformColor, ColorData, createGroupColor, createGroupInstanceColor, createInstanceColor, ColorType } from '../../../../util/color-data'; +import { createUniformSize, SizeData, createGroupSize, createGroupInstanceSize, createInstanceSize, SizeType } from '../../../../util/size-data'; import { ValueCell } from 'mol-util'; import { LocationIterator } from '../../../../util/location-iterator'; import { Mesh } from '../../../../mesh/mesh'; -import { MeshValues } from 'mol-gl/renderable'; +import { MeshValues, PointValues } from 'mol-gl/renderable'; import { getMeshData } from '../../../../util/mesh-data'; -import { MeshProps, createMeshValues, createRenderableState, createIdentityTransform } from '../../../util'; +import { MeshProps, createMeshValues, createRenderableState, createPointValues } from '../../../util'; import { StructureProps } from '../..'; import { createMarkers } from '../../../../util/marker-data'; -import { createMeshRenderObject } from 'mol-gl/render-object'; +import { createMeshRenderObject, createPointRenderObject } from 'mol-gl/render-object'; import { ColorThemeProps, ColorTheme } from 'mol-view/theme/color'; import { SizeThemeProps, SizeTheme } from 'mol-view/theme/size'; +import { RuntimeContext } from 'mol-task'; +import { PointProps } from 'mol-geo/representation/structure/representation/point'; +import { fillSerial } from 'mol-util/array'; +import { TransformData, createIdentityTransform, createTransforms } from '../../../../util/transform-data'; -export function createTransforms({ units }: Unit.SymmetryGroup, transforms?: ValueCell<Float32Array>) { - const unitCount = units.length - const n = unitCount * 16 - const array = transforms && transforms.ref.value.length >= n ? transforms.ref.value : new Float32Array(n) - for (let i = 0; i < unitCount; i++) { - Mat4.toArray(units[i].conformation.operator.matrix, array, i * 16) - } - return transforms ? ValueCell.update(transforms, array) : ValueCell.create(array) +function getGranularity(locationIt: LocationIterator, granularity: ColorType | SizeType) { + // Always use 'group' granularity for 'complex' location iterators, + // i.e. for which an instance may include multiple units + return granularity === 'instance' && locationIt.isComplex ? 'group' : granularity } -export function createColors(locationIt: LocationIterator, props: ColorThemeProps, colorData?: ColorData) { +export function createColors(ctx: RuntimeContext, locationIt: LocationIterator, props: ColorThemeProps, colorData?: ColorData): Promise<ColorData> { const colorTheme = ColorTheme(props) - switch (colorTheme.kind) { - case 'uniform': return createUniformColor(locationIt, colorTheme.color, colorData) - case 'group': return createGroupColor(locationIt, colorTheme.color, colorData) - case 'groupInstance': return createGroupInstanceColor(locationIt, colorTheme.color, colorData) - case 'instance': return createInstanceColor(locationIt, colorTheme.color, colorData) + switch (getGranularity(locationIt, colorTheme.granularity)) { + case 'uniform': return createUniformColor(ctx, locationIt, colorTheme.color, colorData) + case 'group': return createGroupColor(ctx, locationIt, colorTheme.color, colorData) + case 'groupInstance': return createGroupInstanceColor(ctx, locationIt, colorTheme.color, colorData) + case 'instance': return createInstanceColor(ctx, locationIt, colorTheme.color, colorData) } } -export function createSizes(locationIt: LocationIterator, props: SizeThemeProps, sizeData?: SizeData): SizeData { +export async function createSizes(ctx: RuntimeContext, locationIt: LocationIterator, props: SizeThemeProps, sizeData?: SizeData): Promise<SizeData> { const sizeTheme = SizeTheme(props) - switch (sizeTheme.kind) { - case 'uniform': return createUniformSize(locationIt, sizeTheme.size, sizeData) - case 'group': return createGroupSize(locationIt, sizeTheme.size, sizeData) - case 'groupInstance': return createGroupInstanceSize(locationIt, sizeTheme.size, sizeData) - case 'instance': return createInstanceSize(locationIt, sizeTheme.size, sizeData) + switch (getGranularity(locationIt, sizeTheme.granularity)) { + case 'uniform': return createUniformSize(ctx, locationIt, sizeTheme.size, sizeData) + case 'group': return createGroupSize(ctx, locationIt, sizeTheme.size, sizeData) + case 'groupInstance': return createGroupInstanceSize(ctx, locationIt, sizeTheme.size, sizeData) + case 'instance': return createInstanceSize(ctx, locationIt, sizeTheme.size, sizeData) } } +// mesh + type StructureMeshProps = Required<MeshProps & StructureProps> -function _createMeshValues(transforms: ValueCell<Float32Array>, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps): MeshValues { +async function _createMeshValues(ctx: RuntimeContext, transforms: TransformData, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps): Promise<MeshValues> { const { instanceCount, groupCount } = locationIt - const color = createColors(locationIt, props.colorTheme) + const color = await createColors(ctx, locationIt, props.colorTheme) const marker = createMarkers(instanceCount * groupCount) const counts = { drawCount: mesh.triangleCount * 3, groupCount, instanceCount } @@ -65,35 +65,69 @@ function _createMeshValues(transforms: ValueCell<Float32Array>, mesh: Mesh, loca ...getMeshData(mesh), ...color, ...marker, - aTransform: transforms, + ...transforms, elements: mesh.indexBuffer, ...createMeshValues(props, counts) } } -export function createComplexMeshValues(structure: Structure, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps): MeshValues { +export async function createComplexMeshValues(ctx: RuntimeContext, structure: Structure, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps): Promise<MeshValues> { const transforms = createIdentityTransform() - return _createMeshValues(transforms, mesh, locationIt, props) + return _createMeshValues(ctx, transforms, mesh, locationIt, props) } -export function createUnitsMeshValues(group: Unit.SymmetryGroup, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps): MeshValues { +export async function createUnitsMeshValues(ctx: RuntimeContext, group: Unit.SymmetryGroup, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps): Promise<MeshValues> { const transforms = createTransforms(group) - return _createMeshValues(transforms, mesh, locationIt, props) + return _createMeshValues(ctx, transforms, mesh, locationIt, props) } -export function createComplexMeshRenderObject(structure: Structure, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps) { - const values = createComplexMeshValues(structure, mesh, locationIt, props) +export async function createComplexMeshRenderObject(ctx: RuntimeContext, structure: Structure, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps) { + const values = await createComplexMeshValues(ctx, structure, mesh, locationIt, props) const state = createRenderableState(props) return createMeshRenderObject(values, state) } -export function createUnitsMeshRenderObject(group: Unit.SymmetryGroup, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps) { - const values = createUnitsMeshValues(group, mesh, locationIt, props) +export async function createUnitsMeshRenderObject(ctx: RuntimeContext, group: Unit.SymmetryGroup, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps) { + const values = await createUnitsMeshValues(ctx, group, mesh, locationIt, props) const state = createRenderableState(props) return createMeshRenderObject(values, state) } -export function updateComplexMeshRenderObject(structure: Structure, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps): MeshValues { +export async function updateComplexMeshRenderObject(ctx: RuntimeContext, structure: Structure, mesh: Mesh, locationIt: LocationIterator, props: StructureMeshProps): Promise<MeshValues> { const transforms = createIdentityTransform() - return _createMeshValues(transforms, mesh, locationIt, props) + return _createMeshValues(ctx, transforms, mesh, locationIt, props) +} + +// point + +type StructurePointProps = Required<PointProps & StructureProps> + +async function _createPointValues(ctx: RuntimeContext, transforms: TransformData, vertices: ValueCell<Float32Array>, locationIt: LocationIterator, props: StructurePointProps): Promise<PointValues> { + const { instanceCount, groupCount } = locationIt + const color = await createColors(ctx, locationIt, props.colorTheme) + const size = await createSizes(ctx, locationIt, props.sizeTheme) + const marker = createMarkers(instanceCount * groupCount) + + const counts = { drawCount: groupCount, groupCount, instanceCount } + + return { + aPosition: vertices, + aGroup: ValueCell.create(fillSerial(new Float32Array(groupCount))), + ...color, + ...size, + ...marker, + ...transforms, + ...createPointValues(props, counts) + } +} + +export async function createUnitsPointValues(ctx: RuntimeContext, group: Unit.SymmetryGroup, vertices: ValueCell<Float32Array>, locationIt: LocationIterator, props: StructurePointProps): Promise<PointValues> { + const transforms = createTransforms(group) + return _createPointValues(ctx, transforms, vertices, locationIt, props) +} + +export async function createUnitsPointRenderObject(ctx: RuntimeContext, group: Unit.SymmetryGroup, vertices: ValueCell<Float32Array>, locationIt: LocationIterator, props: StructurePointProps) { + const values = await createUnitsPointValues(ctx, group, vertices, locationIt, props) + const state = createRenderableState(props) + return createPointRenderObject(values, state) } \ No newline at end of file diff --git a/src/mol-geo/representation/structure/visual/util/element.ts b/src/mol-geo/representation/structure/visual/util/element.ts index d469d6f2f53d63d0233fcf6c3defd0122c4483dc..0de1861eefddaeac2d28b8d6e8f20790f4eca963 100644 --- a/src/mol-geo/representation/structure/visual/util/element.ts +++ b/src/mol-geo/representation/structure/visual/util/element.ts @@ -57,8 +57,8 @@ export function markElement(loci: Loci, group: Unit.SymmetryGroup, apply: (inter let changed = false if (StructureElement.isLoci(loci)) { for (const e of loci.elements) { - const unitIdx = Unit.findUnitById(e.unit.id, group.units) - if (unitIdx !== -1) { + const unitIdx = group.unitIndexMap.get(e.unit.id) + if (unitIdx !== undefined) { if (Interval.is(e.indices)) { const start = unitIdx * elementCount + Interval.start(e.indices); const end = unitIdx * elementCount + Interval.end(e.indices); @@ -87,11 +87,12 @@ export function getElementLoci(pickingId: PickingId, group: Unit.SymmetryGroup, export namespace StructureElementIterator { export function fromGroup(group: Unit.SymmetryGroup): LocationIterator { - const unit = group.units[0] const groupCount = group.elements.length const instanceCount = group.units.length - const location = StructureElement.create(unit) - const getLocation = (groupIndex: number) => { + const location = StructureElement.create() + const getLocation = (groupIndex: number, instanceIndex: number) => { + const unit = group.units[instanceIndex] + location.unit = unit location.element = unit.elements[groupIndex] return location } diff --git a/src/mol-geo/representation/structure/visual/util/link.ts b/src/mol-geo/representation/structure/visual/util/link.ts index 865e52d99668e5d38d9c950928953049444e2253..ca73e55b9cb017173c7cb9af15701e7aff21a1fc 100644 --- a/src/mol-geo/representation/structure/visual/util/link.ts +++ b/src/mol-geo/representation/structure/visual/util/link.ts @@ -126,8 +126,10 @@ export namespace LinkIterator { const unit = group.units[0] const groupCount = Unit.isAtomic(unit) ? unit.links.edgeCount * 2 : 0 const instanceCount = group.units.length - const location = StructureElement.create(unit) - const getLocation = (groupIndex: number) => { + const location = StructureElement.create() + const getLocation = (groupIndex: number, instanceIndex: number) => { + const unit = group.units[instanceIndex] + location.unit = unit location.element = unit.elements[(unit as Unit.Atomic).links.a[groupIndex]] return location } @@ -146,6 +148,6 @@ export namespace LinkIterator { location.bIndex = bond.indexB as StructureElement.UnitIndex return location } - return LocationIterator(groupCount, instanceCount, getLocation) + return LocationIterator(groupCount, instanceCount, getLocation, true) } } \ No newline at end of file diff --git a/src/mol-geo/representation/structure/visual/util/nucleotide.ts b/src/mol-geo/representation/structure/visual/util/nucleotide.ts new file mode 100644 index 0000000000000000000000000000000000000000..d62fae0e165047165c9d0266c5c4c8a8735fa18b --- /dev/null +++ b/src/mol-geo/representation/structure/visual/util/nucleotide.ts @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { Unit, StructureElement } from 'mol-model/structure'; +import { LocationIterator } from '../../../../util/location-iterator'; +import { getNucleotideElements } from 'mol-model/structure/structure/util/nucleotide'; +import { PickingId } from '../../../../util/picking'; +import { Loci, EmptyLoci } from 'mol-model/loci'; +import { OrderedSet, Interval } from 'mol-data/int'; + +export namespace NucleotideLocationIterator { + export function fromGroup(group: Unit.SymmetryGroup): LocationIterator { + const u = group.units[0] + const nucleotideElementIndices = Unit.isAtomic(u) ? getNucleotideElements(u) : [] + const groupCount = nucleotideElementIndices.length + const instanceCount = group.units.length + const location = StructureElement.create() + const getLocation = (groupIndex: number, instanceIndex: number) => { + const unit = group.units[instanceIndex] + location.unit = unit + location.element = nucleotideElementIndices[groupIndex] + return location + } + return LocationIterator(groupCount, instanceCount, getLocation) + } +} + +export function getNucleotideElementLoci(pickingId: PickingId, group: Unit.SymmetryGroup, id: number) { + const { objectId, instanceId, groupId } = pickingId + if (id === objectId) { + const unit = group.units[instanceId] + if (Unit.isAtomic(unit)) { + const unitIndex = OrderedSet.indexOf(unit.elements, unit.nucleotideElements[groupId]) as StructureElement.UnitIndex + if (unitIndex !== -1) { + const indices = OrderedSet.ofSingleton(unitIndex) + return StructureElement.Loci([{ unit, indices }]) + } + } + } + return EmptyLoci +} + +export function markNucleotideElement(loci: Loci, group: Unit.SymmetryGroup, apply: (interval: Interval) => boolean) { + let changed = false + const u = group.units[0] + if (StructureElement.isLoci(loci) && Unit.isAtomic(u)) { + const groupCount = u.nucleotideElements.length + for (const e of loci.elements) { + const unitIdx = group.unitIndexMap.get(e.unit.id) + if (unitIdx !== undefined && Unit.isAtomic(e.unit)) { + if (Interval.is(e.indices)) { + const min = OrderedSet.indexOf(e.unit.nucleotideElements, e.unit.elements[Interval.min(e.indices)]) + const max = OrderedSet.indexOf(e.unit.nucleotideElements, e.unit.elements[Interval.max(e.indices)]) + if (min !== -1 && max !== -1) { + if (apply(Interval.ofRange(unitIdx * groupCount + min, unitIdx * groupCount + max))) changed = true + } + } else { + for (let i = 0, _i = e.indices.length; i < _i; i++) { + const idx = OrderedSet.indexOf(e.unit.nucleotideElements, e.unit.elements[e.indices[i]]) + if (idx !== -1) { + if (apply(Interval.ofSingleton(unitIdx * groupCount + idx))) changed = true + } + } + } + } + } + } + return changed +} \ No newline at end of file diff --git a/src/mol-geo/representation/structure/visual/util/polymer.ts b/src/mol-geo/representation/structure/visual/util/polymer.ts index 24d9b7e937e2953c1e1acb3c958a6f69de6b264c..8586b9e50ad5188b6ab1d7a0b37e3d70944a112d 100644 --- a/src/mol-geo/representation/structure/visual/util/polymer.ts +++ b/src/mol-geo/representation/structure/visual/util/polymer.ts @@ -4,9 +4,12 @@ * @author Alexander Rose <alexander.rose@weirdbyte.de> */ -import { Unit, ElementIndex } from 'mol-model/structure'; -import { Segmentation, OrderedSet, Interval } from 'mol-data/int'; +import { Unit, ElementIndex, StructureElement, Link } from 'mol-model/structure'; import SortedRanges from 'mol-data/int/sorted-ranges'; +import { LocationIterator } from '../../../../util/location-iterator'; +import { PickingId } from '../../../../util/picking'; +import { OrderedSet, Interval } from 'mol-data/int'; +import { EmptyLoci, Loci } from 'mol-model/loci'; export * from './polymer/backbone-iterator' export * from './polymer/gap-iterator' @@ -29,41 +32,106 @@ export function getGapRanges(unit: Unit): SortedRanges<ElementIndex> { } } -export function getPolymerElementCount(unit: Unit) { - let count = 0 - const { elements } = unit - const polymerIt = SortedRanges.transientSegments(getPolymerRanges(unit), elements) - switch (unit.kind) { - case Unit.Kind.Atomic: - const residueIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, elements) - while (polymerIt.hasNext) { - const polymerSegment = polymerIt.move() - residueIt.setSegment(polymerSegment) - while (residueIt.hasNext) { - const residueSegment = residueIt.move() - const { start, end } = residueSegment - if (OrderedSet.areIntersecting(Interval.ofBounds(elements[start], elements[end - 1]), elements)) ++count +export namespace PolymerLocationIterator { + export function fromGroup(group: Unit.SymmetryGroup): LocationIterator { + const polymerElements = group.units[0].polymerElements + const groupCount = polymerElements.length + const instanceCount = group.units.length + const location = StructureElement.create() + const getLocation = (groupIndex: number, instanceIndex: number) => { + const unit = group.units[instanceIndex] + location.unit = unit + location.element = polymerElements[groupIndex] + return location + } + return LocationIterator(groupCount, instanceCount, getLocation) + } +} + +export namespace PolymerGapLocationIterator { + export function fromGroup(group: Unit.SymmetryGroup): LocationIterator { + const gapElements = group.units[0].gapElements + const groupCount = gapElements.length + const instanceCount = group.units.length + const location = StructureElement.create() + const getLocation = (groupIndex: number, instanceIndex: number) => { + const unit = group.units[instanceIndex] + location.unit = unit + location.element = gapElements[groupIndex] + return location + } + return LocationIterator(groupCount, instanceCount, getLocation) + } +} + +export function getPolymerElementLoci(pickingId: PickingId, group: Unit.SymmetryGroup, id: number) { + const { objectId, instanceId, groupId } = pickingId + if (id === objectId) { + const unit = group.units[instanceId] + const unitIndex = OrderedSet.indexOf(unit.elements, unit.polymerElements[groupId]) as StructureElement.UnitIndex + if (unitIndex !== -1) { + const indices = OrderedSet.ofSingleton(unitIndex) + return StructureElement.Loci([{ unit, indices }]) + } + } + return EmptyLoci +} + +export function markPolymerElement(loci: Loci, group: Unit.SymmetryGroup, apply: (interval: Interval) => boolean) { + const groupCount = group.units[0].polymerElements.length + + let changed = false + if (StructureElement.isLoci(loci)) { + for (const e of loci.elements) { + const unitIdx = group.unitIndexMap.get(e.unit.id) + if (unitIdx !== undefined) { + if (Interval.is(e.indices)) { + const min = + OrderedSet.indexOf(e.unit.polymerElements, e.unit.elements[Interval.min(e.indices)]) + const max = OrderedSet.indexOf(e.unit.polymerElements, e.unit.elements[Interval.max(e.indices)]) + if (min !== -1 && max !== -1) { + if (apply(Interval.ofRange(unitIdx * groupCount + min, unitIdx * groupCount + max))) changed = true + } + } else { + for (let i = 0, _i = e.indices.length; i < _i; i++) { + const idx = OrderedSet.indexOf(e.unit.polymerElements, e.unit.elements[e.indices[i]]) + if (idx !== -1) { + if (apply(Interval.ofSingleton(unitIdx * groupCount + idx))) changed = true + } + } } } - break - case Unit.Kind.Spheres: - case Unit.Kind.Gaussians: - while (polymerIt.hasNext) { - const { start, end } = polymerIt.move() - count += OrderedSet.intersectionSize(Interval.ofBounds(elements[start], elements[end - 1]), elements) - } - break + } } - return count + return changed } -export function getPolymerGapCount(unit: Unit) { - let count = 0 - const { elements } = unit - const gapIt = SortedRanges.transientSegments(getGapRanges(unit), elements) - while (gapIt.hasNext) { - const { start, end } = gapIt.move() - if (OrderedSet.areIntersecting(Interval.ofBounds(elements[start], elements[end - 1]), elements)) ++count +export function getPolymerGapElementLoci(pickingId: PickingId, group: Unit.SymmetryGroup, id: number) { + const { objectId, instanceId, groupId } = pickingId + if (id === objectId) { + const unit = group.units[instanceId] + const unitIndexA = OrderedSet.indexOf(unit.elements, unit.gapElements[groupId]) as StructureElement.UnitIndex + const unitIndexB = OrderedSet.indexOf(unit.elements, unit.gapElements[groupId % 2 ? groupId - 1 : groupId + 1]) as StructureElement.UnitIndex + if (unitIndexA !== -1 && unitIndexB !== -1) { + return Link.Loci([ Link.Location(unit, unitIndexA, unit, unitIndexB) ]) + } } - return count + return EmptyLoci } + +export function markPolymerGapElement(loci: Loci, group: Unit.SymmetryGroup, apply: (interval: Interval) => boolean) { + let changed = false + if (Link.isLoci(loci)) { + 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 + } + } + } + } + return changed +} \ No newline at end of file diff --git a/src/mol-geo/representation/structure/visual/util/polymer/backbone-iterator.ts b/src/mol-geo/representation/structure/visual/util/polymer/backbone-iterator.ts index eaa3c029c7ab1455a093701b7b7b3bc3fb582da1..31170313fb9d345db914146b927b1650c2a38a14 100644 --- a/src/mol-geo/representation/structure/visual/util/polymer/backbone-iterator.ts +++ b/src/mol-geo/representation/structure/visual/util/polymer/backbone-iterator.ts @@ -10,6 +10,7 @@ import Iterator from 'mol-data/iterator'; import SortedRanges from 'mol-data/int/sorted-ranges'; import { getElementIndexForAtomRole } from 'mol-model/structure/util'; import { getPolymerRanges } from '../polymer'; +import { AtomRole } from 'mol-model/structure/model/types'; /** Iterates over consecutive pairs of residues/coarse elements in polymers */ export function PolymerBackboneIterator(unit: Unit): Iterator<PolymerBackbonePair> { @@ -43,13 +44,19 @@ export class AtomicPolymerBackboneIterator implements Iterator<PolymerBackbonePa private residueSegment: Segmentation.Segment<ResidueIndex> hasNext: boolean = false; + private getElementIndex(residueIndex: ResidueIndex, atomRole: AtomRole) { + const { atomicHierarchy } = this.unit.model + const elementIndex = getElementIndexForAtomRole(this.unit.model, residueIndex, atomRole) + return elementIndex === -1 ? atomicHierarchy.residueAtomSegments.offsets[residueIndex] : elementIndex + } + move() { if (this.state === AtomicPolymerBackboneIteratorState.nextPolymer) { while (this.polymerIt.hasNext) { this.residueIt.setSegment(this.polymerIt.move()); if (this.residueIt.hasNext) { this.residueSegment = this.residueIt.move() - this.value.centerB.element = getElementIndexForAtomRole(this.unit.model, this.residueSegment.index, 'trace') + this.value.centerB.element = this.getElementIndex(this.residueSegment.index, 'trace') this.state = AtomicPolymerBackboneIteratorState.nextResidue break } @@ -59,7 +66,7 @@ export class AtomicPolymerBackboneIterator implements Iterator<PolymerBackbonePa if (this.state === AtomicPolymerBackboneIteratorState.nextResidue) { this.residueSegment = this.residueIt.move() this.value.centerA.element = this.value.centerB.element - this.value.centerB.element = getElementIndexForAtomRole(this.unit.model, this.residueSegment.index, 'trace') + this.value.centerB.element = this.getElementIndex(this.residueSegment.index, 'trace') if (!this.residueIt.hasNext) { if (this.unit.model.atomicHierarchy.cyclicPolymerMap.has(this.residueSegment.index)) { this.state = AtomicPolymerBackboneIteratorState.cycle @@ -71,7 +78,7 @@ export class AtomicPolymerBackboneIterator implements Iterator<PolymerBackbonePa } else if (this.state === AtomicPolymerBackboneIteratorState.cycle) { const { cyclicPolymerMap } = this.unit.model.atomicHierarchy this.value.centerA.element = this.value.centerB.element - this.value.centerB.element = getElementIndexForAtomRole(this.unit.model, cyclicPolymerMap.get(this.residueSegment.index)!, 'trace') + this.value.centerB.element = this.getElementIndex(cyclicPolymerMap.get(this.residueSegment.index)!, 'trace') // TODO need to advance to a polymer that has two or more residues (can't assume it has) this.state = AtomicPolymerBackboneIteratorState.nextPolymer } diff --git a/src/mol-geo/representation/structure/visual/util/polymer/gap-iterator.ts b/src/mol-geo/representation/structure/visual/util/polymer/gap-iterator.ts index 9b91f2894e01ebd89546cee4fafa59514248ee4d..663f10162596a611ff62efc0c4811a5508e99255 100644 --- a/src/mol-geo/representation/structure/visual/util/polymer/gap-iterator.ts +++ b/src/mol-geo/representation/structure/visual/util/polymer/gap-iterator.ts @@ -5,7 +5,6 @@ */ import { Unit, StructureElement, ElementIndex, ResidueIndex } from 'mol-model/structure'; -import { SortedArray } from 'mol-data/int'; import { AtomRole } from 'mol-model/structure/model/types'; import Iterator from 'mol-data/iterator'; import SortedRanges from 'mol-data/int/sorted-ranges'; @@ -40,13 +39,8 @@ export class AtomicPolymerGapIterator implements Iterator<PolymerGapPair> { hasNext: boolean = false; private getElementIndex(residueIndex: ResidueIndex, atomRole: AtomRole) { - const index = getElementIndexForAtomRole(this.unit.model, residueIndex, atomRole) - // TODO handle case when it returns -1 - const elementIndex = SortedArray.indexOf(this.unit.elements, index) as ElementIndex - if (elementIndex === -1) { - console.log('-1', residueIndex, atomRole, index) - } - return elementIndex === -1 ? 0 as ElementIndex : elementIndex + const elementIndex = getElementIndexForAtomRole(this.unit.model, residueIndex, atomRole) + return elementIndex === -1 ? this.unit.model.atomicHierarchy.residueAtomSegments.offsets[residueIndex] : elementIndex } move() { diff --git a/src/mol-geo/representation/structure/visual/util/polymer/trace-iterator.ts b/src/mol-geo/representation/structure/visual/util/polymer/trace-iterator.ts index df4cfabc5ae57639e16fed87086ab7af9f5f78ec..e4b98c009cfe449ea2bd183a5280b6c564fcff9c 100644 --- a/src/mol-geo/representation/structure/visual/util/polymer/trace-iterator.ts +++ b/src/mol-geo/representation/structure/visual/util/polymer/trace-iterator.ts @@ -89,24 +89,25 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement> this.residueSegmentMax = index[this.unit.elements[polymerSegment.end - 1]] } - private getAtomIndex(residueIndex: ResidueIndex, atomRole: AtomRole) { - const { cyclicPolymerMap } = this.unit.model.atomicHierarchy + private getElementIndex(residueIndex: ResidueIndex, atomRole: AtomRole) { + const { atomicHierarchy } = this.unit.model if (residueIndex < this.residueSegmentMin) { - const cyclicIndex = cyclicPolymerMap.get(this.residueSegmentMin) + const cyclicIndex = atomicHierarchy.cyclicPolymerMap.get(this.residueSegmentMin) if (cyclicIndex !== undefined) { residueIndex = cyclicIndex - (this.residueSegmentMin - residueIndex - 1) as ResidueIndex } else { residueIndex = this.residueSegmentMin } } else if (residueIndex > this.residueSegmentMax) { - const cyclicIndex = cyclicPolymerMap.get(this.residueSegmentMax) + const cyclicIndex = atomicHierarchy.cyclicPolymerMap.get(this.residueSegmentMax) if (cyclicIndex !== undefined) { residueIndex = cyclicIndex + (residueIndex - this.residueSegmentMax - 1) as ResidueIndex } else { residueIndex = this.residueSegmentMax } } - return getElementIndexForAtomRole(this.unit.model, residueIndex, atomRole) + const elementIndex = getElementIndexForAtomRole(this.unit.model, residueIndex, atomRole) + return elementIndex === -1 ? atomicHierarchy.residueAtomSegments.offsets[residueIndex] : elementIndex } private setControlPoint(out: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, residueIndex: ResidueIndex) { @@ -135,19 +136,19 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement> if (this.state === AtomicPolymerTraceIteratorState.nextResidue) { const { index: residueIndex } = residueIt.move(); - value.center.element = this.getAtomIndex(residueIndex, 'trace') + value.center.element = this.getElementIndex(residueIndex, 'trace') - this.pos(this.p0, this.getAtomIndex(residueIndex - 3 as ResidueIndex, 'trace')) - this.pos(this.p1, this.getAtomIndex(residueIndex - 2 as ResidueIndex, 'trace')) - this.pos(this.p2, this.getAtomIndex(residueIndex - 1 as ResidueIndex, 'trace')) - this.pos(this.p3, this.getAtomIndex(residueIndex, 'trace')) - this.pos(this.p4, this.getAtomIndex(residueIndex + 1 as ResidueIndex, 'trace')) - this.pos(this.p5, this.getAtomIndex(residueIndex + 2 as ResidueIndex, 'trace')) - this.pos(this.p6, this.getAtomIndex(residueIndex + 3 as ResidueIndex, 'trace')) + this.pos(this.p0, this.getElementIndex(residueIndex - 3 as ResidueIndex, 'trace')) + this.pos(this.p1, this.getElementIndex(residueIndex - 2 as ResidueIndex, 'trace')) + this.pos(this.p2, this.getElementIndex(residueIndex - 1 as ResidueIndex, 'trace')) + this.pos(this.p3, this.getElementIndex(residueIndex, 'trace')) + this.pos(this.p4, this.getElementIndex(residueIndex + 1 as ResidueIndex, 'trace')) + this.pos(this.p5, this.getElementIndex(residueIndex + 2 as ResidueIndex, 'trace')) + this.pos(this.p6, this.getElementIndex(residueIndex + 3 as ResidueIndex, 'trace')) // this.pos(this.v01, this.getAtomIndex(residueIndex - 2 as ResidueIndex, 'direction')) - this.pos(this.v12, this.getAtomIndex(residueIndex - 1 as ResidueIndex, 'direction')) - this.pos(this.v23, this.getAtomIndex(residueIndex, 'direction')) + this.pos(this.v12, this.getElementIndex(residueIndex - 1 as ResidueIndex, 'direction')) + this.pos(this.v23, this.getElementIndex(residueIndex, 'direction')) // this.pos(this.v34, this.getAtomIndex(residueIndex + 1 as ResidueIndex, 'direction')) this.value.secStrucType = this.unit.model.properties.secondaryStructure.type[residueIndex] diff --git a/src/mol-geo/representation/util.ts b/src/mol-geo/representation/util.ts index 4edca150e52f42e76d67a5b28fd3f76754d4ed19..a7fdea3c87c5a3dab8addee34d4b16257e39987c 100644 --- a/src/mol-geo/representation/util.ts +++ b/src/mol-geo/representation/util.ts @@ -9,14 +9,12 @@ import { BaseValues } from 'mol-gl/renderable/schema'; import { MeshValues, RenderableState } from 'mol-gl/renderable'; import { defaults } from 'mol-util'; import { Structure } from 'mol-model/structure'; -import { fillSerial } from 'mol-util/array'; -import { Mat4 } from 'mol-math/linear-algebra'; export const DefaultBaseProps = { alpha: 1, visible: true, depthMask: true, - useFog: true, + useFog: false, quality: 'auto' as VisualQuality } export type BaseProps = typeof DefaultBaseProps @@ -29,22 +27,20 @@ export const DefaultMeshProps = { } export type MeshProps = typeof DefaultMeshProps -const identityTransform = new Float32Array(16) -Mat4.toArray(Mat4.identity(), identityTransform, 0) -export function createIdentityTransform(transforms?: ValueCell<Float32Array>) { - return transforms ? ValueCell.update(transforms, identityTransform) : ValueCell.create(identityTransform) +export const DefaultPointProps = { + ...DefaultBaseProps, + pointSizeAttenuation: true } +export type PointProps = typeof DefaultPointProps type Counts = { drawCount: number, groupCount: number, instanceCount: number } export function createBaseValues(props: Required<BaseProps>, counts: Counts) { return { uAlpha: ValueCell.create(props.alpha), - uInstanceCount: ValueCell.create(counts.instanceCount), uGroupCount: ValueCell.create(counts.groupCount), - aInstance: ValueCell.create(fillSerial(new Float32Array(counts.instanceCount))), drawCount: ValueCell.create(counts.drawCount), - instanceCount: ValueCell.create(counts.instanceCount), + dUseFog: ValueCell.create(props.useFog), } } @@ -54,7 +50,13 @@ export function createMeshValues(props: Required<MeshProps>, counts: Counts) { dDoubleSided: ValueCell.create(props.doubleSided), dFlatShaded: ValueCell.create(props.flatShaded), dFlipSided: ValueCell.create(props.flipSided), - dUseFog: ValueCell.create(props.useFog), + } +} + +export function createPointValues(props: Required<PointProps>, counts: Counts) { + return { + ...createBaseValues(props, counts), + dPointSizeAttenuation: ValueCell.create(props.pointSizeAttenuation), } } @@ -67,6 +69,7 @@ export function createRenderableState(props: Required<BaseProps>): RenderableSta export function updateBaseValues(values: BaseValues, props: Required<BaseProps>) { ValueCell.updateIfChanged(values.uAlpha, props.alpha) + ValueCell.updateIfChanged(values.dUseFog, props.useFog) } export function updateMeshValues(values: MeshValues, props: Required<MeshProps>) { @@ -74,7 +77,6 @@ export function updateMeshValues(values: MeshValues, props: Required<MeshProps>) ValueCell.updateIfChanged(values.dDoubleSided, props.doubleSided) ValueCell.updateIfChanged(values.dFlatShaded, props.flatShaded) ValueCell.updateIfChanged(values.dFlipSided, props.flipSided) - ValueCell.updateIfChanged(values.dUseFog, props.useFog) } export function updateRenderableState(state: RenderableState, props: Required<BaseProps>) { @@ -82,20 +84,32 @@ export function updateRenderableState(state: RenderableState, props: Required<Ba state.depthMask = props.depthMask } -export type VisualQuality = 'custom' | 'auto' | 'highest' | 'high' | 'medium' | 'low' | 'lowest' +export const VisualQualityInfo = { + 'custom': {}, + 'auto': {}, + 'highest': {}, + 'high': {}, + 'medium': {}, + 'low': {}, + 'lowest': {}, +} +export type VisualQuality = keyof typeof VisualQualityInfo +export const VisualQualityNames = Object.keys(VisualQualityInfo) -interface QualityProps { +export interface QualityProps { quality: VisualQuality detail: number radialSegments: number + linearSegments: number } -export function getQualityProps(props: Partial<QualityProps>, structure: Structure) { +export function getQualityProps(props: Partial<QualityProps>, structure?: Structure) { let quality = defaults(props.quality, 'auto' as VisualQuality) - let detail = 1 - let radialSegments = 12 + let detail = defaults(props.detail, 1) + let radialSegments = defaults(props.radialSegments, 12) + let linearSegments = defaults(props.linearSegments, 8) - if (quality === 'auto') { + if (quality === 'auto' && structure) { const score = structure.elementCount if (score > 500_000) { quality = 'lowest' @@ -110,33 +124,38 @@ export function getQualityProps(props: Partial<QualityProps>, structure: Structu switch (quality) { case 'highest': - detail = 2 + detail = 3 radialSegments = 36 + linearSegments = 18 break case 'high': - detail = 1 + detail = 2 radialSegments = 24 + linearSegments = 12 break case 'medium': - detail = 0 + detail = 1 radialSegments = 12 + linearSegments = 8 break case 'low': detail = 0 radialSegments = 5 + linearSegments = 3 break case 'lowest': detail = 0 radialSegments = 3 + linearSegments = 2 break case 'custom': - detail = defaults(props.detail, 1) - radialSegments = defaults(props.radialSegments, 12) + // use defaults or given props as set above break } return { detail, - radialSegments + radialSegments, + linearSegments } } \ No newline at end of file diff --git a/src/mol-geo/representation/volume/index.ts b/src/mol-geo/representation/volume/index.ts index 75555e2ca1ac77678a46c18ca5ffd9f89c9a3d09..9197885d3c9cebb758d2cd37218437ec3164f561 100644 --- a/src/mol-geo/representation/volume/index.ts +++ b/src/mol-geo/representation/volume/index.ts @@ -27,31 +27,32 @@ export function VolumeRepresentation<P extends VolumeProps>(visualCtor: (volumeD let _volumeData: VolumeData let _props: P - function create(volumeData: VolumeData, props: Partial<P> = {}) { + function createOrUpdate(props: Partial<P> = {}, volumeData?: VolumeData) { _props = Object.assign({}, DefaultVolumeProps, _props, props) return Task.create('VolumeRepresentation.create', async ctx => { - _volumeData = volumeData - const visual = visualCtor(_volumeData) - await visual.create(ctx, _volumeData, props) - renderObjects.push(visual.renderObject) + if (volumeData) { + _volumeData = volumeData + const visual = visualCtor(_volumeData) + await visual.createOrUpdate(ctx, props, _volumeData) + if (visual.renderObject) renderObjects.push(visual.renderObject) + } else { + throw new Error('missing volumeData') + } }); } - function update(props: Partial<P>) { - return Task.create('VolumeRepresentation.update', async ctx => {}) - } - return { + label: 'Volume mesh', get renderObjects () { return renderObjects }, get props () { return _props }, - create, - update, + createOrUpdate, getLoci(pickingId: PickingId) { // TODO return EmptyLoci }, mark(loci: Loci, action: MarkerAction) { // TODO + return false }, destroy() { // TODO diff --git a/src/mol-geo/representation/volume/surface.ts b/src/mol-geo/representation/volume/surface.ts index 1d289273d6e5783431b9185c98c8d09c3d1778ae..46671107fcfff72a825598c02692718e6c1953b7 100644 --- a/src/mol-geo/representation/volume/surface.ts +++ b/src/mol-geo/representation/volume/surface.ts @@ -57,9 +57,11 @@ export default function SurfaceVisual(): VolumeVisual<SurfaceProps> { return { get renderObject () { return renderObject }, - async create(ctx: RuntimeContext, volume: VolumeData, props: SurfaceProps = {}) { + async createOrUpdate(ctx: RuntimeContext, props: SurfaceProps = {}, volume?: VolumeData) { props = { ...DefaultSurfaceProps, ...props } + if (!volume) return + const mesh = await computeVolumeSurface(volume, curProps.isoValue).runAsChild(ctx) if (!props.flatShaded) { Mesh.computeNormalsImmediate(mesh) @@ -97,16 +99,13 @@ export default function SurfaceVisual(): VolumeVisual<SurfaceProps> { renderObject = createMeshRenderObject(values, state) }, - async update(ctx: RuntimeContext, props: SurfaceProps) { - // TODO - return false - }, getLoci(pickingId: PickingId) { // TODO return EmptyLoci }, mark(loci: Loci, action: MarkerAction) { // TODO + return false }, destroy() { // TODO diff --git a/src/mol-geo/util/color-data.ts b/src/mol-geo/util/color-data.ts index d8f0c3d2e72397637ce8d6064e6d834e31dfeb8b..e47eef2d8da951efe7169f051df225828dbc83bb 100644 --- a/src/mol-geo/util/color-data.ts +++ b/src/mol-geo/util/color-data.ts @@ -9,7 +9,9 @@ import { TextureImage, createTextureImage } from 'mol-gl/renderable/util'; import { Color } from 'mol-util/color'; import { Vec2, Vec3 } from 'mol-math/linear-algebra'; import { LocationIterator } from './location-iterator'; -import { Location, NullLocation } from 'mol-model/location'; +import { NullLocation } from 'mol-model/location'; +import { LocationColor } from 'mol-view/theme/color'; +import { RuntimeContext } from 'mol-task'; export type ColorType = 'uniform' | 'instance' | 'group' | 'groupInstance' @@ -21,8 +23,6 @@ export type ColorData = { dColorType: ValueCell<string>, } -export type LocationColor = (location: Location, isSecondary: boolean) => Color - const emptyColorTexture = { array: new Uint8Array(3), width: 1, height: 1 } function createEmptyColorTexture() { return { @@ -49,8 +49,8 @@ export function createValueColor(value: Color, colorData?: ColorData): ColorData } /** Creates color uniform */ -export function createUniformColor(locationIt: LocationIterator, colorFn: LocationColor, colorData?: ColorData): ColorData { - return createValueColor(colorFn(NullLocation, false), colorData) +export async function createUniformColor(ctx: RuntimeContext, locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): Promise<ColorData> { + return createValueColor(color(NullLocation, false), colorData) } export function createTextureColor(colors: TextureImage, type: ColorType, colorData?: ColorData): ColorData { @@ -73,36 +73,51 @@ export function createTextureColor(colors: TextureImage, type: ColorType, colorD } /** Creates color texture with color for each instance/unit */ -export function createInstanceColor(locationIt: LocationIterator, colorFn: LocationColor, colorData?: ColorData): ColorData { +export async function createInstanceColor(ctx: RuntimeContext, locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): Promise<ColorData> { const { instanceCount} = locationIt const colors = colorData && colorData.tColor.ref.value.array.length >= instanceCount * 3 ? colorData.tColor.ref.value : createTextureImage(instanceCount, 3) - while (locationIt.hasNext && !locationIt.isNextNewInstance) { + let i = 0 + while (locationIt.hasNext) { const { location, isSecondary, instanceIndex } = locationIt.move() - Color.toArray(colorFn(location, isSecondary), colors.array, instanceIndex * 3) + Color.toArray(color(location, isSecondary), colors.array, instanceIndex * 3) locationIt.skipInstance() + if (i % 10000 === 0 && ctx.shouldUpdate) { + await ctx.update({ message: 'Creating instance colors', current: i, max: instanceCount }); + } + ++i } return createTextureColor(colors, 'instance', colorData) } /** Creates color texture with color for each group (i.e. shared across instances/units) */ -export function createGroupColor(locationIt: LocationIterator, colorFn: LocationColor, colorData?: ColorData): ColorData { +export async function createGroupColor(ctx: RuntimeContext, locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): Promise<ColorData> { const { groupCount } = locationIt const colors = colorData && colorData.tColor.ref.value.array.length >= groupCount * 3 ? colorData.tColor.ref.value : createTextureImage(groupCount, 3) + let i = 0 while (locationIt.hasNext && !locationIt.isNextNewInstance) { const { location, isSecondary, groupIndex } = locationIt.move() - Color.toArray(colorFn(location, isSecondary), colors.array, groupIndex * 3) + Color.toArray(color(location, isSecondary), colors.array, groupIndex * 3) + if (i % 10000 === 0 && ctx.shouldUpdate) { + await ctx.update({ message: 'Creating group colors', current: i, max: groupCount }); + } + ++i } return createTextureColor(colors, 'group', colorData) } /** Creates color texture with color for each group in each instance (i.e. for each unit) */ -export function createGroupInstanceColor(locationIt: LocationIterator, colorFn: LocationColor, colorData?: ColorData): ColorData { +export async function createGroupInstanceColor(ctx: RuntimeContext, locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): Promise<ColorData> { const { groupCount, instanceCount } = locationIt const count = instanceCount * groupCount const colors = colorData && colorData.tColor.ref.value.array.length >= count * 3 ? colorData.tColor.ref.value : createTextureImage(count, 3) - while (locationIt.hasNext && !locationIt.isNextNewInstance) { + let i = 0 + while (locationIt.hasNext) { const { location, isSecondary, index } = locationIt.move() - Color.toArray(colorFn(location, isSecondary), colors.array, index * 3) + Color.toArray(color(location, isSecondary), colors.array, index * 3) + if (i % 10000 === 0 && ctx.shouldUpdate) { + await ctx.update({ message: 'Creating group instance colors', current: i, max: count }); + } + ++i } return createTextureColor(colors, 'groupInstance', colorData) } \ No newline at end of file diff --git a/src/mol-geo/util/location-iterator.ts b/src/mol-geo/util/location-iterator.ts index 435d0c45c3be8181456098ec99331c4cf7f579d5..16c3517098b273acfd6fc7c48f58055fd07b335e 100644 --- a/src/mol-geo/util/location-iterator.ts +++ b/src/mol-geo/util/location-iterator.ts @@ -28,6 +28,8 @@ export interface LocationIterator extends Iterator<LocationValue> { readonly isNextNewInstance: boolean readonly groupCount: number readonly instanceCount: number + /** If true, may have multiple units per instance; if false one unit per instance */ + readonly isComplex: boolean move(): LocationValue reset(): void skipInstance(): void @@ -36,7 +38,7 @@ export interface LocationIterator extends Iterator<LocationValue> { type LocationGetter = (groupIndex: number, instanceIndex: number) => Location type IsSecondaryGetter = (groupIndex: number, instanceIndex: number) => boolean -export function LocationIterator(groupCount: number, instanceCount: number, getLocation: LocationGetter, isSecondary: IsSecondaryGetter = () => false): LocationIterator { +export function LocationIterator(groupCount: number, instanceCount: number, getLocation: LocationGetter, isComplex = false, isSecondary: IsSecondaryGetter = () => false): LocationIterator { const value: LocationValue = { location: NullLocation as Location, index: 0, @@ -55,6 +57,7 @@ export function LocationIterator(groupCount: number, instanceCount: number, getL get isNextNewInstance () { return isNextNewInstance }, get groupCount () { return groupCount }, get instanceCount () { return instanceCount }, + isComplex, move() { if (hasNext) { value.groupIndex = groupIndex diff --git a/src/mol-geo/util/marker-data.ts b/src/mol-geo/util/marker-data.ts index 55dc2494560d9bc734dba5a1a11f73f835f797b7..ed813f2fa8432c84960cab9368716ff21eda7f8b 100644 --- a/src/mol-geo/util/marker-data.ts +++ b/src/mol-geo/util/marker-data.ts @@ -18,7 +18,6 @@ export enum MarkerAction { RemoveHighlight, Select, Deselect, - ToggleSelect, Clear } @@ -30,42 +29,26 @@ export function applyMarkerAction(array: Uint8Array, start: number, end: number, case MarkerAction.Highlight: if (v % 2 === 0) { v += 1 - changed = true } break case MarkerAction.RemoveHighlight: if (v % 2 !== 0) { v -= 1 - changed = true } break case MarkerAction.Select: v += 2 - changed = true break case MarkerAction.Deselect: if (v >= 2) { v -= 2 - changed = true } break - case MarkerAction.ToggleSelect: - if (v === 0) { - v = 2 - } else if (v === 1) { - v = 3 - } else if (v === 2) { - v = 0 - } else { - v -= 2 - } - changed = true - break case MarkerAction.Clear: v = 0 - changed = true break } + changed = array[i] !== v || changed array[i] = v } return changed diff --git a/src/mol-geo/util/size-data.ts b/src/mol-geo/util/size-data.ts index f1a59a8fa6448b1931576d5d8c3b3b2f4e0af48c..2afbf89eee56c8c0d9bbfe32f3ad56e727d26037 100644 --- a/src/mol-geo/util/size-data.ts +++ b/src/mol-geo/util/size-data.ts @@ -9,6 +9,7 @@ import { Vec2 } from 'mol-math/linear-algebra'; import { TextureImage, createTextureImage } from 'mol-gl/renderable/util'; import { LocationIterator } from './location-iterator'; import { Location, NullLocation } from 'mol-model/location'; +import { RuntimeContext } from 'mol-task'; export type SizeType = 'uniform' | 'instance' | 'group' | 'groupInstance' @@ -48,7 +49,7 @@ export function createValueSize(value: number, sizeData?: SizeData): SizeData { } /** Creates size uniform */ -export function createUniformSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData { +export async function createUniformSize(ctx: RuntimeContext, locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): Promise<SizeData> { return createValueSize(sizeFn(NullLocation), sizeData) } @@ -72,36 +73,51 @@ export function createTextureSize(sizes: TextureImage, type: SizeType, sizeData? } /** Creates size texture with size for each instance/unit */ -export function createInstanceSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData { +export async function createInstanceSize(ctx: RuntimeContext, locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): Promise<SizeData> { const { instanceCount} = locationIt const sizes = sizeData && sizeData.tSize.ref.value.array.length >= instanceCount ? sizeData.tSize.ref.value : createTextureImage(instanceCount, 1) + let i = 0 while (locationIt.hasNext && !locationIt.isNextNewInstance) { const v = locationIt.move() sizes.array[v.instanceIndex] = sizeFn(v.location) locationIt.skipInstance() + if (i % 10000 === 0 && ctx.shouldUpdate) { + await ctx.update({ message: 'Creating instance sizes', current: i, max: instanceCount }); + } + ++i } return createTextureSize(sizes, 'instance', sizeData) } /** Creates size texture with size for each group (i.e. shared across instances/units) */ -export function createGroupSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData { +export async function createGroupSize(ctx: RuntimeContext, locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): Promise<SizeData> { const { groupCount } = locationIt const sizes = sizeData && sizeData.tSize.ref.value.array.length >= groupCount ? sizeData.tSize.ref.value : createTextureImage(groupCount, 1) + let i = 0 while (locationIt.hasNext && !locationIt.isNextNewInstance) { const v = locationIt.move() sizes.array[v.groupIndex] = sizeFn(v.location) + if (i % 10000 === 0 && ctx.shouldUpdate) { + await ctx.update({ message: 'Creating group sizes', current: i, max: groupCount }); + } + ++i } return createTextureSize(sizes, 'group', sizeData) } /** Creates size texture with size for each group in each instance (i.e. for each unit) */ -export function createGroupInstanceSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData { +export async function createGroupInstanceSize(ctx: RuntimeContext, locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): Promise<SizeData> { const { groupCount, instanceCount } = locationIt const count = instanceCount * groupCount const sizes = sizeData && sizeData.tSize.ref.value.array.length >= count ? sizeData.tSize.ref.value : createTextureImage(count, 1) + let i = 0 while (locationIt.hasNext && !locationIt.isNextNewInstance) { const v = locationIt.move() sizes.array[v.index] = sizeFn(v.location) + if (i % 10000 === 0 && ctx.shouldUpdate) { + await ctx.update({ message: 'Creating group instance sizes', current: i, max: count }); + } + ++i } return createTextureSize(sizes, 'groupInstance', sizeData) } \ No newline at end of file diff --git a/src/mol-geo/util/transform-data.ts b/src/mol-geo/util/transform-data.ts new file mode 100644 index 0000000000000000000000000000000000000000..c1f83a7c5a095022def40d9eca3fc079755d1863 --- /dev/null +++ b/src/mol-geo/util/transform-data.ts @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { ValueCell } from 'mol-util'; +import { Mat4 } from 'mol-math/linear-algebra'; +import { fillSerial } from 'mol-util/array'; +import { Unit } from 'mol-model/structure'; + +export type TransformData = { + aTransform: ValueCell<Float32Array>, + uInstanceCount: ValueCell<number>, + instanceCount: ValueCell<number>, + aInstance: ValueCell<Float32Array>, +} + +export function _createTransforms(transformArray: Float32Array, instanceCount: number, transformData?: TransformData): TransformData { + if (transformData) { + ValueCell.update(transformData.aTransform, transformArray) + ValueCell.update(transformData.uInstanceCount, instanceCount) + ValueCell.update(transformData.instanceCount, instanceCount) + const aInstance = transformData.aInstance.ref.value.length >= instanceCount ? transformData.aInstance.ref.value : new Float32Array(instanceCount) + ValueCell.update(transformData.aInstance, fillSerial(aInstance, instanceCount)) + return transformData + } else { + return { + aTransform: ValueCell.create(transformArray), + uInstanceCount: ValueCell.create(instanceCount), + instanceCount: ValueCell.create(instanceCount), + aInstance: ValueCell.create(fillSerial(new Float32Array(instanceCount))) + } + } +} + +const identityTransform = new Float32Array(16) +Mat4.toArray(Mat4.identity(), identityTransform, 0) +export function createIdentityTransform(transformData?: TransformData): TransformData { + return _createTransforms(identityTransform, 1, transformData) +} + +export function createTransforms({ units }: Unit.SymmetryGroup, transformData?: TransformData) { + const unitCount = units.length + const n = unitCount * 16 + const array = transformData && transformData.aTransform.ref.value.length >= n ? transformData.aTransform.ref.value : new Float32Array(n) + for (let i = 0; i < unitCount; i++) { + Mat4.toArray(units[i].conformation.operator.matrix, array, i * 16) + } + return _createTransforms(array, unitCount, transformData) +} + diff --git a/src/mol-gl/renderable/util.ts b/src/mol-gl/renderable/util.ts index 312ab7d4dc299e1dbb0b74c3f9fe4d246c4e4827..9f64d61e805ef12a33636b639909f572ef2c7c55 100644 --- a/src/mol-gl/renderable/util.ts +++ b/src/mol-gl/renderable/util.ts @@ -9,10 +9,10 @@ import { Mat4, Vec3 } from 'mol-math/linear-algebra' import { ValueCell } from 'mol-util'; export function calculateTextureInfo (n: number, itemSize: number) { - const sqN = Math.sqrt(n * itemSize) + const sqN = Math.sqrt(n) let width = Math.ceil(sqN) width = width + (itemSize - (width % itemSize)) % itemSize - const height = width > 0 ? Math.ceil(n * itemSize / width) : 0 + const height = width > 0 ? Math.ceil(n / width) : 0 return { width, height, length: width * height * itemSize } } @@ -43,7 +43,7 @@ function getPositionDataFromValues(values: PositionValues) { } } -export function calculateBoundingSphereFromValues(values: PositionValues){ +export function calculateBoundingSphereFromValues(values: PositionValues) { const { position, positionCount, transform, transformCount } = getPositionDataFromValues(values) return calculateBoundingSphere(position, positionCount, transform, transformCount) } diff --git a/src/mol-gl/renderer.ts b/src/mol-gl/renderer.ts index 22e64a84405ff4e48940befa7e46dcd04113dd8b..45e6d11f24ba1fcb719467e72ab6cf20b6706bb7 100644 --- a/src/mol-gl/renderer.ts +++ b/src/mol-gl/renderer.ts @@ -92,16 +92,28 @@ namespace Renderer { program.setUniforms(globalUniforms) currentProgramId = program.id } - if (r.values.dDoubleSided.ref.value) { - gl.disable(gl.CULL_FACE) + + if (r.values.dDoubleSided) { + if (r.values.dDoubleSided.ref.value) { + gl.disable(gl.CULL_FACE) + } else { + gl.enable(gl.CULL_FACE) + } } else { - gl.enable(gl.CULL_FACE) + // webgl default + gl.disable(gl.CULL_FACE) } - if (r.values.dFlipSided.ref.value) { - gl.frontFace(gl.CW) - gl.cullFace(gl.FRONT) + if (r.values.dFlipSided) { + if (r.values.dFlipSided.ref.value) { + gl.frontFace(gl.CW) + gl.cullFace(gl.FRONT) + } else { + gl.frontFace(gl.CCW) + gl.cullFace(gl.BACK) + } } else { + // webgl default gl.frontFace(gl.CCW) gl.cullFace(gl.BACK) } diff --git a/src/mol-gl/scene.ts b/src/mol-gl/scene.ts index 57764dd718ca6ca31a704306ea8fd8ad5dbda4a0..71e1adbc6b843f61a825ba020cf3b4420b6cf01f 100644 --- a/src/mol-gl/scene.ts +++ b/src/mol-gl/scene.ts @@ -60,10 +60,10 @@ namespace Scene { update: () => { update() - renderableMap.forEach((o, r) => o.update()) + renderableMap.forEach(o => o.update()) boundingSphere = undefined }, - + add: (o: RenderObject) => { if (!renderableMap.has(o)) { renderableMap.set(o, createRenderable(ctx, o)) diff --git a/src/mol-gl/shader-code.ts b/src/mol-gl/shader-code.ts index 2fa41cdaea2b58a1bab762bca1a56b19c0da0dcf..93f933e6bb7e8ec21921d1a37775da287f0e0696 100644 --- a/src/mol-gl/shader-code.ts +++ b/src/mol-gl/shader-code.ts @@ -6,25 +6,33 @@ */ import { ValueCell } from 'mol-util'; +import { idFactory } from 'mol-util/id-factory'; export type DefineKind = 'boolean' | 'string' export type DefineType = boolean | string export type DefineValues = { [k: string]: ValueCell<DefineType> } +const shaderCodeId = idFactory() + export interface ShaderCode { + id: number vert: string frag: string } -export const PointShaderCode: ShaderCode = { - vert: require('mol-gl/shader/point.vert'), - frag: require('mol-gl/shader/point.frag') +export function ShaderCode(vert: string, frag: string): ShaderCode { + return { id: shaderCodeId(), vert, frag } } -export const MeshShaderCode: ShaderCode = { - vert: require('mol-gl/shader/mesh.vert'), - frag: require('mol-gl/shader/mesh.frag') -} +export const PointShaderCode = ShaderCode( + require('mol-gl/shader/point.vert'), + require('mol-gl/shader/point.frag') +) + +export const MeshShaderCode = ShaderCode( + require('mol-gl/shader/mesh.vert'), + require('mol-gl/shader/mesh.frag') +) export type ShaderDefines = { [k: string]: ValueCell<DefineType> @@ -47,9 +55,10 @@ function getDefinesCode (defines: ShaderDefines) { return lines.join('\n') + '\n' } -export function addShaderDefines(defines: ShaderDefines, shaders: ShaderCode) { +export function addShaderDefines(defines: ShaderDefines, shaders: ShaderCode): ShaderCode { const header = getDefinesCode(defines) return { + id: shaderCodeId(), vert: `${header}${shaders.vert}`, frag: `${header}${shaders.frag}` } diff --git a/src/mol-gl/shader/chunks/assign-color-varying.glsl b/src/mol-gl/shader/chunks/assign-color-varying.glsl index f4f5dad7358e2e52f052023500745156e891bd75..b946c1025af3ac570541316a7989167747ea8a1f 100644 --- a/src/mol-gl/shader/chunks/assign-color-varying.glsl +++ b/src/mol-gl/shader/chunks/assign-color-varying.glsl @@ -5,7 +5,7 @@ #elif defined(dColorType_group) vColor.rgb = readFromTexture(tColor, aGroup, uColorTexDim).rgb; #elif defined(dColorType_groupInstance) - vColor.rgb = readFromTexture(tColor, aGroup * float(uGroupCount) + aGroup, uColorTexDim).rgb; + vColor.rgb = readFromTexture(tColor, aInstance * float(uGroupCount) + aGroup, uColorTexDim).rgb; #elif defined(dColorType_objectPicking) vColor = encodeIdRGBA(float(uObjectId)); #elif defined(dColorType_instancePicking) diff --git a/src/mol-gl/shader/chunks/assign-material-color.glsl b/src/mol-gl/shader/chunks/assign-material-color.glsl index 5969c88c34b534a0556d2da9f30e6b22f41a557e..11b1779411269ab223e03cbbc42a90865a286d69 100644 --- a/src/mol-gl/shader/chunks/assign-material-color.glsl +++ b/src/mol-gl/shader/chunks/assign-material-color.glsl @@ -1,5 +1,5 @@ #if defined(dColorType_uniform) - vec4 material = vec4(uColor, 1.0); + vec4 material = vec4(uColor, uAlpha); #elif defined(dColorType_attribute) || defined(dColorType_instance) || defined(dColorType_group) || defined(dColorType_groupInstance) || defined(dColorType_objectPicking) || defined(dColorType_instancePicking) || defined(dColorType_groupPicking) vec4 material = vColor; #endif \ No newline at end of file diff --git a/src/mol-gl/shader/chunks/common-frag-params.glsl b/src/mol-gl/shader/chunks/common-frag-params.glsl index a1b1b64d8eb9b2a560e4de1b101bb5cc331748ab..c757d1b2f5afa54efad4236d533b9f4535bc64da 100644 --- a/src/mol-gl/shader/chunks/common-frag-params.glsl +++ b/src/mol-gl/shader/chunks/common-frag-params.glsl @@ -6,6 +6,10 @@ uniform vec3 uHighlightColor; uniform vec3 uSelectColor; varying float vMarker; +varying vec3 vViewPosition; + uniform float uFogNear; uniform float uFogFar; -uniform vec3 uFogColor; \ No newline at end of file +uniform vec3 uFogColor; + +uniform float uAlpha; \ No newline at end of file diff --git a/src/mol-gl/shader/chunks/common-vert-params.glsl b/src/mol-gl/shader/chunks/common-vert-params.glsl index 30bc2caea18865056975654eeccac9b43348ba66..5028d0abb23450b2c7126a1ed256a6ebfeff95d6 100644 --- a/src/mol-gl/shader/chunks/common-vert-params.glsl +++ b/src/mol-gl/shader/chunks/common-vert-params.glsl @@ -7,4 +7,7 @@ uniform int uGroupCount; uniform vec2 uMarkerTexDim; uniform sampler2D tMarker; varying float vMarker; + +varying vec3 vViewPosition; + #pragma glslify: readFromTexture = require(../utils/read-from-texture.glsl) \ No newline at end of file diff --git a/src/mol-gl/shader/mesh.frag b/src/mol-gl/shader/mesh.frag index ce282933ea1281f35e1699e3d0acc373ec3a814f..5b6a6e7218aa4d896bed9a19df9692d36bf9f366 100644 --- a/src/mol-gl/shader/mesh.frag +++ b/src/mol-gl/shader/mesh.frag @@ -18,12 +18,10 @@ precision highp int; uniform vec3 uLightColor; uniform vec3 uLightAmbient; uniform mat4 uView; -uniform float uAlpha; #ifndef dFlatShaded varying vec3 vNormal; #endif -varying vec3 vViewPosition; #pragma glslify: attenuation = require(./utils/attenuation.glsl) #pragma glslify: calculateSpecular = require(./utils/phong-specular.glsl) @@ -74,7 +72,7 @@ void main() { // gl_FragColor.a = 1.0; // gl_FragColor.rgb = vec3(1.0, 0.0, 0.0); gl_FragColor.rgb = finalColor; - gl_FragColor.a = uAlpha; + gl_FragColor.a = material.a; #pragma glslify: import('./chunks/apply-marker-color.glsl') #pragma glslify: import('./chunks/apply-fog.glsl') diff --git a/src/mol-gl/shader/mesh.vert b/src/mol-gl/shader/mesh.vert index af38be41e65a3be4d05412a1cadd618ce5d38438..b3f1a943085fda68654f05d072db68d6bc803577 100644 --- a/src/mol-gl/shader/mesh.vert +++ b/src/mol-gl/shader/mesh.vert @@ -20,8 +20,6 @@ attribute float aGroup; varying vec3 vNormal; #endif -varying vec3 vViewPosition; - #pragma glslify: inverse = require(./utils/inverse.glsl) #pragma glslify: transpose = require(./utils/transpose.glsl) diff --git a/src/mol-gl/shader/point.frag b/src/mol-gl/shader/point.frag index ae18b4a35f8ac2bf100ba5b52873c1bb5ab4d1e5..861bfef3fa6b59cf498fb9f57747fde9f2a1de94 100644 --- a/src/mol-gl/shader/point.frag +++ b/src/mol-gl/shader/point.frag @@ -10,12 +10,10 @@ precision highp int; #pragma glslify: import('./chunks/common-frag-params.glsl') #pragma glslify: import('./chunks/color-frag-params.glsl') -uniform float uAlpha; - void main(){ #pragma glslify: import('./chunks/assign-material-color.glsl') - - gl_FragColor = vec4(material, uAlpha); + + gl_FragColor = material; #pragma glslify: import('./chunks/apply-marker-color.glsl') #pragma glslify: import('./chunks/apply-fog.glsl') diff --git a/src/mol-gl/shader/point.vert b/src/mol-gl/shader/point.vert index 950ba3db17bef0d249d56e2ebac4e63a0222d3fb..5340044c2adb4d2545c15e6d67ff506da6f58543 100644 --- a/src/mol-gl/shader/point.vert +++ b/src/mol-gl/shader/point.vert @@ -17,6 +17,10 @@ uniform float uViewportHeight; uniform float uSize; #elif defined(dSizeType_attribute) attribute float aSize; +#elif defined(dSizeType_instance) || defined(dSizeType_group) || defined(dSizeType_groupInstance) + varying vec4 vSize; + uniform vec2 uSizeTexDim; + uniform sampler2D tSize; #endif attribute vec3 aPosition; diff --git a/src/mol-gl/webgl/program.ts b/src/mol-gl/webgl/program.ts index d64d1ca58868a7552bf222f8cd93e9158ff3a3c0..34fa9501e2ca78895a1789634ccb70b5702dcd46 100644 --- a/src/mol-gl/webgl/program.ts +++ b/src/mol-gl/webgl/program.ts @@ -4,7 +4,7 @@ * @author Alexander Rose <alexander.rose@weirdbyte.de> */ -import { ShaderCode } from '../shader-code' +import { ShaderCode, DefineValues, addShaderDefines } from '../shader-code' import { Context } from './context'; import { getUniformUpdaters, getTextureUniformUpdaters, UniformValues } from './uniform'; import { AttributeBuffers } from './buffer'; @@ -12,6 +12,7 @@ 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'; const getNextProgramId = idFactory() @@ -46,19 +47,21 @@ function getAttributeLocations(ctx: Context, program: WebGLProgram, schema: Rend } export interface ProgramProps { + defineValues: DefineValues, shaderCode: ShaderCode, schema: RenderableSchema } export function createProgram(ctx: Context, props: ProgramProps): Program { const { gl, shaderCache } = ctx - const { shaderCode, schema } = props + const { defineValues, shaderCode: _shaderCode, schema } = props const program = gl.createProgram() if (program === null) { throw new Error('Could not create WebGL program') } + const shaderCode = addShaderDefines(defineValues, _shaderCode) const vertShaderRef = shaderCache.get(ctx, { type: 'vert', source: shaderCode.vert }) const fragShaderRef = shaderCache.get(ctx, { type: 'frag', source: shaderCode.frag }) @@ -113,7 +116,14 @@ export type ProgramCache = ReferenceCache<Program, ProgramProps, Context> export function createProgramCache(): ProgramCache { return createReferenceCache( - (props: ProgramProps) => JSON.stringify(props), + (props: ProgramProps) => { + const array = [ props.shaderCode.id ] + Object.keys(props.defineValues).forEach(k => { + const v = props.defineValues[k].ref.value + array.push(hashString(k), typeof v === 'boolean' ? v ? 1 : 0 : hashString(v)) + }) + return hashFnv32a(array).toString() + }, (ctx: Context, props: ProgramProps) => createProgram(ctx, props), (program: Program) => { program.destroy() } ) diff --git a/src/mol-gl/webgl/render-item.ts b/src/mol-gl/webgl/render-item.ts index 284591374c09de8e144518f3bff698b72036fc20..d7277fec66e11fe6630b8165714f5c400f9c6242 100644 --- a/src/mol-gl/webgl/render-item.ts +++ b/src/mol-gl/webgl/render-item.ts @@ -7,7 +7,7 @@ import { createAttributeBuffers, createElementsBuffer, ElementsBuffer, createAttributeBuffer, ArrayKind } from './buffer'; import { createTextures } from './texture'; import { Context } from './context'; -import { ShaderCode, addShaderDefines } from '../shader-code'; +import { ShaderCode } from '../shader-code'; import { Program } from './program'; import { RenderableSchema, RenderableValues, AttributeSpec, getValueVersions, splitValues, Values } from '../renderable/schema'; import { idFactory } from 'mol-util/id-factory'; @@ -74,7 +74,8 @@ export function createRenderItem(ctx: Context, drawMode: DrawMode, shaderCode: S Object.keys(RenderVariantDefines).forEach(k => { const variantDefineValues: Values<RenderableSchema> = (RenderVariantDefines as any)[k] programs[k] = programCache.get(ctx, { - shaderCode: addShaderDefines({ ...defineValues, ...variantDefineValues }, shaderCode), + defineValues: { ...defineValues, ...variantDefineValues }, + shaderCode, schema }) }) @@ -117,7 +118,7 @@ export function createRenderItem(ctx: Context, drawMode: DrawMode, shaderCode: S program.setUniforms(uniformValues) if (oesVertexArrayObject && vertexArray) { oesVertexArrayObject.bindVertexArrayOES(vertexArray) - // TODO need to bind elements buffer explicitely since it is not always recorded in the VAO + // need to bind elements buffer explicitely since it is not always recorded in the VAO if (elementsBuffer) elementsBuffer.bind() } else { if (elementsBuffer) elementsBuffer.bind() @@ -147,7 +148,8 @@ export function createRenderItem(ctx: Context, drawMode: DrawMode, shaderCode: S const variantDefineValues: Values<RenderableSchema> = (RenderVariantDefines as any)[k] programs[k].free() programs[k] = programCache.get(ctx, { - shaderCode: addShaderDefines({ ...defineValues, ...variantDefineValues }, shaderCode), + defineValues: { ...defineValues, ...variantDefineValues }, + shaderCode, schema }) }) diff --git a/src/mol-math/geometry/lookup3d/grid.ts b/src/mol-math/geometry/lookup3d/grid.ts index 7abb47781969df8deb176c5e1f9918f3cf2b532d..db2409a1ba38052418d98605c79918063bf61db2 100644 --- a/src/mol-math/geometry/lookup3d/grid.ts +++ b/src/mol-math/geometry/lookup3d/grid.ts @@ -159,7 +159,7 @@ function _build(state: BuildState): Grid3D { function build(data: PositionData) { const boundingBox = Box3D.computeBounding(data); // need to expand the grid bounds to avoid rounding errors - const expandedBox = Box3D.expand(boundingBox, Vec3.create(0.5, 0.5, 0.5)); + const expandedBox = Box3D.expand(Box3D.empty(), boundingBox, Vec3.create(0.5, 0.5, 0.5)); const boundingSphere = Sphere3D.computeBounding(data); const { indices } = data; diff --git a/src/mol-math/geometry/primitives/box3d.ts b/src/mol-math/geometry/primitives/box3d.ts index 6d0b5bd5f7da032fed3942b44558ef6d4f279ecc..8b48c524a31614bd354ef8468068071240007e8f 100644 --- a/src/mol-math/geometry/primitives/box3d.ts +++ b/src/mol-math/geometry/primitives/box3d.ts @@ -2,9 +2,10 @@ * Copyright (c) 2018 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 { Vec3 } from '../../linear-algebra' +import { Vec3, Mat4 } from '../../linear-algebra' import { PositionData } from '../common' import { OrderedSet } from 'mol-data/int'; @@ -12,10 +13,11 @@ interface Box3D { min: Vec3, max: Vec3 } 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 computeBounding(data: PositionData): Box3D { - const min = [Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE]; - const max = [-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE]; + const min = Vec3.create(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE); + const max = Vec3.create(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE); const { x, y, z, indices } = data; for (let t = 0, _t = OrderedSet.size(indices); t < _t; t++) { @@ -27,18 +29,49 @@ namespace Box3D { max[1] = Math.max(y[i], max[1]); max[2] = Math.max(z[i], max[2]); } - return { min: Vec3.create(min[0], min[1], min[2]), max: Vec3.create(max[0], max[1], max[2]) } + return { min, max } } - export function size(box: Box3D) { - return Vec3.sub(Vec3.zero(), box.max, box.min); + /** Get size of the box */ + export function size(size: Vec3, box: Box3D): Vec3 { + return Vec3.sub(size, box.max, box.min); } - export function expand(box: Box3D, delta: Vec3): Box3D { - return { - min: Vec3.sub(Vec3.zero(), box.min, delta), - max: Vec3.add(Vec3.zero(), box.max, delta) - } + export function setEmpty(box: Box3D): Box3D { + Vec3.set(box.min, Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE) + Vec3.set(box.max, -Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE) + return box + } + + /** Add point to box */ + export function add(box: Box3D, point: Vec3): Box3D { + Vec3.min(box.min, box.min, point) + Vec3.max(box.max, box.max, point) + return box + } + + /** Expand box by delta */ + export function expand(out: Box3D, box: Box3D, delta: Vec3): Box3D { + Vec3.sub(out.min, box.min, delta) + Vec3.add(out.max, box.max, delta) + return out + } + + const tmpTransformV = Vec3.zero() + /** Transform box with a Mat4 */ + export function transform(out: Box3D, box: Box3D, m: Mat4): Box3D { + const [ minX, minY, minZ ] = box.min + const [ maxX, maxY, maxZ ] = box.max + setEmpty(out) + add(out, Vec3.transformMat4(tmpTransformV, Vec3.set(tmpTransformV, minX, minY, minZ), m)) + add(out, Vec3.transformMat4(tmpTransformV, Vec3.set(tmpTransformV, minX, minY, maxZ), m)) + add(out, Vec3.transformMat4(tmpTransformV, Vec3.set(tmpTransformV, minX, maxY, minZ), m)) + add(out, Vec3.transformMat4(tmpTransformV, Vec3.set(tmpTransformV, minX, maxY, maxZ), m)) + add(out, Vec3.transformMat4(tmpTransformV, Vec3.set(tmpTransformV, maxX, minY, minZ), m)) + add(out, Vec3.transformMat4(tmpTransformV, Vec3.set(tmpTransformV, maxX, minY, maxZ), m)) + add(out, Vec3.transformMat4(tmpTransformV, Vec3.set(tmpTransformV, maxX, maxY, minZ), m)) + add(out, Vec3.transformMat4(tmpTransformV, Vec3.set(tmpTransformV, maxX, maxY, maxZ), m)) + return out } } diff --git a/src/mol-math/geometry/primitives/sphere3d.ts b/src/mol-math/geometry/primitives/sphere3d.ts index 2132583d80c6937669c3d3186603d804bd8793c3..c38e9275a421d9935b913c028091c5e4370eb11d 100644 --- a/src/mol-math/geometry/primitives/sphere3d.ts +++ b/src/mol-math/geometry/primitives/sphere3d.ts @@ -2,18 +2,18 @@ * Copyright (c) 2018 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 { Vec3 } from '../../linear-algebra' +import { Vec3, Mat4 } from '../../linear-algebra' import { PositionData } from '../common' import { OrderedSet } from 'mol-data/int'; interface Sphere3D { center: Vec3, radius: number } namespace Sphere3D { - export function create(center: Vec3, radius: number): Sphere3D { - return { center, radius }; - } + export function create(center: Vec3, radius: number): Sphere3D { return { center, radius }; } + export function zero(): Sphere3D { return { center: Vec3.zero(), radius: 0 }; } export function computeBounding(data: PositionData): Sphere3D { const { x, y, z, indices } = data; @@ -43,6 +43,13 @@ namespace Sphere3D { return { center: Vec3.create(cx, cy, cz), radius: Math.sqrt(radiusSq) }; } + + /** Transform sphere with a Mat4 */ + export function transform(out: Sphere3D, sphere: Sphere3D, m: Mat4): Sphere3D { + Vec3.transformMat4(out.center, sphere.center, m) + out.radius = sphere.radius * Mat4.getMaxScaleOnAxis(m) + return out + } } export { Sphere3D } \ No newline at end of file diff --git a/src/mol-math/geometry/symmetry-operator.ts b/src/mol-math/geometry/symmetry-operator.ts index aa575c77289110a74d22d197f439c239c0c37ec0..6c233e11d5b49f3d544893a42d8fad372486a181 100644 --- a/src/mol-math/geometry/symmetry-operator.ts +++ b/src/mol-math/geometry/symmetry-operator.ts @@ -92,7 +92,7 @@ function isW1(m: Mat4) { return m[3] === 0 && m[7] === 0 && m[11] === 0 && m[15] === 1; } -function projectX({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs}: SymmetryOperator.Coordinates) { +function projectX({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs }: SymmetryOperator.Coordinates) { const xx = m[0], yy = m[4], zz = m[8], tx = m[12]; if (isW1(m)) { @@ -106,7 +106,7 @@ function projectX({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs}: Symme } } -function projectY({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs}: SymmetryOperator.Coordinates) { +function projectY({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs }: SymmetryOperator.Coordinates) { const xx = m[1], yy = m[5], zz = m[9], ty = m[13]; if (isW1(m)) { @@ -120,7 +120,7 @@ function projectY({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs}: Symme } } -function projectZ({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs}: SymmetryOperator.Coordinates) { +function projectZ({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs }: SymmetryOperator.Coordinates) { const xx = m[2], yy = m[6], zz = m[10], tz = m[14]; if (isW1(m)) { diff --git a/src/mol-math/linear-algebra/3d/mat4.ts b/src/mol-math/linear-algebra/3d/mat4.ts index ddc230eb5c8cf3c45fa68129d8f2a15be19b1248..442f8e3927a3f2ad3358fabba82a26cbe01096fb 100644 --- a/src/mol-math/linear-algebra/3d/mat4.ts +++ b/src/mol-math/linear-algebra/3d/mat4.ts @@ -873,6 +873,13 @@ namespace Mat4 { return out; } + export function getMaxScaleOnAxis(m: Mat4) { + const scaleXSq = m[0] * m[0] + m[1] * m[1] + m[2] * m[2] + const scaleYSq = m[4] * m[4] + m[5] * m[5] + m[6] * m[6] + const scaleZSq = m[8] * m[8] + m[9] * m[9] + m[10] * m[10] + return Math.sqrt(Math.max(scaleXSq, scaleYSq, scaleZSq)) + } + /** Rotation matrix for 90deg around x-axis */ export const rotX90: ReadonlyMat4 = Mat4.fromRotation(Mat4.identity(), degToRad(90), Vec3.create(1, 0, 0)) /** Rotation matrix for 180deg around x-axis */ diff --git a/src/mol-model-props/rcsb/symmetry.ts b/src/mol-model-props/rcsb/symmetry.ts index b3114645c81af51635607625ccdd9b592f86f210..c1727ae3a9fbddd7c45be8fabd32f4e802aa23f6 100644 --- a/src/mol-model-props/rcsb/symmetry.ts +++ b/src/mol-model-props/rcsb/symmetry.ts @@ -22,8 +22,8 @@ const { str, int, float, Aliased, Vector, List } = Column.Schema; function getInstance(name: keyof AssemblySymmetry.Schema): (ctx: CifExportContext) => CifWriter.Category.Instance<any, any> { return function(ctx: CifExportContext) { - const db = AssemblySymmetry.get(ctx.model); - return db ? Category.ofTable(db[name]) : CifWriter.Category.Empty; + const assemblySymmetry = AssemblySymmetry.get(ctx.model); + return assemblySymmetry ? Category.ofTable(assemblySymmetry.db[name]) : CifWriter.Category.Empty; } } @@ -38,7 +38,7 @@ function createDatabase(assemblies: ReadonlyArray<AssemblySymmetryGraphQL.Assemb const clusterRows: Table.Row<typeof Schema.rcsb_assembly_symmetry_cluster>[] = [] const axisRows: Table.Row<typeof Schema.rcsb_assembly_symmetry_axis>[] = [] - let id = 0 + let id = 1 // start feature ids at 1 for (let i = 0, il = assemblies.length; i < il; ++i) { const { assembly_id: _assembly_id, rcsb_assembly_symmetry } = assemblies[i] if (!rcsb_assembly_symmetry) continue @@ -65,7 +65,7 @@ function createDatabase(assemblies: ReadonlyArray<AssemblySymmetryGraphQL.Assemb clusterRows.push({ feature_id: id, avg_rmsd: c.avg_rmsd || 0, // TODO upstream, should not be nullable, or??? - members: c.members as string[] + members: c.members as string[] // TODO upstream, array members should not be nullable }) } } @@ -109,6 +109,26 @@ const _Descriptor: ModelPropertyDescriptor = { const client = new GraphQLClient('http://rest-experimental.rcsb.org/graphql') +export interface AssemblySymmetry { + db: AssemblySymmetry.Database + getFeatures(assemblyId: string): Table<AssemblySymmetry.Schema['rcsb_assembly_symmetry_feature']> + getClusters(featureId: number): Table<AssemblySymmetry.Schema['rcsb_assembly_symmetry_cluster']> + getAxes(featureId: number): Table<AssemblySymmetry.Schema['rcsb_assembly_symmetry_axis']> +} + +export function AssemblySymmetry(db: AssemblySymmetry.Database): AssemblySymmetry { + const f = db.rcsb_assembly_symmetry_feature + const c = db.rcsb_assembly_symmetry_cluster + const a = db.rcsb_assembly_symmetry_axis + + return { + db, + getFeatures: (assemblyId: string) => Table.pick(f, f._schema, i => f.assembly_id.value(i) === assemblyId), + getClusters: (featureId: number) => Table.pick(c, c._schema, i => c.feature_id.value(i) === featureId), + getAxes: (featureId: number) => Table.pick(a, a._schema, i => a.feature_id.value(i) === featureId) + } +} + export namespace AssemblySymmetry { export const Schema = { rcsb_assembly_symmetry_feature: { @@ -195,11 +215,11 @@ export namespace AssemblySymmetry { } model.customProperties.add(Descriptor); - model._staticPropertyData.__AssemblySymmetry__ = db; + model._staticPropertyData.__AssemblySymmetry__ = AssemblySymmetry(db); return true; } - export function get(model: Model): Database | undefined { + export function get(model: Model): AssemblySymmetry | undefined { return model._staticPropertyData.__AssemblySymmetry__; } } \ No newline at end of file diff --git a/src/mol-model/loci.ts b/src/mol-model/loci.ts index d84498e133ae0e709429c81c6b57a5a715f981b2..8e0b1c7157a542549cfd05a46eac617072a00c8a 100644 --- a/src/mol-model/loci.ts +++ b/src/mol-model/loci.ts @@ -22,4 +22,19 @@ export function isEmptyLoci(x: any): x is EmptyLoci { return !!x && x.kind === 'empty-loci'; } +export function areLociEqual(lociA: Loci, lociB: Loci) { + if (isEveryLoci(lociA) && isEveryLoci(lociB)) return true + if (isEmptyLoci(lociA) && isEmptyLoci(lociB)) return true + if (StructureElement.isLoci(lociA) && StructureElement.isLoci(lociB)) { + return StructureElement.areLociEqual(lociA, lociB) + } + if (Link.isLoci(lociA) && Link.isLoci(lociB)) { + return Link.areLociEqual(lociA, lociB) + } + if (Shape.isLoci(lociA) && Shape.isLoci(lociB)) { + return Shape.areLociEqual(lociA, lociB) + } + return false +} + export type Loci = StructureElement.Loci | Link.Loci | EveryLoci | EmptyLoci | Shape.Loci \ No newline at end of file diff --git a/src/mol-model/shape/shape.ts b/src/mol-model/shape/shape.ts index 58f930b6ed8e685b4038c2be3f06ff4a9d417691..ffc602342db05a6419dd471d38b755ff41c51fa3 100644 --- a/src/mol-model/shape/shape.ts +++ b/src/mol-model/shape/shape.ts @@ -68,4 +68,15 @@ export namespace Shape { export function isLoci(x: any): x is Loci { return !!x && x.kind === 'group-loci'; } + + export function areLociEqual(a: Loci, b: Loci) { + if (a.groups.length !== b.groups.length) return false + for (let i = 0, il = a.groups.length; i < il; ++i) { + const groupA = a.groups[i] + const groupB = b.groups[i] + if (groupA.shape.id !== groupB.shape.id) return false + if (!OrderedSet.areEqual(groupA.ids, groupB.ids)) return false + } + return true + } } \ No newline at end of file diff --git a/src/mol-model/structure/model/formats/mmcif.ts b/src/mol-model/structure/model/formats/mmcif.ts index 43e072d461944f2a18902fd29d3af69fd09605db..cb9ddfa8c7ec9ab019517bc8883c6f8d542a985e 100644 --- a/src/mol-model/structure/model/formats/mmcif.ts +++ b/src/mol-model/structure/model/formats/mmcif.ts @@ -141,7 +141,12 @@ function getFormatData(format: mmCIF_Format): FormatData { function createStandardModel(format: mmCIF_Format, atom_site: AtomSite, entities: Entities, formatData: FormatData, previous?: Model): Model { const atomic = getAtomicHierarchyAndConformation(format, atom_site, entities, formatData, previous); if (previous && atomic.sameAsPrevious) { - return { ...previous, atomicConformation: atomic.conformation }; + return { + ...previous, + id: UUID.create(), + modelNum: atom_site.pdbx_PDB_model_num.value(0), + atomicConformation: atomic.conformation + }; } const coarse = EmptyIHMCoarse; diff --git a/src/mol-model/structure/model/formats/mmcif/assembly.ts b/src/mol-model/structure/model/formats/mmcif/assembly.ts index e89690e1723ca31eca3c7295114331e97c9c786a..ddc0e6f478fe75f912992338a7e4032dde1c482f 100644 --- a/src/mol-model/structure/model/formats/mmcif/assembly.ts +++ b/src/mol-model/structure/model/formats/mmcif/assembly.ts @@ -110,14 +110,14 @@ function expandOperators1(operatorNames: string[][], list: string[][], i: number function getAssemblyOperators(matrices: Matrices, operatorNames: string[][], startIndex: number) { const operators: SymmetryOperator[] = []; - let index = startIndex; for (let op of operatorNames) { let m = Mat4.identity(); for (let i = 0; i < op.length; i++) { Mat4.mul(m, m, matrices.get(op[i])!); } - index++; - operators[operators.length] = SymmetryOperator.create(`A-${index}`, m); + // TODO currently using the original operator name for the symmetry operator to be able + // to link it to the original operator but it might be clearer to introduce an extra field??? + operators[operators.length] = SymmetryOperator.create(`A-${op.join(',')}`, m); } return operators; diff --git a/src/mol-model/structure/structure/carbohydrates/constants.ts b/src/mol-model/structure/structure/carbohydrates/constants.ts index cad746542b66e98c5890255dfcfe9bd7ecaab7d9..ed3949f122fb46a6bd1c7de14cce975daf741952 100644 --- a/src/mol-model/structure/structure/carbohydrates/constants.ts +++ b/src/mol-model/structure/structure/carbohydrates/constants.ts @@ -174,6 +174,19 @@ const Monosaccharides: SaccharideComponent[] = [ { abbr: 'Psi', name: 'Psicose', color: SaccharideColors.Pink, type: SaccharideType.Assigned }, ] +export const MonosaccharidesColorTable: [string, Color][] = [ + ['Glc-family', SaccharideColors.Blue], + ['Man-family', SaccharideColors.Green], + ['Gal-family', SaccharideColors.Yellow], + ['Gul-family', SaccharideColors.Orange], + ['Alt-family', SaccharideColors.Pink], + ['All-family', SaccharideColors.Purple], + ['Tal-family', SaccharideColors.LightBlue], + ['Ido-family', SaccharideColors.Blue], + ['Fuc-family', SaccharideColors.Red], + ['Generic/Unknown/Secondary', SaccharideColors.Secondary], +] + const CommonSaccharideNames: { [k: string]: string[] } = { // Hexose Glc: [ diff --git a/src/mol-model/structure/structure/element.ts b/src/mol-model/structure/structure/element.ts index 0c83fbcd981cba97b08393086738f339d080aa4b..a8ad08e1ae17fcaded071c74d54ab69eaa3c5443 100644 --- a/src/mol-model/structure/structure/element.ts +++ b/src/mol-model/structure/structure/element.ts @@ -63,6 +63,17 @@ namespace StructureElement { return !!x && x.kind === 'element-loci'; } + export function areLociEqual(a: Loci, b: Loci) { + if (a.elements.length !== b.elements.length) return false + for (let i = 0, il = a.elements.length; i < il; ++i) { + const elementA = a.elements[i] + const elementB = b.elements[i] + if (elementA.unit.id !== elementB.unit.id) return false + if (!OrderedSet.areEqual(elementA.indices, elementB.indices)) return false + } + return true + } + export function isLocation(x: any): x is StructureElement { return !!x && x.kind === 'element-location'; } diff --git a/src/mol-model/structure/structure/structure.ts b/src/mol-model/structure/structure/structure.ts index 6bbf69ff3a5729d38ba2b0ba493e86c8a86b6b17..29a2be47685bcf25bc74255f0335db6c3f306f08 100644 --- a/src/mol-model/structure/structure/structure.ts +++ b/src/mol-model/structure/structure/structure.ts @@ -8,7 +8,7 @@ import { IntMap, SortedArray, Iterator, Segmentation } from 'mol-data/int' import { UniqueArray } from 'mol-data/generic' import { SymmetryOperator } from 'mol-math/geometry/symmetry-operator' import { Model, ElementIndex } from '../model' -import { sort, arraySwap, hash1, sortArray } from 'mol-data/util'; +import { sort, arraySwap, hash1, sortArray, hashString } from 'mol-data/util'; import StructureElement from './element' import Unit from './unit' import { StructureLookup3D } from './util/lookup3d'; @@ -22,9 +22,12 @@ import { ResidueIndex } from '../model/indexing'; import { Carbohydrates } from './carbohydrates/data'; import { computeCarbohydrates } from './carbohydrates/compute'; import { Vec3 } from 'mol-math/linear-algebra'; +import { idFactory } from 'mol-util/id-factory'; class Structure { + /** Maps unit.id to unit */ readonly unitMap: IntMap<Unit>; + /** Array of all units in the structure, sorted by unit.id */ readonly units: ReadonlyArray<Unit>; private _props: { @@ -66,6 +69,7 @@ class Structure { return hash; } + /** Returns a new element location iterator */ elementLocations(): Iterator<StructureElement> { return new Structure.ElementLocationIterator(this); } @@ -197,9 +201,10 @@ namespace Structure { export class StructureBuilder { private units: Unit[] = []; + private invariantId = idFactory() addUnit(kind: Unit.Kind, model: Model, operator: SymmetryOperator, elements: StructureElement.Set): Unit { - const unit = Unit.create(this.units.length, kind, model, operator, elements); + const unit = Unit.create(this.units.length, this.invariantId(), kind, model, operator, elements); this.units.push(unit); return unit; } @@ -225,6 +230,10 @@ namespace Structure { return s.hashCode; } + export function conformationHash(s: Structure) { + return hashString(s.units.map(u => Unit.conformationId(u)).join('|')) + } + export function areEqual(a: Structure, b: Structure) { if (a.elementCount !== b.elementCount) return false; const len = a.units.length; diff --git a/src/mol-model/structure/structure/symmetry.ts b/src/mol-model/structure/structure/symmetry.ts index e3e9c4b6b4b3a8fa45a5a8dc8aef1f7ded41598b..1b35f8341f75d86dccf08e88b08cc8a475e3506f 100644 --- a/src/mol-model/structure/structure/symmetry.ts +++ b/src/mol-model/structure/structure/symmetry.ts @@ -1,7 +1,8 @@ /** - * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> + * @author Alexander Rose <alexander.rose@weirdbyte.de> */ import Structure from './structure' @@ -10,7 +11,7 @@ import { ModelSymmetry } from '../model' import { Task, RuntimeContext } from 'mol-task'; import { SortedArray } from 'mol-data/int'; import Unit from './unit'; -import { EquivalenceClasses, hash2 } from 'mol-data/util'; +import { EquivalenceClasses } from 'mol-data/util'; import { Vec3 } from 'mol-math/linear-algebra'; import { SymmetryOperator, Spacegroup, SpacegroupCell } from 'mol-math/geometry'; @@ -58,16 +59,12 @@ namespace StructureSymmetry { return Task.create('Build NCS', ctx => _buildNCS(ctx, structure)); } - function hashUnit(u: Unit) { - return hash2(u.invariantId, SortedArray.hashCode(u.elements)); - } - export function areUnitsEquivalent(a: Unit, b: Unit) { return a.invariantId === b.invariantId && a.model.id === b.model.id && SortedArray.areEqual(a.elements, b.elements); } export function UnitEquivalenceBuilder() { - return EquivalenceClasses<number, Unit>(hashUnit, areUnitsEquivalent); + return EquivalenceClasses<number, Unit>(Unit.hashUnit, areUnitsEquivalent); } export function computeTransformGroups(s: Structure): ReadonlyArray<Unit.SymmetryGroup> { @@ -76,12 +73,7 @@ namespace StructureSymmetry { const ret: Unit.SymmetryGroup[] = []; for (const eqUnits of groups.groups) { - const first = s.unitMap.get(eqUnits[0]); - ret.push({ - elements: first.elements, - units: eqUnits.map(id => s.unitMap.get(id)), - hashCode: hashUnit(first) - }); + ret.push(Unit.SymmetryGroup(eqUnits.map(id => s.unitMap.get(id)))) } return ret; diff --git a/src/mol-model/structure/structure/unit.ts b/src/mol-model/structure/structure/unit.ts index 3bb205123d1961beddc044f7add9ab6ea7b3bc6c..4092d9d3cbb5521fe300c6c8cf45e35e9f845c0b 100644 --- a/src/mol-model/structure/structure/unit.ts +++ b/src/mol-model/structure/structure/unit.ts @@ -2,18 +2,22 @@ * Copyright (c) 2017-2018 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 { SymmetryOperator } from 'mol-math/geometry/symmetry-operator' import { Model } from '../model' import { GridLookup3D, Lookup3D } from 'mol-math/geometry' -import { idFactory } from 'mol-util/id-factory'; import { IntraUnitLinks, computeIntraUnitBonds } from './unit/links' import { CoarseElements, CoarseSphereConformation, CoarseGaussianConformation } from '../model/properties/coarse'; import { ValueRef } from 'mol-util'; import { UnitRings } from './unit/rings'; import StructureElement from './element' -import { ChainIndex, ResidueIndex } from '../model/indexing'; +import { ChainIndex, ResidueIndex, ElementIndex } from '../model/indexing'; +import { IntMap, SortedArray } from 'mol-data/int'; +import { hash2 } from 'mol-data/util'; +import { getAtomicPolymerElements, getCoarsePolymerElements, getAtomicGapElements, getCoarseGapElements } from './util/polymer'; +import { getNucleotideElements } from './util/nucleotide'; // A building block of a structure that corresponds to an atomic or a coarse grained representation // 'conveniently grouped together'. @@ -27,27 +31,54 @@ namespace Unit { export function isSpheres(u: Unit): u is Spheres { return u.kind === Kind.Spheres; } export function isGaussians(u: Unit): u is Gaussians { return u.kind === Kind.Gaussians; } - export function create(id: number, kind: Kind, model: Model, operator: SymmetryOperator, elements: StructureElement.Set): Unit { + export function create(id: number, invariantId: number, kind: Kind, model: Model, operator: SymmetryOperator, elements: StructureElement.Set): Unit { switch (kind) { - case Kind.Atomic: return new Atomic(id, unitIdFactory(), model, elements, SymmetryOperator.createMapping(operator, model.atomicConformation, void 0), AtomicProperties()); - case Kind.Spheres: return createCoarse(id, unitIdFactory(), model, Kind.Spheres, elements, SymmetryOperator.createMapping(operator, model.coarseConformation.spheres, getSphereRadiusFunc(model))); - case Kind.Gaussians: return createCoarse(id, unitIdFactory(), model, Kind.Gaussians, elements, SymmetryOperator.createMapping(operator, model.coarseConformation.gaussians, getGaussianRadiusFunc(model))); + case Kind.Atomic: return new Atomic(id, invariantId, model, elements, SymmetryOperator.createMapping(operator, model.atomicConformation, void 0), AtomicProperties()); + case Kind.Spheres: return createCoarse(id, invariantId, model, Kind.Spheres, elements, SymmetryOperator.createMapping(operator, model.coarseConformation.spheres, getSphereRadiusFunc(model)), CoarseProperties()); + case Kind.Gaussians: return createCoarse(id, invariantId, model, Kind.Gaussians, elements, SymmetryOperator.createMapping(operator, model.coarseConformation.gaussians, getGaussianRadiusFunc(model)), CoarseProperties()); } } /** A group of units that differ only by symmetry operators. */ export type SymmetryGroup = { - readonly elements: StructureElement.Set, + readonly elements: StructureElement.Set readonly units: ReadonlyArray<Unit> + /** Maps unit.id to index of unit in units array */ + readonly unitIndexMap: IntMap<number> readonly hashCode: number } - /** Find index of unit with given id, returns -1 if not found */ - export function findUnitById(id: number, units: ReadonlyArray<Unit>) { - for (let i = 0, il = units.length; i < il; ++i) { - if (units[i].id === id) return i + function getUnitIndexMap(units: Unit[]) { + const unitIndexMap = IntMap.Mutable<number>(); + for (let i = 0, _i = units.length; i < _i; i++) { + unitIndexMap.set(units[i].id, i); + } + return unitIndexMap + } + + export function SymmetryGroup(units: Unit[]) { + const props: { + unitIndexMap?: IntMap<number> + } = {} + + return { + elements: units[0].elements, + units, + get unitIndexMap () { + if (props.unitIndexMap) return props.unitIndexMap + props.unitIndexMap = getUnitIndexMap(units) + return props.unitIndexMap + }, + hashCode: hashUnit(units[0]) } - return -1 + } + + export function conformationId (unit: Unit) { + return Unit.isAtomic(unit) ? unit.model.atomicConformation.id : unit.model.coarseConformation.id + } + + export function hashUnit(u: Unit) { + return hash2(u.invariantId, SortedArray.hashCode(u.elements)); } export interface Base { @@ -62,6 +93,8 @@ namespace Unit { applyOperator(id: number, operator: SymmetryOperator, dontCompose?: boolean /* = false */): Unit, readonly lookup3d: Lookup3D + readonly polymerElements: SortedArray<ElementIndex> + readonly gapElements: SortedArray<ElementIndex> } function getSphereRadiusFunc(model: Model) { @@ -74,8 +107,6 @@ namespace Unit { return (i: number) => 0; } - const unitIdFactory = idFactory(); - // A bulding block of a structure that corresponds // to a "natural group of atoms" (most often a "chain") // together with a tranformation (rotation and translation) @@ -127,6 +158,24 @@ namespace Unit { return this.props.rings.ref; } + get polymerElements() { + if (this.props.polymerElements.ref) return this.props.polymerElements.ref; + this.props.polymerElements.ref = getAtomicPolymerElements(this); + return this.props.polymerElements.ref; + } + + get gapElements() { + if (this.props.gapElements.ref) return this.props.gapElements.ref; + this.props.gapElements.ref = getAtomicGapElements(this); + return this.props.gapElements.ref; + } + + get nucleotideElements() { + if (this.props.nucleotideElements.ref) return this.props.nucleotideElements.ref; + this.props.nucleotideElements.ref = getNucleotideElements(this); + return this.props.nucleotideElements.ref; + } + getResidueIndex(elementIndex: StructureElement.UnitIndex) { return this.model.atomicHierarchy.residueAtomSegments.index[this.elements[elementIndex]]; } @@ -148,10 +197,20 @@ namespace Unit { lookup3d: ValueRef<Lookup3D | undefined>, links: ValueRef<IntraUnitLinks | undefined>, rings: ValueRef<UnitRings | undefined> + polymerElements: ValueRef<SortedArray<ElementIndex> | undefined> + gapElements: ValueRef<SortedArray<ElementIndex> | undefined> + nucleotideElements: ValueRef<SortedArray<ElementIndex> | undefined> } function AtomicProperties(): AtomicProperties { - return { lookup3d: ValueRef.create(void 0), links: ValueRef.create(void 0), rings: ValueRef.create(void 0) }; + return { + lookup3d: ValueRef.create(void 0), + links: ValueRef.create(void 0), + rings: ValueRef.create(void 0), + polymerElements: ValueRef.create(void 0), + gapElements: ValueRef.create(void 0), + nucleotideElements: ValueRef.create(void 0), + }; } class Coarse<K extends Kind.Gaussians | Kind.Spheres, C extends CoarseSphereConformation | CoarseGaussianConformation> implements Base { @@ -166,32 +225,45 @@ namespace Unit { readonly coarseElements: CoarseElements; readonly coarseConformation: C; + private props: CoarseProperties; + getChild(elements: StructureElement.Set): Unit { if (elements.length === this.elements.length) return this as any as Unit /** lets call this an ugly temporary hack */; - return createCoarse(this.id, this.invariantId, this.model, this.kind, elements, this.conformation); + return createCoarse(this.id, this.invariantId, this.model, this.kind, elements, this.conformation, CoarseProperties()); } applyOperator(id: number, operator: SymmetryOperator, dontCompose = false): Unit { const op = dontCompose ? operator : SymmetryOperator.compose(this.conformation.operator, operator); - const ret = createCoarse(id, this.invariantId, this.model, this.kind, this.elements, SymmetryOperator.createMapping(op, this.getCoarseElements(), this.conformation.r)); - (ret as Coarse<K, C>)._lookup3d = this._lookup3d; + const ret = createCoarse(id, this.invariantId, this.model, this.kind, this.elements, SymmetryOperator.createMapping(op, this.getCoarseElements(), this.conformation.r), this.props); + // (ret as Coarse<K, C>)._lookup3d = this._lookup3d; return ret; } - private _lookup3d: ValueRef<Lookup3D | undefined> = ValueRef.create(void 0); get lookup3d() { - if (this._lookup3d.ref) return this._lookup3d.ref; + if (this.props.lookup3d.ref) return this.props.lookup3d.ref; // TODO: support sphere radius? const { x, y, z } = this.getCoarseElements(); - this._lookup3d.ref = GridLookup3D({ x, y, z, indices: this.elements }); - return this._lookup3d.ref; + this.props.lookup3d.ref = GridLookup3D({ x, y, z, indices: this.elements }); + return this.props.lookup3d.ref; + } + + get polymerElements() { + if (this.props.polymerElements.ref) return this.props.polymerElements.ref; + this.props.polymerElements.ref = getCoarsePolymerElements(this as Unit.Spheres | Unit.Gaussians); // TODO + return this.props.polymerElements.ref; + } + + get gapElements() { + if (this.props.gapElements.ref) return this.props.gapElements.ref; + this.props.gapElements.ref = getCoarseGapElements(this as Unit.Spheres | Unit.Gaussians); // TODO + return this.props.gapElements.ref; } private getCoarseElements() { return this.kind === Kind.Spheres ? this.model.coarseConformation.spheres : this.model.coarseConformation.gaussians; } - constructor(id: number, invariantId: number, model: Model, kind: K, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping) { + constructor(id: number, invariantId: number, model: Model, kind: K, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping, props: CoarseProperties) { this.kind = kind; this.id = id; this.invariantId = invariantId; @@ -200,11 +272,26 @@ namespace Unit { this.conformation = conformation; this.coarseElements = kind === Kind.Spheres ? model.coarseHierarchy.spheres : model.coarseHierarchy.gaussians; this.coarseConformation = (kind === Kind.Spheres ? model.coarseConformation.spheres : model.coarseConformation.gaussians) as C; + this.props = props; } } - function createCoarse<K extends Kind.Gaussians | Kind.Spheres>(id: number, invariantId: number, model: Model, kind: K, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping): Unit { - return new Coarse(id, invariantId, model, kind, elements, conformation) as any as Unit /** lets call this an ugly temporary hack */; + interface CoarseProperties { + lookup3d: ValueRef<Lookup3D | undefined>, + polymerElements: ValueRef<SortedArray<ElementIndex> | undefined> + gapElements: ValueRef<SortedArray<ElementIndex> | undefined> + } + + function CoarseProperties(): CoarseProperties { + return { + lookup3d: ValueRef.create(void 0), + polymerElements: ValueRef.create(void 0), + gapElements: ValueRef.create(void 0), + }; + } + + function createCoarse<K extends Kind.Gaussians | Kind.Spheres>(id: number, invariantId: number, model: Model, kind: K, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping, props: CoarseProperties): Unit { + return new Coarse(id, invariantId, model, kind, elements, conformation, props) as any as Unit /** lets call this an ugly temporary hack */; } export class Spheres extends Coarse<Kind.Spheres, CoarseSphereConformation> { } diff --git a/src/mol-model/structure/structure/unit/links.ts b/src/mol-model/structure/structure/unit/links.ts index 5534b62f0670e027baad12e20c4f5a8187f47192..12f24b5c0ff5908752907602b2177b5913f49aae 100644 --- a/src/mol-model/structure/structure/unit/links.ts +++ b/src/mol-model/structure/structure/unit/links.ts @@ -32,6 +32,13 @@ namespace Link { return !!x && x.kind === 'link-location'; } + export function areLocationsEqual(locA: Location, locB: Location) { + return ( + locA.aIndex === locB.aIndex && locA.bIndex === locB.bIndex && + locA.aUnit.id === locB.aUnit.id && locA.bUnit.id === locB.bUnit.id + ) + } + export interface Loci { readonly kind: 'link-loci', readonly links: ReadonlyArray<Location> @@ -45,6 +52,14 @@ namespace Link { return !!x && x.kind === 'link-loci'; } + export function areLociEqual(a: Loci, b: Loci) { + if (a.links.length !== b.links.length) return false + for (let i = 0, il = a.links.length; i < il; ++i) { + if (!areLocationsEqual(a.links[i], b.links[i])) return false + } + return true + } + export function getType(structure: Structure, link: Location<Unit.Atomic>): LinkType { if (link.aUnit === link.bUnit) { const links = link.aUnit.links; diff --git a/src/mol-model/structure/structure/unit/links/data.ts b/src/mol-model/structure/structure/unit/links/data.ts index 8b570768b55d0fc0ff99e4ff0a0c3dd5a9b66e6e..1ee6243c4b6b2ecda3473a0240a36d63e43a5ef1 100644 --- a/src/mol-model/structure/structure/unit/links/data.ts +++ b/src/mol-model/structure/structure/unit/links/data.ts @@ -18,10 +18,13 @@ namespace IntraUnitLinks { } class InterUnitBonds { + /** Number of inter-unit bonds */ readonly bondCount: number + /** Array of inter-unit bonds */ readonly bonds: ReadonlyArray<InterUnitBonds.Bond> private readonly bondKeyIndex: Map<string, number> + /** Get an array of unit-pair-bonds that are linked to the given unit */ getLinkedUnits(unit: Unit): ReadonlyArray<InterUnitBonds.UnitPairBonds> { if (!this.map.has(unit.id)) return emptyArray; return this.map.get(unit.id)!; @@ -34,11 +37,13 @@ class InterUnitBonds { return index !== undefined ? index : -1 } + /** Get inter-unit bond given a pair of indices and units */ getBond(indexA: StructureElement.UnitIndex, unitA: Unit, indexB: StructureElement.UnitIndex, unitB: Unit): InterUnitBonds.Bond | undefined { const index = this.getBondIndex(indexA, unitA, indexB, unitB) return index !== -1 ? this.bonds[index] : undefined } + /** Get inter-unit bond given a link-location */ getBondFromLocation(l: Link.Location) { return this.getBond(l.aIndex, l.aUnit, l.bIndex, l.bUnit); } diff --git a/src/mol-model/structure/structure/util/boundary.ts b/src/mol-model/structure/structure/util/boundary.ts index 2a595f8cd7551a060e19d12388e7ad222f55dfd7..b90370614883b4518fb361814074a6696bb74090 100644 --- a/src/mol-model/structure/structure/util/boundary.ts +++ b/src/mol-model/structure/structure/util/boundary.ts @@ -2,66 +2,105 @@ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> + * @author Alexander Rose <alexander.rose@weirdbyte.de> */ import Structure from '../structure' -import { Box3D, Sphere3D } from 'mol-math/geometry'; +import Unit from '../unit'; +import { Box3D, Sphere3D, SymmetryOperator } from 'mol-math/geometry'; import { Vec3 } from 'mol-math/linear-algebra'; +import { SortedArray } from 'mol-data/int'; +import { ElementIndex } from '../../model/indexing'; -function computeStructureBoundary(s: Structure): { box: Box3D, sphere: Sphere3D } { - const min = [Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE]; - const max = [-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE]; +export type Boundary = { box: Box3D, sphere: Sphere3D } - const { units } = s; +function computeElementsPositionBoundary(elements: SortedArray<ElementIndex>, position: SymmetryOperator.CoordinateMapper): Boundary { + const min = Vec3.create(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE) + const max = Vec3.create(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE) + const center = Vec3.zero() - let cx = 0, cy = 0, cz = 0; - let radiusSq = 0; - let size = 0; + let radiusSq = 0 + let size = 0 - for (let i = 0, _i = units.length; i < _i; i++) { - const { x, y, z } = units[i].conformation; - - const elements = units[i].elements; - size += elements.length; - for (let j = 0, _j = elements.length; j < _j; j++) { - const e = elements[j]; - const xx = x(e), yy = y(e), zz = z(e); - - min[0] = Math.min(xx, min[0]); - min[1] = Math.min(yy, min[1]); - min[2] = Math.min(zz, min[2]); - max[0] = Math.max(xx, max[0]); - max[1] = Math.max(yy, max[1]); - max[2] = Math.max(zz, max[2]); - - cx += xx; - cy += yy; - cz += zz; - } - } + const p = Vec3.zero() - if (size > 0) { - cx /= size; - cy /= size; - cz /= size; + size += elements.length + for (let j = 0, _j = elements.length; j < _j; j++) { + position(elements[j], p) + Vec3.min(min, min, p) + Vec3.max(max, max, p) + Vec3.add(center, center, p) } - for (let i = 0, _i = units.length; i < _i; i++) { - const { x, y, z } = units[i].conformation; - - const elements = units[i].elements; - for (let j = 0, _j = elements.length; j < _j; j++) { - const e = elements[j]; - const dx = x(e) - cx, dy = y(e) - cy, dz = z(e) - cz; - const d = dx * dx + dy * dy + dz * dz; - if (d > radiusSq) radiusSq = d; - } + if (size > 0) Vec3.scale(center, center, 1/size) + + for (let j = 0, _j = elements.length; j < _j; j++) { + position(elements[j], p) + const d = Vec3.squaredDistance(p, center) + if (d > radiusSq) radiusSq = d } return { - box: { min: Vec3.ofArray(min), max: Vec3.ofArray(max) }, - sphere: { center: Vec3.create(cx, cy, cz), radius: Math.sqrt(radiusSq) } - }; + box: { min, max }, + sphere: { center, radius: Math.sqrt(radiusSq) } + } } -export { computeStructureBoundary } \ No newline at end of file +function computeInvariantUnitBoundary(u: Unit): Boundary { + return computeElementsPositionBoundary(u.elements, u.conformation.invariantPosition) +} + +export function computeUnitBoundary(u: Unit): Boundary { + return computeElementsPositionBoundary(u.elements, u.conformation.position) +} + +const tmpBox = Box3D.empty() +const tmpSphere = Sphere3D.zero() + +export function computeStructureBoundary(s: Structure): Boundary { + const min = Vec3.create(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE) + const max = Vec3.create(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE) + const center = Vec3.zero() + + const { units } = s + + const boundaryMap: Map<number, Boundary> = new Map() + function getInvariantBoundary(u: Unit) { + let boundary: Boundary + if (boundaryMap.has(u.invariantId)) { + boundary = boundaryMap.get(u.invariantId)! + } else { + boundary = computeInvariantUnitBoundary(u) + boundaryMap.set(u.invariantId, boundary) + } + return boundary + } + + let radius = 0 + let size = 0 + + for (let i = 0, _i = units.length; i < _i; i++) { + const u = units[i] + const invariantBoundary = getInvariantBoundary(u) + const m = u.conformation.operator.matrix + size += u.elements.length + Box3D.transform(tmpBox, invariantBoundary.box, m) + Vec3.min(min, min, tmpBox.min) + Vec3.max(max, max, tmpBox.max) + Sphere3D.transform(tmpSphere, invariantBoundary.sphere, m) + Vec3.scaleAndAdd(center, center, tmpSphere.center, u.elements.length) + } + + if (size > 0) Vec3.scale(center, center, 1/size) + + for (let i = 0, _i = units.length; i < _i; i++) { + const u = units[i] + const invariantBoundary = getInvariantBoundary(u) + const m = u.conformation.operator.matrix + Sphere3D.transform(tmpSphere, invariantBoundary.sphere, m) + const d = Vec3.distance(tmpSphere.center, center) + tmpSphere.radius + if (d > radius) radius = d + } + + return { box: { min, max }, sphere: { center, radius } } +} \ No newline at end of file diff --git a/src/mol-model/structure/structure/util/nucleotide.ts b/src/mol-model/structure/structure/util/nucleotide.ts new file mode 100644 index 0000000000000000000000000000000000000000..8dbb668b74a5330781041f33c30c7d3d23713351 --- /dev/null +++ b/src/mol-model/structure/structure/util/nucleotide.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { Unit, ElementIndex } from 'mol-model/structure'; +import { Segmentation, SortedArray } from 'mol-data/int'; +import { isNucleic, MoleculeType } from 'mol-model/structure/model/types'; +import { getElementIndexForAtomRole } from 'mol-model/structure/util'; + +export function getNucleotideElements(unit: Unit.Atomic) { + const indices: ElementIndex[] = [] + const { elements, model } = unit + const { chemicalComponentMap } = model.properties + const { chainAtomSegments, residueAtomSegments, residues } = model.atomicHierarchy + const { label_comp_id } = residues + const chainIt = Segmentation.transientSegments(chainAtomSegments, elements) + const residueIt = Segmentation.transientSegments(residueAtomSegments, elements) + while (chainIt.hasNext) { + residueIt.setSegment(chainIt.move()); + + while (residueIt.hasNext) { + const { index } = residueIt.move(); + const cc = chemicalComponentMap.get(label_comp_id.value(index)) + const moleculeType = cc ? cc.moleculeType : MoleculeType.unknown + + if (isNucleic(moleculeType)) { + const elementIndex = getElementIndexForAtomRole(model, index, 'trace') + indices.push(elementIndex === -1 ? residueAtomSegments.offsets[index] : elementIndex) + } + } + } + return SortedArray.ofSortedArray<ElementIndex>(indices) +} \ No newline at end of file diff --git a/src/mol-model/structure/structure/util/polymer.ts b/src/mol-model/structure/structure/util/polymer.ts new file mode 100644 index 0000000000000000000000000000000000000000..e3e9825b4e8f674e1be14efddbeeed2ec77c01e3 --- /dev/null +++ b/src/mol-model/structure/structure/util/polymer.ts @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { Unit, ElementIndex } from 'mol-model/structure'; +import { Segmentation, OrderedSet, Interval, SortedArray } from 'mol-data/int'; +import SortedRanges from 'mol-data/int/sorted-ranges'; +import { getElementIndexForAtomRole } from 'mol-model/structure/util'; + +export function getAtomicPolymerElements(unit: Unit.Atomic) { + const indices: ElementIndex[] = [] + const { elements, model } = unit + const { residueAtomSegments } = unit.model.atomicHierarchy + const polymerIt = SortedRanges.transientSegments(unit.model.atomicHierarchy.polymerRanges, elements) + const residueIt = Segmentation.transientSegments(residueAtomSegments, elements) + while (polymerIt.hasNext) { + const polymerSegment = polymerIt.move() + residueIt.setSegment(polymerSegment) + while (residueIt.hasNext) { + const residueSegment = residueIt.move() + const { start, end, index } = residueSegment + if (OrderedSet.areIntersecting(Interval.ofBounds(elements[start], elements[end - 1]), elements)) { + const elementIndex = getElementIndexForAtomRole(model, index, 'trace') + indices.push(elementIndex === -1 ? residueAtomSegments.offsets[index] : elementIndex) + } + } + } + return SortedArray.ofSortedArray<ElementIndex>(indices) +} + +export function getCoarsePolymerElements(unit: Unit.Spheres | Unit.Gaussians) { + const indices: ElementIndex[] = [] + const { elements, model } = unit + const { spheres, gaussians } = model.coarseHierarchy + const polymerRanges = Unit.isSpheres(unit) ? spheres.polymerRanges : gaussians.polymerRanges + const polymerIt = SortedRanges.transientSegments(polymerRanges, elements) + while (polymerIt.hasNext) { + const { start, end } = polymerIt.move() + for (let i = start; i < end; ++i) { indices.push(elements[i]) } + } + return SortedArray.ofSortedArray<ElementIndex>(indices) +} + +export function getAtomicGapElements(unit: Unit.Atomic) { + const indices: ElementIndex[] = [] + const { elements, model, residueIndex } = unit + const { residueAtomSegments } = unit.model.atomicHierarchy + const gapIt = SortedRanges.transientSegments(unit.model.atomicHierarchy.gapRanges, unit.elements); + while (gapIt.hasNext) { + const gapSegment = gapIt.move(); + const indexStart = residueIndex[elements[gapSegment.start]] + const indexEnd = residueIndex[elements[gapSegment.end - 1]] + const elementIndexStart = getElementIndexForAtomRole(model, indexStart, 'trace') + const elementIndexEnd = getElementIndexForAtomRole(model, indexEnd, 'trace') + indices.push(elementIndexStart === -1 ? residueAtomSegments.offsets[indexStart] : elementIndexStart) + indices.push(elementIndexEnd === -1 ? residueAtomSegments.offsets[indexEnd] : elementIndexEnd) + + } + return SortedArray.ofSortedArray<ElementIndex>(indices) +} + +export function getCoarseGapElements(unit: Unit.Spheres | Unit.Gaussians) { + const indices: ElementIndex[] = [] + const { elements, model } = unit + const { spheres, gaussians } = model.coarseHierarchy + const gapRanges = Unit.isSpheres(unit) ? spheres.gapRanges : gaussians.gapRanges + const gapIt = SortedRanges.transientSegments(gapRanges, elements) + while (gapIt.hasNext) { + const { start, end } = gapIt.move() + indices.push(elements[start], elements[end - 1]) + } + return SortedArray.ofSortedArray<ElementIndex>(indices) +} \ No newline at end of file diff --git a/src/mol-model/structure/util.ts b/src/mol-model/structure/util.ts index a20abb42e47bf97df10a7d7feb76d9dc01794407..ba416db932b2ef93f2ae7a66884dcbc214773c16 100644 --- a/src/mol-model/structure/util.ts +++ b/src/mol-model/structure/util.ts @@ -26,13 +26,13 @@ export function getAtomIdForAtomRole(moleculeType: MoleculeType, atomRole: AtomR return '' } -export function getElementIndexForAtomId(model: Model, rI: ResidueIndex, atomId: string): ElementIndex { +export function getElementIndexForAtomId(model: Model, rI: ResidueIndex, atomId: string): ElementIndex | -1 { const { offsets } = model.atomicHierarchy.residueAtomSegments const { label_atom_id } = model.atomicHierarchy.atoms for (let j = offsets[rI], _j = offsets[rI + 1]; j < _j; j++) { if (label_atom_id.value(j) === atomId) return j as ElementIndex } - return offsets[rI] as ElementIndex + return -1 } export function getElementIndexForAtomRole(model: Model, rI: ResidueIndex, atomRole: AtomRole) { diff --git a/src/mol-model/volume/data.ts b/src/mol-model/volume/data.ts index e0313d893b7e373774868005566cfbde0f02c1eb..592a2d936f29d5bcf93579af38b70b79acec0737 100644 --- a/src/mol-model/volume/data.ts +++ b/src/mol-model/volume/data.ts @@ -24,7 +24,7 @@ namespace VolumeData { const _scale = Mat4.zero(), _translate = Mat4.zero(); export function getGridToCartesianTransform(volume: VolumeData) { const { data: { space } } = volume; - const scale = Mat4.fromScaling(_scale, Vec3.div(Vec3.zero(), Box3D.size(volume.fractionalBox), Vec3.ofArray(space.dimensions))); + const scale = Mat4.fromScaling(_scale, Vec3.div(Vec3.zero(), Box3D.size(Vec3.zero(), volume.fractionalBox), Vec3.ofArray(space.dimensions))); const translate = Mat4.fromTranslation(_translate, volume.fractionalBox.min); return Mat4.mul3(Mat4.zero(), volume.cell.fromFractional, translate, scale); } diff --git a/src/mol-util/array.ts b/src/mol-util/array.ts index 11998ff2d6d9391ca612620e34f2e5d5706bb641..6895f81ed2b5b7a5db5172170a0b76656bad1aba 100644 --- a/src/mol-util/array.ts +++ b/src/mol-util/array.ts @@ -50,9 +50,8 @@ export function arrayRms(array: Helpers.NumberArray) { return Math.sqrt(sumSq / n) } -/** Fill an array with serial numbers starting from 0 */ -export function fillSerial<T extends Helpers.NumberArray> (array: T) { - const n = array.length - for (let i = 0; i < n; ++i) array[ i ] = i +/** Fill an array with serial numbers starting from 0 until n - 1 (defaults to array.length) */ +export function fillSerial<T extends Helpers.NumberArray> (array: T, n?: number) { + for (let i = 0, il = n ? Math.min(n, array.length) : array.length; i < il; ++i) array[ i ] = i return array } \ No newline at end of file diff --git a/src/mol-util/color/color.ts b/src/mol-util/color/color.ts index ea0c4efc0d8803f383cc94598c1e6bbdff017b41..764875f9470a22faa0e0de4d2d4a8701c41301b1 100644 --- a/src/mol-util/color/color.ts +++ b/src/mol-util/color/color.ts @@ -10,6 +10,10 @@ export type Color = { readonly '@type': 'color' } & number export function Color(hex: number) { return hex as Color } export namespace Color { + export function toStyle(hexColor: Color) { + return `rgb(${hexColor >> 16 & 255}, ${hexColor >> 8 & 255}, ${hexColor & 255})` + } + export function toRgb(hexColor: Color) { return [ hexColor >> 16 & 255, hexColor >> 8 & 255, hexColor & 255 ] } diff --git a/src/mol-util/color/scale.ts b/src/mol-util/color/scale.ts index f03b7539969d8af3a7f29f46592d95a9625e8670..ed486d44d46aa382cf8e5f24402b2060e43f6983 100644 --- a/src/mol-util/color/scale.ts +++ b/src/mol-util/color/scale.ts @@ -6,6 +6,7 @@ import { Color } from './color' import { ColorBrewer } from './tables' +import { ScaleLegend } from 'mol-view/theme/color'; export interface ColorScale { /** Returns hex color for given value */ @@ -14,19 +15,22 @@ export interface ColorScale { colorToArray: (value: number, array: Helpers.NumberArray, offset: number) => void /** Copies normalized (0 to 1) hex color to rgb array */ normalizedColorToArray: (value: number, array: Helpers.NumberArray, offset: number) => void + /** */ + readonly legend: ScaleLegend } export const DefaultColorScale = { domain: [0, 1], reverse: false, - colors: ColorBrewer.RdYlBu as Color[] + colors: ColorBrewer.RdYlBu, } export type ColorScaleProps = Partial<typeof DefaultColorScale> export namespace ColorScale { export function create(props: ColorScaleProps): ColorScale { - const { domain, reverse, colors } = { ...DefaultColorScale, ...props } - const [ min, max ] = reverse ? domain.slice().reverse() : domain + const { domain, reverse, colors: _colors } = { ...DefaultColorScale, ...props } + const [ min, max ] = domain + const colors = reverse ? _colors.slice().reverse() : _colors const count1 = colors.length - 1 const diff = (max - min) || 1 @@ -45,6 +49,7 @@ export namespace ColorScale { normalizedColorToArray: (value: number, array: Helpers.NumberArray, offset: number) => { Color.toArrayNormalized(color(value), array, offset) }, + get legend() { return ScaleLegend(min, max, colors) } } } } diff --git a/src/mol-util/input/input-observer.ts b/src/mol-util/input/input-observer.ts index f622969cf430a93fc288f0f899c0278cbfdb83fa..af4e96d4cb0e2257a7f89f8261f932cdeba54740 100644 --- a/src/mol-util/input/input-observer.ts +++ b/src/mol-util/input/input-observer.ts @@ -231,10 +231,6 @@ namespace InputObserver { window.removeEventListener('resize', onResize, false) } - function preventDefault (ev: Event | Touch) { - if ('preventDefault' in ev) ev.preventDefault() - } - function onContextMenu(event: Event) { if (noContextMenu) { event.preventDefault() @@ -273,8 +269,6 @@ namespace InputObserver { } function onTouchStart (ev: TouchEvent) { - preventDefault(ev) - if (ev.touches.length === 1) { buttons = ButtonsFlag.Primary onPointerDown(ev.touches[0]) @@ -286,13 +280,9 @@ namespace InputObserver { } } - function onTouchEnd (ev: TouchEvent) { - preventDefault(ev) - } + function onTouchEnd (ev: TouchEvent) {} function onTouchMove (ev: TouchEvent) { - preventDefault(ev) - if (ev.touches.length === 1) { buttons = ButtonsFlag.Primary onPointerMove(ev.touches[0]) @@ -313,22 +303,16 @@ namespace InputObserver { } function onMouseDown (ev: MouseEvent) { - preventDefault(ev) - buttons = getButtons(ev) onPointerDown(ev) } function onMouseMove (ev: MouseEvent) { - preventDefault(ev) - buttons = getButtons(ev) onPointerMove(ev) } function onMouseUp (ev: MouseEvent) { - preventDefault(ev) - buttons = getButtons(ev) onPointerUp(ev) } diff --git a/src/mol-util/url-query.ts b/src/mol-util/url-query.ts new file mode 100644 index 0000000000000000000000000000000000000000..a4a10a8371c24b55995ea175df35fb14bc6cec86 --- /dev/null +++ b/src/mol-util/url-query.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +export function urlQueryParameter (id: string) { + if (typeof window === 'undefined') return undefined + const a = new RegExp(`${id}=([^&#=]*)`) + const m = a.exec(window.location.search) + return m ? decodeURIComponent(m[1]) : undefined +} \ No newline at end of file diff --git a/src/mol-view/label.ts b/src/mol-view/label.ts index fe766334660d7072333af05996eebe43e4685d4b..39fad6853ccecbd87d3a706083bc9708eba2d550 100644 --- a/src/mol-view/label.ts +++ b/src/mol-view/label.ts @@ -5,12 +5,13 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { Unit, StructureElement, StructureProperties as Props } from 'mol-model/structure'; +import { Unit, StructureElement, StructureProperties as Props, Link } from 'mol-model/structure'; import { Loci } from 'mol-model/loci'; import { OrderedSet } from 'mol-data/int'; -const elementLocA = StructureElement.create() -const elementLocB = StructureElement.create() +// for `labelFirst`, don't create right away to avaiod problems with circular dependencies/imports +let elementLocA: StructureElement +let elementLocB: StructureElement function setElementLocation(loc: StructureElement, unit: Unit, index: StructureElement.UnitIndex) { loc.unit = unit @@ -28,14 +29,8 @@ export function labelFirst(loci: Loci): string { return 'Unknown' } case 'link-loci': - const bond = loci.links[0] - if (bond) { - setElementLocation(elementLocA, bond.aUnit, bond.aIndex) - setElementLocation(elementLocB, bond.bUnit, bond.bIndex) - return `${elementLabel(elementLocA)} - ${elementLabel(elementLocB)}` - } else { - return 'Unknown' - } + const link = loci.links[0] + return link ? linkLabel(link) : 'Unknown' case 'group-loci': const g = loci.groups[0] if (g) { @@ -50,32 +45,40 @@ export function labelFirst(loci: Loci): string { } } -export function elementLabel(loc: StructureElement) { - const model = loc.unit.model.label - const instance = loc.unit.conformation.operator.name - let element = '' +export function linkLabel(link: Link.Location) { + if (!elementLocA) elementLocA = StructureElement.create() + if (!elementLocB) elementLocB = StructureElement.create() + setElementLocation(elementLocA, link.aUnit, link.aIndex) + setElementLocation(elementLocB, link.bUnit, link.bIndex) + return `${elementLabel(elementLocA)} - ${elementLabel(elementLocB)}` +} + +export function elementLabel(element: StructureElement) { + const model = element.unit.model.label + const instance = element.unit.conformation.operator.name + let label = '' - if (Unit.isAtomic(loc.unit)) { - const asym_id = Props.chain.auth_asym_id(loc) - const seq_id = Props.residue.auth_seq_id(loc) - const comp_id = Props.residue.auth_comp_id(loc) - const atom_id = Props.atom.auth_atom_id(loc) - element = `[${comp_id}]${seq_id}:${asym_id}.${atom_id}` - } else if (Unit.isCoarse(loc.unit)) { - const asym_id = Props.coarse.asym_id(loc) - const seq_id_begin = Props.coarse.seq_id_begin(loc) - const seq_id_end = Props.coarse.seq_id_end(loc) + if (Unit.isAtomic(element.unit)) { + const asym_id = Props.chain.auth_asym_id(element) + const seq_id = Props.residue.auth_seq_id(element) + const comp_id = Props.residue.auth_comp_id(element) + const atom_id = Props.atom.auth_atom_id(element) + label = `[${comp_id}]${seq_id}:${asym_id}.${atom_id}` + } else if (Unit.isCoarse(element.unit)) { + const asym_id = Props.coarse.asym_id(element) + const seq_id_begin = Props.coarse.seq_id_begin(element) + const seq_id_end = Props.coarse.seq_id_end(element) if (seq_id_begin === seq_id_end) { - const entityKey = Props.coarse.entityKey(loc) - const seq = loc.unit.model.sequence.byEntityKey[entityKey] + const entityKey = Props.coarse.entityKey(element) + const seq = element.unit.model.sequence.byEntityKey[entityKey] const comp_id = seq.compId.value(seq_id_begin - 1) // 1-indexed - element = `[${comp_id}]${seq_id_begin}:${asym_id}` + label = `[${comp_id}]${seq_id_begin}:${asym_id}` } else { - element = `${seq_id_begin}-${seq_id_end}:${asym_id}` + label = `${seq_id_begin}-${seq_id_end}:${asym_id}` } } else { - element = 'unknown' + label = 'unknown' } - return `${model} ${instance} ${element}` + return `${model} ${instance} ${label}` } \ No newline at end of file diff --git a/src/mol-view/stage.ts b/src/mol-view/stage.ts index 6ecac7aa58dcde699b76c542eb2d80852050fb27..271b2f9d703695a6b7717fe17d632e4de7445fd0 100644 --- a/src/mol-view/stage.ts +++ b/src/mol-view/stage.ts @@ -122,8 +122,8 @@ export class Stage { // this.loadMmcifUrl(`../../examples/1crn.cif`) // this.loadPdbid('5u0q') // mixed dna/rna in same polymer // this.loadPdbid('1xj9') // PNA (peptide nucleic acid) - this.loadPdbid('5eme') // PNA (peptide nucleic acid) and RNA - // this.loadPdbid('5eme') // temp + // this.loadPdbid('5eme') // PNA (peptide nucleic acid) and RNA + this.loadPdbid('2X3T') // temp // this.loadMmcifUrl(`../../../test/pdb-dev/PDBDEV_00000001.cif`) // ok // this.loadMmcifUrl(`../../../test/pdb-dev/PDBDEV_00000002.cif`) // ok diff --git a/src/mol-view/state/transform.ts b/src/mol-view/state/transform.ts index bda97a320ad07592ce4b0590f791a5b96643bc63..bb454c3c5d7711e2869a085e253fb52c769f2074 100644 --- a/src/mol-view/state/transform.ts +++ b/src/mol-view/state/transform.ts @@ -99,7 +99,7 @@ export type StructureToSpacefill = StateTransform<StructureEntity, SpacefillEnti export const StructureToSpacefill: StructureToSpacefill = StateTransform.create('structure', 'spacefill', 'structure-to-spacefill', async function (ctx: StateContext, structureEntity: StructureEntity, props: Partial<SpacefillProps> = {}) { const spacefillRepr = SpacefillRepresentation() - await spacefillRepr.create(structureEntity.value, props).run(ctx.log) + await spacefillRepr.createOrUpdate(props, structureEntity.value).run(ctx.log) ctx.viewer.add(spacefillRepr) ctx.viewer.requestDraw() console.log('stats', ctx.viewer.stats) @@ -110,7 +110,7 @@ export type StructureToBallAndStick = StateTransform<StructureEntity, BallAndSti export const StructureToBallAndStick: StructureToBallAndStick = StateTransform.create('structure', 'ballandstick', 'structure-to-ballandstick', async function (ctx: StateContext, structureEntity: StructureEntity, props: Partial<BallAndStickProps> = {}) { const ballAndStickRepr = BallAndStickRepresentation() - await ballAndStickRepr.create(structureEntity.value, props).run(ctx.log) + await ballAndStickRepr.createOrUpdate(props, structureEntity.value).run(ctx.log) ctx.viewer.add(ballAndStickRepr) ctx.viewer.requestDraw() console.log('stats', ctx.viewer.stats) @@ -121,7 +121,7 @@ export type StructureToDistanceRestraint = StateTransform<StructureEntity, Dista export const StructureToDistanceRestraint: StructureToDistanceRestraint = StateTransform.create('structure', 'distancerestraint', 'structure-to-distancerestraint', async function (ctx: StateContext, structureEntity: StructureEntity, props: Partial<DistanceRestraintProps> = {}) { const distanceRestraintRepr = DistanceRestraintRepresentation() - await distanceRestraintRepr.create(structureEntity.value, props).run(ctx.log) + await distanceRestraintRepr.createOrUpdate(props, structureEntity.value).run(ctx.log) ctx.viewer.add(distanceRestraintRepr) ctx.viewer.requestDraw() console.log('stats', ctx.viewer.stats) @@ -132,7 +132,7 @@ export type StructureToBackbone = StateTransform<StructureEntity, BackboneEntity export const StructureToBackbone: StructureToBackbone = StateTransform.create('structure', 'backbone', 'structure-to-backbone', async function (ctx: StateContext, structureEntity: StructureEntity, props: Partial<BackboneProps> = {}) { const backboneRepr = BackboneRepresentation() - await backboneRepr.create(structureEntity.value, props).run(ctx.log) + await backboneRepr.createOrUpdate(props, structureEntity.value).run(ctx.log) ctx.viewer.add(backboneRepr) ctx.viewer.requestDraw() console.log('stats', ctx.viewer.stats) @@ -143,7 +143,7 @@ export type StructureToCartoon = StateTransform<StructureEntity, CartoonEntity, export const StructureToCartoon: StructureToCartoon = StateTransform.create('structure', 'cartoon', 'structure-to-cartoon', async function (ctx: StateContext, structureEntity: StructureEntity, props: Partial<CartoonProps> = {}) { const cartoonRepr = CartoonRepresentation() - await cartoonRepr.create(structureEntity.value, props).run(ctx.log) + await cartoonRepr.createOrUpdate(props, structureEntity.value).run(ctx.log) ctx.viewer.add(cartoonRepr) ctx.viewer.requestDraw() console.log('stats', ctx.viewer.stats) @@ -154,7 +154,7 @@ export type StructureToCarbohydrate = StateTransform<StructureEntity, Carbohydra export const StructureToCarbohydrate: StructureToCarbohydrate = StateTransform.create('structure', 'carbohydrate', 'structure-to-cartoon', async function (ctx: StateContext, structureEntity: StructureEntity, props: Partial<CarbohydrateProps> = {}) { const carbohydrateRepr = CarbohydrateRepresentation() - await carbohydrateRepr.create(structureEntity.value, props).run(ctx.log) + await carbohydrateRepr.createOrUpdate(props, structureEntity.value).run(ctx.log) ctx.viewer.add(carbohydrateRepr) ctx.viewer.requestDraw() console.log('stats', ctx.viewer.stats) @@ -165,7 +165,7 @@ export type SpacefillUpdate = StateTransform<SpacefillEntity, NullEntity, Partia export const SpacefillUpdate: SpacefillUpdate = StateTransform.create('spacefill', 'null', 'spacefill-update', async function (ctx: StateContext, spacefillEntity: SpacefillEntity, props: Partial<SpacefillProps> = {}) { const spacefillRepr = spacefillEntity.value - await spacefillRepr.update(props).run(ctx.log) + await spacefillRepr.createOrUpdate(props).run(ctx.log) ctx.viewer.add(spacefillRepr) ctx.viewer.requestDraw() console.log('stats', ctx.viewer.stats) @@ -176,7 +176,7 @@ export type BallAndStickUpdate = StateTransform<BallAndStickEntity, NullEntity, export const BallAndStickUpdate: BallAndStickUpdate = StateTransform.create('ballandstick', 'null', 'ballandstick-update', async function (ctx: StateContext, ballAndStickEntity: BallAndStickEntity, props: Partial<BallAndStickProps> = {}) { const ballAndStickRepr = ballAndStickEntity.value - await ballAndStickRepr.update(props).run(ctx.log) + await ballAndStickRepr.createOrUpdate(props).run(ctx.log) ctx.viewer.add(ballAndStickRepr) ctx.viewer.requestDraw() console.log('stats', ctx.viewer.stats) @@ -187,7 +187,7 @@ export type DistanceRestraintUpdate = StateTransform<DistanceRestraintEntity, Nu export const DistanceRestraintUpdate: DistanceRestraintUpdate = StateTransform.create('distancerestraint', 'null', 'distancerestraint-update', async function (ctx: StateContext, distanceRestraintEntity: DistanceRestraintEntity, props: Partial<DistanceRestraintProps> = {}) { const distanceRestraintRepr = distanceRestraintEntity.value - await distanceRestraintRepr.update(props).run(ctx.log) + await distanceRestraintRepr.createOrUpdate(props).run(ctx.log) ctx.viewer.add(distanceRestraintRepr) ctx.viewer.requestDraw() console.log('stats', ctx.viewer.stats) @@ -198,7 +198,7 @@ export type BackboneUpdate = StateTransform<BackboneEntity, NullEntity, Partial< export const BackboneUpdate: BackboneUpdate = StateTransform.create('backbone', 'null', 'backbone-update', async function (ctx: StateContext, backboneEntity: BackboneEntity, props: Partial<BackboneProps> = {}) { const backboneRepr = backboneEntity.value - await backboneRepr.update(props).run(ctx.log) + await backboneRepr.createOrUpdate(props).run(ctx.log) ctx.viewer.add(backboneRepr) ctx.viewer.requestDraw() console.log('stats', ctx.viewer.stats) @@ -209,7 +209,7 @@ export type CartoonUpdate = StateTransform<CartoonEntity, NullEntity, Partial<Ca export const CartoonUpdate: CartoonUpdate = StateTransform.create('cartoon', 'null', 'cartoon-update', async function (ctx: StateContext, cartoonEntity: CartoonEntity, props: Partial<CartoonProps> = {}) { const cartoonRepr = cartoonEntity.value - await cartoonRepr.update(props).run(ctx.log) + await cartoonRepr.createOrUpdate(props).run(ctx.log) ctx.viewer.add(cartoonRepr) ctx.viewer.requestDraw() console.log('stats', ctx.viewer.stats) @@ -220,7 +220,7 @@ export type CarbohydrateUpdate = StateTransform<CarbohydrateEntity, NullEntity, export const CarbohydrateUpdate: CarbohydrateUpdate = StateTransform.create('carbohydrate', 'null', 'carbohydrate-update', async function (ctx: StateContext, carbohydrateEntity: CarbohydrateEntity, props: Partial<CarbohydrateProps> = {}) { const carbohydrateRepr = carbohydrateEntity.value - await carbohydrateRepr.update(props).run(ctx.log) + await carbohydrateRepr.createOrUpdate(props).run(ctx.log) ctx.viewer.add(carbohydrateRepr) ctx.viewer.requestDraw() console.log('stats', ctx.viewer.stats) diff --git a/src/mol-view/theme/color.ts b/src/mol-view/theme/color.ts index f02f54b3a11d757f1d636d71a99a12f8d414ee1f..7b1f0e4d2f6275d31d13506303a5a512db649a1a 100644 --- a/src/mol-view/theme/color.ts +++ b/src/mol-view/theme/color.ts @@ -6,7 +6,7 @@ import { Color } from 'mol-util/color'; import { Structure } from 'mol-model/structure'; -import { ColorType, LocationColor } from 'mol-geo/util/color-data'; +import { Location } from 'mol-model/location'; import { ElementIndexColorTheme } from './color/element-index'; import { CarbohydrateSymbolColorTheme } from './color/carbohydrate-symbol'; @@ -16,10 +16,34 @@ import { UnitIndexColorTheme } from './color/unit-index'; import { UniformColorTheme } from './color/uniform'; import { CrossLinkColorTheme } from './color/cross-link'; import { ShapeGroupColorTheme } from './color/shape-group'; +import { CustomColorTheme } from './color/custom'; +import { ColorType } from 'mol-geo/util/color-data'; + +export type LocationColor = (location: Location, isSecondary: boolean) => Color + +export interface ScaleLegend { + kind: 'scale-legend' + min: number, + max: number, + colors: Color[] +} +export function ScaleLegend(min: number, max: number, colors: Color[]): ScaleLegend { + return { kind: 'scale-legend', min, max, colors } +} + +export interface TableLegend { + kind: 'table-legend' + table: [ string, Color ][] +} +export function TableLegend(table: [ string, Color ][]): TableLegend { + return { kind: 'table-legend', table } +} export interface ColorTheme { - kind: ColorType + granularity: ColorType color: LocationColor + description?: string + legend?: ScaleLegend | TableLegend } export function ColorTheme(props: ColorThemeProps): ColorTheme { @@ -32,6 +56,7 @@ export function ColorTheme(props: ColorThemeProps): ColorTheme { case 'unit-index': return UnitIndexColorTheme(props) case 'uniform': return UniformColorTheme(props) case 'shape-group': return ShapeGroupColorTheme(props) + case 'custom': return CustomColorTheme(props) } } @@ -40,6 +65,10 @@ export interface ColorThemeProps { domain?: [number, number] value?: Color structure?: Structure + color?: LocationColor + granularity?: ColorType, + description?: string, + legend?: ScaleLegend | TableLegend } export const ColorThemeInfo = { @@ -50,7 +79,8 @@ export const ColorThemeInfo = { 'element-symbol': {}, 'unit-index': {}, 'uniform': {}, - 'shape-group': {} + 'shape-group': {}, + 'custom': {} } export type ColorThemeName = keyof typeof ColorThemeInfo export const ColorThemeNames = Object.keys(ColorThemeInfo) \ No newline at end of file diff --git a/src/mol-view/theme/color/carbohydrate-symbol.ts b/src/mol-view/theme/color/carbohydrate-symbol.ts index 7c3093a0b2ffce1e3e8ac4e865516bf8976f1ce7..c777c91d538684085041c23698ee19657fb18de7 100644 --- a/src/mol-view/theme/color/carbohydrate-symbol.ts +++ b/src/mol-view/theme/color/carbohydrate-symbol.ts @@ -6,16 +6,16 @@ import { StructureElement, Link, ElementIndex, Unit } from 'mol-model/structure'; -import { SaccharideColors } from 'mol-model/structure/structure/carbohydrates/constants'; +import { SaccharideColors, MonosaccharidesColorTable } from 'mol-model/structure/structure/carbohydrates/constants'; import { Location } from 'mol-model/location'; -import { ColorThemeProps, ColorTheme } from '../color'; -import { LocationColor } from 'mol-geo/util/color-data'; +import { ColorThemeProps, ColorTheme, LocationColor, TableLegend } from '../color'; import { Color } from 'mol-util/color'; -const DefaultColor = 0xCCCCCC as Color +const DefaultColor = Color(0xCCCCCC) +const Description = 'Assigns colors according to the Symbol Nomenclature for Glycans (SNFG).' export function CarbohydrateSymbolColorTheme(props: ColorThemeProps): ColorTheme { - let colorFn: LocationColor + let color: LocationColor if (props.structure) { const { elements, getElementIndex, getAnomericCarbon } = props.structure.carbohydrates @@ -30,7 +30,7 @@ export function CarbohydrateSymbolColorTheme(props: ColorThemeProps): ColorTheme return DefaultColor } - colorFn = (location: Location, isSecondary: boolean) => { + color = (location: Location, isSecondary: boolean) => { if (isSecondary) { return SaccharideColors.Secondary } else { @@ -43,11 +43,13 @@ export function CarbohydrateSymbolColorTheme(props: ColorThemeProps): ColorTheme return DefaultColor } } else { - colorFn = () => DefaultColor + color = () => DefaultColor } return { - kind: 'group', - color: colorFn + granularity: 'group', + color: color, + description: Description, + legend: TableLegend(MonosaccharidesColorTable) } } \ No newline at end of file diff --git a/src/mol-view/theme/color/chain-id.ts b/src/mol-view/theme/color/chain-id.ts index 9e15eec0f430d12805b680c155c03033cc7223e8..e281dc9a20dc39fd36c3f937ec8920bb87e1f9b3 100644 --- a/src/mol-view/theme/color/chain-id.ts +++ b/src/mol-view/theme/color/chain-id.ts @@ -8,9 +8,10 @@ import { Unit, StructureProperties, StructureElement, Link } from 'mol-model/str import { ColorScale, Color } from 'mol-util/color'; import { Location } from 'mol-model/location'; -import { ColorThemeProps, ColorTheme } from '../color'; +import { ColorThemeProps, ColorTheme, LocationColor } from '../color'; -const DefaultColor = 0xCCCCCC as Color +const DefaultColor = Color(0xCCCCCC) +const Description = 'Gives every chain a color based on its `asym_id` value.' function getAsymId(unit: Unit): StructureElement.Property<string> { switch (unit.kind) { @@ -23,27 +24,49 @@ function getAsymId(unit: Unit): StructureElement.Property<string> { } export function ChainIdColorTheme(props: ColorThemeProps): ColorTheme { - const l = StructureElement.create() - - function colorFn(location: Location): Color { - if (StructureElement.isLocation(location)) { - const map = location.unit.model.properties.asymIdSerialMap - const scale = ColorScale.create({ domain: [ 0, map.size - 1 ] }) - const asym_id = getAsymId(location.unit) - return scale.color(map.get(asym_id(location)) || 0) - } else if (Link.isLocation(location)) { - const map = location.aUnit.model.properties.asymIdSerialMap - const scale = ColorScale.create({ domain: [ 0, map.size - 1 ] }) - const asym_id = getAsymId(location.aUnit) - l.unit = location.aUnit - l.element = location.aUnit.elements[location.aIndex] - return scale.color(map.get(asym_id(l)) || 0) + let color: LocationColor + let scale: ColorScale | undefined = undefined + // const table: [string, Color][] = [] + + if (props.structure) { + const l = StructureElement.create() + const { models } = props.structure + const asymIdSerialMap = new Map<string, number>() + let j = 0 + for (let i = 0, il = models.length; i <il; ++i) { + models[i].properties.asymIdSerialMap.forEach((v, k) => { + if (!asymIdSerialMap.has(k)) { + asymIdSerialMap.set(k, j) + j += 1 + } + }) + } + scale = ColorScale.create({ domain: [ 0, asymIdSerialMap.size - 1 ] }) + const scaleColor = scale.color + + // asymIdSerialMap.forEach((v, k) => table.push([k, scaleColor(v)])) + + color = (location: Location): Color => { + if (StructureElement.isLocation(location)) { + const asym_id = getAsymId(location.unit) + return scaleColor(asymIdSerialMap.get(asym_id(location)) || 0) + } else if (Link.isLocation(location)) { + const asym_id = getAsymId(location.aUnit) + l.unit = location.aUnit + l.element = location.aUnit.elements[location.aIndex] + return scaleColor(asymIdSerialMap.get(asym_id(l)) || 0) + } + return DefaultColor } - return DefaultColor + } else { + color = () => DefaultColor } return { - kind: 'group', - color: colorFn + granularity: 'group', + color, + description: Description, + // legend: scale ? TableLegend(table) : undefined + legend: scale ? scale.legend : undefined } } \ No newline at end of file diff --git a/src/mol-view/theme/color/cross-link.ts b/src/mol-view/theme/color/cross-link.ts index 5a4709dc550531cafeb512eef140987399612dfd..6f8ab371e6ff957f71fd081850f25acc55b23ac0 100644 --- a/src/mol-view/theme/color/cross-link.ts +++ b/src/mol-view/theme/color/cross-link.ts @@ -8,11 +8,11 @@ import { Link } from 'mol-model/structure'; import { Color, ColorScale, ColorBrewer } from 'mol-util/color'; import { Location } from 'mol-model/location'; -import { ColorThemeProps, ColorTheme } from '../color'; -import { LocationColor } from 'mol-geo/util/color-data'; +import { ColorThemeProps, ColorTheme, LocationColor } from '../color'; import { Vec3 } from 'mol-math/linear-algebra'; -const DefaultColor = 0xCCCCCC as Color +const DefaultColor = Color(0xCCCCCC) +const Description = 'Colors cross-links by the deviation of the observed distance versus the modeled distance (e.g. `ihm_cross_link_restraint.distance_threshold`).' const distVecA = Vec3.zero(), distVecB = Vec3.zero() function linkDistance(link: Link.Location) { @@ -22,27 +22,31 @@ function linkDistance(link: Link.Location) { } export function CrossLinkColorTheme(props: ColorThemeProps): ColorTheme { - let colorFn: LocationColor + let color: LocationColor + let scale: ColorScale | undefined = undefined if (props.structure) { const crosslinks = props.structure.crossLinkRestraints - const scale = ColorScale.create({ domain: [ -10, 10 ], colors: ColorBrewer.RdYlBu }) + scale = ColorScale.create({ domain: [ -10, 10 ], colors: ColorBrewer.RdYlBu }) + const scaleColor = scale.color - colorFn = (location: Location): Color => { + color = (location: Location): Color => { if (Link.isLocation(location)) { const pairs = crosslinks.getPairs(location.aIndex, location.aUnit, location.bIndex, location.bUnit) if (pairs) { - return scale.color(linkDistance(location) - pairs[0].distanceThreshold) + return scaleColor(linkDistance(location) - pairs[0].distanceThreshold) } } return DefaultColor } } else { - colorFn = () => DefaultColor + color = () => DefaultColor } return { - kind: 'group', - color: colorFn + granularity: 'group', + color, + description: Description, + legend: scale ? scale.legend : undefined } } \ No newline at end of file diff --git a/src/mol-view/theme/color/custom.ts b/src/mol-view/theme/color/custom.ts new file mode 100644 index 0000000000000000000000000000000000000000..4901ac06f6860660a7cfb1edb9a1505d9e052c17 --- /dev/null +++ b/src/mol-view/theme/color/custom.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { Color } from 'mol-util/color'; +import { ColorThemeProps, ColorTheme } from '../color'; +import { defaults } from 'mol-util'; + +const DefaultColor = Color(0xCCCCCC) + +export function CustomColorTheme(props: ColorThemeProps): ColorTheme { + const value = defaults(props.value, DefaultColor) + return { + granularity: defaults(props.granularity, 'uniform'), + color: defaults(props.color, () => value), + description: props.description, + legend: props.legend + } +} \ No newline at end of file diff --git a/src/mol-view/theme/color/element-index.ts b/src/mol-view/theme/color/element-index.ts index f6abd54f3cdee2a02e2531e660a9b93449d831ba..8ca2d273a0c5c30c2d16c323adefd77ed043295d 100644 --- a/src/mol-view/theme/color/element-index.ts +++ b/src/mol-view/theme/color/element-index.ts @@ -6,45 +6,51 @@ import { ColorScale, Color } from 'mol-util/color'; import { Location } from 'mol-model/location'; -import { StructureElement, Link, Unit } from 'mol-model/structure'; +import { StructureElement, Link } from 'mol-model/structure'; import { OrderedSet } from 'mol-data/int'; -import { LocationColor } from 'mol-geo/util/color-data'; -import { ColorThemeProps, ColorTheme } from '../color'; +import { ColorThemeProps, ColorTheme, LocationColor } from '../color'; -const DefaultColor = 0xCCCCCC as Color +const DefaultColor = Color(0xCCCCCC) +const Description = 'Gives every element (atom or coarse sphere/gaussian) a unique color based on the position (index) of the element in the list of elements in the structure.' export function ElementIndexColorTheme(props: ColorThemeProps): ColorTheme { - let colorFn: LocationColor + let color: LocationColor + let scale: ColorScale | undefined = undefined if (props.structure) { const { units } = props.structure const unitCount = units.length const cummulativeElementCount = new Map<number, number>() + const unitIdIndex = new Map<number, number>() let elementCount = 0 for (let i = 0; i < unitCount; ++i) { cummulativeElementCount.set(i, elementCount) elementCount += units[i].elements.length + unitIdIndex.set(units[i].id, i) } - const scale = ColorScale.create({ domain: [ 0, elementCount ] }) + scale = ColorScale.create({ domain: [ 0, elementCount - 1 ] }) + const scaleColor = scale.color - colorFn = (location: Location): Color => { + color = (location: Location): Color => { if (StructureElement.isLocation(location)) { - const unitIndex = Unit.findUnitById(location.unit.id, units) + const unitIndex = unitIdIndex.get(location.unit.id)! const unitElementIndex = OrderedSet.findPredecessorIndex(location.unit.elements, location.element) - return scale.color(cummulativeElementCount.get(unitIndex) || 0 + unitElementIndex) + return scaleColor(cummulativeElementCount.get(unitIndex)! + unitElementIndex) } else if (Link.isLocation(location)) { - const unitId = Unit.findUnitById(location.aUnit.id, units) - return scale.color(cummulativeElementCount.get(unitId) || 0 + location.aIndex) + const unitIndex = unitIdIndex.get(location.aUnit.id)! + return scaleColor(cummulativeElementCount.get(unitIndex)! + location.aIndex) } return DefaultColor } } else { - colorFn = () => DefaultColor + color = () => DefaultColor } return { - kind: 'groupInstance', - color: colorFn + granularity: 'groupInstance', + color, + description: Description, + legend: scale ? scale.legend : undefined } } \ No newline at end of file diff --git a/src/mol-view/theme/color/element-symbol.ts b/src/mol-view/theme/color/element-symbol.ts index eaa64ebb687eff4b8b8d2d9888ff5215a298eba6..c73ae2d46280e1491535b2d6d79c98a328760d49 100644 --- a/src/mol-view/theme/color/element-symbol.ts +++ b/src/mol-view/theme/color/element-symbol.ts @@ -8,14 +8,15 @@ import { ElementSymbol } from 'mol-model/structure/model/types'; import { Color, ColorMap } from 'mol-util/color'; import { StructureElement, Unit, Link } from 'mol-model/structure'; import { Location } from 'mol-model/location'; -import { ColorThemeProps, ColorTheme } from '../color'; +import { ColorThemeProps, ColorTheme, TableLegend } from '../color'; // from Jmol http://jmol.sourceforge.net/jscolors/ (or 0xFFFFFF) export const ElementSymbolColors = ColorMap({ - 'H': 0xFFFFFF, 'HE': 0xD9FFFF, 'LI': 0xCC80FF, 'BE': 0xC2FF00, 'B': 0xFFB5B5, 'C': 0x909090, 'N': 0x3050F8, 'O': 0xFF0D0D, 'F': 0x90E050, 'NE': 0xB3E3F5, 'NA': 0xAB5CF2, 'MG': 0x8AFF00, 'AL': 0xBFA6A6, 'SI': 0xF0C8A0, 'P': 0xFF8000, 'S': 0xFFFF30, 'CL': 0x1FF01F, 'AR': 0x80D1E3, 'K': 0x8F40D4, 'CA': 0x3DFF00, 'SC': 0xE6E6E6, 'TI': 0xBFC2C7, 'V': 0xA6A6AB, 'CR': 0x8A99C7, 'MN': 0x9C7AC7, 'FE': 0xE06633, 'CO': 0xF090A0, 'NI': 0x50D050, 'CU': 0xC88033, 'ZN': 0x7D80B0, 'GA': 0xC28F8F, 'GE': 0x668F8F, 'AS': 0xBD80E3, 'SE': 0xFFA100, 'BR': 0xA62929, 'KR': 0x5CB8D1, 'RB': 0x702EB0, 'SR': 0x00FF00, 'Y': 0x94FFFF, 'ZR': 0x94E0E0, 'NB': 0x73C2C9, 'MO': 0x54B5B5, 'TC': 0x3B9E9E, 'RU': 0x248F8F, 'RH': 0x0A7D8C, 'PD': 0x006985, 'AG': 0xC0C0C0, 'CD': 0xFFD98F, 'IN': 0xA67573, 'SN': 0x668080, 'SB': 0x9E63B5, 'TE': 0xD47A00, 'I': 0x940094, 'XE': 0x940094, 'CS': 0x57178F, 'BA': 0x00C900, 'LA': 0x70D4FF, 'CE': 0xFFFFC7, 'PR': 0xD9FFC7, 'ND': 0xC7FFC7, 'PM': 0xA3FFC7, 'SM': 0x8FFFC7, 'EU': 0x61FFC7, 'GD': 0x45FFC7, 'TB': 0x30FFC7, 'DY': 0x1FFFC7, 'HO': 0x00FF9C, 'ER': 0x00E675, 'TM': 0x00D452, 'YB': 0x00BF38, 'LU': 0x00AB24, 'HF': 0x4DC2FF, 'TA': 0x4DA6FF, 'W': 0x2194D6, 'RE': 0x267DAB, 'OS': 0x266696, 'IR': 0x175487, 'PT': 0xD0D0E0, 'AU': 0xFFD123, 'HG': 0xB8B8D0, 'TL': 0xA6544D, 'PB': 0x575961, 'BI': 0x9E4FB5, 'PO': 0xAB5C00, 'AT': 0x754F45, 'RN': 0x428296, 'FR': 0x420066, 'RA': 0x007D00, 'AC': 0x70ABFA, 'TH': 0x00BAFF, 'PA': 0x00A1FF, 'U': 0x008FFF, 'NP': 0x0080FF, 'PU': 0x006BFF, 'AM': 0x545CF2, 'CM': 0x785CE3, 'BK': 0x8A4FE3, 'CF': 0xA136D4, 'ES': 0xB31FD4, 'FM': 0xB31FBA, 'MD': 0xB30DA6, 'NO': 0xBD0D87, 'LR': 0xC70066, 'RF': 0xCC0059, 'DB': 0xD1004F, 'SG': 0xD90045, 'BH': 0xE00038, 'HS': 0xE6002E, 'MT': 0xEB0026, 'DS': 0xFFFFFF, 'RG': 0xFFFFFF, 'CN': 0xFFFFFF, 'UUT': 0xFFFFFF, 'FL': 0xFFFFFF, 'UUP': 0xFFFFFF, 'LV': 0xFFFFFF, 'UUH': 0xFFFFFF, 'D': 0xFFFFC0, 'T': 0xFFFFA0 + 'H': 0xFFFFFF, 'D': 0xFFFFC0, 'T': 0xFFFFA0, 'HE': 0xD9FFFF, 'LI': 0xCC80FF, 'BE': 0xC2FF00, 'B': 0xFFB5B5, 'C': 0x909090, 'N': 0x3050F8, 'O': 0xFF0D0D, 'F': 0x90E050, 'NE': 0xB3E3F5, 'NA': 0xAB5CF2, 'MG': 0x8AFF00, 'AL': 0xBFA6A6, 'SI': 0xF0C8A0, 'P': 0xFF8000, 'S': 0xFFFF30, 'CL': 0x1FF01F, 'AR': 0x80D1E3, 'K': 0x8F40D4, 'CA': 0x3DFF00, 'SC': 0xE6E6E6, 'TI': 0xBFC2C7, 'V': 0xA6A6AB, 'CR': 0x8A99C7, 'MN': 0x9C7AC7, 'FE': 0xE06633, 'CO': 0xF090A0, 'NI': 0x50D050, 'CU': 0xC88033, 'ZN': 0x7D80B0, 'GA': 0xC28F8F, 'GE': 0x668F8F, 'AS': 0xBD80E3, 'SE': 0xFFA100, 'BR': 0xA62929, 'KR': 0x5CB8D1, 'RB': 0x702EB0, 'SR': 0x00FF00, 'Y': 0x94FFFF, 'ZR': 0x94E0E0, 'NB': 0x73C2C9, 'MO': 0x54B5B5, 'TC': 0x3B9E9E, 'RU': 0x248F8F, 'RH': 0x0A7D8C, 'PD': 0x006985, 'AG': 0xC0C0C0, 'CD': 0xFFD98F, 'IN': 0xA67573, 'SN': 0x668080, 'SB': 0x9E63B5, 'TE': 0xD47A00, 'I': 0x940094, 'XE': 0x940094, 'CS': 0x57178F, 'BA': 0x00C900, 'LA': 0x70D4FF, 'CE': 0xFFFFC7, 'PR': 0xD9FFC7, 'ND': 0xC7FFC7, 'PM': 0xA3FFC7, 'SM': 0x8FFFC7, 'EU': 0x61FFC7, 'GD': 0x45FFC7, 'TB': 0x30FFC7, 'DY': 0x1FFFC7, 'HO': 0x00FF9C, 'ER': 0x00E675, 'TM': 0x00D452, 'YB': 0x00BF38, 'LU': 0x00AB24, 'HF': 0x4DC2FF, 'TA': 0x4DA6FF, 'W': 0x2194D6, 'RE': 0x267DAB, 'OS': 0x266696, 'IR': 0x175487, 'PT': 0xD0D0E0, 'AU': 0xFFD123, 'HG': 0xB8B8D0, 'TL': 0xA6544D, 'PB': 0x575961, 'BI': 0x9E4FB5, 'PO': 0xAB5C00, 'AT': 0x754F45, 'RN': 0x428296, 'FR': 0x420066, 'RA': 0x007D00, 'AC': 0x70ABFA, 'TH': 0x00BAFF, 'PA': 0x00A1FF, 'U': 0x008FFF, 'NP': 0x0080FF, 'PU': 0x006BFF, 'AM': 0x545CF2, 'CM': 0x785CE3, 'BK': 0x8A4FE3, 'CF': 0xA136D4, 'ES': 0xB31FD4, 'FM': 0xB31FBA, 'MD': 0xB30DA6, 'NO': 0xBD0D87, 'LR': 0xC70066, 'RF': 0xCC0059, 'DB': 0xD1004F, 'SG': 0xD90045, 'BH': 0xE00038, 'HS': 0xE6002E, 'MT': 0xEB0026, 'DS': 0xFFFFFF, 'RG': 0xFFFFFF, 'CN': 0xFFFFFF, 'UUT': 0xFFFFFF, 'FL': 0xFFFFFF, 'UUP': 0xFFFFFF, 'LV': 0xFFFFFF, 'UUH': 0xFFFFFF }) -const DefaultElementSymbolColor = 0xFFFFFF as Color +const DefaultElementSymbolColor = Color(0xFFFFFF) +const Description = 'Assigns a color to every atom according to its chemical element.' export function elementSymbolColor(element: ElementSymbol): Color { const c = (ElementSymbolColors as { [k: string]: Color })[element]; @@ -23,7 +24,7 @@ export function elementSymbolColor(element: ElementSymbol): Color { } export function ElementSymbolColorTheme(props: ColorThemeProps): ColorTheme { - function colorFn(location: Location): Color { + function color(location: Location): Color { if (StructureElement.isLocation(location)) { if (Unit.isAtomic(location.unit)) { const { type_symbol } = location.unit.model.atomicHierarchy.atoms @@ -39,7 +40,11 @@ export function ElementSymbolColorTheme(props: ColorThemeProps): ColorTheme { } return { - kind: 'group', - color: colorFn + granularity: 'group', + color, + description: Description, + legend: TableLegend(Object.keys(ElementSymbolColors).map(name => { + return [name, (ElementSymbolColors as any)[name] as Color] as [string, Color] + })) } } \ No newline at end of file diff --git a/src/mol-view/theme/color/shape-group.ts b/src/mol-view/theme/color/shape-group.ts index f7be8107b23ea16e631fb08bd567e8af292b42e7..6743791a3c58d823359eab0f6f8052c1bb8b55aa 100644 --- a/src/mol-view/theme/color/shape-group.ts +++ b/src/mol-view/theme/color/shape-group.ts @@ -9,16 +9,18 @@ import { Color } from 'mol-util/color'; import { Location } from 'mol-model/location'; import { Shape } from 'mol-model/shape'; -const DefaultColor = 0xCCCCCC as Color +const DefaultColor = Color(0xCCCCCC) export function ShapeGroupColorTheme(props: ColorThemeProps): ColorTheme { return { - kind: 'group', + granularity: 'group', color: (location: Location): Color => { if (Shape.isLocation(location)) { return location.shape.colors.ref.value[location.group] } return DefaultColor - } + }, + description: props.description, + legend: props.legend } } \ No newline at end of file diff --git a/src/mol-view/theme/color/uniform.ts b/src/mol-view/theme/color/uniform.ts index 7236e88fba49d5bf97ea2ddb9ae5d9be28534992..d9ab15c4db1cae6a21d6dd7b3376d3cd87817ce1 100644 --- a/src/mol-view/theme/color/uniform.ts +++ b/src/mol-view/theme/color/uniform.ts @@ -4,16 +4,19 @@ * @author Alexander Rose <alexander.rose@weirdbyte.de> */ -import { ColorTheme, ColorThemeProps } from '../color'; +import { ColorTheme, ColorThemeProps, TableLegend } from '../color'; import { Color } from 'mol-util/color'; -const DefaultColor = 0xCCCCCC as Color +const DefaultColor = Color(0xCCCCCC) +const Description = 'Gives everything the same, uniform color.' export function UniformColorTheme(props: ColorThemeProps): ColorTheme { const color = props.value || DefaultColor return { - kind: 'uniform', - color: () => color + granularity: 'uniform', + color: () => color, + description: Description, + legend: TableLegend([['uniform', color]]) } } \ No newline at end of file diff --git a/src/mol-view/theme/color/unit-index.ts b/src/mol-view/theme/color/unit-index.ts index f980de05b69f3747972679297a3eccd76499001a..92d4f53bbe552f2017b873dd7778ee0d6621e3e2 100644 --- a/src/mol-view/theme/color/unit-index.ts +++ b/src/mol-view/theme/color/unit-index.ts @@ -6,35 +6,40 @@ import { ColorScale, Color } from 'mol-util/color'; import { Location } from 'mol-model/location'; -import { Unit, StructureElement, Link } from 'mol-model/structure'; -import { LocationColor } from 'mol-geo/util/color-data'; -import { ColorTheme, ColorThemeProps } from '../color'; +import { StructureElement, Link } from 'mol-model/structure'; +import { ColorTheme, ColorThemeProps, LocationColor } from '../color'; -const DefaultColor = 0xCCCCCC as Color +const DefaultColor = Color(0xCCCCCC) +const Description = 'Gives every unit (single chain or collection of single elements) a unique color based on the position (index) of the unit in the list of units in the structure.' export function UnitIndexColorTheme(props: ColorThemeProps): ColorTheme { - let colorFn: LocationColor + let color: LocationColor + let scale: ColorScale | undefined = undefined if (props.structure) { const { units } = props.structure - const unitCount = units.length - - const scale = ColorScale.create({ domain: [ 0, unitCount ] }) + scale = ColorScale.create({ domain: [ 0, units.length - 1 ] }) + const unitIdColor = new Map<number, Color>() + for (let i = 0, il = units.length; i <il; ++i) { + unitIdColor.set(units[i].id, scale.color(units[i].id)) + } - colorFn = (location: Location): Color => { + color = (location: Location): Color => { if (StructureElement.isLocation(location)) { - return scale.color(Unit.findUnitById(location.unit.id, units)) + return unitIdColor.get(location.unit.id)! } else if (Link.isLocation(location)) { - return scale.color(Unit.findUnitById(location.aUnit.id, units)) + return unitIdColor.get(location.aUnit.id)! } return DefaultColor } } else { - colorFn = () => DefaultColor + color = () => DefaultColor } return { - kind: 'instance', - color: colorFn + granularity: 'instance', + color, + description: Description, + legend: scale ? scale.legend : undefined } } \ No newline at end of file diff --git a/src/mol-view/theme/size.ts b/src/mol-view/theme/size.ts index 059c8b04fbf5a4d3bd767c1c8e04cd86e490a372..f1d829f4e0af499d2bee5196ed9c0d4f3f5360d0 100644 --- a/src/mol-view/theme/size.ts +++ b/src/mol-view/theme/size.ts @@ -11,7 +11,7 @@ import { PhysicalSizeTheme } from './size/physical'; import { UniformSizeTheme } from './size/uniform'; export interface SizeTheme { - kind: SizeType + granularity: SizeType size: LocationSize } diff --git a/src/mol-view/theme/size/physical.ts b/src/mol-view/theme/size/physical.ts index 87c7ccefe0c8ebe717d9269bf032fa077d7b7668..43a885989c0faf14da2a57fbc8b3f02eabc2aede 100644 --- a/src/mol-view/theme/size/physical.ts +++ b/src/mol-view/theme/size/physical.ts @@ -42,7 +42,7 @@ export function PhysicalSizeTheme(props: SizeThemeProps): SizeTheme { } return { - kind: 'group', + granularity: 'group', size: sizeFn } } \ No newline at end of file diff --git a/src/mol-view/theme/size/uniform.ts b/src/mol-view/theme/size/uniform.ts index f9810f2e72591b46a0dfd626a9863adc30f3f9bd..28634f5ee67867bd55df0f95c95158cce5a27dbf 100644 --- a/src/mol-view/theme/size/uniform.ts +++ b/src/mol-view/theme/size/uniform.ts @@ -15,7 +15,7 @@ export function UniformSizeTheme(props: SizeThemeProps): SizeTheme { const size = value * factor return { - kind: 'uniform', + granularity: 'uniform', size: () => size } } \ No newline at end of file diff --git a/src/mol-view/viewer.ts b/src/mol-view/viewer.ts index d3eb6d3d22ebb55237a9c35a776fe7b698307bde..4e4384cbcb6f44bbdd38c4fe0da4b64d5025b12f 100644 --- a/src/mol-view/viewer.ts +++ b/src/mol-view/viewer.ts @@ -38,10 +38,10 @@ interface Viewer { clear: () => void draw: (force?: boolean) => void - requestDraw: () => void + requestDraw: (force?: boolean) => void animate: () => void pick: () => void - identify: (x: number, y: number) => PickingId + identify: (x: number, y: number) => PickingId | undefined mark: (loci: Loci, action: MarkerAction) => void getLoci: (pickingId: PickingId) => Loci @@ -99,12 +99,10 @@ namespace Viewer { const ctx = createContext(gl) const scene = Scene.create(ctx) - // const controls = TrackballControls.create(input, scene, {}) const controls = TrackballControls.create(input, camera, {}) - // const renderer = Renderer.create(ctx, camera, { clearColor: 0xFFFFFF }) const renderer = Renderer.create(ctx, camera, { clearColor: Color(0x000000) }) - const pickScale = 1 / 4 + const pickScale = 1 const pickWidth = Math.round(canvas.width * pickScale) const pickHeight = Math.round(canvas.height * pickScale) const objectPickTarget = createRenderTarget(ctx, pickWidth, pickHeight) @@ -112,7 +110,9 @@ namespace Viewer { const groupPickTarget = createRenderTarget(ctx, pickWidth, pickHeight) let pickDirty = true + let isPicking = false let drawPending = false + let lastRenderTime = -1 const prevProjectionView = Mat4.zero() const prevSceneView = Mat4.zero() @@ -129,9 +129,16 @@ namespace Viewer { } function mark(loci: Loci, action: MarkerAction) { - reprMap.forEach((roSet, repr) => repr.mark(loci, action)) - scene.update() - requestDraw() + let changed = false + reprMap.forEach((roSet, repr) => { + changed = repr.mark(loci, action) || changed + }) + if (changed) { + // console.log('changed') + scene.update() + draw(true) + pickDirty = false // picking buffers should not have changed + } } let nearPlaneDelta = 0 @@ -143,6 +150,7 @@ namespace Viewer { } function render(variant: RenderVariant, force?: boolean) { + if (isPicking) return false // const p = scene.boundingSphere.center // console.log(p[0], p[1], p[2]) // Vec3.set(controls.target, p[0], p[1], p[2]) @@ -156,7 +164,7 @@ namespace Viewer { let fogNear = targetDistance - camera.near + 1 * focusRadius - nearPlaneDelta; let fogFar = targetDistance - camera.near + 2 * focusRadius - nearPlaneDelta; - //console.log(fogNear, fogFar); + // console.log(fogNear, fogFar); camera.fogNear = Math.max(fogNear, 0.1); camera.fogFar = Math.max(fogFar, 0.2); @@ -174,15 +182,14 @@ namespace Viewer { let didRender = false controls.update() camera.update() - scene.update() if (force || !Mat4.areEqual(camera.projectionView, prevProjectionView, EPSILON.Value) || !Mat4.areEqual(scene.view, prevSceneView, EPSILON.Value)) { // console.log('foo', force, prevSceneView, scene.view) Mat4.copy(prevProjectionView, camera.projectionView) Mat4.copy(prevSceneView, scene.view) renderer.render(scene, variant) if (variant === 'draw') { + lastRenderTime = performance.now() pickDirty = true - pick() } didRender = true } @@ -196,14 +203,17 @@ namespace Viewer { drawPending = false } - function requestDraw () { + function requestDraw(force?: boolean) { if (drawPending) return drawPending = true - window.requestAnimationFrame(() => draw(true)) + window.requestAnimationFrame(() => draw(force)) } - function animate () { + function animate() { draw(false) + if (performance.now() - lastRenderTime > 200) { + if (pickDirty) pick() + } window.requestAnimationFrame(() => animate()) } @@ -215,7 +225,11 @@ namespace Viewer { pickDirty = false } - function identify (x: number, y: number): PickingId { + function identify(x: number, y: number): PickingId | undefined { + if (pickDirty) return undefined + + isPicking = true + x *= ctx.pixelRatio y *= ctx.pixelRatio y = canvas.height - y // flip y @@ -236,6 +250,8 @@ namespace Viewer { ctx.readPixels(xp, yp, 1, 1, buffer) const groupId = decodeIdRGBA(buffer[0], buffer[1], buffer[2]) + isPicking = false + return { objectId, instanceId, groupId } } @@ -268,6 +284,7 @@ namespace Viewer { } reprMap.set(repr, newRO) reprCount.next(reprMap.size) + scene.update() }, remove: (repr: Representation<any>) => { const renderObjectSet = reprMap.get(repr) @@ -275,6 +292,7 @@ namespace Viewer { renderObjectSet.forEach(o => scene.remove(o)) reprMap.delete(repr) reprCount.next(reprMap.size) + scene.update() } }, update: () => scene.update(),