diff --git a/package.json b/package.json index 531f60c9193d20ca9a1fff0d9c0117874c172c19..ecdd2c6f7f75598a35e945491e0a491b2c30d118 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,6 @@ "test": "jest", "build-viewer": "webpack build/node_modules/apps/viewer/index.js --mode development -o build/viewer/index.js", "watch-viewer": "webpack build/node_modules/apps/viewer/index.js -w --mode development -o build/viewer/index.js", - "build-canvas": "webpack build/node_modules/apps/canvas/index.js --mode development -o build/canvas/index.js", - "watch-canvas": "webpack build/node_modules/apps/canvas/index.js -w --mode development -o build/canvas/index.js", "build-ms-query": "webpack build/node_modules/apps/model-server-query/index.js --mode development -o build/model-server-query/index.js", "watch-ms-query": "webpack build/node_modules/apps/model-server-query/index.js -w --mode development -o build/model-server-query/index.js", "model-server": "node build/node_modules/servers/model/server.js", diff --git a/src/apps/canvas/app.ts b/src/apps/canvas/app.ts deleted file mode 100644 index 2dd92e472497572264c5a63c230c2199aeb5766d..0000000000000000000000000000000000000000 --- a/src/apps/canvas/app.ts +++ /dev/null @@ -1,146 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - */ - -import { Canvas3D } from 'mol-canvas3d/canvas3d'; -import { getCifFromUrl, getModelsFromMmcif, getCifFromFile, getCcp4FromUrl, getVolumeFromCcp4, getCcp4FromFile, getVolumeFromVolcif } from './util'; -import { StructureView } from './structure-view'; -import { BehaviorSubject } from 'rxjs'; -import { CifBlock } from 'mol-io/reader/cif'; -import { VolumeView } from './volume-view'; -import { Ccp4File } from 'mol-io/reader/ccp4/schema'; -import { Progress } from 'mol-task'; -import { ColorTheme } from 'mol-theme/color'; -import { SizeTheme } from 'mol-theme/size'; -import { StructureRepresentationRegistry } from 'mol-repr/structure/registry'; -import { VolumeRepresentationRegistry } from 'mol-repr/volume/registry'; - -export class App { - canvas3d: Canvas3D - container: HTMLDivElement | null = null; - canvas: HTMLCanvasElement | null = null; - structureView: StructureView | null = null; - volumeView: VolumeView | null = null; - - structureLoaded: BehaviorSubject<StructureView | null> = new BehaviorSubject<StructureView | null>(null) - volumeLoaded: BehaviorSubject<VolumeView | null> = new BehaviorSubject<VolumeView | null>(null) - - colorThemeRegistry = new ColorTheme.Registry() - sizeThemeRegistry = new SizeTheme.Registry() - structureRepresentationRegistry = new StructureRepresentationRegistry() - volumeRepresentationRegistry = new VolumeRepresentationRegistry() - - initViewer(_canvas: HTMLCanvasElement, _container: HTMLDivElement) { - this.canvas = _canvas - this.container = _container - - try { - this.canvas3d = Canvas3D.create(this.canvas, this.container) - this.canvas3d.animate() - return true - } catch (e) { - console.error(e) - return false - } - } - - setStatus(msg: string) { - - } - - private taskCount = 0 - taskCountChanged = new BehaviorSubject({ count: 0, info: '' }) - - private changeTaskCount(delta: number, info = '') { - this.taskCount += delta - this.taskCountChanged.next({ count: this.taskCount, info }) - } - - async runTask<T>(promise: Promise<T>, info: string) { - this.changeTaskCount(1, info) - let result: T - try { - result = await promise - } finally { - this.changeTaskCount(-1) - } - return result - } - - log(progress: Progress) { - console.log(Progress.format(progress)) - } - - get reprCtx () { - return { - webgl: this.canvas3d.webgl, - colorThemeRegistry: this.colorThemeRegistry, - sizeThemeRegistry: this.sizeThemeRegistry - } - } - - // - - async loadMmcif(cif: CifBlock, assemblyId?: string) { - const models = await this.runTask(getModelsFromMmcif(cif), 'Build models') - this.structureView = await this.runTask(StructureView(this, this.canvas3d, models, { assemblyId }), 'Init structure view') - this.structureLoaded.next(this.structureView) - } - - async loadPdbIdOrMmcifUrl(idOrUrl: string, options?: { assemblyId?: string, binary?: boolean }) { - if (this.structureView) this.structureView.destroy(); - const url = idOrUrl.length <= 4 ? `https://files.rcsb.org/download/${idOrUrl}.cif` : idOrUrl; - const cif = await this.runTask(getCifFromUrl(url, options ? !!options.binary : false), 'Load mmCIF from URL') - this.loadMmcif(cif.blocks[0], options ? options.assemblyId : void 0) - } - - async loadMmcifFile(file: File) { - if (this.structureView) this.structureView.destroy(); - const binary = /\.bcif$/.test(file.name); - const cif = await this.runTask(getCifFromFile(file, binary), 'Load mmCIF from file') - this.loadMmcif(cif.blocks[0]) - } - - // - - async loadCcp4(ccp4: Ccp4File) { - const volume = await this.runTask(getVolumeFromCcp4(ccp4), 'Get Volume') - this.volumeView = await this.runTask(VolumeView(this, this.canvas3d, volume), 'Init volume view') - this.volumeLoaded.next(this.volumeView) - } - - async loadCcp4File(file: File) { - if (this.volumeView) this.volumeView.destroy(); - const ccp4 = await this.runTask(getCcp4FromFile(file), 'Load CCP4 from file') - this.loadCcp4(ccp4) - } - - async loadCcp4Url(url: string) { - if (this.volumeView) this.volumeView.destroy(); - const ccp4 = await this.runTask(getCcp4FromUrl(url), 'Load CCP4 from URL') - this.loadCcp4(ccp4) - } - - // - - async loadVolcif(cif: CifBlock) { - const volume = await this.runTask(getVolumeFromVolcif(cif), 'Get Volume') - this.volumeView = await this.runTask(VolumeView(this, this.canvas3d, volume), 'Init volume view') - this.volumeLoaded.next(this.volumeView) - } - - async loadVolcifFile(file: File) { - if (this.volumeView) this.volumeView.destroy(); - const binary = /\.bcif$/.test(file.name); - const cif = await this.runTask(getCifFromFile(file, binary), 'Load volCif from file') - this.loadVolcif(cif.blocks[1]) - } - - async loadVolcifUrl(url: string, binary?: boolean) { - if (this.volumeView) this.volumeView.destroy(); - const cif = await this.runTask(getCifFromUrl(url, binary), 'Load volCif from URL') - this.loadVolcif(cif.blocks[1]) - } -} \ No newline at end of file diff --git a/src/apps/canvas/assembly-symmetry.ts b/src/apps/canvas/assembly-symmetry.ts deleted file mode 100644 index e43357c196bd3f16120d60250bd10f502ce5f4e0..0000000000000000000000000000000000000000 --- a/src/apps/canvas/assembly-symmetry.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * 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/geometry/mesh/mesh-builder'; -import { Tensor } from 'mol-math/linear-algebra'; -import { addSphere } from 'mol-geo/geometry/mesh/builder/sphere'; -import { addCylinder } from 'mol-geo/geometry/mesh/builder/cylinder'; -import { Shape } from 'mol-model/shape'; -import { ColorTheme } from 'mol-theme/color'; -import { Location } from 'mol-model/location'; -import { StructureElement, Unit, StructureProperties } from 'mol-model/structure'; - -export function getAxesShape(symmetryId: number, assemblySymmetry: AssemblySymmetry) { - const s = assemblySymmetry.db.rcsb_assembly_symmetry - const symmetry = Table.pickRow(s, i => s.id.value(i) === symmetryId) - if (!symmetry) return - - const axes = assemblySymmetry.getAxes(symmetryId) - 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 ${symmetry.kind} ${symmetry.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.label_asym_id - case Unit.Kind.Spheres: - case Unit.Kind.Gaussians: - return StructureProperties.coarse.asym_id - } -} - -function clusterMemberKey (asym_id: string, oper_list_ids: string[]) { - return `${asym_id}-${oper_list_ids.join('x')}` -} - -export function getClusterColorTheme(symmetryId: number, assemblySymmetry: AssemblySymmetry): ColorTheme { - const DefaultColor = Color(0xCCCCCC) - const s = assemblySymmetry.db.rcsb_assembly_symmetry - const symmetry = Table.pickRow(s, i => s.id.value(i) === symmetryId) - if (!symmetry) return { granularity: 'uniform', color: () => DefaultColor, props: {} } - - const clusters = assemblySymmetry.getClusters(symmetryId) - if (!clusters._rowCount) return { granularity: 'uniform', color: () => DefaultColor, props: {} } - - const clusterByMember = new Map<string, number>() - for (let i = 0, il = clusters._rowCount; i < il; ++i) { - const clusterMembers = assemblySymmetry.getClusterMembers(clusters.id.value(i)) - for (let j = 0, jl = clusterMembers._rowCount; j < jl; ++j) { - const asym_id = clusterMembers.asym_id.value(j) - const oper_list_ids = clusterMembers.pdbx_struct_oper_list_ids.value(j) - clusterByMember.set(clusterMemberKey(asym_id, oper_list_ids), i) - } - } - const scale = ColorScale.create({ domain: [ 0, clusters._rowCount - 1 ] }) - - return { - granularity: 'instance', - color: (location: Location): Color => { - if (StructureElement.isLocation(location)) { - const asym_id = getAsymId(location.unit) - const ns = location.unit.conformation.operator.name.split('-') - const oper_list_ids = ns.length === 2 ? ns[1].split('x') : [] - const cluster = clusterByMember.get(clusterMemberKey(asym_id(location), oper_list_ids)) - return cluster !== undefined ? scale.color(cluster) : DefaultColor - } - return DefaultColor - }, - props: {} - } -} \ No newline at end of file diff --git a/src/apps/canvas/component/app.tsx b/src/apps/canvas/component/app.tsx deleted file mode 100644 index 5d5c388507d29024e39d6c2a2fdd098f792c316a..0000000000000000000000000000000000000000 --- a/src/apps/canvas/component/app.tsx +++ /dev/null @@ -1,130 +0,0 @@ -/** - * 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'; -import { Examples } from '../examples'; -import { VolumeViewComponent } from './volume-view'; -import { VolumeView } from '../volume-view'; - -export interface AppProps { - app: App -} - -export interface AppState { - structureView: StructureView | null, - volumeView: VolumeView | null, - mmcifBinary: boolean, - volcifBinary: boolean -} - -export class AppComponent extends React.Component<AppProps, AppState> { - state = { - structureView: this.props.app.structureView, - volumeView: this.props.app.volumeView, - mmcifBinary: false, - volcifBinary: true - } - - componentDidMount() { - this.props.app.structureLoaded.subscribe((structureView) => { - this.setState({ structureView: this.props.app.structureView }) - }) - this.props.app.volumeLoaded.subscribe((volumeView) => { - this.setState({ volumeView: this.props.app.volumeView }) - }) - } - - render() { - const { structureView, volumeView } = 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 or URL</span> - <input type='checkbox' checked={this.state.mmcifBinary} onChange={e => this.setState({ mmcifBinary: e.target.checked })} /> Binary<br /> - <input - style={{ width: '100%' }} - type='text' - onKeyDown={e => { - if (e.keyCode === 13) { - const value = e.currentTarget.value.trim() - if (value) { - this.props.app.loadPdbIdOrMmcifUrl(value, { binary: this.state.mmcifBinary }) - } - } - }} - /> - </div> - <div> - <span>Load CIF file </span> - <input - accept='*.cif' - type='file' - onChange={e => { - if (e.target.files) this.props.app.loadMmcifFile(e.target.files[0]) - }} - /> - </div> - <div> - <span>Load CCP4/MRC file </span> - <input - accept='*.ccp4,*.mrc, *.map' - type='file' - onChange={e => { - if (e.target.files) this.props.app.loadCcp4File(e.target.files[0]) - }} - /> - </div> - <div style={{marginTop: '10px'}}> - <span>Load DensityServer URL</span> - <input type='checkbox' checked={this.state.volcifBinary} onChange={e => this.setState({ volcifBinary: e.target.checked })} /> Binary<br /> - <input - style={{ width: '100%' }} - type='text' - onKeyDown={e => { - if (e.keyCode === 13) { - const value = e.currentTarget.value.trim() - if (value) { - this.props.app.loadVolcifUrl(value, this.state.volcifBinary) - } - } - }} - /> - </div> - <div> - <span>Load example </span> - <select - style={{width: '200px'}} - onChange={e => { - this.props.app.loadPdbIdOrMmcifUrl(e.target.value) - }} - > - <option value=''></option> - {Examples.map(({label, id, description}, i) => { - return <option key={i} value={id}>{`${label ? label : id} - ${description}`}</option> - })} - </select> - </div> - <hr/> - <div style={{marginBottom: '10px'}}> - {structureView ? <StructureViewComponent structureView={structureView} /> : ''} - </div> - <hr/> - <div style={{marginBottom: '10px'}}> - {volumeView ? <VolumeViewComponent volumeView={volumeView} /> : ''} - </div> - </div> - </div>; - } -} \ No newline at end of file diff --git a/src/apps/canvas/component/representation.tsx b/src/apps/canvas/component/representation.tsx deleted file mode 100644 index 7d395d5e9828882b9c26aa699339ef6a6acdac0d..0000000000000000000000000000000000000000 --- a/src/apps/canvas/component/representation.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/** - * 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 { Canvas3D } from 'mol-canvas3d/canvas3d'; -import { App } from '../app'; -import { ParamDefinition as PD } from 'mol-util/param-definition'; -import { Representation } from 'mol-repr/representation'; -import { ParametersComponent } from 'mol-app/component/parameters'; - -export interface RepresentationComponentProps<P extends PD.Params> { - app: App - canvas3d: Canvas3D - repr: Representation<P> -} - -export interface RepresentationComponentState { - label: string - reprParams: PD.Params - reprProps: Readonly<{}> -} - -export class RepresentationComponent<P extends PD.Params> extends React.Component<RepresentationComponentProps<P>, RepresentationComponentState> { - - private stateFromRepr(repr: Representation<P>) { - return { - label: repr.label, - reprParams: repr.params, - reprProps: repr.props - } - } - - componentWillMount() { - this.setState(this.stateFromRepr(this.props.repr)) - } - - async onChange(k: string, v: any) { - await this.props.app.runTask(this.props.repr.createOrUpdate(this.props.app.reprCtx, { [k]: v }).run( - progress => this.props.app.log(progress) - ), 'Representation Update') - this.setState(this.stateFromRepr(this.props.repr)) - } - - render() { - const { label, reprParams, reprProps } = this.state - // let colorTheme: ColorTheme | undefined = undefined - // if ('colorTheme' in reprProps) { - // colorTheme = ColorTheme(getColorThemeProps(reprProps)) - // } - - return <div> - <div> - <h4>{label}</h4> - </div> - <div> - <ParametersComponent - params={reprParams} - values={reprProps} - onChange={(k, v) => this.onChange(k as string, v)} - /> - </div> - {/* { colorTheme !== undefined ? <ColorThemeComponent colorTheme={colorTheme} /> : '' } */} - </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 deleted file mode 100644 index 6cd0828674ea1d63321f71e07b944c023bfa8614..0000000000000000000000000000000000000000 --- a/src/apps/canvas/component/structure-view.tsx +++ /dev/null @@ -1,190 +0,0 @@ -/** - * 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 { RepresentationComponent } from './representation'; -import { Representation } from 'mol-repr/representation'; -import { StructureRepresentation } from 'mol-repr/structure/representation'; - -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, - structure: sv.structure, - 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' - defaultValue={this.state.modelId.toString()} - 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}> - <RepresentationComponent - repr={structureRepresentations[k] as Representation<any>} - canvas3d={structureView.canvas3d} - app={structureView.app} - /> - </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 deleted file mode 100644 index ee235026150b35891dc4599336cc60f37e534931..0000000000000000000000000000000000000000 --- a/src/apps/canvas/component/viewport.tsx +++ /dev/null @@ -1,187 +0,0 @@ -/** - * 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/geometry/marker-data'; -import { EmptyLoci, Loci, areLociEqual } from 'mol-model/loci'; -import { labelFirst } from 'mol-theme/label'; -import { ButtonsType } from 'mol-util/input/input-observer'; -import { throttleTime } from 'rxjs/operators' -import { Camera } from 'mol-canvas3d/camera'; -import { ColorParamComponent } from 'mol-app/component/parameter/color'; -import { Color } from 'mol-util/color'; -import { ParamDefinition as PD } from 'mol-util/param-definition' - -interface ViewportProps { - app: App -} - -interface ViewportState { - noWebGl: boolean - pickingInfo: string - taskInfo: string - cameraMode: Camera.Mode - backgroundColor: Color -} - -const BackgroundColorParam = PD.Color(Color(0x000000), { label: 'Background Color' }) - -export class Viewport extends React.Component<ViewportProps, ViewportState> { - private container: HTMLDivElement | null = null; - private canvas: HTMLCanvasElement | null = null; - - state: ViewportState = { - noWebGl: false, - pickingInfo: '', - taskInfo: '', - cameraMode: 'perspective', - backgroundColor: Color(0x000000) - }; - - handleResize() { - this.props.app.canvas3d.handleResize() - } - - componentDidMount() { - if (!this.canvas || !this.container || !this.props.app.initViewer(this.canvas, this.container)) { - this.setState({ noWebGl: true }); - } - this.handleResize() - - this.setState({ - cameraMode: this.props.app.canvas3d.props.cameraMode, - backgroundColor: this.props.app.canvas3d.props.backgroundColor - }) - - const canvas3d = this.props.app.canvas3d - - canvas3d.input.resize.subscribe(() => this.handleResize()) - - let prevHighlightLoci: Loci = EmptyLoci - // TODO can the 'only ever have one extra element in the queue' functionality be done with rxjs? - let highlightQueueLength = 0 - canvas3d.input.move.pipe(throttleTime(50)).subscribe(async ({x, y, inside, buttons}) => { - if (!inside || buttons || highlightQueueLength > 2) return - ++highlightQueueLength - const p = await canvas3d.identify(x, y) - --highlightQueueLength - if (p) { - const { loci } = canvas3d.getLoci(p) - - if (!areLociEqual(loci, prevHighlightLoci)) { - canvas3d.mark(prevHighlightLoci, MarkerAction.RemoveHighlight) - canvas3d.mark(loci, MarkerAction.Highlight) - prevHighlightLoci = loci - - const label = labelFirst(loci) - const pickingInfo = `${label}` - this.setState({ pickingInfo }) - } - } - }) - - canvas3d.input.click.subscribe(async ({x, y, buttons}) => { - if (buttons !== ButtonsType.Flag.Primary) return - const p = await canvas3d.identify(x, y) - if (p) { - const { loci } = canvas3d.getLoci(p) - canvas3d.mark(loci, MarkerAction.Toggle) - } - }) - - this.props.app.taskCountChanged.subscribe(({ count, info }) => { - this.setState({ taskInfo: count > 0 ? 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.pickingInfo} - </div> - <div - style={{ - position: 'absolute', - bottom: 10, - right: 10, - padding: 10, - color: 'lightgrey', - background: 'rgba(0, 0, 0, 0.2)' - }} - > - <div> - <span>Camera Mode </span> - <select - value={this.state.cameraMode} - style={{width: '150'}} - onChange={e => { - const p = { cameraMode: e.target.value as Camera.Mode } - this.props.app.canvas3d.setProps(p) - this.setState(p) - }} - > - <option value='perspective'>Perspective</option> - <option value='orthographic'>Orthographic</option> - </select> - </div> - <ColorParamComponent - label={BackgroundColorParam.label || ''} - param={BackgroundColorParam} - value={this.state.backgroundColor} - onChange={value => { - const p = { backgroundColor: value } - this.props.app.canvas3d.setProps(p) - this.setState(p) - }} - /> - </div> - { this.state.taskInfo ? - <div - style={{ - position: 'absolute', - top: 10, - right: 10, - padding: 10, - color: 'lightgrey', - background: 'rgba(0, 0, 0, 0.2)' - }} - > - {this.state.taskInfo} - </div> - : '' } - </div>; - } -} \ No newline at end of file diff --git a/src/apps/canvas/component/volume-view.tsx b/src/apps/canvas/component/volume-view.tsx deleted file mode 100644 index 6a9f309612f8ac006a8aff94e8f7d403e197c9f6..0000000000000000000000000000000000000000 --- a/src/apps/canvas/component/volume-view.tsx +++ /dev/null @@ -1,105 +0,0 @@ -/** - * 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 { RepresentationComponent } from './representation'; -import { Representation } from 'mol-repr/representation'; -import { VolumeView } from '../volume-view'; -import { VolumeRepresentation } from 'mol-repr/volume/representation'; - -export interface VolumeViewComponentProps { - volumeView: VolumeView -} - -export interface VolumeViewComponentState { - volumeView: VolumeView - label: string - active: { [k: string]: boolean } - volumeRepresentations: { [k: string]: VolumeRepresentation<any> } -} - -export class VolumeViewComponent extends React.Component<VolumeViewComponentProps, VolumeViewComponentState> { - state = this.stateFromVolumeView(this.props.volumeView) - - private stateFromVolumeView(vv: VolumeView) { - return { - volumeView: vv, - label: vv.label, - volume: vv.volume, - active: vv.active, - volumeRepresentations: vv.volumeRepresentations - } - } - - componentWillMount() { - this.setState(this.stateFromVolumeView(this.props.volumeView)) - } - - componentDidMount() { - const vv = this.props.volumeView - - this.props.volumeView.updated.subscribe(() => this.setState({ - volumeRepresentations: vv.volumeRepresentations - })) - } - - componentWillReceiveProps(nextProps: VolumeViewComponentProps) { - if (nextProps.volumeView !== this.props.volumeView) { - this.setState(this.stateFromVolumeView(nextProps.volumeView)) - - nextProps.volumeView.updated.subscribe(() => this.setState({ - volumeRepresentations: nextProps.volumeView.volumeRepresentations - })) - } - } - - // async update(state: Partial<VolumeViewComponentState>) { - // const vv = this.state.volumeView - // this.setState(this.stateFromVolumeView(vv)) - // } - - render() { - const { volumeView, label, active, volumeRepresentations } = this.state - - return <div> - <div> - <h2>{label}</h2> - </div> - <div> - <div> - <h4>Active</h4> - { Object.keys(active).map((k, i) => { - return <div key={i}> - <input - type='checkbox' - checked={active[k]} - onChange={(e) => { - volumeView.setVolumeRepresentation(k, e.target.checked) - }} - /> {k} - </div> - } ) } - </div> - <div> - <h3>Volume Representations</h3> - { Object.keys(volumeRepresentations).map((k, i) => { - if (active[k]) { - return <div key={i}> - <RepresentationComponent - repr={volumeRepresentations[k] as Representation<any>} - canvas3d={volumeView.viewer} - app={volumeView.app} - /> - </div> - } else { - return '' - } - } ) } - </div> - </div> - </div>; - } -} \ No newline at end of file diff --git a/src/apps/canvas/examples.ts b/src/apps/canvas/examples.ts deleted file mode 100644 index d998f9b6bc254817cb08592a6833307a7e7f7fe2..0000000000000000000000000000000000000000 --- a/src/apps/canvas/examples.ts +++ /dev/null @@ -1,183 +0,0 @@ - - -export interface Example { - label?: string - id: string - description: string -} - -export const Examples: Example[] = [ - { - id: '1jj2', - description: 'ribosome' - }, - { - id: '1grm', - description: 'helix-like sheets' - }, - { - id: '4umt', - description: 'ligand has bond with order 3' - }, - { - id: '1crn', - description: 'small' - }, - { - id: '1hrv', - description: 'viral assembly' - }, - { - id: '1rb8', - description: 'virus' - }, - { - id: '1blu', - description: 'metal coordination' - }, - { - id: '3pqr', - description: 'inter unit bonds, two polymer chains, ligands, water, carbohydrates linked to protein' - }, - { - id: '4v5a', - description: 'ribosome' - }, - { - id: '6h7w', - description: 'retromer assembled on membrane' - }, - { - id: '3j3q', - description: '...' - }, - { - id: '5gob', - description: 'D-aminoacids' - }, - { - id: '2np2', - description: 'dna' - }, - { - id: '1d66', - description: 'dna' - }, - { - id: '9dna', - description: 'A form dna' - }, - { - id: '1bna', - description: 'B form dna' - }, - { - id: '199d', - description: 'C form dna' - }, - { - id: '4lb6', - description: 'Z form dna' - }, - { - id: '1egk', - description: '4-way dna-rna junction' - }, - { - id: '1y26', - description: 'rna' - }, - { - id: '1xv6', - description: 'rna, modified nucleotides' - }, - { - id: '3bbm', - description: 'rna with linker' - }, - { - id: '1euq', - description: 't-rna' - }, - { - id: '2e2i', - description: 'rna, dna, protein' - }, - { - id: '1gfl', - description: 'GFP, flourophore has carbonyl oxygen removed' - }, - { - id: '1sfi', - description: 'contains cyclic peptid' - }, - { - id: '3sn6', - description: 'discontinuous chains' - }, - { - id: '2zex', - description: 'contains carbohydrate polymer' - }, - { - id: '3sgj', - description: 'contains carbohydrate polymer' - }, - { - id: '3ina', - description: 'contains GlcN and IdoA' - }, - { - id: '1umz', - description: 'contains Xyl (Xyloglucan)' - }, - { - id: '1mfb', - description: 'contains Abe' - }, - { - id: '2gdu', - description: 'contains sucrose' - }, - { - id: '2fnc', - description: 'contains maltotriose' - }, - { - id: '4zs9', - description: 'contains raffinose' - }, - { - id: '2yft', - description: 'contains kestose' - }, - { - id: '2b5t', - description: 'contains large carbohydrate polymer' - }, - { - id: '1b5f', - description: 'contains carbohydrate with alternate locations' - }, - { - id: '5u0q', - description: 'mixed dna/rna in same polymer' - }, - { - id: '1xj9', - description: 'PNA (peptide nucleic acid)' - }, - { - id: '5eme', - description: 'PNA (peptide nucleic acid) and RNA' - }, - { - id: '2X3T', - description: 'temp' - }, - { - label: 'ModelServer/1cbs/full', - id: 'http://localhost:1337/ModelServer/query?%7B%22id%22%3A%221cbs%22%2C%22name%22%3A%22full%22%7D', - description: '1cbs from model server' - } -] \ No newline at end of file diff --git a/src/apps/canvas/index.html b/src/apps/canvas/index.html deleted file mode 100644 index bc9f73aa6dfcb30b9a82e79ed5fa3ce5cdf07e24..0000000000000000000000000000000000000000 --- a/src/apps/canvas/index.html +++ /dev/null @@ -1,33 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> - <head> - <meta charset="utf-8" /> - <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"> - <title>Mol* 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="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 deleted file mode 100644 index 16c12a7201d64423e7d4f3040319728c2dc17a96..0000000000000000000000000000000000000000 --- a/src/apps/canvas/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * 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 * as ReactDOM from 'react-dom' - -import './index.html' - -import { App } from './app'; -import { AppComponent } from './component/app'; -import { urlQueryParameter } from 'mol-util/url-query'; - -const elm = document.getElementById('app') as HTMLElement -if (!elm) throw new Error('Can not find element with id "app".') - -const app = new App() -ReactDOM.render(React.createElement(AppComponent, { app }), elm); - -const assemblyId = urlQueryParameter('assembly') -const pdbId = urlQueryParameter('pdb') -if (pdbId) app.loadPdbIdOrMmcifUrl(pdbId, { assemblyId }) - -// app.loadPdbIdOrMmcifUrl('http://localhost:8091/ngl/data/1crn.cif') - -// app.loadPdbIdOrMmcifUrl('3pqr') -// app.loadCcp4Url('http://localhost:8091/ngl/data/3pqr-mode0.ccp4') - -app.loadPdbIdOrMmcifUrl('1lee') -app.loadCcp4Url('http://localhost:8091/ngl/data/1lee.ccp4') - -// app.loadPdbIdOrMmcifUrl('6DRV') -// app.loadCcp4Url('http://localhost:8091/ngl/data/betaGal.mrc') - -// app.loadPdbIdOrMmcifUrl('3pqr') -// app.loadVolcifUrl('https://webchem.ncbr.muni.cz/DensityServer/x-ray/3pqr/cell?space=fractional', true) - -// app.loadPdbIdOrMmcifUrl('5ire') -// app.loadVolcifUrl('https://webchem.ncbr.muni.cz/DensityServer/em/emd-8116/cell?space=cartesian&detail=6', true) - -// app.loadPdbIdOrMmcifUrl('5gag') -// app.loadVolcifUrl('https://webchem.ncbr.muni.cz/DensityServer/em/emd-8003/cell?detail=3', true) - -// app.loadPdbIdOrMmcifUrl('http://localhost:8091/test/pdb-dev/carb/1B5F-carb.cif') -// app.loadPdbIdOrMmcifUrl('http://localhost:8091/test/pdb-dev/carb/2HYV-carb.cif') -// app.loadPdbIdOrMmcifUrl('http://localhost:8091/test/pdb-dev/carb/2WMG-carb.cif') -// app.loadPdbIdOrMmcifUrl('http://localhost:8091/test/pdb-dev/carb/5KDS-carb.cif') \ No newline at end of file diff --git a/src/apps/canvas/structure-view.ts b/src/apps/canvas/structure-view.ts deleted file mode 100644 index 3d2c20fc46ff4b7106ec7069e48389550b44a002..0000000000000000000000000000000000000000 --- a/src/apps/canvas/structure-view.ts +++ /dev/null @@ -1,369 +0,0 @@ -/** - * 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 { getStructureFromModel } from './util'; -import { AssemblySymmetry } from 'mol-model-props/rcsb/symmetry'; -import { getAxesShape } from './assembly-symmetry'; -import { Canvas3D } from 'mol-canvas3d/canvas3d'; -// 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 { BehaviorSubject } from 'rxjs'; -import { App } from './app'; -import { StructureRepresentation } from 'mol-repr/structure/representation'; -import { ShapeRepresentation, ShapeParams } from 'mol-repr/shape/representation'; - -export interface StructureView { - readonly app: App - readonly canvas3d: Canvas3D - - 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<ShapeParams> - - 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(app: App, canvas3d: Canvas3D, models: ReadonlyArray<Model>, props: StructureViewProps = {}): Promise<StructureView> { - const active: { [k: string]: boolean } = { - 'cartoon': true, - 'ball-and-stick': true, - // point: false, - // surface: false, - // carbohydrate: false, - // spacefill: false, - // distanceRestraint: false, - // symmetryAxes: true, - // polymerSphere: false, - } - - const structureRepresentations: { [k: string]: StructureRepresentation<any> } = {} - - 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 app.runTask(AssemblySymmetry.attachFromCifOrAPI(models[modelId]), 'Load symmetry annotation') - 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 = 'deposited' - } else { - assemblyId = '-1' - } - await getStructure() - await setSymmetryFeature(newSymmetryFeatureId) - } - - function getAssemblyIds() { - const assemblyIds: { id: string, label: string }[] = [ - { id: 'deposited', label: 'deposited' } - ] - 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 s = assemblySymmetry.getSymmetries(assemblyId) - if (s._rowCount) { - symmetryFeatureId = s.id.value(0) - } else { - symmetryFeatureId = -1 - } - } else { - symmetryFeatureId = -1 - } - await createSymmetryRepr() - } - - function getSymmetryFeatureIds() { - const symmetryFeatureIds: { id: number, label: string }[] = [] - if (assemblySymmetry) { - const symmetries = assemblySymmetry.getSymmetries(assemblyId) - for (let i = 0, il = symmetries._rowCount; i < il; ++i) { - const id = symmetries.id.value(i) - const kind = symmetries.kind.value(i) - const type = symmetries.type.value(i) - const stoichiometry = symmetries.stoichiometry.value(i) - const label = `${id}: ${kind} ${type} ${stoichiometry}` - symmetryFeatureIds.push({ id, label }) - } - } - return symmetryFeatureIds - } - - async function getStructure() { - if (model) structure = await app.runTask(getStructureFromModel(model, assemblyId), 'Build structure') - if (model && structure) { - label = `${model.label} - Assembly ${assemblyId}` - } else { - label = '' - } - await createStructureRepr() - } - - async function createStructureRepr() { - if (structure) { - console.log('createStructureRepr') - for (const k in active) { - if (active[k]) { - let repr: StructureRepresentation - if (structureRepresentations[k]) { - repr = structureRepresentations[k] - } else { - const provider = app.structureRepresentationRegistry.get(k) - repr = provider.factory(provider.getParams) - structureRepresentations[k] = repr - canvas3d.add(repr) - } - await app.runTask(repr.createOrUpdate(app.reprCtx, {}, structure).run( - progress => app.log(progress) - ), 'Create/update representation') - } else { - if (structureRepresentations[k]) { - canvas3d.remove(structureRepresentations[k]) - structureRepresentations[k].destroy() - delete structureRepresentations[k] - } - } - } - - canvas3d.camera.setState({ target: 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() - } - - canvas3d.add(polymerSphere) - - updated.next(null) - canvas3d.requestDraw(true) - console.log('stats', canvas3d.stats) - } - - async function createSymmetryRepr() { - if (assemblySymmetry) { - const symmetries = assemblySymmetry.getSymmetries(assemblyId) - if (symmetries._rowCount) { - const axesShape = getAxesShape(symmetryFeatureId, assemblySymmetry) - if (axesShape) { - // const colorTheme = getClusterColorTheme(symmetryFeatureId, assemblySymmetry) - // await structureRepresentations['cartoon'].createOrUpdate({ - // colorTheme: 'custom', - // colorFunction: colorTheme.color, - // colorGranularity: colorTheme.granularity, - // }).run() - await symmetryAxes.createOrUpdate(app.reprCtx, {}, axesShape).run() - canvas3d.add(symmetryAxes) - } else { - canvas3d.remove(symmetryAxes) - } - } else { - canvas3d.remove(symmetryAxes) - } - } else { - canvas3d.remove(symmetryAxes) - } - updated.next(null) - canvas3d.requestDraw(true) - } - - await setModel(0, props.assemblyId, props.symmetryFeatureId) - - return { - app, - canvas3d, - - 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) { - canvas3d.remove(structureRepresentations[k]) - structureRepresentations[k].destroy() - } - canvas3d.remove(polymerSphere) - canvas3d.remove(symmetryAxes) - canvas3d.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 deleted file mode 100644 index e9b7dfdf2b61830f00422f6ffd1ae9d3a3404d52..0000000000000000000000000000000000000000 --- a/src/apps/canvas/util.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - */ - -import { readUrl, readFile, readUrlAsBuffer, readFileAsBuffer } from 'mol-util/read'; -import CIF, { CifBlock } from 'mol-io/reader/cif' -import { Model, Format, StructureSymmetry, Structure } from 'mol-model/structure'; -import CCP4 from 'mol-io/reader/ccp4/parser' -import { FileHandle } from 'mol-io/common/file-handle'; -import { Ccp4File } from 'mol-io/reader/ccp4/schema'; -import { volumeFromCcp4 } from 'mol-model/volume/formats/ccp4'; -import { parseDensityServerData } from 'mol-model/volume'; -// 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 -} - -export async function getCifFromUrl(url: string, binary = false) { - return getCifFromData(await readUrl(url, binary)) -} - -export async function getCifFromFile(file: File, binary = false) { - return getCifFromData(await readFile(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 === 'deposited') { - return Structure.ofModel(model) - } else if (assemblies.find(a => a.id === assembly)) { - return await StructureSymmetry.buildAssembly(Structure.ofModel(model), assembly).run() - } -} - -// - -export async function getCcp4FromUrl(url: string) { - return getCcp4FromData(await readUrlAsBuffer(url)) -} - -export async function getCcp4FromFile(file: File) { - return getCcp4FromData(await readFileAsBuffer(file)) -} - -export async function getCcp4FromData(data: Uint8Array) { - const file = FileHandle.fromBuffer(data) - const parsed = await CCP4(file).run() - if (parsed.isError) throw parsed - return parsed.result -} - -export async function getVolumeFromCcp4(ccp4: Ccp4File) { - return await volumeFromCcp4(ccp4).run() -} - -// - -export async function getVolumeFromVolcif(cif: CifBlock) { - return await parseDensityServerData(CIF.schema.densityServer(cif)).run() -} \ No newline at end of file diff --git a/src/apps/canvas/volume-view.ts b/src/apps/canvas/volume-view.ts deleted file mode 100644 index a0f39495f80aef32f9ad84833791aca5d5605344..0000000000000000000000000000000000000000 --- a/src/apps/canvas/volume-view.ts +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - */ - -import { Canvas3D } from 'mol-canvas3d/canvas3d'; -import { BehaviorSubject } from 'rxjs'; -import { App } from './app'; -import { VolumeData } from 'mol-model/volume'; -import { VolumeRepresentation } from 'mol-repr/volume/representation'; -import { IsosurfaceRepresentation } from 'mol-repr/volume/isosurface-mesh'; -import { DirectVolumeRepresentation } from 'mol-repr/volume/direct-volume'; - -export interface VolumeView { - readonly app: App - readonly viewer: Canvas3D - - readonly label: string - readonly volume: VolumeData - - readonly active: { [k: string]: boolean } - readonly volumeRepresentations: { [k: string]: VolumeRepresentation<any> } - readonly updated: BehaviorSubject<null> - - setVolumeRepresentation(name: string, value: boolean): void - destroy: () => void -} - -interface VolumeViewProps { - -} - -export async function VolumeView(app: App, viewer: Canvas3D, volume: VolumeData, props: VolumeViewProps = {}): Promise<VolumeView> { - const active: { [k: string]: boolean } = { - isosurface: true, - directVolume: false, - } - - const volumeRepresentations: { [k: string]: VolumeRepresentation<any> } = { - isosurface: IsosurfaceRepresentation(), - directVolume: DirectVolumeRepresentation(), - } - - const updated: BehaviorSubject<null> = new BehaviorSubject<null>(null) - - let label: string = 'Volume' - - async function setVolumeRepresentation(k: string, value: boolean) { - active[k] = value - await createVolumeRepr() - } - - async function createVolumeRepr() { - for (const k in volumeRepresentations) { - if (active[k]) { - await app.runTask(volumeRepresentations[k].createOrUpdate(app.reprCtx, {}, volume).run( - progress => app.log(progress) - ), 'Create/update representation') - viewer.add(volumeRepresentations[k]) - } else { - viewer.remove(volumeRepresentations[k]) - } - } - - // const center = Vec3.clone(volume.cell.size) - // Vec3.scale(center, center, 0.5) - // viewer.center(center) - - updated.next(null) - viewer.requestDraw(true) - console.log('stats', viewer.stats) - } - - await createVolumeRepr() - - return { - app, - viewer, - - get label() { return label }, - volume, - - active, - volumeRepresentations, - setVolumeRepresentation, - updated, - - destroy: () => { - for (const k in volumeRepresentations) { - viewer.remove(volumeRepresentations[k]) - volumeRepresentations[k].destroy() - } - viewer.requestDraw(true) - } - } -} \ No newline at end of file diff --git a/src/mol-canvas3d/camera.ts b/src/mol-canvas3d/camera.ts index 9c48d0211022b739dd37f341c37c281916797ba5..7fd63907dece9095c4b802fc5b12bea60d17f327 100644 --- a/src/mol-canvas3d/camera.ts +++ b/src/mol-canvas3d/camera.ts @@ -97,6 +97,7 @@ class Camera implements Object3D { Vec3.setMagnitude(this.deltaDirection, this.state.direction, deltaDistance) if (currentDistance < targetDistance) Vec3.negate(this.deltaDirection, this.deltaDirection) Vec3.add(this.newPosition, this.state.position, this.deltaDirection) + this.setState({ target, position: this.newPosition }) } diff --git a/src/mol-canvas3d/canvas3d.ts b/src/mol-canvas3d/canvas3d.ts index f164a4edb4ab97aa5dc3c49ca8b6e64fc3c9be40..b8d7d83cfb68def49df123cec5ae7a58a349f2a8 100644 --- a/src/mol-canvas3d/canvas3d.ts +++ b/src/mol-canvas3d/canvas3d.ts @@ -35,11 +35,8 @@ export const Canvas3DParams = { cameraMode: PD.Select('perspective', [['perspective', 'Perspective'], ['orthographic', 'Orthographic']]), backgroundColor: PD.Color(Color(0x000000)), // TODO: make this an interval? - clipNear: PD.Numeric(1, { min: 1, max: 100, step: 1 }), - clipFar: PD.Numeric(100, { min: 1, max: 100, step: 1 }), - // TODO: make this an interval? - fogNear: PD.Numeric(50, { min: 1, max: 100, step: 1 }), - fogFar: PD.Numeric(100, { min: 1, max: 100, step: 1 }), + clip: PD.Interval([1, 100], { min: 1, max: 100, step: 1 }), + fog: PD.Interval([50, 100], { min: 1, max: 100, step: 1 }), pickingAlphaThreshold: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }, { description: 'The minimum opacity value needed for an object to be pickable.' }), showBoundingSpheres: PD.Boolean(false, { description: 'Show bounding spheres of render objects.' }), // debug: PD.Group({ @@ -168,13 +165,13 @@ namespace Canvas3D { const cDist = Vec3.distance(camera.state.position, camera.state.target) const bRadius = Math.max(10, scene.boundingSphere.radius) - const nearFactor = (50 - p.clipNear) / 50 - const farFactor = -(50 - p.clipFar) / 50 + const nearFactor = (50 - p.clip[0]) / 50 + const farFactor = -(50 - p.clip[1]) / 50 const near = cDist - (bRadius * nearFactor) const far = cDist + (bRadius * farFactor) - const fogNearFactor = (50 - p.fogNear) / 50 - const fogFarFactor = -(50 - p.fogFar) / 50 + const fogNearFactor = (50 - p.fog[0]) / 50 + const fogFarFactor = -(50 - p.fog[1]) / 50 const fogNear = cDist - (bRadius * fogNearFactor) const fogFar = cDist + (bRadius * fogFarFactor) @@ -321,7 +318,9 @@ namespace Canvas3D { add: (repr: Representation.Any) => { add(repr) - reprUpdatedSubscriptions.set(repr, repr.updated.subscribe(_ => add(repr))) + reprUpdatedSubscriptions.set(repr, repr.updated.subscribe(_ => { + if (!repr.state.syncManually) add(repr) + })) }, remove: (repr: Representation.Any) => { const updatedSubscription = reprUpdatedSubscriptions.get(repr) @@ -378,10 +377,8 @@ namespace Canvas3D { renderer.setClearColor(props.backgroundColor) } - if (props.clipNear !== undefined) p.clipNear = props.clipNear - if (props.clipFar !== undefined) p.clipFar = props.clipFar - if (props.fogNear !== undefined) p.fogNear = props.fogNear - if (props.fogFar !== undefined) p.fogFar = props.fogFar + if (props.clip !== undefined) p.clip = [props.clip[0], props.clip[1]] + if (props.fog !== undefined) p.fog = [props.fog[0], props.fog[1]] if (props.pickingAlphaThreshold !== undefined && props.pickingAlphaThreshold !== renderer.props.pickingAlphaThreshold) { renderer.setPickingAlphaThreshold(props.pickingAlphaThreshold) @@ -396,10 +393,8 @@ namespace Canvas3D { return { cameraMode: camera.state.mode, backgroundColor: renderer.props.clearColor, - clipNear: p.clipNear, - clipFar: p.clipFar, - fogNear: p.fogNear, - fogFar: p.fogFar, + clip: p.clip, + fog: p.fog, pickingAlphaThreshold: renderer.props.pickingAlphaThreshold, showBoundingSpheres: boundingSphereHelper.visible } diff --git a/src/mol-canvas3d/helper/bounding-sphere-helper.ts b/src/mol-canvas3d/helper/bounding-sphere-helper.ts index b3fbe849e241cb242e2f2bce4104626b945ebbe3..dd30418ef777e53a032ebe8fe4f573cdc1b2098f 100644 --- a/src/mol-canvas3d/helper/bounding-sphere-helper.ts +++ b/src/mol-canvas3d/helper/bounding-sphere-helper.ts @@ -27,11 +27,11 @@ export class BoundingSphereHelper { update() { const builder = MeshBuilder.create(1024, 512, this.mesh) if (this.scene.boundingSphere.radius) { - addSphere(builder, this.scene.boundingSphere.center, this.scene.boundingSphere.radius, 3) + addSphere(builder, this.scene.boundingSphere.center, this.scene.boundingSphere.radius, 2) } this.scene.forEach(r => { if (r.boundingSphere.radius) { - addSphere(builder, r.boundingSphere.center, r.boundingSphere.radius, 3) + addSphere(builder, r.boundingSphere.center, r.boundingSphere.radius, 2) } }) this.mesh = builder.getMesh() diff --git a/src/mol-geo/geometry/direct-volume/direct-volume.ts b/src/mol-geo/geometry/direct-volume/direct-volume.ts index a2a660b959bdfcc24a1a7a8b8d13f72cd97c9133..d121e00ceed61b3659185259c688e29fa645eaf3 100644 --- a/src/mol-geo/geometry/direct-volume/direct-volume.ts +++ b/src/mol-geo/geometry/direct-volume/direct-volume.ts @@ -126,6 +126,16 @@ export namespace DirectVolume { } export function updateValues(values: DirectVolumeValues, props: PD.Values<Params>) { + ValueCell.updateIfChanged(values.uIsoValue, props.isoValue) + ValueCell.updateIfChanged(values.uAlpha, props.alpha) + ValueCell.updateIfChanged(values.dUseFog, props.useFog) + ValueCell.updateIfChanged(values.dRenderMode, props.renderMode) + + const controlPoints = getControlPointsFromString(props.controlPoints) + createTransferFunctionTexture(controlPoints, values.tTransferTex) + } + + export function updateBoundingSphere(values: DirectVolumeValues, directVolume: DirectVolume) { const vertices = new Float32Array(values.aPosition.ref.value) transformPositionArray(values.uTransform.ref.value, vertices, 0, vertices.length / 3) const boundingSphere = calculateBoundingSphere( @@ -135,13 +145,5 @@ export namespace DirectVolume { if (!Sphere3D.equals(boundingSphere, values.boundingSphere.ref.value)) { ValueCell.update(values.boundingSphere, boundingSphere) } - - ValueCell.updateIfChanged(values.uIsoValue, props.isoValue) - ValueCell.updateIfChanged(values.uAlpha, props.alpha) - ValueCell.updateIfChanged(values.dUseFog, props.useFog) - ValueCell.updateIfChanged(values.dRenderMode, props.renderMode) - - const controlPoints = getControlPointsFromVec2Array(props.controlPoints) - createTransferFunctionTexture(controlPoints, values.tTransferTex) } } \ No newline at end of file diff --git a/src/mol-geo/geometry/geometry.ts b/src/mol-geo/geometry/geometry.ts index eee463dc14c00e604f72104a60cf03db947ca103..335d2349a9d8c081759dce1ee92aec6ac29d5494 100644 --- a/src/mol-geo/geometry/geometry.ts +++ b/src/mol-geo/geometry/geometry.ts @@ -15,8 +15,6 @@ import { SizeType } from './size-data'; import { Lines } from './lines/lines'; import { ParamDefinition as PD } from 'mol-util/param-definition' import { DirectVolume } from './direct-volume/direct-volume'; -import { BuiltInSizeThemeOptions, getBuiltInSizeThemeParams } from 'mol-theme/size'; -import { BuiltInColorThemeOptions, getBuiltInColorThemeParams } from 'mol-theme/color'; import { Color } from 'mol-util/color'; import { Vec3 } from 'mol-math/linear-algebra'; @@ -67,9 +65,6 @@ export namespace Geometry { selectColor: PD.Color(Color.fromNormalizedRgb(0.2, 1.0, 0.1)), quality: PD.Select<VisualQuality>('auto', VisualQualityOptions), - - colorTheme: PD.Mapped('uniform', BuiltInColorThemeOptions, getBuiltInColorThemeParams), - sizeTheme: PD.Mapped('uniform', BuiltInSizeThemeOptions, getBuiltInSizeThemeParams), } export type Params = typeof Params diff --git a/src/mol-geo/geometry/lines/lines.ts b/src/mol-geo/geometry/lines/lines.ts index 5e2764ce4fb8e6974775dd7d393b0eb115eaeed0..03e67044216b291e60192254ce7e1415eeb80ccb 100644 --- a/src/mol-geo/geometry/lines/lines.ts +++ b/src/mol-geo/geometry/lines/lines.ts @@ -138,21 +138,23 @@ export namespace Lines { } export function updateValues(values: LinesValues, props: PD.Values<Params>) { + Geometry.updateValues(values, props) + ValueCell.updateIfChanged(values.dLineSizeAttenuation, props.lineSizeAttenuation) + } + + export function updateBoundingSphere(values: LinesValues, lines: Lines) { const boundingSphere = Sphere3D.addSphere( calculateBoundingSphere( - values.aStart.ref.value, Math.floor(values.aStart.ref.value.length / 3), + values.aStart.ref.value, lines.lineCount, values.aTransform.ref.value, values.instanceCount.ref.value ), calculateBoundingSphere( - values.aEnd.ref.value, Math.floor(values.aEnd.ref.value.length / 3), + values.aEnd.ref.value, lines.lineCount, values.aTransform.ref.value, values.instanceCount.ref.value ), ) if (!Sphere3D.equals(boundingSphere, values.boundingSphere.ref.value)) { ValueCell.update(values.boundingSphere, boundingSphere) } - - Geometry.updateValues(values, props) - ValueCell.updateIfChanged(values.dLineSizeAttenuation, props.lineSizeAttenuation) } } \ No newline at end of file diff --git a/src/mol-geo/geometry/mesh/builder/sheet.ts b/src/mol-geo/geometry/mesh/builder/sheet.ts index c0d7a473318356bcf891dcb8088d45411065e91a..001e45a233a2866e2d4a2a6184b92ee7118019b6 100644 --- a/src/mol-geo/geometry/mesh/builder/sheet.ts +++ b/src/mol-geo/geometry/mesh/builder/sheet.ts @@ -15,6 +15,8 @@ const tV = Vec3.zero() const horizontalVector = Vec3.zero() const verticalVector = Vec3.zero() +const verticalRightVector = Vec3.zero() +const verticalLeftVector = Vec3.zero() const normalOffset = Vec3.zero() const positionVector = Vec3.zero() const normalVector = Vec3.zero() @@ -25,6 +27,41 @@ const p2 = Vec3.zero() const p3 = Vec3.zero() const p4 = Vec3.zero() +function addCap(offset: number, builder: MeshBuilder, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, width: number, leftHeight: number, rightHeight: number) { + const { vertices, normals, indices } = builder.state + const vertexCount = vertices.elementCount + + Vec3.fromArray(verticalLeftVector, normalVectors, offset) + Vec3.scale(verticalLeftVector, verticalLeftVector, leftHeight) + + Vec3.fromArray(verticalRightVector, normalVectors, offset) + Vec3.scale(verticalRightVector, verticalRightVector, rightHeight) + + Vec3.fromArray(horizontalVector, binormalVectors, offset) + Vec3.scale(horizontalVector, horizontalVector, width) + + Vec3.fromArray(positionVector, controlPoints, offset) + + Vec3.add(p1, Vec3.add(p1, positionVector, horizontalVector), verticalRightVector) + Vec3.sub(p2, Vec3.add(p2, positionVector, horizontalVector), verticalLeftVector) + Vec3.sub(p3, Vec3.sub(p3, positionVector, horizontalVector), verticalLeftVector) + Vec3.add(p4, Vec3.sub(p4, positionVector, horizontalVector), verticalRightVector) + + ChunkedArray.add3(vertices, p1[0], p1[1], p1[2]) + ChunkedArray.add3(vertices, p2[0], p2[1], p2[2]) + ChunkedArray.add3(vertices, p3[0], p3[1], p3[2]) + ChunkedArray.add3(vertices, p4[0], p4[1], p4[2]) + + Vec3.cross(normalVector, horizontalVector, verticalLeftVector) + + for (let i = 0; i < 4; ++i) { + ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]) + } + ChunkedArray.add3(indices, vertexCount + 2, vertexCount + 1, vertexCount) + ChunkedArray.add3(indices, vertexCount, vertexCount + 3, vertexCount + 2) +} + +/** set arrowHeight = 0 for no arrow */ export function addSheet(builder: MeshBuilder, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, width: number, height: number, arrowHeight: number, startCap: boolean, endCap: boolean) { const { currentGroup, vertices, normals, indices, groups } = builder.state @@ -112,67 +149,19 @@ export function addSheet(builder: MeshBuilder, controlPoints: ArrayLike<number>, } if (startCap) { - const offset = 0 - vertexCount = vertices.elementCount - - Vec3.fromArray(verticalVector, normalVectors, offset) - Vec3.scale(verticalVector, verticalVector, arrowHeight === 0 ? height : arrowHeight); - - Vec3.fromArray(horizontalVector, binormalVectors, offset) - Vec3.scale(horizontalVector, horizontalVector, width); - - Vec3.fromArray(positionVector, controlPoints, offset) - - Vec3.add(p1, Vec3.add(p1, positionVector, horizontalVector), verticalVector) - Vec3.sub(p2, Vec3.add(p2, positionVector, horizontalVector), verticalVector) - Vec3.sub(p3, Vec3.sub(p3, positionVector, horizontalVector), verticalVector) - Vec3.add(p4, Vec3.sub(p4, positionVector, horizontalVector), verticalVector) - - ChunkedArray.add3(vertices, p1[0], p1[1], p1[2]) - ChunkedArray.add3(vertices, p2[0], p2[1], p2[2]) - ChunkedArray.add3(vertices, p3[0], p3[1], p3[2]) - ChunkedArray.add3(vertices, p4[0], p4[1], p4[2]) - - Vec3.cross(normalVector, horizontalVector, verticalVector) - - for (let i = 0; i < 4; ++i) { - ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]) - } - ChunkedArray.add3(indices, vertexCount + 2, vertexCount + 1, vertexCount); - ChunkedArray.add3(indices, vertexCount, vertexCount + 3, vertexCount + 2); + const h = arrowHeight === 0 ? height : arrowHeight + addCap(0, builder, controlPoints, normalVectors, binormalVectors, width, h, h) + } else if (arrowHeight > 0) { + addCap(0, builder, controlPoints, normalVectors, binormalVectors, width, -arrowHeight, height) + addCap(0, builder, controlPoints, normalVectors, binormalVectors, width, arrowHeight, -height) } if (endCap && arrowHeight === 0) { - const offset = linearSegments * 3 - vertexCount = vertices.elementCount - - Vec3.fromArray(verticalVector, normalVectors, offset) - Vec3.scale(verticalVector, verticalVector, height); - - Vec3.fromArray(horizontalVector, binormalVectors, offset) - Vec3.scale(horizontalVector, horizontalVector, width); - - Vec3.fromArray(positionVector, controlPoints, offset) - - Vec3.add(p1, Vec3.add(p1, positionVector, horizontalVector), verticalVector) - Vec3.sub(p2, Vec3.add(p2, positionVector, horizontalVector), verticalVector) - Vec3.sub(p3, Vec3.sub(p3, positionVector, horizontalVector), verticalVector) - Vec3.add(p4, Vec3.sub(p4, positionVector, horizontalVector), verticalVector) - - ChunkedArray.add3(vertices, p1[0], p1[1], p1[2]) - ChunkedArray.add3(vertices, p2[0], p2[1], p2[2]) - ChunkedArray.add3(vertices, p3[0], p3[1], p3[2]) - ChunkedArray.add3(vertices, p4[0], p4[1], p4[2]) - - Vec3.cross(normalVector, horizontalVector, verticalVector) - - for (let i = 0; i < 4; ++i) { - ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]) - } - ChunkedArray.add3(indices, vertexCount + 2, vertexCount + 1, vertexCount); - ChunkedArray.add3(indices, vertexCount, vertexCount + 3, vertexCount + 2); + addCap(linearSegments * 3, builder, controlPoints, normalVectors, binormalVectors, width, height, height) } - const addedVertexCount = (linearSegments + 1) * 8 + (startCap ? 4 : 0) + (endCap && arrowHeight === 0 ? 4 : 0) + const addedVertexCount = (linearSegments + 1) * 8 + + (startCap ? 4 : (arrowHeight > 0 ? 8 : 0)) + + (endCap && arrowHeight === 0 ? 4 : 0) for (let i = 0, il = addedVertexCount; i < il; ++i) ChunkedArray.add(groups, currentGroup) } \ No newline at end of file diff --git a/src/mol-geo/geometry/mesh/mesh.ts b/src/mol-geo/geometry/mesh/mesh.ts index 6d4b450d479f5acff8c5a717ba0ee71603c3a8e7..8acf165afaf3b752bb16ca9267bb33a903754be7 100644 --- a/src/mol-geo/geometry/mesh/mesh.ts +++ b/src/mol-geo/geometry/mesh/mesh.ts @@ -409,18 +409,20 @@ export namespace Mesh { } export function updateValues(values: MeshValues, props: PD.Values<Params>) { + Geometry.updateValues(values, props) + ValueCell.updateIfChanged(values.dDoubleSided, props.doubleSided) + ValueCell.updateIfChanged(values.dFlatShaded, props.flatShaded) + ValueCell.updateIfChanged(values.dFlipSided, props.flipSided) + } + + export function updateBoundingSphere(values: MeshValues, mesh: Mesh) { const boundingSphere = calculateBoundingSphere( - values.aPosition.ref.value, Math.floor(values.aPosition.ref.value.length / 3), + values.aPosition.ref.value, mesh.vertexCount, values.aTransform.ref.value, values.instanceCount.ref.value ) if (!Sphere3D.equals(boundingSphere, values.boundingSphere.ref.value)) { ValueCell.update(values.boundingSphere, boundingSphere) } - - Geometry.updateValues(values, props) - ValueCell.updateIfChanged(values.dDoubleSided, props.doubleSided) - ValueCell.updateIfChanged(values.dFlatShaded, props.flatShaded) - ValueCell.updateIfChanged(values.dFlipSided, props.flipSided) } } diff --git a/src/mol-geo/geometry/points/points.ts b/src/mol-geo/geometry/points/points.ts index 4d443427c9d2028e236acf9826816a3abd35c086..08724686b3555a075c737f982a3bb961e7f174e9 100644 --- a/src/mol-geo/geometry/points/points.ts +++ b/src/mol-geo/geometry/points/points.ts @@ -93,17 +93,19 @@ export namespace Points { } export function updateValues(values: PointsValues, props: PD.Values<Params>) { + Geometry.updateValues(values, props) + ValueCell.updateIfChanged(values.dPointSizeAttenuation, props.pointSizeAttenuation) + ValueCell.updateIfChanged(values.dPointFilledCircle, props.pointFilledCircle) + ValueCell.updateIfChanged(values.uPointEdgeBleach, props.pointEdgeBleach) + } + + export function updateBoundingSphere(values: PointsValues, points: Points) { const boundingSphere = calculateBoundingSphere( - values.aPosition.ref.value, Math.floor(values.aPosition.ref.value.length / 3), + values.aPosition.ref.value, points.pointCount, values.aTransform.ref.value, values.instanceCount.ref.value ) if (!Sphere3D.equals(boundingSphere, values.boundingSphere.ref.value)) { ValueCell.update(values.boundingSphere, boundingSphere) } - - Geometry.updateValues(values, props) - ValueCell.updateIfChanged(values.dPointSizeAttenuation, props.pointSizeAttenuation) - ValueCell.updateIfChanged(values.dPointFilledCircle, props.pointFilledCircle) - ValueCell.updateIfChanged(values.uPointEdgeBleach, props.pointEdgeBleach) } } \ No newline at end of file diff --git a/src/mol-gl/scene.ts b/src/mol-gl/scene.ts index a8278c282a6d3811e81e258aef3c82938eedc94c..02f2cee8a516a33ef66a2193db4e45518f90c3f6 100644 --- a/src/mol-gl/scene.ts +++ b/src/mol-gl/scene.ts @@ -11,7 +11,9 @@ import { RenderObject, createRenderable } from './render-object'; import { Object3D } from './object3d'; import { Sphere3D } from 'mol-math/geometry'; import { Vec3 } from 'mol-math/linear-algebra'; +import { BoundaryHelper } from 'mol-math/geometry/boundary-helper'; +const boundaryHelper = new BoundaryHelper(); function calculateBoundingSphere(renderableMap: Map<RenderObject, Renderable<RenderableValues & BaseValues>>, boundingSphere: Sphere3D): Sphere3D { // let count = 0 // const center = Vec3.set(boundingSphere.center, 0, 0, 0) @@ -33,15 +35,20 @@ function calculateBoundingSphere(renderableMap: Map<RenderObject, Renderable<Ren // }) // boundingSphere.radius = radius - const spheres: Sphere3D[] = []; + boundaryHelper.reset(0.1); + + renderableMap.forEach(r => { + if (!r.state.visible || !r.boundingSphere.radius) return; + boundaryHelper.boundaryStep(r.boundingSphere.center, r.boundingSphere.radius); + }); + boundaryHelper.finishBoundaryStep(); renderableMap.forEach(r => { if (!r.state.visible || !r.boundingSphere.radius) return; - spheres.push(r.boundingSphere) + boundaryHelper.extendStep(r.boundingSphere.center, r.boundingSphere.radius); }); - const bs = Sphere3D.getBoundingSphereFromSpheres(spheres, 0.1); - Vec3.copy(boundingSphere.center, bs.center); - boundingSphere.radius = bs.radius; + Vec3.copy(boundingSphere.center, boundaryHelper.center); + boundingSphere.radius = boundaryHelper.radius; return boundingSphere; } diff --git a/src/mol-gl/shader/chunks/apply-marker-color.glsl b/src/mol-gl/shader/chunks/apply-marker-color.glsl index daa93f04d774627a3d84c98b0c9e8d0de371d440..327ba74bb881a097df42ef173ec4bc1db168dc3c 100644 --- a/src/mol-gl/shader/chunks/apply-marker-color.glsl +++ b/src/mol-gl/shader/chunks/apply-marker-color.glsl @@ -1,6 +1,6 @@ // only mark elements with an alpha above the picking threshold if (uAlpha >= uPickingAlphaThreshold) { - float marker = vMarker * 255.0; + float marker = floor(vMarker * 255.0 + 0.5); // rounding required to work on some cards on win if (marker > 0.1) { if (intMod(marker, 2.0) > 0.1) { gl_FragColor.rgb = mix(uHighlightColor, gl_FragColor.rgb, 0.3); diff --git a/src/mol-gl/webgl/render-item.ts b/src/mol-gl/webgl/render-item.ts index bc7d302b1f645f6dc9a6166a568946a1211fffa5..7738c7cc3685a774c9c74254cf59fc4d214c9cde 100644 --- a/src/mol-gl/webgl/render-item.ts +++ b/src/mol-gl/webgl/render-item.ts @@ -216,11 +216,20 @@ export function createRenderItem(ctx: WebGLContext, drawMode: DrawMode, shaderCo } if (valueChanges.attributes || valueChanges.defines || valueChanges.elements) { - // console.log('program/defines or buffers changed, rebuild vaos') - Object.keys(RenderVariantDefines).forEach(k => { - deleteVertexArray(ctx, vertexArrays[k]) - vertexArrays[k] = createVertexArray(ctx, programs[k].value, attributeBuffers, elementsBuffer) - }) + // console.log('program/defines or buffers changed, update vaos') + const { vertexArrayObject } = ctx.extensions + if (vertexArrayObject) { + Object.keys(RenderVariantDefines).forEach(k => { + vertexArrayObject.bindVertexArray(vertexArrays[k]) + if (elementsBuffer && (valueChanges.defines || valueChanges.elements)) { + elementsBuffer.bind() + } + if (valueChanges.attributes || valueChanges.defines) { + programs[k].value.bindAttributes(attributeBuffers) + } + vertexArrayObject.bindVertexArray(null) + }) + } } valueChanges.textures = false diff --git a/src/mol-gl/webgl/vertex-array.ts b/src/mol-gl/webgl/vertex-array.ts index cb9d6d69682dfaffad45ba7af5862fbaef26872a..f6a2315180e2c1cac18bb253fffe65abbd622599 100644 --- a/src/mol-gl/webgl/vertex-array.ts +++ b/src/mol-gl/webgl/vertex-array.ts @@ -17,11 +17,21 @@ export function createVertexArray(ctx: WebGLContext, program: Program, attribute if (elementsBuffer) elementsBuffer.bind() program.bindAttributes(attributeBuffers) ctx.vaoCount += 1 - vertexArrayObject.bindVertexArray(null!) + vertexArrayObject.bindVertexArray(null) } return vertexArray } +export function updateVertexArray(ctx: WebGLContext, vertexArray: WebGLVertexArrayObject | null, program: Program, attributeBuffers: AttributeBuffers, elementsBuffer?: ElementsBuffer) { + const { vertexArrayObject } = ctx.extensions + if (vertexArrayObject && vertexArray) { + vertexArrayObject.bindVertexArray(vertexArray) + if (elementsBuffer) elementsBuffer.bind() + program.bindAttributes(attributeBuffers) + vertexArrayObject.bindVertexArray(null) + } +} + export function deleteVertexArray(ctx: WebGLContext, vertexArray: WebGLVertexArrayObject | null) { const { vertexArrayObject } = ctx.extensions if (vertexArrayObject && vertexArray) { diff --git a/src/mol-math/geometry/boundary-helper.ts b/src/mol-math/geometry/boundary-helper.ts new file mode 100644 index 0000000000000000000000000000000000000000..76ccb3aa69e2cc44a2ebac0cf9375fe49f785aa7 --- /dev/null +++ b/src/mol-math/geometry/boundary-helper.ts @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { Vec3 } from 'mol-math/linear-algebra/3d'; +import { Box3D } from './primitives/box3d'; +import { Sphere3D } from './primitives/sphere3d'; + +/** + * Usage: + * + * 1. .reset(tolerance); tolerance plays part in the "extend" step + * 2. for each point/sphere call boundaryStep() + * 3. .finishBoundaryStep + * 4. for each point/sphere call extendStep + * 5. use .center/.radius or call getSphere/getBox + */ +export class BoundaryHelper { + private count = 0; + private extremes = [Vec3.zero(), Vec3.zero(), Vec3.zero(), Vec3.zero(), Vec3.zero(), Vec3.zero()]; + private u = Vec3.zero(); + private v = Vec3.zero(); + + tolerance = 0; + center: Vec3 = Vec3.zero(); + radius = 0; + + reset(tolerance: number) { + Vec3.set(this.center, 0, 0, 0); + for (let i = 0; i < 6; i++) { + const e = i % 2 === 0 ? Number.MAX_VALUE : -Number.MAX_VALUE; + this.extremes[i] = Vec3.create(e, e, e); + } + this.radius = 0; + this.count = 0; + this.tolerance = tolerance; + } + + boundaryStep(p: Vec3, r: number) { + updateExtremeMin(0, this.extremes[0], p, r); + updateExtremeMax(0, this.extremes[1], p, r); + + updateExtremeMin(1, this.extremes[2], p, r); + updateExtremeMax(1, this.extremes[3], p, r); + + updateExtremeMin(2, this.extremes[4], p, r); + updateExtremeMax(2, this.extremes[5], p, r); + this.count++; + } + + finishBoundaryStep() { + if (this.count === 0) return; + + let maxSpan = 0, mI = 0, mJ = 0; + + for (let i = 0; i < 5; i++) { + for (let j = i + 1; j < 6; j++) { + const d = Vec3.squaredDistance(this.extremes[i], this.extremes[j]); + if (d > maxSpan) { + maxSpan = d; + mI = i; + mJ = j; + } + } + } + + Vec3.add(this.center, this.extremes[mI], this.extremes[mJ]); + Vec3.scale(this.center, this.center, 0.5); + this.radius = Vec3.distance(this.center, this.extremes[mI]); + } + + extendStep(p: Vec3, r: number) { + const d = Vec3.distance(p, this.center); + if ((1 + this.tolerance) * this.radius >= r + d) return; + + Vec3.sub(this.u, p, this.center); + Vec3.normalize(this.u, this.u); + + Vec3.scale(this.v, this.u, -this.radius); + Vec3.add(this.v, this.v, this.center); + Vec3.scale(this.u, this.u, r + d); + Vec3.add(this.u, this.u, this.center); + + Vec3.add(this.center, this.u, this.v); + Vec3.scale(this.center, this.center, 0.5); + this.radius = 0.5 * (r + d + this.radius); + } + + getBox(): Box3D { + Vec3.copy(this.u, this.extremes[0]); + Vec3.copy(this.v, this.extremes[0]); + + for (let i = 1; i < 6; i++) { + Vec3.min(this.u, this.u, this.extremes[i]); + Vec3.max(this.v, this.v, this.extremes[i]); + } + + return { min: Vec3.clone(this.u), max: Vec3.clone(this.v) }; + } + + getSphere(): Sphere3D { + return { center: Vec3.clone(this.center), radius: this.radius }; + } + + constructor() { + this.reset(0); + } +} + +function updateExtremeMin(d: number, e: Vec3, center: Vec3, r: number) { + if (center[d] - r < e[d]) { + Vec3.copy(e, center); + e[d] -= r; + } +} + +function updateExtremeMax(d: number, e: Vec3, center: Vec3, r: number) { + if (center[d] + r > e[d]) { + Vec3.copy(e, center); + e[d] += r; + } +} \ No newline at end of file diff --git a/src/mol-math/geometry/lookup3d/grid.ts b/src/mol-math/geometry/lookup3d/grid.ts index 6fa09806898f2d7122b88a673613ca356d17a4a6..aa9393220b348ac1b0a206fc464bf95d38ca21b5 100644 --- a/src/mol-math/geometry/lookup3d/grid.ts +++ b/src/mol-math/geometry/lookup3d/grid.ts @@ -11,6 +11,7 @@ import { Sphere3D } from '../primitives/sphere3d'; import { PositionData } from '../common'; import { Vec3 } from '../../linear-algebra'; import { OrderedSet } from 'mol-data/int'; +import { BoundaryHelper } from '../boundary-helper'; interface GridLookup3D<T = number> extends Lookup3D<T> { readonly buckets: { readonly offset: ArrayLike<number>, readonly count: ArrayLike<number>, readonly array: ArrayLike<number> } @@ -163,11 +164,30 @@ function _build(state: BuildState): Grid3D { } } +const boundaryHelper = new BoundaryHelper(); +function getBoundary(data: PositionData) { + const { x, y, z, radius, indices } = data; + const p = Vec3.zero(); + boundaryHelper.reset(0); + for (let t = 0, _t = OrderedSet.size(indices); t < _t; t++) { + const i = OrderedSet.getAt(indices, t); + Vec3.set(p, x[i], y[i], z[i]); + boundaryHelper.boundaryStep(p, (radius && radius[i]) || 0); + } + boundaryHelper.finishBoundaryStep(); + for (let t = 0, _t = OrderedSet.size(indices); t < _t; t++) { + const i = OrderedSet.getAt(indices, t); + Vec3.set(p, x[i], y[i], z[i]); + boundaryHelper.extendStep(p, (radius && radius[i]) || 0); + } + + return { boundingBox: boundaryHelper.getBox(), boundingSphere: boundaryHelper.getSphere() }; +} + function build(data: PositionData, cellSize?: Vec3) { - const boundingBox = Box3D.computeBounding(data); + const { boundingBox, boundingSphere } = getBoundary(data); // need to expand the grid bounds to avoid rounding errors const expandedBox = Box3D.expand(Box3D.empty(), boundingBox, Vec3.create(0.5, 0.5, 0.5)); - const boundingSphere = Sphere3D.computeBounding(data); const { indices } = data; const S = Vec3.sub(Vec3.zero(), expandedBox.max, expandedBox.min); diff --git a/src/mol-math/geometry/primitives/sphere3d.ts b/src/mol-math/geometry/primitives/sphere3d.ts index a6dcfc9a5a092a75938a07c6dfa4c7254e7b20e9..ef6c3c685583c12f131ca94ac1cf5419f61e57e3 100644 --- a/src/mol-math/geometry/primitives/sphere3d.ts +++ b/src/mol-math/geometry/primitives/sphere3d.ts @@ -96,83 +96,6 @@ namespace Sphere3D { return (Math.abs(ar - br) <= EPSILON.Value * Math.max(1.0, Math.abs(ar), Math.abs(br)) && Vec3.equals(a.center, b.center)); } - - function updateExtremeMin(d: number, e: Vec3, center: Vec3, r: number) { - if (center[d] - r < e[d]) { - Vec3.copy(e, center); - e[d] -= r; - } - } - - function updateExtremeMax(d: number, e: Vec3, center: Vec3, r: number) { - if (center[d] + r > e[d]) { - Vec3.copy(e, center); - e[d] += r; - } - } - - export function getBoundingSphereFromSpheres(spheres: Sphere3D[], tolerance: number): Sphere3D { - if (spheres.length === 0) { - return { center: Vec3.zero(), radius: 0.1 }; - } - - const extremes: Vec3[] = []; - for (let i = 0; i < 6; i++) { - const e = i % 2 === 0 ? Number.MAX_VALUE : -Number.MAX_VALUE; - extremes[i] = Vec3.create(e, e, e); - } - const u = Vec3.zero(), v = Vec3.zero(); - - let m = 0; - for (const s of spheres) { - updateExtremeMin(0, extremes[0], s.center, s.radius); - updateExtremeMax(0, extremes[1], s.center, s.radius); - - updateExtremeMin(1, extremes[2], s.center, s.radius); - updateExtremeMax(1, extremes[3], s.center, s.radius); - - updateExtremeMin(2, extremes[4], s.center, s.radius); - updateExtremeMax(2, extremes[5], s.center, s.radius); - if (s.radius > m) m = s.radius; - } - - let maxSpan = 0, mI = 0, mJ = 0; - - for (let i = 0; i < 5; i++) { - for (let j = i + 1; j < 6; j++) { - const d = Vec3.squaredDistance(extremes[i], extremes[j]); - if (d > maxSpan) { - maxSpan = d; - mI = i; - mJ = j; - } - } - } - - const center = Vec3.zero(); - Vec3.add(center, extremes[mI], extremes[mJ]); - Vec3.scale(center, center, 0.5); - let radius = Vec3.distance(center, extremes[mI]); - - for (const s of spheres) { - const d = Vec3.distance(s.center, center); - if ((1 + tolerance) * radius >= s.radius + d) continue; - - Vec3.sub(u, s.center, center); - Vec3.normalize(u, u); - - Vec3.scale(v, u, -radius); - Vec3.add(v, v, center); - Vec3.scale(u, u, s.radius + d); - Vec3.add(u, u, center); - - Vec3.add(center, u, v); - Vec3.scale(center, center, 0.5); - radius = Vec3.distance(center, u); - } - - return { center, radius }; - } } 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 88d12323f27647b6921f9d827232f1746729c697..aa703b8be7fbb63321286a33ce8f25119bbbfd12 100644 --- a/src/mol-math/geometry/symmetry-operator.ts +++ b/src/mol-math/geometry/symmetry-operator.ts @@ -58,27 +58,27 @@ namespace SymmetryOperator { return create(second.name, matrix, second.hkl); } - export interface CoordinateMapper { (index: number, slot: Vec3): Vec3 } - export interface ArrayMapping { + export interface CoordinateMapper<T extends number> { (index: T, slot: Vec3): Vec3 } + export interface ArrayMapping<T extends number> { readonly operator: SymmetryOperator, - readonly invariantPosition: CoordinateMapper, - readonly position: CoordinateMapper, - x(index: number): number, - y(index: number): number, - z(index: number): number, - r(index: number): number + readonly invariantPosition: CoordinateMapper<T>, + readonly position: CoordinateMapper<T>, + x(index: T): number, + y(index: T): number, + z(index: T): number, + r(index: T): number } export interface Coordinates { x: ArrayLike<number>, y: ArrayLike<number>, z: ArrayLike<number> } - export function createMapping(operator: SymmetryOperator, coords: Coordinates, radius: ((index: number) => number) | undefined): ArrayMapping { + export function createMapping<T extends number>(operator: SymmetryOperator, coords: Coordinates, radius: ((index: T) => number) | undefined): ArrayMapping<T> { const invariantPosition = SymmetryOperator.createCoordinateMapper(SymmetryOperator.Default, coords); const position = operator.isIdentity ? invariantPosition : SymmetryOperator.createCoordinateMapper(operator, coords); const { x, y, z } = createProjections(operator, coords); return { operator, invariantPosition, position, x, y, z, r: radius ? radius : _zeroRadius }; } - export function createCoordinateMapper(t: SymmetryOperator, coords: Coordinates): CoordinateMapper { + export function createCoordinateMapper<T extends number>(t: SymmetryOperator, coords: Coordinates): CoordinateMapper<T> { if (t.isIdentity) return identityPosition(coords); return generalPosition(t, coords); } @@ -145,7 +145,7 @@ function projectZ({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs }: Symm } } -function identityPosition({ x, y, z }: SymmetryOperator.Coordinates): SymmetryOperator.CoordinateMapper { +function identityPosition<T extends number>({ x, y, z }: SymmetryOperator.Coordinates): SymmetryOperator.CoordinateMapper<T> { return (i, s) => { s[0] = x[i]; s[1] = y[i]; @@ -154,10 +154,10 @@ function identityPosition({ x, y, z }: SymmetryOperator.Coordinates): SymmetryOp } } -function generalPosition({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs }: SymmetryOperator.Coordinates) { +function generalPosition<T extends number>({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs }: SymmetryOperator.Coordinates) { if (isW1(m)) { // this should always be the case. - return (i: number, r: Vec3): Vec3 => { + return (i: T, r: Vec3): Vec3 => { const x = xs[i], y = ys[i], z = zs[i]; r[0] = m[0] * x + m[4] * y + m[8] * z + m[12]; r[1] = m[1] * x + m[5] * y + m[9] * z + m[13]; @@ -165,7 +165,7 @@ function generalPosition({ matrix: m }: SymmetryOperator, { x: xs, y: ys, z: zs return r; } } - return (i: number, r: Vec3): Vec3 => { + return (i: T, r: Vec3): Vec3 => { r[0] = xs[i]; r[1] = ys[i]; r[2] = zs[i]; diff --git a/src/mol-model-props/pdbe/structure-quality-report.ts b/src/mol-model-props/pdbe/structure-quality-report.ts index a770aade4011dfe3204304459b3465f2c8340b85..12cb6d018b773121657e539bc0b86f02dcf47635 100644 --- a/src/mol-model-props/pdbe/structure-quality-report.ts +++ b/src/mol-model-props/pdbe/structure-quality-report.ts @@ -15,6 +15,7 @@ import { CustomPropSymbol } from 'mol-script/language/symbol'; import Type from 'mol-script/language/type'; import { QuerySymbolRuntime } from 'mol-script/runtime/query/compiler'; import { PropertyWrapper } from '../common/wrapper'; +import { Task } from 'mol-task'; export namespace StructureQualityReport { export type IssueMap = IndexedCustomProperty.Residue<string[]> @@ -82,6 +83,33 @@ export namespace StructureQualityReport { } } + export function createAttachTask(mapUrl: (model: Model) => string, fetch: (url: string, type: 'string' | 'binary') => Task<string | Uint8Array>) { + return (model: Model) => Task.create('PDBe Structure Quality Report', async ctx => { + if (get(model)) return true; + + let issueMap: IssueMap | undefined; + let info; + // TODO: return from CIF support once the data is recomputed + // = PropertyWrapper.tryGetInfoFromCif('pdbe_structure_quality_report', model); + // if (info) { + // const data = getCifData(model); + // issueMap = createIssueMapFromCif(model, data.residues, data.groups); + // } else + { + const url = mapUrl(model); + const dataStr = await fetch(url, 'string').runInContext(ctx) as string; + const data = JSON.parse(dataStr)[model.label.toLowerCase()]; + if (!data) return false; + info = PropertyWrapper.createInfo(); + issueMap = createIssueMapFromJson(model, data); + } + + model.customProperties.add(Descriptor); + set(model, { info, data: issueMap }); + return false; + }); + } + export async function attachFromCifOrApi(model: Model, params: { // optional JSON source PDBe_apiSourceJson?: (model: Model) => Promise<any> diff --git a/src/mol-model-props/pdbe/themes/structure-quality-report.ts b/src/mol-model-props/pdbe/themes/structure-quality-report.ts new file mode 100644 index 0000000000000000000000000000000000000000..a527e58f7797addf3ad4b2a3de9109c5179f330b --- /dev/null +++ b/src/mol-model-props/pdbe/themes/structure-quality-report.ts @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { StructureQualityReport } from 'mol-model-props/pdbe/structure-quality-report'; +import { Location } from 'mol-model/location'; +import { StructureElement } from 'mol-model/structure'; +import { ColorTheme, LocationColor } from 'mol-theme/color'; +import { ThemeDataContext } from 'mol-theme/theme'; +import { Color } from 'mol-util/color'; +import { TableLegend } from 'mol-util/color/tables'; + +const ValidationColors = [ + Color.fromRgb(170, 170, 170), // not applicable + Color.fromRgb(0, 255, 0), // 0 issues + Color.fromRgb(255, 255, 0), // 1 + Color.fromRgb(255, 128, 0), // 2 + Color.fromRgb(255, 0, 0), // 3 or more +] + +const ValidationColorTable: [string, Color][] = [ + ['No Issues', ValidationColors[1]], + ['One Issue', ValidationColors[2]], + ['Two Issues', ValidationColors[3]], + ['Three Or More Issues', ValidationColors[4]], + ['Not Applicable', ValidationColors[9]] +] + +export function StructureQualityReportColorTheme(ctx: ThemeDataContext, props: {}): ColorTheme<{}> { + let color: LocationColor + + if (ctx.structure && ctx.structure.models[0].customProperties.has(StructureQualityReport.Descriptor)) { + const getIssues = StructureQualityReport.getIssues; + color = (location: Location) => { + if (StructureElement.isLocation(location)) { + return ValidationColors[Math.min(3, getIssues(location).length) + 1]; + } + return ValidationColors[0]; + } + } else { + color = () => ValidationColors[0]; + } + + return { + factory: StructureQualityReportColorTheme, + granularity: 'group', + color: color, + props: props, + description: 'Assigns residue colors according to the number of issues in the PDBe Validation Report.', + legend: TableLegend(ValidationColorTable) + } +} \ No newline at end of file diff --git a/src/mol-model/loci.ts b/src/mol-model/loci.ts index 6bc2d08de6d013bd7946df8d27fdd6505870fe70..abdd4824ddfb8b200375bd82d2bb762da8bf5955 100644 --- a/src/mol-model/loci.ts +++ b/src/mol-model/loci.ts @@ -11,6 +11,7 @@ import { Sphere3D } from 'mol-math/geometry'; import { CentroidHelper } from 'mol-math/geometry/centroid-helper'; import { Vec3 } from 'mol-math/linear-algebra'; import { OrderedSet } from 'mol-data/int'; +import { Structure } from './structure/structure'; /** A Loci that includes every loci */ export const EveryLoci = { kind: 'every-loci' as 'every-loci' } @@ -29,6 +30,9 @@ export function isEmptyLoci(x: any): x is EmptyLoci { export function areLociEqual(lociA: Loci, lociB: Loci) { if (isEveryLoci(lociA) && isEveryLoci(lociB)) return true if (isEmptyLoci(lociA) && isEmptyLoci(lociB)) return true + if (Structure.isLoci(lociA) && Structure.isLoci(lociB)) { + return Structure.areLociEqual(lociA, lociB) + } if (StructureElement.isLoci(lociA) && StructureElement.isLoci(lociB)) { return StructureElement.areLociEqual(lociA, lociB) } @@ -44,7 +48,7 @@ export function areLociEqual(lociA: Loci, lociB: Loci) { export { Loci } -type Loci = StructureElement.Loci | Link.Loci | EveryLoci | EmptyLoci | Shape.Loci +type Loci = StructureElement.Loci | Structure.Loci | Link.Loci | EveryLoci | EmptyLoci | Shape.Loci namespace Loci { @@ -54,7 +58,9 @@ namespace Loci { if (loci.kind === 'every-loci' || loci.kind === 'empty-loci') return void 0; sphereHelper.reset(); - if (loci.kind === 'element-loci') { + if (loci.kind === 'structure-loci') { + return Sphere3D.clone(loci.structure.boundary.sphere) + } else if (loci.kind === 'element-loci') { for (const e of loci.elements) { const { indices } = e; const pos = e.unit.conformation.position; diff --git a/src/mol-model/structure/model/formats/mmcif.ts b/src/mol-model/structure/model/formats/mmcif.ts index 155a94777881da609608b77cea9d6a0620d21446..ca0cb94051ebfb58b04d716eb8ff204dec86f51c 100644 --- a/src/mol-model/structure/model/formats/mmcif.ts +++ b/src/mol-model/structure/model/formats/mmcif.ts @@ -24,10 +24,10 @@ import { getSequence } from './mmcif/sequence'; import { sortAtomSite } from './mmcif/sort'; import { StructConn } from './mmcif/bonds/struct_conn'; import { ChemicalComponent, ChemicalComponentMap } from '../properties/chemical-component'; -import { ComponentType, getMoleculeType } from '../types'; +import { ComponentType, getMoleculeType, MoleculeType } from '../types'; import mmCIF_Format = Format.mmCIF -import { SaccharideComponentMap, SaccharideComponent, SaccharidesSnfgMap, SaccharideCompIdMap } from 'mol-model/structure/structure/carbohydrates/constants'; +import { SaccharideComponentMap, SaccharideComponent, SaccharidesSnfgMap, SaccharideCompIdMap, UnknownSaccharideComponent } from 'mol-model/structure/structure/carbohydrates/constants'; type AtomSite = mmCIF_Database['atom_site'] @@ -88,24 +88,6 @@ function getModifiedResidueNameMap(format: mmCIF_Format): Model['properties']['m return { parentId, details }; } -function getAsymIdSerialMap(format: mmCIF_Format): ReadonlyMap<string, number> { - const data = format.data.struct_asym; - const map = new Map<string, number>(); - let serial = 0 - - const id = data.id - const count = data._rowCount - for (let i = 0; i < count; ++i) { - const _id = id.value(i) - if (!map.has(_id)) { - map.set(_id, serial) - serial += 1 - } - } - - return map; -} - function getChemicalComponentMap(format: mmCIF_Format): ChemicalComponentMap { const map = new Map<string, ChemicalComponent>(); const { id, type, name, pdbx_synonyms, formula, formula_weight } = format.data.chem_comp @@ -142,15 +124,22 @@ function getSaccharideComponentMap(format: mmCIF_Format): SaccharideComponentMap } } } - return map } else { - return SaccharideCompIdMap + SaccharideCompIdMap.forEach((v, k) => map.set(k, v)) + const { id, type } = format.data.chem_comp + for (let i = 0, il = id.rowCount; i < il; ++i) { + const _id = id.value(i) + const _type = type.value(i) + if (!map.has(_id) && getMoleculeType(_type, _id) === MoleculeType.saccharide) { + map.set(_id, UnknownSaccharideComponent) + } + } } + return map } export interface FormatData { modifiedResidues: Model['properties']['modifiedResidues'] - asymIdSerialMap: Model['properties']['asymIdSerialMap'] chemicalComponentMap: Model['properties']['chemicalComponentMap'] saccharideComponentMap: Model['properties']['saccharideComponentMap'] } @@ -158,7 +147,6 @@ export interface FormatData { function getFormatData(format: mmCIF_Format): FormatData { return { modifiedResidues: getModifiedResidueNameMap(format), - asymIdSerialMap: getAsymIdSerialMap(format), chemicalComponentMap: getChemicalComponentMap(format), saccharideComponentMap: getSaccharideComponentMap(format) } diff --git a/src/mol-model/structure/model/model.ts b/src/mol-model/structure/model/model.ts index 1eaad6e4554730175c54c80fb651b27cb71ac2e3..a9442fc948c6ffe8e1de0f19236ec750760e8a68 100644 --- a/src/mol-model/structure/model/model.ts +++ b/src/mol-model/structure/model/model.ts @@ -47,8 +47,6 @@ export interface Model extends Readonly<{ parentId: ReadonlyMap<string, string>, details: ReadonlyMap<string, string> }>, - /** maps asym id to unique serial number */ - readonly asymIdSerialMap: ReadonlyMap<string, number> /** maps residue name to `ChemicalComponent` data */ readonly chemicalComponentMap: ChemicalComponentMap /** maps residue name to `SaccharideComponent` data */ diff --git a/src/mol-model/structure/query/queries/internal.ts b/src/mol-model/structure/query/queries/internal.ts index 0c65b5c62a320928501be94047bb00f6b57a19e1..8cf1078110a6c3c929f56bada9d17a1e2ea8d8bf 100644 --- a/src/mol-model/structure/query/queries/internal.ts +++ b/src/mol-model/structure/query/queries/internal.ts @@ -11,13 +11,14 @@ import Structure from '../../structure/structure'; import { StructureQuery } from '../query'; import { StructureSelection } from '../selection'; -export function sequence(): StructureQuery { +export function atomicSequence(): StructureQuery { return ctx => { const { inputStructure } = ctx; const l = StructureElement.create(); const units: Unit[] = []; for (const unit of inputStructure.units) { + if (unit.kind !== Unit.Kind.Atomic) continue; l.unit = unit; const elements = unit.elements; l.element = elements[0]; @@ -45,6 +46,8 @@ export function water(): StructureQuery { const units: Unit[] = []; for (const unit of inputStructure.units) { + if (unit.kind !== Unit.Kind.Atomic) continue; + l.unit = unit; const elements = unit.elements; l.element = elements[0]; @@ -55,13 +58,15 @@ export function water(): StructureQuery { }; } -export function lidangs(): StructureQuery { +export function atomicHet(): StructureQuery { return ctx => { const { inputStructure } = ctx; const l = StructureElement.create(); const units: Unit[] = []; for (const unit of inputStructure.units) { + if (unit.kind !== Unit.Kind.Atomic) continue; + l.unit = unit; const elements = unit.elements; l.element = elements[0]; @@ -82,3 +87,16 @@ export function lidangs(): StructureQuery { return StructureSelection.Singletons(inputStructure, new Structure(units)); }; } + +export function spheres(): StructureQuery { + return ctx => { + const { inputStructure } = ctx; + + const units: Unit[] = []; + for (const unit of inputStructure.units) { + if (unit.kind !== Unit.Kind.Spheres) continue; + units.push(unit); + } + return StructureSelection.Singletons(inputStructure, new Structure(units)); + }; +} diff --git a/src/mol-model/structure/structure/carbohydrates/compute.ts b/src/mol-model/structure/structure/carbohydrates/compute.ts index fb753f14a01690e6141f25edf922be8d80294a26..e621cbc2245e2186703566ca7879a6766b112d5b 100644 --- a/src/mol-model/structure/structure/carbohydrates/compute.ts +++ b/src/mol-model/structure/structure/carbohydrates/compute.ts @@ -12,12 +12,11 @@ import { Vec3 } from 'mol-math/linear-algebra'; import PrincipalAxes from 'mol-math/linear-algebra/matrix/principal-axes'; import { fillSerial } from 'mol-util/array'; import { ResidueIndex, Model } from '../../model'; -import { ElementSymbol, MoleculeType } from '../../model/types'; -import { getAtomicMoleculeType, getPositionMatrix } from '../../util'; +import { ElementSymbol } from '../../model/types'; +import { getPositionMatrix } from '../../util'; import StructureElement from '../element'; import Structure from '../structure'; import Unit from '../unit'; -import { UnknownSaccharideComponent, SaccharideComponent } from './constants'; import { CarbohydrateElement, CarbohydrateLink, Carbohydrates, CarbohydrateTerminalLink, PartialCarbohydrateElement } from './data'; import { UnitRings, UnitRing } from '../unit/rings'; import { ElementIndex } from '../../model/indexing'; @@ -118,8 +117,8 @@ function filterFusedRings(unitRings: UnitRings, rings: UnitRings.Index[] | undef } } -function getSaccharideComp(compId: string, model: Model): SaccharideComponent { - return model.properties.saccharideComponentMap.get(compId) || UnknownSaccharideComponent +function getSaccharideComp(compId: string, model: Model) { + return model.properties.saccharideComponentMap.get(compId) } export function computeCarbohydrates(structure: Structure): Carbohydrates { @@ -167,9 +166,7 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates { const { index: residueIndex } = residueIt.move(); const saccharideComp = getSaccharideComp(label_comp_id.value(residueIndex), model) - if (saccharideComp === UnknownSaccharideComponent) { - if (getAtomicMoleculeType(unit.model, residueIndex) !== MoleculeType.saccharide) continue - } + if (!saccharideComp) continue if (!sugarResidueMap) { sugarResidueMap = UnitRings.byFingerprintAndResidue(unit.rings, SugarRingFps); @@ -402,7 +399,7 @@ function buildLookups (elements: CarbohydrateElement[], links: CarbohydrateLink[ let k: string if (fromCarbohydrate) { k = terminalLinksKey(unit, anomericCarbon) - } else{ + } else { k = terminalLinksKey(elementUnit, elementUnit.elements[elementIndex]) } const e = terminalLinksMap.get(k) diff --git a/src/mol-model/structure/structure/carbohydrates/constants.ts b/src/mol-model/structure/structure/carbohydrates/constants.ts index 5c74ddfafa05adbe51aa26bc8030d7680a5fdc81..e8fe42818485783aaf69d986565968206a5938f2 100644 --- a/src/mol-model/structure/structure/carbohydrates/constants.ts +++ b/src/mol-model/structure/structure/carbohydrates/constants.ts @@ -205,7 +205,10 @@ const CommonSaccharideNames: { [k: string]: string[] } = { 'MLR', // via GlyFinder, tri-saccharide but homomer ], Man: ['MAN', 'BMA'], - Gal: ['GAL', 'GLA'], + Gal: [ + 'GAL', 'GLA', + 'GXL' // via PubChem + ], Gul: ['GUP', 'GL0'], Alt: ['ALT'], All: ['ALL', 'AFD'], @@ -296,6 +299,10 @@ const CommonSaccharideNames: { [k: string]: string[] } = { Psi: [], } +const UnknownSaccharideNames = [ + 'NGZ', // via CCD +] + export const SaccharideCompIdMap = (function () { const map = new Map<string, SaccharideComponent>() for (let i = 0, il = Monosaccharides.length; i < il; ++i) { @@ -307,6 +314,9 @@ export const SaccharideCompIdMap = (function () { } } } + for (let i = 0, il = UnknownSaccharideNames.length; i < il; ++i) { + map.set(UnknownSaccharideNames[i], UnknownSaccharideComponent) + } return map })() diff --git a/src/mol-model/structure/structure/structure.ts b/src/mol-model/structure/structure/structure.ts index d4e76255bbccc94bb56e78155ddfd51fad43b24a..cab555fd9d249bf85d853329843356c6c3658d3b 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, hashString } from 'mol-data/util'; +import { sort, arraySwap, hash1, sortArray, hashString, hashFnv32a } from 'mol-data/util'; import StructureElement from './element' import Unit from './unit' import { StructureLookup3D } from './util/lookup3d'; @@ -44,9 +44,11 @@ class Structure { entityIndices?: ReadonlyArray<EntityIndex>, uniqueAtomicResidueIndices?: ReadonlyMap<UUID, ReadonlyArray<ResidueIndex>>, hashCode: number, + /** Hash based on all unit.id values in the structure, reflecting the units transformation */ + transformHash: number, elementCount: number, polymerResidueCount: number, - } = { hashCode: -1, elementCount: 0, polymerResidueCount: 0 }; + } = { hashCode: -1, transformHash: -1, elementCount: 0, polymerResidueCount: 0 }; subsetBuilder(isSorted: boolean) { return new StructureSubsetBuilder(this, isSorted); @@ -74,6 +76,12 @@ class Structure { return this.computeHash(); } + get transformHash() { + if (this._props.transformHash !== -1) return this._props.transformHash; + this._props.transformHash = hashFnv32a(this.units.map(u => u.id)) + return this._props.transformHash; + } + private computeHash() { let hash = 23; for (let i = 0, _i = this.units.length; i < _i; i++) { @@ -272,6 +280,23 @@ function getUniqueAtomicResidueIndices(structure: Structure): ReadonlyMap<UUID, namespace Structure { export const Empty = new Structure([]); + /** Represents a single structure */ + export interface Loci { + readonly kind: 'structure-loci', + readonly structure: Structure, + } + export function Loci(structure: Structure): Loci { + return { kind: 'structure-loci', structure }; + } + + export function isLoci(x: any): x is Loci { + return !!x && x.kind === 'structure-loci'; + } + + export function areLociEqual(a: Loci, b: Loci) { + return a.structure === b.structure + } + export function create(units: ReadonlyArray<Unit>): Structure { return new Structure(units); } /** @@ -389,6 +414,7 @@ namespace Structure { return s.hashCode; } + /** Hash based on all unit.model conformation values in the structure */ export function conformationHash(s: Structure) { return hashString(s.units.map(u => Unit.conformationId(u)).join('|')) } @@ -409,6 +435,13 @@ namespace Structure { return true; } + export function areEquivalent(a: Structure, b: Structure) { + return a === b || ( + a.hashCode === b.hashCode && + StructureSymmetry.areTransformGroupsEquivalent(a.unitSymmetryGroups, b.unitSymmetryGroups) + ) + } + export class ElementLocationIterator implements Iterator<StructureElement> { private current = StructureElement.create(); private unitIndex = 0; diff --git a/src/mol-model/structure/structure/symmetry.ts b/src/mol-model/structure/structure/symmetry.ts index 1b35f8341f75d86dccf08e88b08cc8a475e3506f..5af14696a9e1b5017299ace7cbe5643956a2bc1f 100644 --- a/src/mol-model/structure/structure/symmetry.ts +++ b/src/mol-model/structure/structure/symmetry.ts @@ -78,6 +78,15 @@ namespace StructureSymmetry { return ret; } + + /** Checks if transform groups are equal up to their unit's transformations */ + export function areTransformGroupsEquivalent(a: ReadonlyArray<Unit.SymmetryGroup>, b: ReadonlyArray<Unit.SymmetryGroup>) { + if (a.length !== b.length) return false + for (let i = 0, il = a.length; i < il; ++i) { + if (a[i].hashCode !== b[i].hashCode) return false + } + return true + } } function getOperators(symmetry: ModelSymmetry, ijkMin: Vec3, ijkMax: Vec3) { diff --git a/src/mol-model/structure/structure/unit.ts b/src/mol-model/structure/structure/unit.ts index fd41f3db12facc596554f41aacc7d8af0d36c489..933cd583a02d1194bbabb6ce241899d8e96d5554 100644 --- a/src/mol-model/structure/structure/unit.ts +++ b/src/mol-model/structure/structure/unit.ts @@ -15,7 +15,7 @@ import { UnitRings } from './unit/rings'; import StructureElement from './element' import { ChainIndex, ResidueIndex, ElementIndex } from '../model/indexing'; import { IntMap, SortedArray } from 'mol-data/int'; -import { hash2 } from 'mol-data/util'; +import { hash2, hashFnv32a } from 'mol-data/util'; import { getAtomicPolymerElements, getCoarsePolymerElements, getAtomicGapElements, getCoarseGapElements } from './util/polymer'; import { getNucleotideElements } from './util/nucleotide'; import { GaussianDensityProps, computeUnitGaussianDensityCached } from './unit/gaussian-density'; @@ -48,7 +48,10 @@ namespace Unit { readonly units: ReadonlyArray<Unit> /** Maps unit.id to index of unit in units array */ readonly unitIndexMap: IntMap<number> + /** Hash based on unit.invariantId which is the same for all units in the group */ readonly hashCode: number + /** Hash based on all unit.id values in the group, reflecting the units transformation*/ + readonly transformHash: number } function getUnitIndexMap(units: Unit[]) { @@ -72,7 +75,8 @@ namespace Unit { props.unitIndexMap = getUnitIndexMap(units) return props.unitIndexMap }, - hashCode: hashUnit(units[0]) + hashCode: hashUnit(units[0]), + transformHash: hashFnv32a(units.map(u => u.id)) } } @@ -90,7 +94,7 @@ namespace Unit { readonly invariantId: number, readonly elements: StructureElement.Set, readonly model: Model, - readonly conformation: SymmetryOperator.ArrayMapping, + readonly conformation: SymmetryOperator.ArrayMapping<ElementIndex>, getChild(elements: StructureElement.Set): Unit, applyOperator(id: number, operator: SymmetryOperator, dontCompose?: boolean /* = false */): Unit, @@ -124,7 +128,7 @@ namespace Unit { readonly invariantId: number; readonly elements: StructureElement.Set; readonly model: Model; - readonly conformation: SymmetryOperator.ArrayMapping; + readonly conformation: SymmetryOperator.ArrayMapping<ElementIndex>; // Reference some commonly accessed things for faster access. readonly residueIndex: ArrayLike<ResidueIndex>; @@ -187,7 +191,7 @@ namespace Unit { return computeUnitGaussianDensityCached(this, props, this.props.gaussianDensities, ctx, webgl); } - constructor(id: number, invariantId: number, model: Model, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping, props: AtomicProperties) { + constructor(id: number, invariantId: number, model: Model, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping<ElementIndex>, props: AtomicProperties) { this.id = id; this.invariantId = invariantId; this.model = model; @@ -229,7 +233,7 @@ namespace Unit { readonly invariantId: number; readonly elements: StructureElement.Set; readonly model: Model; - readonly conformation: SymmetryOperator.ArrayMapping; + readonly conformation: SymmetryOperator.ArrayMapping<ElementIndex>; readonly coarseElements: CoarseElements; readonly coarseConformation: C; @@ -276,7 +280,7 @@ namespace Unit { return computeUnitGaussianDensityCached(this as Unit.Spheres | Unit.Gaussians, props, this.props.gaussianDensities, ctx, webgl); // TODO get rid of casting } - constructor(id: number, invariantId: number, model: Model, kind: K, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping, props: CoarseProperties) { + constructor(id: number, invariantId: number, model: Model, kind: K, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping<ElementIndex>, props: CoarseProperties) { this.kind = kind; this.id = id; this.invariantId = invariantId; @@ -305,7 +309,7 @@ namespace Unit { }; } - 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 { + function createCoarse<K extends Kind.Gaussians | Kind.Spheres>(id: number, invariantId: number, model: Model, kind: K, elements: StructureElement.Set, conformation: SymmetryOperator.ArrayMapping<ElementIndex>, props: CoarseProperties): Unit { return new Coarse(id, invariantId, model, kind, elements, conformation, props) as any as Unit /** lets call this an ugly temporary hack */; } diff --git a/src/mol-model/structure/structure/unit/gaussian-density.ts b/src/mol-model/structure/structure/unit/gaussian-density.ts index 328bf2c12ebdaaa0f7402bd34e4a7a03f225ce7e..cefbfcd68c8d90d2f39a2bef20af4ee51f148862 100644 --- a/src/mol-model/structure/structure/unit/gaussian-density.ts +++ b/src/mol-model/structure/structure/unit/gaussian-density.ts @@ -18,7 +18,7 @@ export const GaussianDensityParams = { resolution: PD.Numeric(1, { min: 0.1, max: 10, step: 0.1 }), radiusOffset: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }), smoothness: PD.Numeric(1.5, { min: 0.5, max: 2.5, step: 0.1 }), - useGpu: PD.Boolean(true), + useGpu: PD.Boolean(false), ignoreCache: PD.Boolean(false), } export const DefaultGaussianDensityProps = PD.getDefaultValues(GaussianDensityParams) diff --git a/src/mol-model/structure/structure/util/boundary.ts b/src/mol-model/structure/structure/util/boundary.ts index b90370614883b4518fb361814074a6696bb74090..e283f325bae318b809fd24f4b614e6011aa7aac5 100644 --- a/src/mol-model/structure/structure/util/boundary.ts +++ b/src/mol-model/structure/structure/util/boundary.ts @@ -5,102 +5,60 @@ * @author Alexander Rose <alexander.rose@weirdbyte.de> */ -import Structure from '../structure' -import Unit from '../unit'; -import { Box3D, Sphere3D, SymmetryOperator } from 'mol-math/geometry'; +import { Box3D, Sphere3D } from 'mol-math/geometry'; +import { BoundaryHelper } from 'mol-math/geometry/boundary-helper'; import { Vec3 } from 'mol-math/linear-algebra'; -import { SortedArray } from 'mol-data/int'; -import { ElementIndex } from '../../model/indexing'; +import Structure from '../structure'; export type Boundary = { box: Box3D, sphere: Sphere3D } -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 radiusSq = 0 - let size = 0 - - const p = Vec3.zero() - - 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) - } - - 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, max }, - sphere: { center, radius: Math.sqrt(radiusSq) } - } -} - -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() +const boundaryHelper = new BoundaryHelper(); + 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 { units } = s; + + boundaryHelper.reset(0); + + for (let i = 0, _i = units.length; i < _i; i++) { + const u = units[i]; + const invariantBoundary = u.lookup3d.boundary; + const o = u.conformation.operator; + + if (o.isIdentity) { + Vec3.min(min, min, invariantBoundary.box.min); + Vec3.max(max, max, invariantBoundary.box.max); - const boundaryMap: Map<number, Boundary> = new Map() - function getInvariantBoundary(u: Unit) { - let boundary: Boundary - if (boundaryMap.has(u.invariantId)) { - boundary = boundaryMap.get(u.invariantId)! + boundaryHelper.boundaryStep(invariantBoundary.sphere.center, invariantBoundary.sphere.radius); } else { - boundary = computeInvariantUnitBoundary(u) - boundaryMap.set(u.invariantId, boundary) + Box3D.transform(tmpBox, invariantBoundary.box, o.matrix); + Vec3.min(min, min, tmpBox.min); + Vec3.max(max, max, tmpBox.max); + + Sphere3D.transform(tmpSphere, invariantBoundary.sphere, o.matrix); + boundaryHelper.boundaryStep(tmpSphere.center, tmpSphere.radius); } - return boundary } - let radius = 0 - let size = 0 + boundaryHelper.finishBoundaryStep(); 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) + const u = units[i]; + const invariantBoundary = u.lookup3d.boundary; + const o = u.conformation.operator; - 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 + if (o.isIdentity) { + boundaryHelper.extendStep(invariantBoundary.sphere.center, invariantBoundary.sphere.radius); + } else { + Sphere3D.transform(tmpSphere, invariantBoundary.sphere, o.matrix); + boundaryHelper.extendStep(tmpSphere.center, tmpSphere.radius); + } } - return { box: { min, max }, sphere: { center, radius } } + return { box: { min, max }, sphere: boundaryHelper.getSphere() }; } \ No newline at end of file diff --git a/src/mol-plugin/behavior.ts b/src/mol-plugin/behavior.ts index 98010eb2a0f55405bcabeb769dc9fd2b40e467bb..976e78982558424c4fa084fac2596efc672389f1 100644 --- a/src/mol-plugin/behavior.ts +++ b/src/mol-plugin/behavior.ts @@ -13,6 +13,7 @@ import * as StaticMisc from './behavior/static/misc' import * as DynamicRepresentation from './behavior/dynamic/representation' import * as DynamicCamera from './behavior/dynamic/camera' +import * as DynamicCustomProps from './behavior/dynamic/custom-props' export const BuiltInPluginBehaviors = { State: StaticState, @@ -24,4 +25,5 @@ export const BuiltInPluginBehaviors = { export const PluginBehaviors = { Representation: DynamicRepresentation, Camera: DynamicCamera, + CustomProps: DynamicCustomProps } \ No newline at end of file diff --git a/src/mol-plugin/behavior/behavior.ts b/src/mol-plugin/behavior/behavior.ts index 39a00f7baec767151e2c421b255b2635964e5b1c..6da2a78161c86dd45d11dfd98e5b2ad775f7bc09 100644 --- a/src/mol-plugin/behavior/behavior.ts +++ b/src/mol-plugin/behavior/behavior.ts @@ -43,7 +43,7 @@ namespace PluginBehavior { export function create<P>(params: CreateParams<P>) { // TODO: cache groups etc - return PluginStateTransform.Create<Root, Behavior, P>({ + return PluginStateTransform.CreateBuiltIn<Root, Behavior, P>({ name: params.name, display: params.display, from: [Root], diff --git a/src/mol-plugin/behavior/dynamic/custom-props.ts b/src/mol-plugin/behavior/dynamic/custom-props.ts new file mode 100644 index 0000000000000000000000000000000000000000..7dc6eefd80de94e5f390d467ee542e56aeb5e85c --- /dev/null +++ b/src/mol-plugin/behavior/dynamic/custom-props.ts @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { OrderedSet } from 'mol-data/int'; +import { StructureQualityReport } from 'mol-model-props/pdbe/structure-quality-report'; +import { StructureQualityReportColorTheme } from 'mol-model-props/pdbe/themes/structure-quality-report'; +import { Loci } from 'mol-model/loci'; +import { StructureElement } from 'mol-model/structure'; +import { CustomPropertyRegistry } from 'mol-plugin/util/custom-prop-registry'; +import { ParamDefinition as PD } from 'mol-util/param-definition'; +import { PluginBehavior } from '../behavior'; + +export const PDBeStructureQualityReport = PluginBehavior.create<{ autoAttach: boolean }>({ + name: 'pdbe-structure-quality-report-prop', + display: { name: 'PDBe Structure Quality Report', group: 'Custom Props' }, + ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> { + private attach = StructureQualityReport.createAttachTask( + m => `https://www.ebi.ac.uk/pdbe/api/validation/residuewise_outlier_summary/entry/${m.label.toLowerCase()}`, + this.ctx.fetch + ); + + private provider: CustomPropertyRegistry.Provider = { + option: [StructureQualityReport.Descriptor.name, 'PDBe Structure Quality Report'], + descriptor: StructureQualityReport.Descriptor, + defaultSelected: false, + attachableTo: () => true, + attach: this.attach + } + + register(): void { + this.ctx.customModelProperties.register(this.provider); + this.ctx.lociLabels.addProvider(labelPDBeValidation); + + // TODO: support filtering of themes based on the input structure + // in this case, it would check structure.models[0].customProperties.has(StructureQualityReport.Descriptor) + // TODO: add remove functionality + this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.add('pdbe-structure-quality-report', { + label: 'PDBe Structure Quality Report', + factory: StructureQualityReportColorTheme, + getParams: () => ({}) + }) + } + + update(p: { autoAttach: boolean }) { + let updated = this.params.autoAttach !== p.autoAttach + this.params.autoAttach = p.autoAttach; + this.provider.defaultSelected = p.autoAttach; + return updated; + } + + unregister() { + this.ctx.customModelProperties.unregister(StructureQualityReport.Descriptor.name); + this.ctx.lociLabels.removeProvider(labelPDBeValidation); + + // TODO: add remove functionality to registry + // this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.remove('pdbe-structure-quality-report') + } + }, + params: () => ({ + autoAttach: PD.Boolean(false) + }) +}); + +function labelPDBeValidation(loci: Loci): string | undefined { + switch (loci.kind) { + case 'element-loci': + const e = loci.elements[0]; + const u = e.unit; + if (!u.model.customProperties.has(StructureQualityReport.Descriptor)) return void 0; + + const se = StructureElement.create(u, u.elements[OrderedSet.getAt(e.indices, 0)]); + const issues = StructureQualityReport.getIssues(se); + if (issues.length === 0) return 'PDBe Validation: No Issues'; + return `PDBe Validation: ${issues.join(', ')}`; + + default: return void 0; + } +} \ No newline at end of file diff --git a/src/mol-plugin/behavior/dynamic/representation.ts b/src/mol-plugin/behavior/dynamic/representation.ts index e6b5116048879d7ebc6db458f893ba9addeb4c65..5119494f809bf19e7a89e978daa21be8c5a804c2 100644 --- a/src/mol-plugin/behavior/dynamic/representation.ts +++ b/src/mol-plugin/behavior/dynamic/representation.ts @@ -19,8 +19,8 @@ export const HighlightLoci = PluginBehavior.create({ if (!this.ctx.canvas3d) return; if (current.repr !== prevRepr || !areLociEqual(current.loci, prevLoci)) { - this.ctx.canvas3d.mark(prevLoci, MarkerAction.RemoveHighlight); - this.ctx.canvas3d.mark(current.loci, MarkerAction.Highlight); + this.ctx.canvas3d.mark(prevLoci, MarkerAction.RemoveHighlight, prevRepr); + this.ctx.canvas3d.mark(current.loci, MarkerAction.Highlight, current.repr); prevLoci = current.loci; prevRepr = current.repr; } @@ -38,8 +38,8 @@ export const SelectLoci = PluginBehavior.create({ this.subscribeObservable(this.ctx.behaviors.canvas.selectLoci, current => { if (!this.ctx.canvas3d) return; if (current.repr !== prevRepr || !areLociEqual(current.loci, prevLoci)) { - this.ctx.canvas3d.mark(prevLoci, MarkerAction.Deselect); - this.ctx.canvas3d.mark(current.loci, MarkerAction.Select); + this.ctx.canvas3d.mark(prevLoci, MarkerAction.Deselect, prevRepr); + this.ctx.canvas3d.mark(current.loci, MarkerAction.Select, current.repr); prevLoci = current.loci; prevRepr = current.repr; } else { diff --git a/src/mol-plugin/behavior/static/representation.ts b/src/mol-plugin/behavior/static/representation.ts index a896996419054fe5290b6b75209c65f0765a5718..bf58413b59e2d4d75998175dc1a5867944148e32 100644 --- a/src/mol-plugin/behavior/static/representation.ts +++ b/src/mol-plugin/behavior/static/representation.ts @@ -19,10 +19,10 @@ export function SyncRepresentationToCanvas(ctx: PluginContext) { events.object.created.subscribe(e => { if (!SO.isRepresentation3D(e.obj)) return; updateVisibility(e, e.obj.data); + e.obj.data.setState({ syncManually: true }); ctx.canvas3d.add(e.obj.data); // TODO: only do this if there were no representations previously ctx.canvas3d.resetCamera(); - ctx.canvas3d.requestDraw(true); }); events.object.updated.subscribe(e => { if (e.oldObj && SO.isRepresentation3D(e.oldObj)) { @@ -34,11 +34,10 @@ export function SyncRepresentationToCanvas(ctx: PluginContext) { if (!SO.isRepresentation3D(e.obj)) return; updateVisibility(e, e.obj.data); - if (e.action === 'recreate') { - ctx.canvas3d.add(e.obj.data); - ctx.canvas3d.requestDraw(true); + e.obj.data.setState({ syncManually: true }); } + ctx.canvas3d.add(e.obj.data); }); events.object.removed.subscribe(e => { if (!SO.isRepresentation3D(e.obj)) return; @@ -58,5 +57,5 @@ export function UpdateRepresentationVisibility(ctx: PluginContext) { } function updateVisibility(e: State.ObjectEvent, r: Representation<any>) { - r.setVisibility(!e.state.cellStates.get(e.ref).isHidden); + r.setState({ visible: !e.state.cellStates.get(e.ref).isHidden }); } \ No newline at end of file diff --git a/src/mol-plugin/behavior/static/state.ts b/src/mol-plugin/behavior/static/state.ts index 9553e5f24a94ef37e54aef18543c56c83a92fdbc..5a6e6be01b16ce3deaa39b639cfef7dc14b90100 100644 --- a/src/mol-plugin/behavior/static/state.ts +++ b/src/mol-plugin/behavior/static/state.ts @@ -8,7 +8,9 @@ import { PluginCommands } from '../../command'; import { PluginContext } from '../../context'; import { StateTree, Transform, State } from 'mol-state'; import { PluginStateSnapshotManager } from 'mol-plugin/state/snapshots'; -import { PluginStateObject as SO } from '../../state/objects'; +import { PluginStateObject as SO, PluginStateObject } from '../../state/objects'; +import { EmptyLoci, EveryLoci } from 'mol-model/loci'; +import { Structure } from 'mol-model/structure'; export function registerDefault(ctx: PluginContext) { SyncBehaviors(ctx); @@ -18,6 +20,8 @@ export function registerDefault(ctx: PluginContext) { RemoveObject(ctx); ToggleExpanded(ctx); ToggleVisibility(ctx); + Highlight(ctx); + ClearHighlight(ctx); Snapshots(ctx); } @@ -75,6 +79,27 @@ function setVisibilityVisitor(t: Transform, tree: StateTree, ctx: { state: State ctx.state.updateCellState(t.ref, { isHidden: ctx.value }); } +// TODO make isHighlighted and isSelect part of StateObjectCell.State and subscribe from there??? +// TODO select structures of subtree +// TODO should also work for volumes and shapes +export function Highlight(ctx: PluginContext) { + PluginCommands.State.Highlight.subscribe(ctx, ({ state, ref }) => { + const cell = state.select(ref)[0] + const repr = cell && SO.isRepresentation3D(cell.obj) ? cell.obj.data : undefined + if (cell && cell.obj && cell.obj.type === PluginStateObject.Molecule.Structure.type) { + ctx.behaviors.canvas.highlightLoci.next({ loci: Structure.Loci(cell.obj.data) }) + } else if (repr) { + ctx.behaviors.canvas.highlightLoci.next({ loci: EveryLoci, repr }) + } + }); +} + +export function ClearHighlight(ctx: PluginContext) { + PluginCommands.State.ClearHighlight.subscribe(ctx, ({ state, ref }) => { + ctx.behaviors.canvas.highlightLoci.next({ loci: EmptyLoci }) + }); +} + export function Snapshots(ctx: PluginContext) { PluginCommands.State.Snapshots.Clear.subscribe(ctx, () => { ctx.state.snapshots.clear(); diff --git a/src/mol-plugin/command.ts b/src/mol-plugin/command.ts index 34dee15c61b4b8c41202ea72f7efc25e78ff9a79..d948e992e3e67457386996e08471315328648d77 100644 --- a/src/mol-plugin/command.ts +++ b/src/mol-plugin/command.ts @@ -22,6 +22,8 @@ export const PluginCommands = { ToggleExpanded: PluginCommand<{ state: State, ref: Transform.Ref }>({ isImmediate: true }), ToggleVisibility: PluginCommand<{ state: State, ref: Transform.Ref }>({ isImmediate: true }), + Highlight: PluginCommand<{ state: State, ref: Transform.Ref }>({ isImmediate: true }), + ClearHighlight: PluginCommand<{ state: State, ref: Transform.Ref }>({ isImmediate: true }), Snapshots: { Add: PluginCommand<{ name?: string, description?: string }>({ isImmediate: true }), diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index 38bf3dcb9beaa5bb85d40c82e7ba32d5ea232599..0d43b9b3985c80982c91b430d8e57c5c4170e050 100644 --- a/src/mol-plugin/context.ts +++ b/src/mol-plugin/context.ts @@ -23,6 +23,8 @@ import { PluginState } from './state'; import { TaskManager } from './util/task-manager'; import { Color } from 'mol-util/color'; import { LociLabelEntry, LociLabelManager } from './util/loci-label-manager'; +import { ajaxGet } from 'mol-util/data-source'; +import { CustomPropertyRegistry } from './util/custom-prop-registry'; export class PluginContext { private disposed = false; @@ -57,13 +59,6 @@ export class PluginContext { } }; - readonly lociLabels: LociLabelManager; - - readonly structureReprensentation = { - registry: new StructureRepresentationRegistry(), - themeCtx: { colorThemeRegistry: new ColorTheme.Registry(), sizeThemeRegistry: new SizeTheme.Registry() } as ThemeRegistryContext - } - readonly behaviors = { canvas: { highlightLoci: this.ev.behavior<{ loci: Loci, repr?: Representation.Any }>({ loci: EmptyLoci }), @@ -74,6 +69,14 @@ export class PluginContext { readonly canvas3d: Canvas3D; + readonly lociLabels: LociLabelManager; + + readonly structureRepresentation = { + registry: new StructureRepresentationRegistry(), + themeCtx: { colorThemeRegistry: new ColorTheme.Registry(), sizeThemeRegistry: new SizeTheme.Registry() } as ThemeRegistryContext + } + + readonly customModelProperties = new CustomPropertyRegistry(); initViewer(canvas: HTMLCanvasElement, container: HTMLDivElement) { try { @@ -82,23 +85,28 @@ export class PluginContext { this.canvas3d.animate(); return true; } catch (e) { - this.log(LogEntry.error('' + e)); + this.log.error('' + e); console.error(e); return false; } } - log(e: LogEntry) { - this.events.log.next(e); - } + readonly log = { + entry: (e: LogEntry) => this.events.log.next(e), + error: (msg: string) => this.events.log.next(LogEntry.error(msg)), + message: (msg: string) => this.events.log.next(LogEntry.message(msg)), + info: (msg: string) => this.events.log.next(LogEntry.info(msg)), + warn: (msg: string) => this.events.log.next(LogEntry.warning(msg)), + }; /** * This should be used in all transform related request so that it could be "spoofed" to allow * "static" access to resources. */ - async fetch(url: string, type: 'string' | 'binary' = 'string'): Promise<string | Uint8Array> { - const req = await fetch(url, { referrerPolicy: 'origin-when-cross-origin' }); - return type === 'string' ? await req.text() : new Uint8Array(await req.arrayBuffer()); + fetch(url: string, type: 'string' | 'binary' = 'string'): Task<string | Uint8Array> { + return ajaxGet({ url, type }); + // const req = await fetch(url, { referrerPolicy: 'origin-when-cross-origin' }); + // return type === 'string' ? await req.text() : new Uint8Array(await req.arrayBuffer()); } runTask<T>(task: Task<T>) { diff --git a/src/mol-plugin/index.ts b/src/mol-plugin/index.ts index 7a87cb5049d4271438b60081eeb0aab8bbaacff9..2201de2baf6d9c9a59034c3e437ff6bedcc5769c 100644 --- a/src/mol-plugin/index.ts +++ b/src/mol-plugin/index.ts @@ -10,10 +10,9 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { PluginCommands } from './command'; import { PluginSpec } from './spec'; -import { CreateStructureFromPDBe } from './state/actions/basic'; +import { DownloadStructure, CreateComplexRepresentation, OpenStructure } from './state/actions/basic'; import { StateTransforms } from './state/transforms'; import { PluginBehaviors } from './behavior'; -import { LogEntry } from 'mol-util/log-entry'; function getParam(name: string, regex: string): string { let r = new RegExp(`${name}=(${regex})[&]?`, 'i'); @@ -22,7 +21,9 @@ function getParam(name: string, regex: string): string { const DefaultSpec: PluginSpec = { actions: [ - PluginSpec.Action(CreateStructureFromPDBe), + PluginSpec.Action(DownloadStructure), + PluginSpec.Action(OpenStructure), + PluginSpec.Action(CreateComplexRepresentation), PluginSpec.Action(StateTransforms.Data.Download), PluginSpec.Action(StateTransforms.Data.ParseCif), PluginSpec.Action(StateTransforms.Model.StructureAssemblyFromModel), @@ -34,7 +35,8 @@ const DefaultSpec: PluginSpec = { PluginSpec.Behavior(PluginBehaviors.Representation.HighlightLoci), PluginSpec.Behavior(PluginBehaviors.Representation.SelectLoci), PluginSpec.Behavior(PluginBehaviors.Representation.DefaultLociLabelProvider), - PluginSpec.Behavior(PluginBehaviors.Camera.FocusLociOnSelect, { minRadius: 20, extraRadius: 4 }) + PluginSpec.Behavior(PluginBehaviors.Camera.FocusLociOnSelect, { minRadius: 20, extraRadius: 4 }), + PluginSpec.Behavior(PluginBehaviors.CustomProps.PDBeStructureQualityReport, { autoAttach: false }) ] } @@ -53,7 +55,7 @@ async function trySetSnapshot(ctx: PluginContext) { if (!snapshotUrl) return; await PluginCommands.State.Snapshots.Fetch.dispatch(ctx, { url: snapshotUrl }) } catch (e) { - ctx.log(LogEntry.error('Failed to load snapshot.')); + ctx.log.error('Failed to load snapshot.'); console.warn('Failed to load snapshot', e); } } \ No newline at end of file diff --git a/src/mol-plugin/skin/base/components/controls-base.scss b/src/mol-plugin/skin/base/components/controls-base.scss index d2f8f3287374847531442a684fb1cd97f222a94f..4bd3f6c21b1314a4d4daefe728338d306b86e74c 100644 --- a/src/mol-plugin/skin/base/components/controls-base.scss +++ b/src/mol-plugin/skin/base/components/controls-base.scss @@ -83,7 +83,7 @@ width: 100%; background: $msp-form-control-background; color: $font-color; - border: none !important; + border: none; // !important; padding: 0 $control-spacing; line-height: $row-height - 2px; height: $row-height; diff --git a/src/mol-plugin/skin/base/components/controls.scss b/src/mol-plugin/skin/base/components/controls.scss index 835a585261bff9f9637062e21a414a1154ea924c..31474759cc641cf90c6d4057bd704b551ec518a3 100644 --- a/src/mol-plugin/skin/base/components/controls.scss +++ b/src/mol-plugin/skin/base/components/controls.scss @@ -110,6 +110,57 @@ // } } +.msp-slider2 { + > div:first-child { + position: absolute; + height: $row-height; + line-height: $row-height; + text-align: center; + left: 0; + width: 25px; + top: 0; + bottom: 0; + font-size: 80%; + } + > div:nth-child(2) { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 25px; + width: 100%; + padding-left: 20px; + padding-right: 25px; + display: table; + + > div { + height: $row-height; + display: table-cell; + vertical-align: middle; + padding: 0 ($control-spacing + 4px); + } + } + > div:last-child { + position: absolute; + height: $row-height; + line-height: $row-height; + text-align: center; + right: 0; + width: 25px; + top: 0; + bottom: 0; + font-size: 80%; + } + + // input[type=text] { + // text-align: right; + // } + + // input[type=range] { + // width: 100%; + // } +} + .msp-toggle-color-picker { button { border: $control-spacing solid $msp-form-control-background !important; diff --git a/src/mol-plugin/skin/base/components/temp.scss b/src/mol-plugin/skin/base/components/temp.scss index 7dbe81f208e5a833b4044c24fb773f2b55f84a31..84624ff7e3e050ed85c8ed67f911b260419f572c 100644 --- a/src/mol-plugin/skin/base/components/temp.scss +++ b/src/mol-plugin/skin/base/components/temp.scss @@ -9,6 +9,9 @@ text-align: center; font-weight: bold; background: $default-background; + // border-right: $control-spacing solid $entity-color-Group; // TODO separate color + border-top: 1px solid $entity-color-Group; // TODO separate color + // border-bottom: 1px solid $entity-color-Group; // TODO separate color } .msp-btn-row-group { diff --git a/src/mol-plugin/skin/base/components/transformer.scss b/src/mol-plugin/skin/base/components/transformer.scss index cf4c5ff797bbe54ad284791e141e41d9caaad4cf..aca44d435bbafbec2138e5da13b8974509273875 100644 --- a/src/mol-plugin/skin/base/components/transformer.scss +++ b/src/mol-plugin/skin/base/components/transformer.scss @@ -44,6 +44,7 @@ .msp-transform-header { position: relative; + border-top: 1px solid $entity-color-Behaviour; // TODO: separate color > button { text-align: left; diff --git a/src/mol-plugin/state/actions/basic.ts b/src/mol-plugin/state/actions/basic.ts index 002ca7f8afe5dca28ea926a444c4c8a4dccd89a3..b50889e6b87914b1300cd26bd7def51795c7e3ab 100644 --- a/src/mol-plugin/state/actions/basic.ts +++ b/src/mol-plugin/state/actions/basic.ts @@ -11,81 +11,162 @@ import { ParamDefinition as PD } from 'mol-util/param-definition'; import { StateSelection } from 'mol-state/state/selection'; import { CartoonParams } from 'mol-repr/structure/representation/cartoon'; import { BallAndStickParams } from 'mol-repr/structure/representation/ball-and-stick'; +import { Download } from '../transforms/data'; +import { StateTree, Transformer } from 'mol-state'; +import { StateTreeBuilder } from 'mol-state/tree/builder'; +import { PolymerIdColorThemeParams } from 'mol-theme/color/polymer-id'; +import { UniformSizeThemeParams } from 'mol-theme/size/uniform'; +import { ElementSymbolColorThemeParams } from 'mol-theme/color/element-symbol'; -export const CreateStructureFromPDBe = StateAction.create<PluginStateObject.Root, void, { id: string }>({ - from: [PluginStateObject.Root], - display: { - name: 'Entry from PDBe', - description: 'Download a structure from PDBe and create its default Assembly and visual' - }, - params: () => ({ id: PD.Text('1grm', { label: 'PDB id' }) }), - apply({ params, state }) { - const url = `http://www.ebi.ac.uk/pdbe/static/entry/${params.id.toLowerCase()}_updated.cif`; - const b = state.build(); - - // const query = MolScriptBuilder.struct.generator.atomGroups({ - // // 'atom-test': MolScriptBuilder.core.rel.eq([ - // // MolScriptBuilder.struct.atomProperty.macromolecular.label_comp_id(), - // // MolScriptBuilder.es('C') - // // ]), - // 'residue-test': MolScriptBuilder.core.rel.eq([ - // MolScriptBuilder.struct.atomProperty.macromolecular.label_comp_id(), - // 'ALA' - // ]) - // }); - - const root = b.toRoot() - .apply(StateTransforms.Data.Download, { url }) - .apply(StateTransforms.Data.ParseCif) - .apply(StateTransforms.Model.TrajectoryFromMmCif, {}) - .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 }) - .apply(StateTransforms.Model.StructureAssemblyFromModel); - - root.apply(StateTransforms.Model.StructureComplexElement, { type: 'sequence' }) - .apply(StateTransforms.Representation.StructureRepresentation3D, { type: { name: 'cartoon', params: PD.getDefaultValues(CartoonParams) } }); - root.apply(StateTransforms.Model.StructureComplexElement, { type: 'ligands' }) - .apply(StateTransforms.Representation.StructureRepresentation3D, { type: { name: 'ball-and-stick', params: PD.getDefaultValues(BallAndStickParams) } }); - root.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' }) - .apply(StateTransforms.Representation.StructureRepresentation3D, { type: { name: 'ball-and-stick', params: { ...PD.getDefaultValues(BallAndStickParams), alpha: 0.51 } } }); - - return state.update(root.getTree()); +// TODO: "structure parser provider" + +export { DownloadStructure } +type DownloadStructure = typeof DownloadStructure +const DownloadStructure = StateAction.build({ + from: PluginStateObject.Root, + display: { name: 'Download Structure', description: 'Load a structure from the provided source and create its default Assembly and visual.' }, + params: { + source: PD.MappedStatic('bcif-static', { + 'pdbe-updated': PD.Group({ + id: PD.Text('1cbs', { label: 'Id' }), + supportProps: PD.Boolean(false) + }, { isFlat: true }), + 'rcsb': PD.Group({ + id: PD.Text('1tqn', { label: 'Id' }), + supportProps: PD.Boolean(false) + }, { isFlat: true }), + 'bcif-static': PD.Group({ + id: PD.Text('1tqn', { label: 'Id' }), + supportProps: PD.Boolean(false) + }, { isFlat: true }), + 'url': PD.Group({ + url: PD.Text(''), + isBinary: PD.Boolean(false), + supportProps: PD.Boolean(false) + }, { isFlat: true }) + }, { + options: [ + ['pdbe-updated', 'PDBe Updated'], + ['rcsb', 'RCSB'], + ['bcif-static', 'BinaryCIF (static PDBe Updated)'], + ['url', 'URL'] + ] + }) + } +})(({ params, state }) => { + const b = state.build(); + const src = params.source; + let url: Transformer.Params<Download>; + + switch (src.name) { + case 'url': + url = src.params; + break; + case 'pdbe-updated': + url = { url: `https://www.ebi.ac.uk/pdbe/static/entry/${src.params.id.toLowerCase()}_updated.cif`, isBinary: false, label: `PDBe: ${src.params.id}` }; + break; + case 'rcsb': + url = { url: `https://files.rcsb.org/download/${src.params.id.toUpperCase()}.cif`, isBinary: false, label: `RCSB: ${src.params.id}` }; + break; + case 'bcif-static': + url = { url: `https://webchem.ncbr.muni.cz/ModelServer/static/bcif/${src.params.id.toLowerCase()}`, isBinary: true, label: `BinaryCIF: ${src.params.id}` }; + break; + default: throw new Error(`${(src as any).name} not supported.`); } + + const data = b.toRoot().apply(StateTransforms.Data.Download, url); + return state.update(createStructureTree(data, params.source.params.supportProps)); }); -export const UpdateTrajectory = StateAction.create<PluginStateObject.Root, void, { action: 'advance' | 'reset', by?: number }>({ - from: [], - display: { - name: 'Update Trajectory' - }, - params: () => ({ - action: PD.Select('advance', [['advance', 'Advance'], ['reset', 'Reset']]), - by: PD.Numeric(1, { min: -1, max: 1, step: 1 }, { isOptional: true }) - }), - apply({ params, state }) { - const models = state.select(q => q.rootsOfType(PluginStateObject.Molecule.Model) - .filter(c => c.transform.transformer === StateTransforms.Model.ModelFromTrajectory)); - - const update = state.build(); - - if (params.action === 'reset') { - for (const m of models) { - update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory, - () => ({ modelIndex: 0})); - } - } else { - for (const m of models) { - const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, [PluginStateObject.Molecule.Trajectory]); - if (!parent || !parent.obj) continue; - const traj = parent.obj as PluginStateObject.Molecule.Trajectory; - update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory, - old => { - let modelIndex = (old.modelIndex + params.by!) % traj.data.length; - if (modelIndex < 0) modelIndex += traj.data.length; - return { modelIndex }; - }); - } - } +export const OpenStructure = StateAction.build({ + display: { name: 'Open Structure', description: 'Load a structure from file and create its default Assembly and visual' }, + from: PluginStateObject.Root, + params: { file: PD.File({ accept: '.cif,.bcif' }) } +})(({ params, state }) => { + const b = state.build(); + const data = b.toRoot().apply(StateTransforms.Data.ReadFile, { file: params.file, isBinary: /\.bcif$/i.test(params.file.name) }); + return state.update(createStructureTree(data, false)); +}); + +function createStructureTree(b: StateTreeBuilder.To<PluginStateObject.Data.Binary | PluginStateObject.Data.String>, supportProps: boolean): StateTree { + let root = b + .apply(StateTransforms.Data.ParseCif) + .apply(StateTransforms.Model.TrajectoryFromMmCif, {}) + .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 }); + + if (supportProps) { + // TODO: implement automatic default property assigment in State.update + root = root.apply(StateTransforms.Model.CustomModelProperties, { properties: [] }); + } + root = root.apply(StateTransforms.Model.StructureAssemblyFromModel); + + complexRepresentation(root); - return state.update(update); + return root.getTree(); +} + +function complexRepresentation(root: StateTreeBuilder.To<PluginStateObject.Molecule.Structure>) { + root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' }) + .apply(StateTransforms.Representation.StructureRepresentation3D, { + type: { name: 'cartoon', params: PD.getDefaultValues(CartoonParams) }, + colorTheme: { name: 'polymer-id', params: PD.getDefaultValues(PolymerIdColorThemeParams) }, + sizeTheme: { name: 'uniform', params: PD.getDefaultValues(UniformSizeThemeParams) }, + }); + root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' }) + .apply(StateTransforms.Representation.StructureRepresentation3D, { + type: { name: 'ball-and-stick', params: PD.getDefaultValues(BallAndStickParams) }, + colorTheme: { name: 'element-symbol', params: PD.getDefaultValues(ElementSymbolColorThemeParams) }, + sizeTheme: { name: 'uniform', params: PD.getDefaultValues(UniformSizeThemeParams) }, + }); + root.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' }) + .apply(StateTransforms.Representation.StructureRepresentation3D, { + type: { name: 'ball-and-stick', params: { ...PD.getDefaultValues(BallAndStickParams), alpha: 0.51 } }, + colorTheme: { name: 'element-symbol', params: PD.getDefaultValues(ElementSymbolColorThemeParams) }, + sizeTheme: { name: 'uniform', params: PD.getDefaultValues(UniformSizeThemeParams) }, + }) + root.apply(StateTransforms.Model.StructureComplexElement, { type: 'spheres' }); + // TODO: create spheres visual +} + +export const CreateComplexRepresentation = StateAction.build({ + display: { name: 'Create Complex', description: 'Split the structure into Sequence/Water/Ligands/... ' }, + from: PluginStateObject.Molecule.Structure +})(({ ref, state }) => { + const root = state.build().to(ref); + complexRepresentation(root); + return state.update(root.getTree()); +}); + +export const UpdateTrajectory = StateAction.build({ + display: { name: 'Update Trajectory' }, + params: { + action: PD.Select<'advance' | 'reset'>('advance', [['advance', 'Advance'], ['reset', 'Reset']]), + by: PD.makeOptional(PD.Numeric(1, { min: -1, max: 1, step: 1 })) + } +})(({ params, state }) => { + const models = state.select(q => q.rootsOfType(PluginStateObject.Molecule.Model) + .filter(c => c.transform.transformer === StateTransforms.Model.ModelFromTrajectory)); + + const update = state.build(); + + if (params.action === 'reset') { + for (const m of models) { + update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory, + () => ({ modelIndex: 0 })); + } + } else { + for (const m of models) { + const parent = StateSelection.findAncestorOfType(state.tree, state.cells, m.transform.ref, [PluginStateObject.Molecule.Trajectory]); + if (!parent || !parent.obj) continue; + const traj = parent.obj as PluginStateObject.Molecule.Trajectory; + update.to(m.transform.ref).update(StateTransforms.Model.ModelFromTrajectory, + old => { + let modelIndex = (old.modelIndex + params.by!) % traj.data.length; + if (modelIndex < 0) modelIndex += traj.data.length; + return { modelIndex }; + }); + } } + + return state.update(update); }); \ No newline at end of file diff --git a/src/mol-plugin/state/objects.ts b/src/mol-plugin/state/objects.ts index 24347d8c0a1d1856036b4ee43286a3b9aaa5218f..8f90acb581763d535695903fb3e84aea9c0f5623 100644 --- a/src/mol-plugin/state/objects.ts +++ b/src/mol-plugin/state/objects.ts @@ -46,8 +46,6 @@ export namespace PluginStateObject { export namespace Data { export class String extends Create<string>({ name: 'String Data', typeClass: 'Data', }) { } export class Binary extends Create<Uint8Array>({ name: 'Binary Data', typeClass: 'Data' }) { } - export class Json extends Create<any>({ name: 'JSON Data', typeClass: 'Data' }) { } - export class Cif extends Create<CifFile>({ name: 'CIF File', typeClass: 'Data' }) { } // TODO // export class MultipleRaw extends Create<{ @@ -55,6 +53,11 @@ export namespace PluginStateObject { // }>({ name: 'Data', typeClass: 'Data', shortName: 'MD', description: 'Multiple Keyed Data.' }) { } } + export namespace Format { + export class Json extends Create<any>({ name: 'JSON Data', typeClass: 'Data' }) { } + export class Cif extends Create<CifFile>({ name: 'CIF File', typeClass: 'Data' }) { } + } + export namespace Molecule { export class Trajectory extends Create<ReadonlyArray<_Model>>({ name: 'Trajectory', typeClass: 'Object' }) { } export class Model extends Create<_Model>({ name: 'Model', typeClass: 'Object' }) { } @@ -69,5 +72,6 @@ export namespace PluginStateObject { } export namespace PluginStateTransform { - export const Create = Transformer.factory('ms-plugin'); + export const CreateBuiltIn = Transformer.factory('ms-plugin'); + export const BuiltIn = Transformer.builderFactory('ms-plugin'); } \ No newline at end of file diff --git a/src/mol-plugin/state/transforms/data.ts b/src/mol-plugin/state/transforms/data.ts index d2f76a919cdf03315877531c7bc7ed44df5f9204..0878ceb369e3b4567db73515b131ea54e8f22409 100644 --- a/src/mol-plugin/state/transforms/data.ts +++ b/src/mol-plugin/state/transforms/data.ts @@ -11,26 +11,24 @@ import CIF from 'mol-io/reader/cif' import { PluginContext } from 'mol-plugin/context'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import { Transformer } from 'mol-state'; +import { readFromFile } from 'mol-util/data-source'; export { Download } -namespace Download { export interface Params { url: string, isBinary?: boolean, label?: string } } -const Download = PluginStateTransform.Create<SO.Root, SO.Data.String | SO.Data.Binary, Download.Params>({ +type Download = typeof Download +const Download = PluginStateTransform.BuiltIn({ name: 'download', - display: { - name: 'Download', - description: 'Download string or binary data from the specified URL' - }, + display: { name: 'Download', description: 'Download string or binary data from the specified URL' }, from: [SO.Root], to: [SO.Data.String, SO.Data.Binary], - params: () => ({ + params: { url: PD.Text('https://www.ebi.ac.uk/pdbe/static/entry/1cbs_updated.cif', { description: 'Resource URL. Must be the same domain or support CORS.' }), - label: PD.Text('', { isOptional: true }), - isBinary: PD.Boolean(false, { description: 'If true, download data as binary (string otherwise)', isOptional: true }) - }), + label: PD.makeOptional(PD.Text('')), + isBinary: PD.makeOptional(PD.Boolean(false, { description: 'If true, download data as binary (string otherwise)' })) + } +})({ apply({ params: p }, globalCtx: PluginContext) { return Task.create('Download', async ctx => { - // TODO: track progress - const data = await globalCtx.fetch(p.url, p.isBinary ? 'binary' : 'string'); + const data = await globalCtx.fetch(p.url, p.isBinary ? 'binary' : 'string').runInContext(ctx); return p.isBinary ? new SO.Data.Binary(data as Uint8Array, { label: p.label ? p.label : p.url }) : new SO.Data.String(data as string, { label: p.label ? p.label : p.url }); @@ -46,21 +44,50 @@ const Download = PluginStateTransform.Create<SO.Root, SO.Data.String | SO.Data.B } }); +export { ReadFile } +type ReadFile = typeof ReadFile +const ReadFile = PluginStateTransform.BuiltIn({ + name: 'read-file', + display: { name: 'Read File', description: 'Read string or binary data from the specified file' }, + from: SO.Root, + to: [SO.Data.String, SO.Data.Binary], + params: { + file: PD.File(), + label: PD.makeOptional(PD.Text('')), + isBinary: PD.makeOptional(PD.Boolean(false, { description: 'If true, open file as as binary (string otherwise)' })) + } +})({ + apply({ params: p }) { + return Task.create('Open File', async ctx => { + const data = await readFromFile(p.file, p.isBinary ? 'binary' : 'string').runInContext(ctx); + return p.isBinary + ? new SO.Data.Binary(data as Uint8Array, { label: p.label ? p.label : p.file.name }) + : new SO.Data.String(data as string, { label: p.label ? p.label : p.file.name }); + }); + }, + update({ oldParams, newParams, b }) { + if (oldParams.label !== newParams.label) { + (b.label as string) = newParams.label || oldParams.file.name; + return Transformer.UpdateResult.Updated; + } + return Transformer.UpdateResult.Unchanged; + }, + isSerializable: () => ({ isSerializable: false, reason: 'Cannot serialize user loaded files.' }) +}); + export { ParseCif } -namespace ParseCif { export interface Params { } } -const ParseCif = PluginStateTransform.Create<SO.Data.String | SO.Data.Binary, SO.Data.Cif, ParseCif.Params>({ +type ParseCif = typeof ParseCif +const ParseCif = PluginStateTransform.BuiltIn({ name: 'parse-cif', - display: { - name: 'Parse CIF', - description: 'Parse CIF from String or Binary data' - }, + display: { name: 'Parse CIF', description: 'Parse CIF from String or Binary data' }, from: [SO.Data.String, SO.Data.Binary], - to: [SO.Data.Cif], + to: SO.Format.Cif +})({ apply({ a }) { return Task.create('Parse CIF', async ctx => { const parsed = await (SO.Data.String.is(a) ? CIF.parse(a.data) : CIF.parseBinary(a.data)).runInContext(ctx); if (parsed.isError) throw new Error(parsed.message); - return new SO.Data.Cif(parsed.result); + return new SO.Format.Cif(parsed.result); }); } }); \ No newline at end of file diff --git a/src/mol-plugin/state/transforms/model.ts b/src/mol-plugin/state/transforms/model.ts index 0c29e936769d896809048c88086573ab9c60410b..440d3eb78d6501808bf4a355a0c75a24cd4133b1 100644 --- a/src/mol-plugin/state/transforms/model.ts +++ b/src/mol-plugin/state/transforms/model.ts @@ -6,31 +6,29 @@ import { PluginStateTransform } from '../objects'; import { PluginStateObject as SO } from '../objects'; -import { Task } from 'mol-task'; +import { Task, RuntimeContext } from 'mol-task'; import { Model, Format, Structure, ModelSymmetry, StructureSymmetry, QueryContext, StructureSelection as Sel, StructureQuery, Queries } from 'mol-model/structure'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import Expression from 'mol-script/language/expression'; import { compile } from 'mol-script/runtime/query/compiler'; import { MolScriptBuilder } from 'mol-script/language/builder'; import { StateObject } from 'mol-state'; +import { PluginContext } from 'mol-plugin/context'; export { TrajectoryFromMmCif } -namespace TrajectoryFromMmCif { export interface Params { blockHeader?: string } } -const TrajectoryFromMmCif = PluginStateTransform.Create<SO.Data.Cif, SO.Molecule.Trajectory, TrajectoryFromMmCif.Params>({ +type TrajectoryFromMmCif = typeof TrajectoryFromMmCif +const TrajectoryFromMmCif = PluginStateTransform.BuiltIn({ name: 'trajectory-from-mmcif', - display: { - name: 'Models from mmCIF', - description: 'Identify and create all separate models in the specified CIF data block' - }, - from: [SO.Data.Cif], - to: [SO.Molecule.Trajectory], + display: { name: 'Trajectory from mmCIF', description: 'Identify and create all separate models in the specified CIF data block' }, + from: SO.Format.Cif, + to: SO.Molecule.Trajectory, params(a) { const { blocks } = a.data; - if (blocks.length === 0) return { }; return { - blockHeader: PD.Select(blocks[0].header, blocks.map(b => [b.header, b.header] as [string, string]), { description: 'Header of the block to parse' }) + blockHeader: PD.makeOptional(PD.Select(blocks[0] && blocks[0].header, blocks.map(b => [b.header, b.header] as [string, string]), { description: 'Header of the block to parse' })) }; - }, + } +})({ isApplicable: a => a.data.blocks.length > 0, apply({ a, params }) { return Task.create('Parse mmCIF', async ctx => { @@ -47,16 +45,14 @@ const TrajectoryFromMmCif = PluginStateTransform.Create<SO.Data.Cif, SO.Molecule export { ModelFromTrajectory } const plus1 = (v: number) => v + 1, minus1 = (v: number) => v - 1; -namespace ModelFromTrajectory { export interface Params { modelIndex: number } } -const ModelFromTrajectory = PluginStateTransform.Create<SO.Molecule.Trajectory, SO.Molecule.Model, ModelFromTrajectory.Params>({ +type ModelFromTrajectory = typeof ModelFromTrajectory +const ModelFromTrajectory = PluginStateTransform.BuiltIn({ name: 'model-from-trajectory', - display: { - name: 'Model from Trajectory', - description: 'Create a molecular structure from the specified model.' - }, - from: [SO.Molecule.Trajectory], - to: [SO.Molecule.Model], - params: a => ({ modelIndex: PD.Converted(plus1, minus1, PD.Numeric(1, { min: 1, max: a.data.length, step: 1 }, { description: 'Model Index' })) }), + display: { name: 'Model from Trajectory', description: 'Create a molecular structure from the specified model.' }, + from: SO.Molecule.Trajectory, + to: SO.Molecule.Model, + params: a => ({ modelIndex: PD.Converted(plus1, minus1, PD.Numeric(1, { min: 1, max: a.data.length, step: 1 }, { description: 'Model Index' })) }) +})({ isApplicable: a => a.data.length > 0, apply({ a, params }) { if (params.modelIndex < 0 || params.modelIndex >= a.data.length) throw new Error(`Invalid modelIndex ${params.modelIndex}`); @@ -67,15 +63,13 @@ const ModelFromTrajectory = PluginStateTransform.Create<SO.Molecule.Trajectory, }); export { StructureFromModel } -namespace StructureFromModel { export interface Params { } } -const StructureFromModel = PluginStateTransform.Create<SO.Molecule.Model, SO.Molecule.Structure, StructureFromModel.Params>({ +type StructureFromModel = typeof StructureFromModel +const StructureFromModel = PluginStateTransform.BuiltIn({ name: 'structure-from-model', - display: { - name: 'Structure from Model', - description: 'Create a molecular structure from the specified model.' - }, - from: [SO.Molecule.Model], - to: [SO.Molecule.Structure], + display: { name: 'Structure from Model', description: 'Create a molecular structure from the specified model.' }, + from: SO.Molecule.Model, + to: SO.Molecule.Structure +})({ apply({ a }) { let s = Structure.ofModel(a.data); const label = { label: a.data.label, description: s.elementCount === 1 ? '1 element' : `${s.elementCount} elements` }; @@ -88,29 +82,33 @@ function structureDesc(s: Structure) { } export { StructureAssemblyFromModel } -namespace StructureAssemblyFromModel { export interface Params { /** if not specified, use the 1st */ id?: string } } -const StructureAssemblyFromModel = PluginStateTransform.Create<SO.Molecule.Model, SO.Molecule.Structure, StructureAssemblyFromModel.Params>({ +type StructureAssemblyFromModel = typeof StructureAssemblyFromModel +const StructureAssemblyFromModel = PluginStateTransform.BuiltIn({ name: 'structure-assembly-from-model', - display: { - name: 'Structure Assembly', - description: 'Create a molecular structure assembly.' - }, - from: [SO.Molecule.Model], - to: [SO.Molecule.Structure], + display: { name: 'Structure Assembly', description: 'Create a molecular structure assembly.' }, + from: SO.Molecule.Model, + to: SO.Molecule.Structure, params(a) { const model = a.data; const ids = model.symmetry.assemblies.map(a => [a.id, a.id] as [string, string]); - return { id: PD.Select(ids.length ? ids[0][0] : '', ids, { label: 'Asm Id', description: 'Assembly Id' }) }; - }, - apply({ a, params }) { + return { id: PD.makeOptional(PD.Select(ids.length ? ids[0][0] : '', ids, { label: 'Asm Id', description: 'Assembly Id' })) }; + } +})({ + apply({ a, params }, plugin: PluginContext) { return Task.create('Build Assembly', async ctx => { - let id = params.id; + let id = (params.id || '').trim(); const model = a.data; if (!id && model.symmetry.assemblies.length) id = model.symmetry.assemblies[0].id; - const asm = ModelSymmetry.findAssembly(model, id || ''); - if (!asm) throw new Error(`Assembly '${id}' not found`); + const asm = ModelSymmetry.findAssembly(model, id); + if (id && !asm) throw new Error(`Assembly '${id}' not found`); const base = Structure.ofModel(model); + if (!asm) { + plugin.log.warn(`Model '${a.label}' has no assembly, returning default structure.`); + const label = { label: a.data.label, description: structureDesc(base) }; + return new SO.Molecule.Structure(base, label); + } + const s = await StructureSymmetry.buildAssembly(base, id!).runInContext(ctx); const label = { label: `Assembly ${id}`, description: structureDesc(s) }; return new SO.Molecule.Structure(s, label); @@ -119,19 +117,17 @@ const StructureAssemblyFromModel = PluginStateTransform.Create<SO.Molecule.Model }); export { StructureSelection } -namespace StructureSelection { export interface Params { query: Expression, label?: string } } -const StructureSelection = PluginStateTransform.Create<SO.Molecule.Structure, SO.Molecule.Structure, StructureSelection.Params>({ +type StructureSelection = typeof StructureSelection +const StructureSelection = PluginStateTransform.BuiltIn({ name: 'structure-selection', - display: { - name: 'Structure Selection', - description: 'Create a molecular structure from the specified model.' - }, - from: [SO.Molecule.Structure], - to: [SO.Molecule.Structure], - params: () => ({ + display: { name: 'Structure Selection', description: 'Create a molecular structure from the specified model.' }, + from: SO.Molecule.Structure, + to: SO.Molecule.Structure, + params: { query: PD.Value<Expression>(MolScriptBuilder.struct.generator.all, { isHidden: true }), - label: PD.Text('', { isOptional: true, isHidden: true }) - }), + label: PD.makeOptional(PD.Text('', { isHidden: true })) + } +})({ apply({ a, params }) { // TODO: use cache, add "update" const compiled = compile<Sel>(params.query); @@ -143,25 +139,25 @@ const StructureSelection = PluginStateTransform.Create<SO.Molecule.Structure, SO }); export { StructureComplexElement } -namespace StructureComplexElement { export interface Params { type: 'sequence' | 'water' | 'ligands' } } -const StructureComplexElement = PluginStateTransform.Create<SO.Molecule.Structure, SO.Molecule.Structure, StructureComplexElement.Params>({ +namespace StructureComplexElement { export type Types = 'atomic-sequence' | 'water' | 'atomic-het' | 'spheres' } +type StructureComplexElement = typeof StructureComplexElement +const StructureComplexElement = PluginStateTransform.BuiltIn({ name: 'structure-complex-element', - display: { - name: 'Complex Element', - description: 'Create a molecular structure from the specified model.' - }, - from: [SO.Molecule.Structure], - to: [SO.Molecule.Structure], - params: () => ({ type: PD.Text('sequence', { isHidden: true }) }), + display: { name: 'Complex Element', description: 'Create a molecular structure from the specified model.' }, + from: SO.Molecule.Structure, + to: SO.Molecule.Structure, + params: { type: PD.Text<StructureComplexElement.Types>('atomic-sequence', { isHidden: true }) } +})({ apply({ a, params }) { // TODO: update function. let query: StructureQuery, label: string; switch (params.type) { - case 'sequence': query = Queries.internal.sequence(); label = 'Sequence'; break; + case 'atomic-sequence': query = Queries.internal.atomicSequence(); label = 'Sequence'; break; case 'water': query = Queries.internal.water(); label = 'Water'; break; - case 'ligands': query = Queries.internal.lidangs(); label = 'Ligands'; break; - default: throw new Error(`${params.type} is a valid complex element.`); + case 'atomic-het': query = Queries.internal.atomicHet(); label = 'HET Groups/Ligands'; break; + case 'spheres': query = Queries.internal.spheres(); label = 'Coarse Spheres'; break; + default: throw new Error(`${params.type} is a not valid complex element.`); } const result = query(new QueryContext(a.data)); @@ -172,3 +168,25 @@ const StructureComplexElement = PluginStateTransform.Create<SO.Molecule.Structur } }); +export { CustomModelProperties } +type CustomModelProperties = typeof CustomModelProperties +const CustomModelProperties = PluginStateTransform.BuiltIn({ + name: 'custom-model-properties', + display: { name: 'Custom Model Properties' }, + from: SO.Molecule.Model, + to: SO.Molecule.Model, + params: (a, ctx: PluginContext) => ({ properties: ctx.customModelProperties.getSelect(a.data) }) +})({ + apply({ a, params }, ctx: PluginContext) { + return Task.create('Custom Props', async taskCtx => { + await attachProps(a.data, ctx, taskCtx, params.properties); + return new SO.Molecule.Model(a.data, { label: 'Props', description: `${params.properties.length} Selected` }); + }); + } +}); +async function attachProps(model: Model, ctx: PluginContext, taskCtx: RuntimeContext, names: string[]) { + for (const name of names) { + const p = ctx.customModelProperties.get(name); + await p.attach(model).runInContext(taskCtx); + } +} \ No newline at end of file diff --git a/src/mol-plugin/state/transforms/representation.ts b/src/mol-plugin/state/transforms/representation.ts index cd51ffcbcf7c09e928055be01cd891955acb4554..ac80dc2cedcc6d0f653527415aba829f1da71718 100644 --- a/src/mol-plugin/state/transforms/representation.ts +++ b/src/mol-plugin/state/transforms/representation.ts @@ -2,6 +2,7 @@ * 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 { Transformer } from 'mol-state'; @@ -10,37 +11,55 @@ import { PluginStateTransform } from '../objects'; import { PluginStateObject as SO } from '../objects'; import { PluginContext } from 'mol-plugin/context'; import { ParamDefinition as PD } from 'mol-util/param-definition'; +import { createTheme } from 'mol-theme/theme'; export { StructureRepresentation3D } -namespace StructureRepresentation3D { - export interface Params { - type: { name: string, params: any /** todo is there "common type" */ }, - } -} -const StructureRepresentation3D = PluginStateTransform.Create<SO.Molecule.Structure, SO.Molecule.Representation3D, StructureRepresentation3D.Params>({ +type StructureRepresentation3D = typeof StructureRepresentation3D +const StructureRepresentation3D = PluginStateTransform.BuiltIn({ name: 'structure-representation-3d', - display: { name: '3D Representation' }, - from: [SO.Molecule.Structure], - to: [SO.Molecule.Representation3D], + display: '3D Representation', + from: SO.Molecule.Structure, + to: SO.Molecule.Representation3D, params: (a, ctx: PluginContext) => ({ - type: PD.Mapped( - ctx.structureReprensentation.registry.default.name, - ctx.structureReprensentation.registry.types, - name => PD.Group<any>(ctx.structureReprensentation.registry.get(name).getParams(ctx.structureReprensentation.themeCtx, a.data))) - }), + type: PD.Mapped<any>( + ctx.structureRepresentation.registry.default.name, + ctx.structureRepresentation.registry.types, + name => PD.Group<any>(ctx.structureRepresentation.registry.get(name).getParams(ctx.structureRepresentation.themeCtx, a.data))), + colorTheme: PD.Mapped<any>( + // TODO how to get a default color theme dependent on the repr type? + ctx.structureRepresentation.themeCtx.colorThemeRegistry.default.name, + ctx.structureRepresentation.themeCtx.colorThemeRegistry.types, + name => PD.Group<any>(ctx.structureRepresentation.themeCtx.colorThemeRegistry.get(name).getParams({ structure: a.data })) + ), + sizeTheme: PD.Mapped<any>( + // TODO how to get a default size theme dependent on the repr type? + ctx.structureRepresentation.themeCtx.sizeThemeRegistry.default.name, + ctx.structureRepresentation.themeCtx.sizeThemeRegistry.types, + name => PD.Group<any>(ctx.structureRepresentation.themeCtx.sizeThemeRegistry.get(name).getParams({ structure: a.data })) + ) + }) +})({ + canAutoUpdate({ oldParams, newParams }) { + // TODO: allow for small molecules + return oldParams.type.name === newParams.type.name; + }, apply({ a, params }, plugin: PluginContext) { return Task.create('Structure Representation', async ctx => { - const provider = plugin.structureReprensentation.registry.get(params.type.name) - const repr = provider.factory(provider.getParams) - await repr.createOrUpdate({ webgl: plugin.canvas3d.webgl, ...plugin.structureReprensentation.themeCtx }, params.type.params || {}, a.data).runInContext(ctx); + const provider = plugin.structureRepresentation.registry.get(params.type.name) + const props = params.type.params || {} + const repr = provider.factory({ webgl: plugin.canvas3d.webgl, ...plugin.structureRepresentation.themeCtx }, provider.getParams) + repr.setTheme(createTheme(plugin.structureRepresentation.themeCtx, { structure: a.data }, params)) + // TODO set initial state, repr.setState({}) + await repr.createOrUpdate(props, a.data).runInContext(ctx); return new SO.Molecule.Representation3D(repr, { label: provider.label }); }); }, update({ a, b, oldParams, newParams }, plugin: PluginContext) { return Task.create('Structure Representation', async ctx => { if (newParams.type.name !== oldParams.type.name) return Transformer.UpdateResult.Recreate; - - await b.data.createOrUpdate({ webgl: plugin.canvas3d.webgl, ...plugin.structureReprensentation.themeCtx }, { ...b.data.props, ...newParams.type.params }, a.data).runInContext(ctx); + const props = { ...b.data.props, ...newParams.type.params } + b.data.setTheme(createTheme(plugin.structureRepresentation.themeCtx, { structure: a.data }, newParams)) + await b.data.createOrUpdate(props, a.data).runInContext(ctx); return Transformer.UpdateResult.Updated; }); } diff --git a/src/mol-plugin/ui/controls.tsx b/src/mol-plugin/ui/controls.tsx index ac3330b70dfe374eb44afa13e6455cc75639265c..9d5a802b96009435dadc2175cb8a9236034c8b9a 100644 --- a/src/mol-plugin/ui/controls.tsx +++ b/src/mol-plugin/ui/controls.tsx @@ -45,7 +45,7 @@ export class LociLabelControl extends PluginComponent<{}, { entries: ReadonlyArr } render() { - return <div> + return <div style={{ textAlign: 'right' }}> {this.state.entries.map((e, i) => <div key={'' + i}>{e}</div>)} </div> } diff --git a/src/mol-plugin/ui/controls/parameters.tsx b/src/mol-plugin/ui/controls/parameters.tsx index ecacbe9a7ca127ec2fbe4f025bef160540a6eb90..b6eb3c67ab5bf36d3582bad664a84c8adf7089c9 100644 --- a/src/mol-plugin/ui/controls/parameters.tsx +++ b/src/mol-plugin/ui/controls/parameters.tsx @@ -11,11 +11,14 @@ import CanvasComponent from './Canvas/CanvasComponent'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import { camelCaseToWords } from 'mol-util/string'; -import { ColorNames } from 'mol-util/color/tables'; +import { ColorNames, ColorNamesValueMap } from 'mol-util/color/tables'; import { Color } from 'mol-util/color'; import { Slider } from './slider'; import { Vec2 } from 'mol-math/linear-algebra'; +import { Slider, Slider2 } from './slider'; + + export interface ParameterControlsProps<P extends PD.Params = PD.Params> { params: P, values: any, @@ -50,14 +53,17 @@ function controlFor(param: PD.Any): ParamControl | undefined { case 'multi-select': return MultiSelectControl; case 'color': return ColorControl; case 'vec3': return Vec3Control; + case 'file': return FileControl; case 'select': return SelectControl; case 'text': return TextControl; - case 'interval': return IntervalControl; + case 'interval': return typeof param.min !== 'undefined' && typeof param.max !== 'undefined' + ? BoundedIntervalControl : IntervalControl; case 'group': return GroupControl; case 'mapped': return MappedControl; case 'line-graph': return LineGraphControl; } - throw new Error('not supported'); + console.warn(`${(param as any).type} has no associated UI component.`); + return void 0; } // type ParamWrapperProps = { name: string, value: any, param: PD.Base<any>, onChange: ParamOnChange, control: ValueControl, onEnter?: () => void, isEnabled?: boolean } @@ -199,26 +205,47 @@ export class SelectControl extends SimpleParam<PD.Select<any>> { } export class IntervalControl extends SimpleParam<PD.Interval> { - // onChange = (e: React.ChangeEvent<HTMLSelectElement>) => { - // this.setState({ value: e.target.value }); - // this.props.onChange(e.target.value); - // } - + onChange = (v: [number, number]) => { this.update(v); } renderControl() { return <span>interval TODO</span>; } } +export class BoundedIntervalControl extends SimpleParam<PD.Interval> { + onChange = (v: [number, number]) => { this.update(v); } + renderControl() { + return <Slider2 value={this.props.value} min={this.props.param.min!} max={this.props.param.max!} + step={this.props.param.step} onChange={this.onChange} disabled={this.props.isDisabled} />; + } +} + +let _colors: React.ReactFragment | undefined = void 0; +function ColorOptions() { + if (_colors) return _colors; + _colors = <>{Object.keys(ColorNames).map(name => + <option key={name} value={(ColorNames as { [k: string]: Color})[name]} style={{ background: `${Color.toStyle((ColorNames as { [k: string]: Color})[name])}` }} > + {name} + </option> + )}</>; + return _colors; +} + +function ColorValueOption(color: Color) { + return !ColorNamesValueMap.has(color) ? <option key={Color.toHexString(color)} value={color} style={{ background: `${Color.toStyle(color)}` }} > + {Color.toHexString(color)} + </option> : null +} + + export class ColorControl extends SimpleParam<PD.Color> { onChange = (e: React.ChangeEvent<HTMLSelectElement>) => { this.update(Color(parseInt(e.target.value))); } renderControl() { - return <select value={this.props.value} onChange={this.onChange}> - {Object.keys(ColorNames).map(name => { - return <option key={name} value={(ColorNames as { [k: string]: Color})[name]}>{name}</option> - })} + return <select value={this.props.value} onChange={this.onChange} style={{ borderLeft: `16px solid ${Color.toStyle(this.props.value)}` }}> + {ColorValueOption(this.props.value)} + {ColorOptions()} </select>; } } @@ -234,6 +261,25 @@ export class Vec3Control extends SimpleParam<PD.Vec3> { } } +export class FileControl extends React.PureComponent<ParamProps<PD.FileParam>> { + change(value: File) { + this.props.onChange({ name: this.props.name, param: this.props.param, value }); + } + + onChangeFile = (e: React.ChangeEvent<HTMLInputElement>) => { + this.change(e.target.files![0]); + } + + render() { + const value = this.props.value; + + // return <input disabled={this.props.isDisabled} value={void 0} type='file' multiple={false} /> + return <div className='msp-btn msp-btn-block msp-btn-action msp-loader-msp-btn-file' style={{ marginTop: '1px' }}> + {value ? value.name : 'Select a file...'} <input disabled={this.props.isDisabled} onChange={this.onChangeFile} type='file' multiple={false} accept={this.props.param.accept} /> + </div> + } +} + export class MultiSelectControl extends React.PureComponent<ParamProps<PD.MultiSelect<any>>, { isExpanded: boolean }> { state = { isExpanded: false } @@ -267,36 +313,41 @@ export class MultiSelectControl extends React.PureComponent<ParamProps<PD.MultiS </div> </div> <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none' }}> - {this.props.param.options.map(([value, label]) => - <div key={value} className='msp-row'> + {this.props.param.options.map(([value, label]) => { + const sel = current.indexOf(value) >= 0; + return <div key={value} className='msp-row'> <button onClick={this.toggle(value)} disabled={this.props.isDisabled}> - {current.indexOf(value) >= 0 ? `✓ ${label}` : `✗ ${label}`} + <span style={{ float: sel ? 'left' : 'right' }}>{sel ? `✓ ${label}` : `${label} ✗`}</span> </button> - </div>)} + </div> })} </div> </>; } } export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>, { isExpanded: boolean }> { - state = { isExpanded: false } + state = { isExpanded: !!this.props.param.isExpanded } - change(value: PD.Mapped<any>['defaultValue'] ) { + change(value: any ) { this.props.onChange({ name: this.props.name, param: this.props.param, value }); } onChangeParam: ParamOnChange = e => { - const value: PD.Mapped<any>['defaultValue'] = this.props.value; - this.change({ ...value.params, [e.name]: e.value }); + this.change({ ...this.props.value, [e.name]: e.value }); } toggleExpanded = () => this.setState({ isExpanded: !this.state.isExpanded }); render() { - const value: PD.Mapped<any>['defaultValue'] = this.props.value; const params = this.props.param.params; const label = this.props.param.label || camelCaseToWords(this.props.name); + const controls = <ParameterControls params={params} onChange={this.onChangeParam} values={this.props.value} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} />; + + if (this.props.param.isFlat) { + return controls; + } + return <div className='msp-control-group-wrapper'> <div className='msp-control-group-header'> <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded}> @@ -305,7 +356,7 @@ export class GroupControl extends React.PureComponent<ParamProps<PD.Group<any>>, </button> </div> {this.state.isExpanded && <div className='msp-control-offset' style={{ display: this.state.isExpanded ? 'block' : 'none' }}> - <ParameterControls params={params} onChange={this.onChangeParam} values={value.params} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} /> + {controls} </div> } </div> @@ -323,8 +374,7 @@ export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any> } onChangeParam: ParamOnChange = e => { - const value: PD.Mapped<any>['defaultValue'] = this.props.value; - this.change({ name: value.name, params: e.value }); + this.change({ name: this.props.value.name, params: e.value }); } render() { @@ -343,7 +393,7 @@ export class MappedControl extends React.PureComponent<ParamProps<PD.Mapped<any> return <div> {select} - <Mapped param={param} value={value} name={`${label} Properties`} onChange={this.onChangeParam} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} /> + <Mapped param={param} value={value.params} name={`${label} Properties`} onChange={this.onChangeParam} onEnter={this.props.onEnter} isDisabled={this.props.isDisabled} /> </div> } } diff --git a/src/mol-plugin/ui/controls/slider.tsx b/src/mol-plugin/ui/controls/slider.tsx index 260903bbfa6ffd50c32716303c2ba18448b1cd90..f930a20fe558d0496ba4eeec0689a97ad4bb506d 100644 --- a/src/mol-plugin/ui/controls/slider.tsx +++ b/src/mol-plugin/ui/controls/slider.tsx @@ -38,12 +38,12 @@ export class Slider extends React.Component<{ render() { let step = this.props.step; if (step === void 0) step = 1; - return <div className='msp-slider'> - <div> + return <div className='msp-slider'> <div> - <SliderBase min={this.props.min} max={this.props.max} step={step} value={this.state.current} disabled={this.props.disabled} - onBeforeChange={this.begin} - onChange={this.updateCurrent as any} onAfterChange={this.end as any} /> + <div> + <SliderBase min={this.props.min} max={this.props.max} step={step} value={this.state.current} disabled={this.props.disabled} + onBeforeChange={this.begin} + onChange={this.updateCurrent as any} onAfterChange={this.end as any} /> </div></div> <div> {`${Math.round(100 * this.state.current) / 100}`} @@ -52,6 +52,54 @@ export class Slider extends React.Component<{ } } +export class Slider2 extends React.Component<{ + min: number, + max: number, + value: [number, number], + step?: number, + onChange: (v: [number, number]) => void, + disabled?: boolean +}, { isChanging: boolean, current: [number, number] }> { + + state = { isChanging: false, current: [0, 1] as [number, number] } + + static getDerivedStateFromProps(props: { value: [number, number] }, state: { isChanging: boolean, current: [number, number] }) { + if (state.isChanging || (props.value[0] === state.current[0]) && (props.value[1] === state.current[1])) return null; + return { current: props.value }; + } + + begin = () => { + this.setState({ isChanging: true }); + } + + end = (v: [number, number]) => { + this.setState({ isChanging: false }); + this.props.onChange(v); + } + + updateCurrent = (current: [number, number]) => { + this.setState({ current }); + } + + render() { + let step = this.props.step; + if (step === void 0) step = 1; + return <div className='msp-slider2'> + <div> + {`${Math.round(100 * this.state.current[0]) / 100}`} + </div> + <div> + <div> + <SliderBase min={this.props.min} max={this.props.max} step={step} value={this.state.current} disabled={this.props.disabled} + onBeforeChange={this.begin} onChange={this.updateCurrent as any} onAfterChange={this.end as any} range={true} pushable={true} /> + </div></div> + <div> + {`${Math.round(100 * this.state.current[1]) / 100}`} + </div> + </div>; + } +} + /** * The following code was adapted from react-components/slider library. * diff --git a/src/mol-plugin/ui/plugin.tsx b/src/mol-plugin/ui/plugin.tsx index 3d91ea82467227d25b02e7eb5cc93d079e0a7e50..f804c554890b23404552d6d1f3052309b2ee9e92 100644 --- a/src/mol-plugin/ui/plugin.tsx +++ b/src/mol-plugin/ui/plugin.tsx @@ -19,6 +19,7 @@ import { BackgroundTaskProgress } from './task'; import { ApplyActionContol } from './state/apply-action'; import { PluginState } from 'mol-plugin/state'; import { UpdateTransformContol } from './state/update-transform'; +import { StateObjectCell } from 'mol-state'; export class Plugin extends React.Component<{ plugin: PluginContext }, {}> { @@ -81,13 +82,13 @@ export class State extends PluginComponent { render() { const kind = this.plugin.state.behavior.kind.value; - return <> + return <div className='msp-scrollable-container'> <div className='msp-btn-row-group msp-data-beh'> <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.set('data')} style={{ fontWeight: kind === 'data' ? 'bold' : 'normal'}}>Data</button> <button className='msp-btn msp-btn-block msp-form-control' onClick={() => this.set('behavior')} style={{ fontWeight: kind === 'behavior' ? 'bold' : 'normal'}}>Behavior</button> </div> <StateTree state={kind === 'data' ? this.plugin.state.dataState : this.plugin.state.behaviorState} /> - </> + </div> } } @@ -95,6 +96,7 @@ export class Log extends PluginComponent<{}, { entries: List<LogEntry> }> { private wrapper = React.createRef<HTMLDivElement>(); componentDidMount() { + // TODO: only show last 100 entries. this.subscribe(this.plugin.events.log, e => this.setState({ entries: this.state.entries.push(e) })); } @@ -110,10 +112,12 @@ export class Log extends PluginComponent<{}, { entries: List<LogEntry> }> { } render() { - return <div ref={this.wrapper} style={{ position: 'absolute', top: '0', right: '0', bottom: '0', left: '0', overflowY: 'auto' }}> - <ul style={{ listStyle: 'none' }} className='msp-log-list'> + return <div ref={this.wrapper} className='msp-log' style={{ position: 'absolute', top: '0', right: '0', bottom: '0', left: '0', overflowY: 'auto' }}> + <ul className='msp-list-unstyled'> {this.state.entries.map((e, i) => <li key={i}> - <b>[{formatTime(e!.timestamp)} | {e!.type}]</b> {e!.message} + <div className={'msp-log-entry-badge msp-log-entry-' + e!.type} /> + <div className='msp-log-timestamp'>{formatTime(e!.timestamp)}</div> + <div className='msp-log-entry'>{e!.message}</div> </li>)} </ul> </div>; @@ -141,22 +145,20 @@ export class CurrentObject extends PluginComponent { const current = this.current; const ref = current.ref; - // const n = this.props.plugin.state.data.tree.nodes.get(ref)!; - const obj = current.state.cells.get(ref)!; - - const type = obj && obj.obj ? obj.obj.type : void 0; + const cell = current.state.cells.get(ref)!; + const parent: StateObjectCell | undefined = (cell.sourceRef && current.state.cells.get(cell.sourceRef)!) || void 0; - const transform = current.state.transforms.get(ref); + const type = cell && cell.obj ? cell.obj.type : void 0; + const transform = cell.transform; + const def = transform.transformer.definition; - const actions = type - ? current.state.actions.fromType(type) - : [] + const actions = type ? current.state.actions.fromType(type) : []; return <> <div className='msp-section-header'> - {obj.obj ? obj.obj.label : ref} + {cell.obj ? cell.obj.label : (def.display && def.display.name) || def.name} </div> - <UpdateTransformContol state={current.state} transform={transform} /> - { + { (parent && parent.status === 'ok') && <UpdateTransformContol state={current.state} transform={transform} /> } + {cell.status === 'ok' && actions.map((act, i) => <ApplyActionContol plugin={this.plugin} key={`${act.id}`} state={current.state} action={act} nodeRef={ref} />) } </>; diff --git a/src/mol-plugin/ui/state-tree.tsx b/src/mol-plugin/ui/state-tree.tsx index 5c56a979c7e226df685bbd70dcf26dcf46874e8d..34486bd4790ff6dffe66dd726be8d93c33b44016 100644 --- a/src/mol-plugin/ui/state-tree.tsx +++ b/src/mol-plugin/ui/state-tree.tsx @@ -141,6 +141,18 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State e.currentTarget.blur(); } + highlight = (e: React.MouseEvent<HTMLElement>) => { + e.preventDefault(); + PluginCommands.State.Highlight.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef }); + e.currentTarget.blur(); + } + + clearHighlight = (e: React.MouseEvent<HTMLElement>) => { + e.preventDefault(); + PluginCommands.State.ClearHighlight.dispatch(this.plugin, { state: this.props.state, ref: this.props.nodeRef }); + e.currentTarget.blur(); + } + render() { const n = this.props.state.transforms.get(this.props.nodeRef)!; const cell = this.props.state.cells.get(this.props.nodeRef)!; @@ -150,7 +162,7 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State let label: any; if (cell.status !== 'ok' || !cell.obj) { - const name = (n.transformer.definition.display && n.transformer.definition.display.name) || n.transformer.definition.name; + const name = n.transformer.definition.display.name; const title = `${cell.errorText}` label = <><b>{cell.status}</b> <a title={title} href='#' onClick={this.setCurrent}>{name}</a>: <i>{cell.errorText}</i></>; } else { @@ -170,7 +182,7 @@ class StateTreeNodeLabel extends PluginComponent<{ nodeRef: string, state: State <span className='msp-icon msp-icon-visual-visibility' /> </button>; - return <div className={`msp-tree-row${isCurrent ? ' msp-tree-row-current' : ''}`}> + return <div className={`msp-tree-row${isCurrent ? ' msp-tree-row-current' : ''}`} onMouseEnter={this.highlight} onMouseLeave={this.clearHighlight}> {isCurrent ? <b>{label}</b> : label} {children.size > 0 && <button onClick={this.toggleExpanded} className='msp-btn msp-btn-link msp-tree-toggle-exp-button'> <span className={`msp-icon msp-icon-${cellState.isCollapsed ? 'expand' : 'collapse'}`} /> diff --git a/src/mol-plugin/ui/state.tsx b/src/mol-plugin/ui/state.tsx index 93488635613d9ac84ab0d96ca6267855ab6d3e4d..a30896fb1fc4c12d4b5cf9736ab68324724cb0ae 100644 --- a/src/mol-plugin/ui/state.tsx +++ b/src/mol-plugin/ui/state.tsx @@ -9,7 +9,6 @@ import * as React from 'react'; import { PluginComponent } from './base'; import { shallowEqual } from 'mol-util'; import { List } from 'immutable'; -import { LogEntry } from 'mol-util/log-entry'; import { ParameterControls } from './controls/parameters'; import { ParamDefinition as PD} from 'mol-util/param-definition'; import { Subject } from 'rxjs'; @@ -58,7 +57,7 @@ class StateSnapshotControls extends PluginComponent<{ serverUrl: string, serverC this.setState({ isUploading: true }); await PluginCommands.State.Snapshots.Upload.dispatch(this.plugin, { name: this.state.name, description: this.state.description, serverUrl: this.state.serverUrl }); this.setState({ isUploading: false }); - this.plugin.log(LogEntry.message('Snapshot uploaded.')); + this.plugin.log.message('Snapshot uploaded.'); UploadedEvent.next(); } @@ -128,7 +127,7 @@ class RemoteStateSnapshotList extends PluginComponent<{ serverUrl: string }, { e }))), isFetching: false }) } catch (e) { - this.plugin.log(LogEntry.error('Fetching Remote Snapshots: ' + e)); + this.plugin.log.error('Fetching Remote Snapshots: ' + e); this.setState({ entries: List<RemoteEntry>(), isFetching: false }) } } @@ -149,7 +148,7 @@ class RemoteStateSnapshotList extends PluginComponent<{ serverUrl: string }, { e render() { return <div> - <button title='Click to Refresh' style={{fontWeight: 'bold'}} className='msp-btn msp-btn-block msp-form-control' onClick={this.refresh} disabled={this.state.isFetching}>↻ Remote Snapshots</button> + <button title='Click to Refresh' style={{fontWeight: 'bold'}} className='msp-btn msp-btn-block msp-form-control msp-section-header' onClick={this.refresh} disabled={this.state.isFetching}>↻ Remote Snapshots</button> <ul style={{ listStyle: 'none' }} className='msp-state-list'> {this.state.entries.valueSeq().map(e =><li key={e!.id}> diff --git a/src/mol-plugin/ui/state/apply-action.tsx b/src/mol-plugin/ui/state/apply-action.tsx index 585c8fe6700126b747a01eeb463e2c9669d42870..93a6cd3ab99833a0e9ee7207f979ef641f14131d 100644 --- a/src/mol-plugin/ui/state/apply-action.tsx +++ b/src/mol-plugin/ui/state/apply-action.tsx @@ -42,9 +42,10 @@ class ApplyActionContol extends TransformContolBase<ApplyActionContol.Props, App } getInfo() { return this._getInfo(this.props.nodeRef, this.props.state.transforms.get(this.props.nodeRef).version); } getHeader() { return this.props.action.definition.display; } - getHeaderFallback() { return this.props.action.id; } canApply() { return !this.state.error && !this.state.busy; } + canAutoApply() { return false; } applyText() { return 'Apply'; } + isUpdate() { return false; } private _getInfo = memoizeOne((t: Transform.Ref, v: string) => StateTransformParameters.infoFromAction(this.plugin, this.props.state, this.props.action, this.props.nodeRef)); diff --git a/src/mol-plugin/ui/state/common.tsx b/src/mol-plugin/ui/state/common.tsx index d58978b55e65a0edcddab82fce23ebe1aa6ebc51..13c1127003fa784628283831752b528d5c7e9bf2 100644 --- a/src/mol-plugin/ui/state/common.tsx +++ b/src/mol-plugin/ui/state/common.tsx @@ -99,9 +99,10 @@ abstract class TransformContolBase<P, S extends TransformContolBase.ControlState abstract applyAction(): Promise<void>; abstract getInfo(): StateTransformParameters.Props['info']; abstract getHeader(): Transformer.Definition['display']; - abstract getHeaderFallback(): string; abstract canApply(): boolean; + abstract canAutoApply(newParams: any): boolean; abstract applyText(): string; + abstract isUpdate(): boolean; abstract state: S; private busy: Subject<boolean>; @@ -111,12 +112,29 @@ abstract class TransformContolBase<P, S extends TransformContolBase.ControlState this.apply(); } + private autoApplyHandle: number | undefined = void 0; + private clearAutoApply() { + if (this.autoApplyHandle !== void 0) { + clearTimeout(this.autoApplyHandle); + this.autoApplyHandle = void 0; + } + } + events: StateTransformParameters.Props['events'] = { onEnter: this.onEnter, - onChange: (params, isInitial, errors) => this.setState({ params, isInitial, error: errors && errors[0] }) + onChange: (params, isInitial, errors) => { + this.clearAutoApply(); + this.setState({ params, isInitial, error: errors && errors[0] }, () => { + if (!isInitial && !this.state.error && this.canAutoApply(params)) { + this.clearAutoApply(); + this.autoApplyHandle = setTimeout(this.apply, 50) as any as number; + } + }); + } } apply = async () => { + this.clearAutoApply(); this.setState({ busy: true }); try { await this.applyAction(); @@ -146,13 +164,13 @@ abstract class TransformContolBase<P, S extends TransformContolBase.ControlState render() { const info = this.getInfo(); - if (info.isEmpty) return null; + if (info.isEmpty && this.isUpdate()) return null; const display = this.getHeader(); return <div className='msp-transform-wrapper'> <div className='msp-transform-header'> - <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded}>{(display && display.name) || this.getHeaderFallback()}</button> + <button className='msp-btn msp-btn-block' onClick={this.toggleExpanded}>{display.name}</button> {!this.state.isCollapsed && <button className='msp-btn msp-btn-link msp-transform-default-params' onClick={this.setDefault} disabled={this.state.busy} style={{ float: 'right'}} title='Set default params'>↻</button>} </div> {!this.state.isCollapsed && <> diff --git a/src/mol-plugin/ui/state/update-transform.tsx b/src/mol-plugin/ui/state/update-transform.tsx index 040e0195b9241eaa39b147e0789c1f5071742992..c0fa6982706453424f3a1e9fac036975d9edc1c4 100644 --- a/src/mol-plugin/ui/state/update-transform.tsx +++ b/src/mol-plugin/ui/state/update-transform.tsx @@ -29,9 +29,21 @@ class UpdateTransformContol extends TransformContolBase<UpdateTransformContol.Pr applyAction() { return this.plugin.updateTransform(this.props.state, this.props.transform.ref, this.state.params); } getInfo() { return this._getInfo(this.props.transform); } getHeader() { return this.props.transform.transformer.definition.display; } - getHeaderFallback() { return this.props.transform.transformer.definition.name; } canApply() { return !this.state.error && !this.state.busy && !this.state.isInitial; } applyText() { return this.canApply() ? 'Update' : 'Nothing to Update'; } + isUpdate() { return true; } + + canAutoApply(newParams: any) { + const autoUpdate = this.props.transform.transformer.definition.canAutoUpdate + if (!autoUpdate) return false; + + const { state } = this.props; + const cell = state.cells.get(this.props.transform.ref); + if (!cell || !cell.sourceRef || cell.status !== 'ok') return false; + const parentCell = state.cells.get(cell.sourceRef)!; + + return autoUpdate({ a: cell.obj!, b: parentCell.obj!, oldParams: this.props.transform.params, newParams }, this.plugin); + } private _getInfo = memoizeOne((t: Transform) => StateTransformParameters.infoFromTransform(this.plugin, this.props.state, this.props.transform)); diff --git a/src/mol-plugin/util/custom-prop-registry.ts b/src/mol-plugin/util/custom-prop-registry.ts new file mode 100644 index 0000000000000000000000000000000000000000..b537d0df3752caf9a4679d8deae0b0909c568bb6 --- /dev/null +++ b/src/mol-plugin/util/custom-prop-registry.ts @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { ModelPropertyDescriptor, Model } from 'mol-model/structure'; +import { OrderedMap } from 'immutable'; +import { ParamDefinition } from 'mol-util/param-definition'; +import { Task } from 'mol-task'; + +export { CustomPropertyRegistry } + +class CustomPropertyRegistry { + private providers = OrderedMap<string, CustomPropertyRegistry.Provider>().asMutable(); + + getSelect(model: Model) { + const values = this.providers.values(); + const options: [string, string][] = [], selected: string[] = []; + while (true) { + const v = values.next(); + if (v.done) break; + if (!v.value.attachableTo(model)) continue; + options.push(v.value.option); + if (v.value.defaultSelected) selected.push(v.value.option[0]); + } + return ParamDefinition.MultiSelect(selected, options); + } + + getDefault(model: Model) { + const values = this.providers.values(); + const selected: string[] = []; + while (true) { + const v = values.next(); + if (v.done) break; + if (!v.value.attachableTo(model)) continue; + if (v.value.defaultSelected) selected.push(v.value.option[0]); + } + return selected; + } + + get(name: string) { + const prop = this.providers.get(name); + if (!prop) throw new Error(`Custom prop '${name}' is not registered.`); + return this.providers.get(name); + } + + register(provider: CustomPropertyRegistry.Provider) { + this.providers.set(provider.descriptor.name, provider); + } + + unregister(name: string) { + this.providers.delete(name); + } +} + +namespace CustomPropertyRegistry { + export interface Provider { + option: [string, string], + defaultSelected: boolean, + descriptor: ModelPropertyDescriptor<any, any>, + attachableTo: (model: Model) => boolean, + attach: (model: Model) => Task<boolean> + } +} \ No newline at end of file diff --git a/src/mol-repr/representation.ts b/src/mol-repr/representation.ts index f2c8b683fd2ffe30ddc353e1af8ee5fc75c4f2ae..b990ba87aa66314b6cc4a8f3f641007431b5690f 100644 --- a/src/mol-repr/representation.ts +++ b/src/mol-repr/representation.ts @@ -14,23 +14,31 @@ import { WebGLContext } from 'mol-gl/webgl/context'; import { getQualityProps } from './util'; import { ColorTheme } from 'mol-theme/color'; import { SizeTheme } from 'mol-theme/size'; -import { Theme, ThemeRegistryContext } from 'mol-theme/theme'; +import { Theme, ThemeRegistryContext, createEmptyTheme } from 'mol-theme/theme'; import { Subject } from 'rxjs'; +import { Mat4 } from 'mol-math/linear-algebra'; // export interface RepresentationProps { // visuals?: string[] // } export type RepresentationProps = { [k: string]: any } +export interface RepresentationContext { + readonly webgl?: WebGLContext + readonly colorThemeRegistry: ColorTheme.Registry + readonly sizeThemeRegistry: SizeTheme.Registry +} + export type RepresentationParamsGetter<D, P extends PD.Params> = (ctx: ThemeRegistryContext, data: D) => P +export type RepresentationFactory<D, P extends PD.Params> = (ctx: RepresentationContext, getParams: RepresentationParamsGetter<D, P>) => Representation<D, P> // export interface RepresentationProvider<D, P extends PD.Params> { readonly label: string readonly description: string - readonly factory: (getParams: RepresentationParamsGetter<D, P>) => Representation<D, P> - readonly getParams: (ctx: ThemeRegistryContext, data: D) => P + readonly factory: RepresentationFactory<D, P> + readonly getParams: RepresentationParamsGetter<D, P> readonly defaultValues: PD.Values<P> } @@ -71,12 +79,6 @@ export class RepresentationRegistry<D> { // -export interface RepresentationContext { - webgl?: WebGLContext - colorThemeRegistry: ColorTheme.Registry - sizeThemeRegistry: SizeTheme.Registry -} - export { Representation } interface Representation<D, P extends PD.Params = {}> { readonly label: string @@ -86,30 +88,50 @@ interface Representation<D, P extends PD.Params = {}> { readonly renderObjects: ReadonlyArray<RenderObject> readonly props: Readonly<PD.Values<P>> readonly params: Readonly<P> - createOrUpdate: (ctx: RepresentationContext, props?: Partial<PD.Values<P>>, data?: D) => Task<void> + readonly state: Readonly<Representation.State> + readonly theme: Readonly<Theme> + createOrUpdate: (props?: Partial<PD.Values<P>>, data?: D) => Task<void> + setState: (state: Partial<Representation.State>) => void + setTheme: (theme: Theme) => void getLoci: (pickingId: PickingId) => Loci mark: (loci: Loci, action: MarkerAction) => boolean - setVisibility: (value: boolean) => void - setPickable: (value: boolean) => void destroy: () => void } namespace Representation { + export interface State { + visible: boolean + pickable: boolean + syncManually: boolean + transform: Mat4 + } + export function createState() { + return { visible: false, pickable: false, syncManually: false, transform: Mat4.identity() } + } + export function updateState(state: State, update: Partial<State>) { + if (update.visible !== undefined) state.visible = update.visible + if (update.pickable !== undefined) state.pickable = update.pickable + if (update.syncManually !== undefined) state.syncManually = update.syncManually + if (update.transform !== undefined) Mat4.copy(state.transform, update.transform) + } + export type Any = Representation<any> export const Empty: Any = { - label: '', groupCount: 0, renderObjects: [], props: {}, params: {}, updated: new Subject(), + label: '', groupCount: 0, renderObjects: [], props: {}, params: {}, updated: new Subject(), state: createState(), theme: createEmptyTheme(), createOrUpdate: () => Task.constant('', undefined), + setState: () => {}, + setTheme: () => {}, getLoci: () => EmptyLoci, mark: () => false, - setVisibility: () => {}, - setPickable: () => {}, destroy: () => {} } - export type Def<D, P extends PD.Params = {}> = { [k: string]: (getParams: RepresentationParamsGetter<D, P>) => Representation<any, P> } + export type Def<D, P extends PD.Params = {}> = { [k: string]: RepresentationFactory<D, P> } - export function createMulti<D, P extends PD.Params = {}>(label: string, getParams: RepresentationParamsGetter<D, P>, reprDefs: Def<D, P>): Representation<D, P> { + export function createMulti<D, P extends PD.Params = {}>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<D, P>, reprDefs: Def<D, P>): Representation<D, P> { let version = 0 const updated = new Subject<number>() + const currentState = Representation.createState() + let currentTheme = createEmptyTheme() let currentParams: P let currentProps: PD.Values<P> @@ -118,7 +140,7 @@ namespace Representation { const reprMap: { [k: number]: string } = {} const reprList: Representation<D, P>[] = Object.keys(reprDefs).map((name, i) => { reprMap[i] = name - return reprDefs[name](getParams) + return reprDefs[name](ctx, getParams) }) return { @@ -154,7 +176,7 @@ namespace Representation { return props as P }, get params() { return currentParams }, - createOrUpdate: (ctx: RepresentationContext, props: Partial<P> = {}, data?: D) => { + createOrUpdate: (props: Partial<P> = {}, data?: D) => { if (data && data !== currentData) { currentParams = getParams(ctx, data) currentData = data @@ -167,12 +189,14 @@ namespace Representation { return Task.create(`Creating '${label}' representation`, async runtime => { for (let i = 0, il = reprList.length; i < il; ++i) { if (!visuals || visuals.includes(reprMap[i])) { - await reprList[i].createOrUpdate(ctx, currentProps, currentData).runInContext(runtime) + await reprList[i].createOrUpdate(currentProps, currentData).runInContext(runtime) } } updated.next(version++) }) }, + get state() { return currentState }, + get theme() { return currentTheme }, getLoci: (pickingId: PickingId) => { for (let i = 0, il = reprList.length; i < il; ++i) { const loci = reprList[i].getLoci(pickingId) @@ -187,14 +211,15 @@ namespace Representation { } return marked }, - setVisibility: (value: boolean) => { + setState: (state: Partial<State>) => { for (let i = 0, il = reprList.length; i < il; ++i) { - reprList[i].setVisibility(value) + reprList[i].setState(state) } + Representation.updateState(currentState, state) }, - setPickable: (value: boolean) => { + setTheme: (theme: Theme) => { for (let i = 0, il = reprList.length; i < il; ++i) { - reprList[i].setPickable(value) + reprList[i].setTheme(theme) } }, destroy() { @@ -209,9 +234,10 @@ namespace Representation { // export interface VisualContext { - webgl?: WebGLContext - runtime: RuntimeContext, + readonly runtime: RuntimeContext + readonly webgl?: WebGLContext } +// export type VisualFactory<D, P extends PD.Params> = (ctx: VisualContext) => Visual<D, P> export interface Visual<D, P extends PD.Params> { /** Number of addressable groups in all instances of the visual */ diff --git a/src/mol-repr/shape/representation.ts b/src/mol-repr/shape/representation.ts index 7908459ac25ddff00f3762d81ce8d7dfb3c43a53..ef2a5076c7605af4184f2dabddff4f313dfbbb18 100644 --- a/src/mol-repr/shape/representation.ts +++ b/src/mol-repr/shape/representation.ts @@ -18,7 +18,7 @@ import { createRenderableState } from 'mol-geo/geometry/geometry'; import { PickingId } from 'mol-geo/geometry/picking'; import { MarkerAction, applyMarkerAction } from 'mol-geo/geometry/marker-data'; import { LocationIterator } from 'mol-geo/util/location-iterator'; -import { createTheme } from 'mol-theme/theme'; +import { createEmptyTheme, Theme } from 'mol-theme/theme'; import { Subject } from 'rxjs'; export interface ShapeRepresentation<P extends ShapeParams> extends Representation<Shape, P> { } @@ -30,17 +30,19 @@ export const ShapeParams = { } export type ShapeParams = typeof ShapeParams -export function ShapeRepresentation<P extends ShapeParams>(): ShapeRepresentation<P> { +export function ShapeRepresentation<P extends ShapeParams>(ctx: RepresentationContext): ShapeRepresentation<P> { let version = 0 const updated = new Subject<number>() + const _state = Representation.createState() const renderObjects: RenderObject[] = [] let _renderObject: MeshRenderObject | undefined let _shape: Shape + let _theme = createEmptyTheme() let currentProps: PD.Values<P> = PD.getDefaultValues(ShapeParams) as PD.Values<P> let currentParams: P let locationIt: LocationIterator - function createOrUpdate(ctx: RepresentationContext, props: Partial<PD.Values<P>> = {}, shape?: Shape) { + function createOrUpdate(props: Partial<PD.Values<P>> = {}, shape?: Shape) { currentProps = Object.assign({}, currentProps, props) if (shape) _shape = shape @@ -51,10 +53,9 @@ export function ShapeRepresentation<P extends ShapeParams>(): ShapeRepresentatio const mesh = _shape.mesh locationIt = ShapeGroupIterator.fromShape(_shape) - const theme = createTheme(ctx, currentProps, {}) const transform = createIdentityTransform() - const values = await Mesh.createValues(runtime, mesh, transform, locationIt, theme, currentProps) + const values = await Mesh.createValues(runtime, mesh, transform, locationIt, _theme, currentProps) const state = createRenderableState(currentProps) _renderObject = createMeshRenderObject(values, state) @@ -65,11 +66,13 @@ export function ShapeRepresentation<P extends ShapeParams>(): ShapeRepresentatio return { label: 'Shape mesh', - updated, get groupCount () { return locationIt ? locationIt.count : 0 }, get renderObjects () { return renderObjects }, - get params () { return currentParams }, get props () { return currentProps }, + get params () { return currentParams }, + get state() { return _state }, + get theme() { return _theme }, + updated, createOrUpdate, getLoci(pickingId: PickingId) { const { objectId, groupId } = pickingId @@ -103,11 +106,14 @@ export function ShapeRepresentation<P extends ShapeParams>(): ShapeRepresentatio } return changed }, - setVisibility(value: boolean) { - renderObjects.forEach(ro => ro.state.visible = value) + setState(state: Partial<Representation.State>) { + if (state.visible !== undefined) renderObjects.forEach(ro => ro.state.visible = state.visible!) + if (state.pickable !== undefined) renderObjects.forEach(ro => ro.state.pickable = state.pickable!) + + Representation.updateState(_state, state) }, - setPickable(value: boolean) { - renderObjects.forEach(ro => ro.state.pickable = value) + setTheme(theme: Theme) { + _theme = theme }, destroy() { // TODO diff --git a/src/mol-repr/structure/complex-representation.ts b/src/mol-repr/structure/complex-representation.ts index fc6253b81b6d069d089f6ad3981691ac9fea4440..2bbfa2955385afab3043401d355f415b231acc6a 100644 --- a/src/mol-repr/structure/complex-representation.ts +++ b/src/mol-repr/structure/complex-representation.ts @@ -12,33 +12,33 @@ import { StructureRepresentation, StructureParams } from './representation'; import { ComplexVisual } from './complex-visual'; import { PickingId } from 'mol-geo/geometry/picking'; import { MarkerAction } from 'mol-geo/geometry/marker-data'; -import { RepresentationContext, RepresentationParamsGetter } from 'mol-repr/representation'; -import { Theme, createTheme } from 'mol-theme/theme'; +import { RepresentationContext, RepresentationParamsGetter, Representation } from 'mol-repr/representation'; +import { Theme, createEmptyTheme } from 'mol-theme/theme'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import { Subject } from 'rxjs'; -export function ComplexRepresentation<P extends StructureParams>(label: string, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: () => ComplexVisual<P>): StructureRepresentation<P> { +export function ComplexRepresentation<P extends StructureParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: () => ComplexVisual<P>): StructureRepresentation<P> { let version = 0 const updated = new Subject<number>() + const _state = Representation.createState() let visual: ComplexVisual<P> | undefined let _structure: Structure let _params: P let _props: PD.Values<P> - let _theme: Theme + let _theme = createEmptyTheme() - function createOrUpdate(ctx: RepresentationContext, props: Partial<PD.Values<P>> = {}, structure?: Structure) { + function createOrUpdate(props: Partial<PD.Values<P>> = {}, structure?: Structure) { if (structure && structure !== _structure) { _params = getParams(ctx, structure) _structure = structure if (!_props) _props = PD.getDefaultValues(_params) } _props = Object.assign({}, _props, props) - _theme = createTheme(ctx, { structure: _structure }, props, _theme) return Task.create('Creating or updating ComplexRepresentation', async runtime => { if (!visual) visual = visualCtor() - await visual.createOrUpdate({ ...ctx, runtime }, _theme, _props, structure) + await visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, structure) updated.next(version++) }); } @@ -51,12 +51,15 @@ export function ComplexRepresentation<P extends StructureParams>(label: string, return visual ? visual.mark(loci, action) : false } - function setVisibility(value: boolean) { - if (visual) visual.setVisibility(value) + function setState(state: Partial<Representation.State>) { + if (state.visible !== undefined && visual) visual.setVisibility(state.visible) + if (state.pickable !== undefined && visual) visual.setPickable(state.pickable) + + Representation.updateState(_state, state) } - function setPickable(value: boolean) { - if (visual) visual.setPickable(value) + function setTheme(theme: Theme) { + _theme = theme } function destroy() { @@ -73,12 +76,14 @@ export function ComplexRepresentation<P extends StructureParams>(label: string, }, get props() { return _props }, get params() { return _params }, - get updated() { return updated }, + get state() { return _state }, + get theme() { return _theme }, + updated, createOrUpdate, + setState, + setTheme, getLoci, mark, - setVisibility, - setPickable, destroy } } \ No newline at end of file diff --git a/src/mol-repr/structure/complex-visual.ts b/src/mol-repr/structure/complex-visual.ts index 4e39ce449219d0c663d2c9f8cad3bb24fe9aa0fc..ac70ac744eeb38023e28d6707acb07de71e62567 100644 --- a/src/mol-repr/structure/complex-visual.ts +++ b/src/mol-repr/structure/complex-visual.ts @@ -48,12 +48,13 @@ interface ComplexVisualBuilder<P extends ComplexParams, G extends Geometry> { interface ComplexVisualGeometryBuilder<P extends ComplexParams, G extends Geometry> extends ComplexVisualBuilder<P, G> { createEmptyGeometry(geometry?: G): G createRenderObject(ctx: VisualContext, structure: Structure, geometry: Geometry, locationIt: LocationIterator, theme: Theme, currentProps: PD.Values<P>): Promise<ComplexRenderObject> - updateValues(values: RenderableValues, newProps: PD.Values<P>): void + updateValues(values: RenderableValues, newProps: PD.Values<P>): void, + updateBoundingSphere(values: RenderableValues, geometry: Geometry): void } export function ComplexVisual<P extends ComplexParams>(builder: ComplexVisualGeometryBuilder<P, Geometry>): ComplexVisual<P> { const { defaultProps, createGeometry, createLocationIterator, getLoci, mark, setUpdateState } = builder - const { createRenderObject, updateValues } = builder + const { createRenderObject, updateValues, updateBoundingSphere } = builder const updateState = VisualUpdateState.create() let renderObject: ComplexRenderObject | undefined @@ -85,20 +86,21 @@ export function ComplexVisual<P extends ComplexParams>(builder: ComplexVisualGeo VisualUpdateState.reset(updateState) setUpdateState(updateState, newProps, currentProps, theme, currentTheme) + if (!ColorTheme.areEqual(theme.color, currentTheme.color)) updateState.updateColor = true + if (!deepEqual(newProps.unitKinds, currentProps.unitKinds)) updateState.createGeometry = true + const newConformationHash = Structure.conformationHash(currentStructure) if (newConformationHash !== conformationHash) { conformationHash = newConformationHash updateState.createGeometry = true } - if (ColorTheme.areEqual(theme.color, currentTheme.color)) updateState.updateColor = true - if (!deepEqual(newProps.unitKinds, currentProps.unitKinds)) updateState.createGeometry = true - // if (updateState.createGeometry) { geometry = await createGeometry(ctx, currentStructure, theme, newProps, geometry) ValueCell.update(renderObject.values.drawCount, Geometry.getDrawCount(geometry)) + updateBoundingSphere(renderObject.values, geometry) updateState.updateColor = true } @@ -129,7 +131,7 @@ export function ComplexVisual<P extends ComplexParams>(builder: ComplexVisualGeo throw new Error('missing structure') } else if (structure && (!currentStructure || !renderObject)) { await create(ctx, structure, theme, props) - } else if (structure && structure.hashCode !== currentStructure.hashCode) { + } else if (structure && !Structure.areEquivalent(structure, currentStructure)) { await create(ctx, structure, theme, props) } else { if (structure && Structure.conformationHash(structure) !== Structure.conformationHash(currentStructure)) { @@ -153,7 +155,7 @@ export function ComplexVisual<P extends ComplexParams>(builder: ComplexVisualGeo } let changed = false - if (isEveryLoci(loci)) { + if (isEveryLoci(loci) || (Structure.isLoci(loci) && loci.structure === currentStructure)) { changed = apply(Interval.ofBounds(0, groupCount * instanceCount)) } else { changed = mark(loci, currentStructure, apply) @@ -195,6 +197,7 @@ export function ComplexMeshVisual<P extends ComplexMeshParams>(builder: ComplexM }, createEmptyGeometry: Mesh.createEmpty, createRenderObject: createComplexMeshRenderObject, - updateValues: Mesh.updateValues + updateValues: Mesh.updateValues, + updateBoundingSphere: Mesh.updateBoundingSphere }) } \ No newline at end of file diff --git a/src/mol-repr/structure/representation/ball-and-stick.ts b/src/mol-repr/structure/representation/ball-and-stick.ts index 9b594c30f3eb3d961659bda01539057bc3fc6d57..3fcb185079b9099965a8eb1138bbde590781552f 100644 --- a/src/mol-repr/structure/representation/ball-and-stick.ts +++ b/src/mol-repr/structure/representation/ball-and-stick.ts @@ -11,16 +11,15 @@ import { ParamDefinition as PD } from 'mol-util/param-definition'; import { UnitsRepresentation } from '../units-representation'; import { ComplexRepresentation } from '../complex-representation'; import { StructureRepresentation, StructureRepresentationProvider } from '../representation'; -import { Representation, RepresentationParamsGetter } from 'mol-repr/representation'; +import { Representation, RepresentationParamsGetter, RepresentationContext } from 'mol-repr/representation'; import { ThemeRegistryContext } from 'mol-theme/theme'; import { Structure } from 'mol-model/structure'; -import { BuiltInColorThemeOptions, BuiltInColorThemes, ColorTheme } from 'mol-theme/color'; import { UnitKind, UnitKindOptions } from '../visual/util/common'; const BallAndStickVisuals = { - 'element-sphere': (getParams: RepresentationParamsGetter<Structure, ElementSphereParams>) => UnitsRepresentation('Element sphere mesh', getParams, ElementSphereVisual), - 'intra-link': (getParams: RepresentationParamsGetter<Structure, IntraUnitLinkParams>) => UnitsRepresentation('Intra-unit link cylinder', getParams, IntraUnitLinkVisual), - 'inter-link': (getParams: RepresentationParamsGetter<Structure, InterUnitLinkParams>) => ComplexRepresentation('Inter-unit link cylinder', getParams, InterUnitLinkVisual), + 'element-sphere': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, ElementSphereParams>) => UnitsRepresentation('Element sphere mesh', ctx, getParams, ElementSphereVisual), + 'intra-link': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, IntraUnitLinkParams>) => UnitsRepresentation('Intra-unit link cylinder', ctx, getParams, IntraUnitLinkVisual), + 'inter-link': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, InterUnitLinkParams>) => ComplexRepresentation('Inter-unit link cylinder', ctx, getParams, InterUnitLinkVisual), } type BallAndStickVisualName = keyof typeof BallAndStickVisuals const BallAndStickVisualOptions = Object.keys(BallAndStickVisuals).map(name => [name, name] as [BallAndStickVisualName, string]) @@ -32,7 +31,6 @@ export const BallAndStickParams = { unitKinds: PD.MultiSelect<UnitKind>(['atomic'], UnitKindOptions), sizeFactor: PD.Numeric(0.3, { min: 0.01, max: 10, step: 0.01 }), sizeAspectRatio: PD.Numeric(2/3, { min: 0.01, max: 3, step: 0.01 }), - colorTheme: PD.Mapped('element-symbol', BuiltInColorThemeOptions, name => PD.Group((BuiltInColorThemes as { [k: string]: ColorTheme.Provider<any> })[name].getParams({}))), visuals: PD.MultiSelect<BallAndStickVisualName>(['element-sphere', 'intra-link', 'inter-link'], BallAndStickVisualOptions), } export type BallAndStickParams = typeof BallAndStickParams @@ -41,8 +39,8 @@ export function getBallAndStickParams(ctx: ThemeRegistryContext, structure: Stru } export type BallAndStickRepresentation = StructureRepresentation<BallAndStickParams> -export function BallAndStickRepresentation(getParams: RepresentationParamsGetter<Structure, BallAndStickParams>): BallAndStickRepresentation { - return Representation.createMulti('Ball & Stick', getParams, BallAndStickVisuals as unknown as Representation.Def<Structure, BallAndStickParams>) +export function BallAndStickRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, BallAndStickParams>): BallAndStickRepresentation { + return Representation.createMulti('Ball & Stick', ctx, getParams, BallAndStickVisuals as unknown as Representation.Def<Structure, BallAndStickParams>) } export const BallAndStickRepresentationProvider: StructureRepresentationProvider<typeof BallAndStickParams> = { diff --git a/src/mol-repr/structure/representation/carbohydrate.ts b/src/mol-repr/structure/representation/carbohydrate.ts index ac2e0d87bd53066ddf807ea3b39c2c2f73c3cc2f..2aa06816317593e32be5cbe96affc16facf12c47 100644 --- a/src/mol-repr/structure/representation/carbohydrate.ts +++ b/src/mol-repr/structure/representation/carbohydrate.ts @@ -10,15 +10,14 @@ import { CarbohydrateTerminalLinkParams, CarbohydrateTerminalLinkVisual } from ' import { ParamDefinition as PD } from 'mol-util/param-definition'; import { ComplexRepresentation } from '../complex-representation'; import { StructureRepresentation, StructureRepresentationProvider } from '../representation'; -import { Representation, RepresentationParamsGetter } from 'mol-repr/representation'; +import { Representation, RepresentationParamsGetter, RepresentationContext } from 'mol-repr/representation'; import { ThemeRegistryContext } from 'mol-theme/theme'; import { Structure } from 'mol-model/structure'; -import { BuiltInColorThemeOptions, getBuiltInColorThemeParams } from 'mol-theme/color'; const CarbohydrateVisuals = { - 'carbohydrate-symbol': (getParams: RepresentationParamsGetter<Structure, CarbohydrateSymbolParams>) => ComplexRepresentation('Carbohydrate symbol mesh', getParams, CarbohydrateSymbolVisual), - 'carbohydrate-link': (getParams: RepresentationParamsGetter<Structure, CarbohydrateLinkParams>) => ComplexRepresentation('Carbohydrate link cylinder', getParams, CarbohydrateLinkVisual), - 'carbohydrate-terminal-link': (getParams: RepresentationParamsGetter<Structure, CarbohydrateTerminalLinkParams>) => ComplexRepresentation('Carbohydrate terminal link cylinder', getParams, CarbohydrateTerminalLinkVisual), + 'carbohydrate-symbol': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, CarbohydrateSymbolParams>) => ComplexRepresentation('Carbohydrate symbol mesh', ctx, getParams, CarbohydrateSymbolVisual), + 'carbohydrate-link': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, CarbohydrateLinkParams>) => ComplexRepresentation('Carbohydrate link cylinder', ctx, getParams, CarbohydrateLinkVisual), + 'carbohydrate-terminal-link': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, CarbohydrateTerminalLinkParams>) => ComplexRepresentation('Carbohydrate terminal link cylinder', ctx, getParams, CarbohydrateTerminalLinkVisual), } type CarbohydrateVisualName = keyof typeof CarbohydrateVisuals const CarbohydrateVisualOptions = Object.keys(CarbohydrateVisuals).map(name => [name, name] as [CarbohydrateVisualName, string]) @@ -27,18 +26,16 @@ export const CarbohydrateParams = { ...CarbohydrateSymbolParams, ...CarbohydrateLinkParams, ...CarbohydrateTerminalLinkParams, - colorTheme: PD.Mapped('carbohydrate-symbol', BuiltInColorThemeOptions, getBuiltInColorThemeParams), visuals: PD.MultiSelect<CarbohydrateVisualName>(['carbohydrate-symbol', 'carbohydrate-link', 'carbohydrate-terminal-link'], CarbohydrateVisualOptions), } -PD.getDefaultValues(CarbohydrateParams).colorTheme.name export type CarbohydrateParams = typeof CarbohydrateParams export function getCarbohydrateParams(ctx: ThemeRegistryContext, structure: Structure) { return PD.clone(CarbohydrateParams) } export type CarbohydrateRepresentation = StructureRepresentation<CarbohydrateParams> -export function CarbohydrateRepresentation(getParams: RepresentationParamsGetter<Structure, CarbohydrateParams>): CarbohydrateRepresentation { - return Representation.createMulti('Carbohydrate', getParams, CarbohydrateVisuals as unknown as Representation.Def<Structure, CarbohydrateParams>) +export function CarbohydrateRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, CarbohydrateParams>): CarbohydrateRepresentation { + return Representation.createMulti('Carbohydrate', ctx, getParams, CarbohydrateVisuals as unknown as Representation.Def<Structure, CarbohydrateParams>) } export const CarbohydrateRepresentationProvider: StructureRepresentationProvider<CarbohydrateParams> = { diff --git a/src/mol-repr/structure/representation/cartoon.ts b/src/mol-repr/structure/representation/cartoon.ts index ccb54f9332aca73d05b0e589cc31cbe7352a5028..f6fbff786bb2110467c35605d73cee718c764c58 100644 --- a/src/mol-repr/structure/representation/cartoon.ts +++ b/src/mol-repr/structure/representation/cartoon.ts @@ -10,17 +10,16 @@ import { NucleotideBlockVisual, NucleotideBlockParams } from '../visual/nucleoti import { ParamDefinition as PD } from 'mol-util/param-definition'; import { UnitsRepresentation } from '../units-representation'; import { StructureRepresentation, StructureRepresentationProvider } from '../representation'; -import { Representation, RepresentationParamsGetter } from 'mol-repr/representation'; +import { Representation, RepresentationParamsGetter, RepresentationContext } from 'mol-repr/representation'; import { PolymerDirectionVisual, PolymerDirectionParams } from '../visual/polymer-direction-wedge'; import { Structure } from 'mol-model/structure'; import { ThemeRegistryContext } from 'mol-theme/theme'; -import { BuiltInColorThemeOptions, getBuiltInColorThemeParams } from 'mol-theme/color'; const CartoonVisuals = { - 'polymer-trace': (getParams: RepresentationParamsGetter<Structure, PolymerTraceParams>) => UnitsRepresentation('Polymer trace mesh', getParams, PolymerTraceVisual), - 'polymer-gap': (getParams: RepresentationParamsGetter<Structure, PolymerGapParams>) => UnitsRepresentation('Polymer gap cylinder', getParams, PolymerGapVisual), - 'nucleotide-block': (getParams: RepresentationParamsGetter<Structure, NucleotideBlockParams>) => UnitsRepresentation('Nucleotide block mesh', getParams, NucleotideBlockVisual), - 'direction-wedge': (getParams: RepresentationParamsGetter<Structure, PolymerDirectionParams>) => UnitsRepresentation('Polymer direction wedge', getParams, PolymerDirectionVisual) + 'polymer-trace': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, PolymerTraceParams>) => UnitsRepresentation('Polymer trace mesh', ctx, getParams, PolymerTraceVisual), + 'polymer-gap': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, PolymerGapParams>) => UnitsRepresentation('Polymer gap cylinder', ctx, getParams, PolymerGapVisual), + 'nucleotide-block': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, NucleotideBlockParams>) => UnitsRepresentation('Nucleotide block mesh', ctx, getParams, NucleotideBlockVisual), + 'direction-wedge': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, PolymerDirectionParams>) => UnitsRepresentation('Polymer direction wedge', ctx, getParams, PolymerDirectionVisual) } type CartoonVisualName = keyof typeof CartoonVisuals const CartoonVisualOptions = Object.keys(CartoonVisuals).map(name => [name, name] as [CartoonVisualName, string]) @@ -31,23 +30,21 @@ export const CartoonParams = { ...NucleotideBlockParams, ...PolymerDirectionParams, sizeFactor: PD.Numeric(0.2, { min: 0, max: 10, step: 0.01 }), - colorTheme: PD.Mapped('polymer-index', BuiltInColorThemeOptions, getBuiltInColorThemeParams), visuals: PD.MultiSelect<CartoonVisualName>(['polymer-trace', 'polymer-gap', 'nucleotide-block'], CartoonVisualOptions), } -PD.getDefaultValues(CartoonParams).colorTheme.name export type CartoonParams = typeof CartoonParams export function getCartoonParams(ctx: ThemeRegistryContext, structure: Structure) { return PD.clone(CartoonParams) } export type CartoonRepresentation = StructureRepresentation<CartoonParams> -export function CartoonRepresentation(getParams: RepresentationParamsGetter<Structure, CartoonParams>): CartoonRepresentation { - return Representation.createMulti('Cartoon', getParams, CartoonVisuals as unknown as Representation.Def<Structure, CartoonParams>) +export function CartoonRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, CartoonParams>): CartoonRepresentation { + return Representation.createMulti('Cartoon', ctx, getParams, CartoonVisuals as unknown as Representation.Def<Structure, CartoonParams>) } export const CartoonRepresentationProvider: StructureRepresentationProvider<CartoonParams> = { label: 'Cartoon', - description: 'Displays a ribbon smoothly following the trace atom of polymers.', + description: 'Displays a ribbon smoothly following the trace atoms of polymers.', factory: CartoonRepresentation, getParams: getCartoonParams, defaultValues: PD.getDefaultValues(CartoonParams) diff --git a/src/mol-repr/structure/representation/molecular-surface.ts b/src/mol-repr/structure/representation/molecular-surface.ts index cdf89b914e547ee033b46552881bc8d4383440ca..4181217d34234849783d03a6351ad2a59a01de0a 100644 --- a/src/mol-repr/structure/representation/molecular-surface.ts +++ b/src/mol-repr/structure/representation/molecular-surface.ts @@ -10,15 +10,14 @@ import { GaussianWireframeVisual, GaussianWireframeParams } from '../visual/gaus import { ParamDefinition as PD } from 'mol-util/param-definition'; import { GaussianDensityVolumeParams, GaussianDensityVolumeVisual } from '../visual/gaussian-density-volume'; import { StructureRepresentation, StructureRepresentationProvider } from '../representation'; -import { Representation, RepresentationParamsGetter } from 'mol-repr/representation'; +import { Representation, RepresentationParamsGetter, RepresentationContext } from 'mol-repr/representation'; import { ThemeRegistryContext } from 'mol-theme/theme'; import { Structure } from 'mol-model/structure'; -import { BuiltInColorThemeOptions, getBuiltInColorThemeParams } from 'mol-theme/color'; const MolecularSurfaceVisuals = { - 'gaussian-surface': (getParams: RepresentationParamsGetter<Structure, GaussianSurfaceParams>) => UnitsRepresentation('Gaussian surface', getParams, GaussianSurfaceVisual), - 'gaussian-wireframe': (getParams: RepresentationParamsGetter<Structure, GaussianWireframeParams>) => UnitsRepresentation('Gaussian wireframe', getParams, GaussianWireframeVisual), - 'gaussian-volume': (getParams: RepresentationParamsGetter<Structure, GaussianDensityVolumeParams>) => UnitsRepresentation('Gaussian volume', getParams, GaussianDensityVolumeVisual) + 'gaussian-surface': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, GaussianSurfaceParams>) => UnitsRepresentation('Gaussian surface', ctx, getParams, GaussianSurfaceVisual), + 'gaussian-wireframe': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, GaussianWireframeParams>) => UnitsRepresentation('Gaussian wireframe', ctx, getParams, GaussianWireframeVisual), + 'gaussian-volume': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, GaussianDensityVolumeParams>) => UnitsRepresentation('Gaussian volume', ctx, getParams, GaussianDensityVolumeVisual) } type MolecularSurfaceVisualName = keyof typeof MolecularSurfaceVisuals const MolecularSurfaceVisualOptions = Object.keys(MolecularSurfaceVisuals).map(name => [name, name] as [MolecularSurfaceVisualName, string]) @@ -27,18 +26,16 @@ export const MolecularSurfaceParams = { ...GaussianSurfaceParams, ...GaussianWireframeParams, ...GaussianDensityVolumeParams, - colorTheme: PD.Mapped('polymer-index', BuiltInColorThemeOptions, getBuiltInColorThemeParams), visuals: PD.MultiSelect<MolecularSurfaceVisualName>(['gaussian-surface'], MolecularSurfaceVisualOptions), } -PD.getDefaultValues(MolecularSurfaceParams).colorTheme.name export type MolecularSurfaceParams = typeof MolecularSurfaceParams export function getMolecularSurfaceParams(ctx: ThemeRegistryContext, structure: Structure) { return PD.clone(MolecularSurfaceParams) } export type MolecularSurfaceRepresentation = StructureRepresentation<MolecularSurfaceParams> -export function MolecularSurfaceRepresentation(getParams: RepresentationParamsGetter<Structure, MolecularSurfaceParams>): MolecularSurfaceRepresentation { - return Representation.createMulti('Molecular Surface', getParams, MolecularSurfaceVisuals as unknown as Representation.Def<Structure, MolecularSurfaceParams>) +export function MolecularSurfaceRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, MolecularSurfaceParams>): MolecularSurfaceRepresentation { + return Representation.createMulti('Molecular Surface', ctx, getParams, MolecularSurfaceVisuals as unknown as Representation.Def<Structure, MolecularSurfaceParams>) } export const MolecularSurfaceRepresentationProvider: StructureRepresentationProvider<MolecularSurfaceParams> = { @@ -47,26 +44,4 @@ export const MolecularSurfaceRepresentationProvider: StructureRepresentationProv factory: MolecularSurfaceRepresentation, getParams: getMolecularSurfaceParams, defaultValues: PD.getDefaultValues(MolecularSurfaceParams) -} - - - -// export const MolecularSurfaceParams = { -// ...GaussianSurfaceParams, -// ...GaussianWireframeParams, -// ...GaussianDensityVolumeParams, -// } -// export function getMolecularSurfaceParams(ctx: ThemeRegistryContext, structure: Structure) { -// return MolecularSurfaceParams // TODO return copy -// } -// export type MolecularSurfaceProps = PD.DefaultValues<typeof MolecularSurfaceParams> - -// export type MolecularSurfaceRepresentation = StructureRepresentation<MolecularSurfaceProps> - -// export function MolecularSurfaceRepresentation(defaultProps: MolecularSurfaceProps): MolecularSurfaceRepresentation { -// return Representation.createMulti('Molecular Surface', defaultProps, [ -// UnitsRepresentation('Gaussian surface', defaultProps, GaussianSurfaceVisual), -// UnitsRepresentation('Gaussian wireframe', defaultProps, GaussianWireframeVisual), -// UnitsRepresentation('Gaussian volume', defaultProps, GaussianDensityVolumeVisual) -// ]) -// } \ No newline at end of file +} \ No newline at end of file diff --git a/src/mol-repr/structure/units-representation.ts b/src/mol-repr/structure/units-representation.ts index abd56451a196e91b1ada57ad8a2319b1129224f5..01b381c9e57b6f9fd55861aa1dad9774bc0b20c2 100644 --- a/src/mol-repr/structure/units-representation.ts +++ b/src/mol-repr/structure/units-representation.ts @@ -8,13 +8,13 @@ import { Structure, Unit } from 'mol-model/structure'; import { Task } from 'mol-task' import { RenderObject } from 'mol-gl/render-object'; -import { Visual, RepresentationContext, RepresentationParamsGetter } from '../representation'; +import { Visual, RepresentationContext, RepresentationParamsGetter, Representation } from '../representation'; import { Loci, EmptyLoci, isEmptyLoci } from 'mol-model/loci'; import { StructureGroup } from './units-visual'; import { StructureRepresentation, StructureParams } from './representation'; import { PickingId } from 'mol-geo/geometry/picking'; import { MarkerAction } from 'mol-geo/geometry/marker-data'; -import { Theme, createTheme } from 'mol-theme/theme'; +import { Theme, createEmptyTheme } from 'mol-theme/theme'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import { UnitKind, UnitKindOptions } from './visual/util/common'; import { Subject } from 'rxjs'; @@ -27,40 +27,40 @@ export type UnitsParams = typeof UnitsParams export interface UnitsVisual<P extends UnitsParams> extends Visual<StructureGroup, P> { } -export function UnitsRepresentation<P extends UnitsParams>(label: string, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: () => UnitsVisual<P>): StructureRepresentation<P> { +export function UnitsRepresentation<P extends UnitsParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: () => UnitsVisual<P>): StructureRepresentation<P> { let version = 0 const updated = new Subject<number>() + const _state = Representation.createState() let visuals = new Map<number, { group: Unit.SymmetryGroup, visual: UnitsVisual<P> }>() let _structure: Structure let _groups: ReadonlyArray<Unit.SymmetryGroup> let _params: P let _props: PD.Values<P> - let _theme: Theme + let _theme = createEmptyTheme() - function createOrUpdate(ctx: RepresentationContext, props: Partial<PD.Values<P>> = {}, structure?: Structure) { + function createOrUpdate(props: Partial<PD.Values<P>> = {}, structure?: Structure) { if (structure && structure !== _structure) { _params = getParams(ctx, structure) if (!_props) _props = PD.getDefaultValues(_params) } _props = Object.assign({}, _props, props) - _theme = createTheme(ctx, { structure: structure || _structure }, props, _theme) return Task.create('Creating or updating UnitsRepresentation', async runtime => { if (!_structure && !structure) { throw new Error('missing structure') } else if (structure && !_structure) { - // console.log('initial structure') + // console.log(label, '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.createOrUpdate({ ...ctx, runtime }, _theme, _props, { group, structure }) + await visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, { group, structure }) visuals.set(group.hashCode, { visual, group }) } - } else if (structure && _structure.hashCode !== structure.hashCode) { - // console.log('_structure.hashCode !== structure.hashCode') + } else if (structure && !Structure.areEquivalent(structure, _structure)) { + // console.log(label, 'structure not equivalent') // Tries to re-use existing visuals for the groups of the new structure. // Creates additional visuals if needed, destroys left-over visuals. _groups = structure.unitSymmetryGroups; @@ -71,18 +71,25 @@ export function UnitsRepresentation<P extends UnitsParams>(label: string, getPar const group = _groups[i]; const visualGroup = oldVisuals.get(group.hashCode) if (visualGroup) { + // console.log(label, 'found visualGroup to reuse') + // console.log('old', visualGroup.group) + // console.log('new', group) const { visual } = visualGroup - await visual.createOrUpdate({ ...ctx, runtime }, _theme, _props, { group, structure }) + await visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, { group, structure }) visuals.set(group.hashCode, { visual, group }) oldVisuals.delete(group.hashCode) } else { + // console.log(label, 'not found visualGroup to reuse, creating new') // newGroups.push(group) const visual = visualCtor() - await visual.createOrUpdate({ ...ctx, runtime }, _theme, _props, { group, structure }) + await visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, { group, structure }) visuals.set(group.hashCode, { visual, group }) } } - oldVisuals.forEach(({ visual }) => visual.destroy()) + oldVisuals.forEach(({ visual }) => { + // console.log(label, 'removed unused visual') + visual.destroy() + }) // TODO review logic // For new groups, re-use left-over visuals @@ -94,30 +101,32 @@ export function UnitsRepresentation<P extends UnitsParams>(label: string, getPar // visuals.set(group.hashCode, { visual, group }) // }) // unusedVisuals.forEach(visual => visual.destroy()) - } else if (structure && structure !== _structure && _structure.hashCode === structure.hashCode) { - // console.log('_structure.hashCode === structure.hashCode') + } else if (structure && structure !== _structure && Structure.areEquivalent(structure, _structure)) { + // console.log(label, 'structures equivalent but not identical') // 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; + // console.log('new', structure.unitSymmetryGroups) + // console.log('old', _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, runtime }, _theme, _props, { group, structure }) + await visualGroup.visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, { group, structure }) visualGroup.group = group } else { throw new Error(`expected to find visual for hashCode ${group.hashCode}`) } } } else { - // console.log('no new structure') + // console.log(label, 'no new structure') // No new structure given, just update all visuals with new props. const visualsList: [ UnitsVisual<P>, Unit.SymmetryGroup ][] = [] // TODO avoid allocation visuals.forEach(({ visual, group }) => visualsList.push([ visual, group ])) for (let i = 0, il = visualsList.length; i < il; ++i) { const [ visual ] = visualsList[i] - await visual.createOrUpdate({ ...ctx, runtime }, _theme, _props) + await visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props) } } if (structure) _structure = structure @@ -142,16 +151,15 @@ export function UnitsRepresentation<P extends UnitsParams>(label: string, getPar return changed } - function setVisibility(value: boolean) { - visuals.forEach(({ visual }) => { - visual.setVisibility(value) - }) + function setState(state: Partial<Representation.State>) { + if (state.visible !== undefined) visuals.forEach(({ visual }) => visual.setVisibility(state.visible!)) + if (state.pickable !== undefined) visuals.forEach(({ visual }) => visual.setPickable(state.pickable!)) + + Representation.updateState(_state, state) } - function setPickable(value: boolean) { - visuals.forEach(({ visual }) => { - visual.setPickable(value) - }) + function setTheme(theme: Theme) { + _theme = theme } function destroy() { @@ -177,12 +185,14 @@ export function UnitsRepresentation<P extends UnitsParams>(label: string, getPar }, get props() { return _props }, get params() { return _params }, + get state() { return _state }, + get theme() { return _theme }, updated, createOrUpdate, + setState, + setTheme, getLoci, mark, - setVisibility, - setPickable, destroy } } \ No newline at end of file diff --git a/src/mol-repr/structure/units-visual.ts b/src/mol-repr/structure/units-visual.ts index e0f4ba605d0ae2ef534336adb59ef618c43812e5..8679baa90ebda0c5aabd234660fd1a57b535522c 100644 --- a/src/mol-repr/structure/units-visual.ts +++ b/src/mol-repr/structure/units-visual.ts @@ -34,13 +34,6 @@ export type StructureGroup = { structure: Structure, group: Unit.SymmetryGroup } export interface UnitsVisual<P extends RepresentationProps = {}> extends Visual<StructureGroup, P> { } -function sameGroupConformation(groupA: Unit.SymmetryGroup, groupB: Unit.SymmetryGroup) { - return ( - groupA.units.length === groupB.units.length && - Unit.conformationId(groupA.units[0]) === Unit.conformationId(groupB.units[0]) - ) -} - type UnitsRenderObject = MeshRenderObject | LinesRenderObject | PointsRenderObject | DirectVolumeRenderObject interface UnitsVisualBuilder<P extends UnitsParams, G extends Geometry> { @@ -56,11 +49,12 @@ interface UnitsVisualGeometryBuilder<P extends UnitsParams, G extends Geometry> createEmptyGeometry(geometry?: G): G createRenderObject(ctx: VisualContext, group: Unit.SymmetryGroup, geometry: Geometry, locationIt: LocationIterator, theme: Theme, currentProps: PD.Values<P>): Promise<UnitsRenderObject> updateValues(values: RenderableValues, newProps: PD.Values<P>): void + updateBoundingSphere(values: RenderableValues, geometry: Geometry): void } export function UnitsVisual<P extends UnitsParams>(builder: UnitsVisualGeometryBuilder<P, Geometry>): UnitsVisual<P> { const { defaultProps, createGeometry, createLocationIterator, getLoci, mark, setUpdateState } = builder - const { createEmptyGeometry, createRenderObject, updateValues } = builder + const { createEmptyGeometry, createRenderObject, updateValues, updateBoundingSphere } = builder const updateState = VisualUpdateState.create() let renderObject: UnitsRenderObject | undefined @@ -88,53 +82,78 @@ export function UnitsVisual<P extends UnitsParams>(builder: UnitsVisualGeometryB renderObject = await createRenderObject(ctx, group, geometry, locationIt, theme, currentProps) } - async function update(ctx: VisualContext, theme: Theme, props: Partial<PD.Values<P>> = {}) { + async function update(ctx: VisualContext, group: Unit.SymmetryGroup, theme: Theme, props: Partial<PD.Values<P>> = {}) { if (!renderObject) return const newProps = Object.assign({}, currentProps, props, { structure: currentStructure }) - const unit = currentGroup.units[0] + const unit = group.units[0] locationIt.reset() VisualUpdateState.reset(updateState) setUpdateState(updateState, newProps, currentProps, theme, currentTheme) + if (!ColorTheme.areEqual(theme.color, currentTheme.color)) { + // console.log('new colorTheme') + updateState.updateColor = true + } + if (!deepEqual(newProps.unitKinds, currentProps.unitKinds)) { + // console.log('new unitKinds') + updateState.createGeometry = true + } + + if (group.transformHash !== currentGroup.transformHash) { + // console.log('new transformHash') + if (group.units.length !== currentGroup.units.length || updateState.updateColor) { + updateState.updateTransform = true + } else { + updateState.updateMatrix = true + } + } + + // check if the conformation of unit.model has changed const newConformationId = Unit.conformationId(unit) if (newConformationId !== currentConformationId) { + // console.log('new conformation') currentConformationId = newConformationId updateState.createGeometry = true } - if (currentGroup.units.length !== locationIt.instanceCount) updateState.updateTransform = true - - if (ColorTheme.areEqual(theme.color, currentTheme.color)) updateState.updateColor = true - if (!deepEqual(newProps.unitKinds, currentProps.unitKinds)) updateState.createGeometry = true - // if (updateState.updateTransform) { - locationIt = createLocationIterator(currentGroup) + // console.log('update transform') + locationIt = createLocationIterator(group) const { instanceCount, groupCount } = locationIt - createUnitsTransform(currentGroup, renderObject.values) createMarkers(instanceCount * groupCount, renderObject.values) updateState.updateColor = true + updateState.updateMatrix = true + } + + if (updateState.updateMatrix) { + // console.log('update matrix') + createUnitsTransform(group, renderObject.values) } if (updateState.createGeometry) { + // console.log('update geometry') geometry = includesUnitKind(newProps.unitKinds, unit) ? await createGeometry(ctx, unit, currentStructure, theme, newProps, geometry) : createEmptyGeometry(geometry) ValueCell.update(renderObject.values.drawCount, Geometry.getDrawCount(geometry)) + updateBoundingSphere(renderObject.values, geometry) updateState.updateColor = true } if (updateState.updateSize) { // not all geometries have size data, so check here if ('uSize' in renderObject.values) { + // console.log('update size') await createSizes(ctx.runtime, locationIt, theme.size, renderObject.values) } } if (updateState.updateColor) { + // console.log('update color') await createColors(ctx.runtime, locationIt, theme.color, renderObject.values) } @@ -143,6 +162,7 @@ export function UnitsVisual<P extends UnitsParams>(builder: UnitsVisualGeometryB currentProps = newProps currentTheme = theme + currentGroup = group } return { @@ -161,11 +181,7 @@ export function UnitsVisual<P extends UnitsParams>(builder: UnitsVisualGeometryB await create(ctx, group, theme, props) } else { // console.log('unit-visual update') - if (group && !sameGroupConformation(group, currentGroup)) { - // console.log('unit-visual new conformation') - currentGroup = group - } - await update(ctx, theme, props) + await update(ctx, group || currentGroup, theme, props) } }, getLoci(pickingId: PickingId) { @@ -183,7 +199,7 @@ export function UnitsVisual<P extends UnitsParams>(builder: UnitsVisualGeometryB } let changed = false - if (isEveryLoci(loci)) { + if (isEveryLoci(loci) || (Structure.isLoci(loci) && loci.structure === currentStructure)) { changed = apply(Interval.ofBounds(0, groupCount * instanceCount)) } else { changed = mark(loci, { structure: currentStructure, group: currentGroup }, apply) @@ -224,7 +240,8 @@ export function UnitsMeshVisual<P extends UnitsMeshParams>(builder: UnitsMeshVis }, createEmptyGeometry: Mesh.createEmpty, createRenderObject: createUnitsMeshRenderObject, - updateValues: Mesh.updateValues + updateValues: Mesh.updateValues, + updateBoundingSphere: Mesh.updateBoundingSphere }) } @@ -246,7 +263,8 @@ export function UnitsPointsVisual<P extends UnitsPointsParams>(builder: UnitsPoi builder.setUpdateState(state, newProps, currentProps, newTheme, currentTheme) if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.updateSize = true }, - updateValues: Points.updateValues + updateValues: Points.updateValues, + updateBoundingSphere: Points.updateBoundingSphere }) } @@ -268,7 +286,8 @@ export function UnitsLinesVisual<P extends UnitsLinesParams>(builder: UnitsLines builder.setUpdateState(state, newProps, currentProps, newTheme, currentTheme) if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.updateSize = true }, - updateValues: Lines.updateValues + updateValues: Lines.updateValues, + updateBoundingSphere: Lines.updateBoundingSphere }) } @@ -290,6 +309,7 @@ export function UnitsDirectVolumeVisual<P extends UnitsDirectVolumeParams>(build builder.setUpdateState(state, newProps, currentProps, newTheme, currentTheme) if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.createGeometry = true }, - updateValues: DirectVolume.updateValues + updateValues: DirectVolume.updateValues, + updateBoundingSphere: DirectVolume.updateBoundingSphere }) } \ No newline at end of file diff --git a/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts b/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts index ba07ae3a78f973fe468237d50406c9571ac03208..dbdf3ada034ac6cf98b672a53c1a5c868b83444f 100644 --- a/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts +++ b/src/mol-repr/structure/visual/carbohydrate-symbol-mesh.ts @@ -203,12 +203,12 @@ function markCarbohydrate(loci: Loci, structure: Structure, apply: (interval: In for (const e of loci.elements) { OrderedSet.forEach(e.indices, v => { const { model, elements } = e.unit - const { index, offsets } = model.atomicHierarchy.residueAtomSegments + const { index, offsets } = model.atomicHierarchy.residueAtomSegments const rI = index[elements[v]] const unitIndexMin = OrderedSet.findPredecessorIndex(elements, offsets[rI]) const unitIndexMax = OrderedSet.findPredecessorIndex(elements, offsets[rI + 1] - 1) const unitIndexInterval = Interval.ofRange(unitIndexMin, unitIndexMax) - if(!OrderedSet.isSubset(e.indices, unitIndexInterval)) return + if (!OrderedSet.isSubset(e.indices, unitIndexInterval)) return const eI = getAnomericCarbon(e.unit, rI) if (eI !== undefined) { const idx = getElementIndex(e.unit, eI) diff --git a/src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts b/src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts index 30b64384b8d6098f7870bb289faf5f529d6c1e1b..fe9d2414e1b9bccce3b00675ef9792e55ed6ecc1 100644 --- a/src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts +++ b/src/mol-repr/structure/visual/carbohydrate-terminal-link-cylinder.ts @@ -34,9 +34,9 @@ async function createCarbohydrateTerminalLinkCylinderMesh(ctx: VisualContext, st const l = terminalLinks[edgeIndex] if (l.fromCarbohydrate) { Vec3.copy(posA, elements[l.carbohydrateIndex].geometry.center) - l.elementUnit.conformation.position(l.elementIndex, posB) + l.elementUnit.conformation.position(l.elementUnit.elements[l.elementIndex], posB) } else { - l.elementUnit.conformation.position(l.elementIndex, posA) + l.elementUnit.conformation.position(l.elementUnit.elements[l.elementIndex], posA) Vec3.copy(posB, elements[l.carbohydrateIndex].geometry.center) } }, @@ -123,7 +123,7 @@ function getTerminalLinkLoci(pickingId: PickingId, structure: Structure, id: num l.elementUnit, l.elementIndex, carb.unit, carbIndex as StructureElement.UnitIndex ) - ]) + ]) } return EmptyLoci } diff --git a/src/mol-repr/structure/visual/element-sphere.ts b/src/mol-repr/structure/visual/element-sphere.ts index 50c2abc39217083513b82f5cd984b3b2ac853e11..2b2f09c0ff7c95822b4c65f843cae686454f2a7e 100644 --- a/src/mol-repr/structure/visual/element-sphere.ts +++ b/src/mol-repr/structure/visual/element-sphere.ts @@ -10,11 +10,9 @@ import { VisualUpdateState } from '../../util'; import { createElementSphereMesh, markElement, getElementLoci, StructureElementIterator } from './util/element'; import { UnitsMeshVisual, UnitsMeshParams } from '../units-visual'; import { ParamDefinition as PD } from 'mol-util/param-definition'; -import { BuiltInSizeThemeOptions, getBuiltInSizeThemeParams } from 'mol-theme/size'; export const ElementSphereParams = { ...UnitsMeshParams, - sizeTheme: PD.Mapped('physical', BuiltInSizeThemeOptions, getBuiltInSizeThemeParams), sizeFactor: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }), detail: PD.Numeric(0, { min: 0, max: 3, step: 1 }), } diff --git a/src/mol-repr/structure/visual/nucleotide-block-mesh.ts b/src/mol-repr/structure/visual/nucleotide-block-mesh.ts index 8e5188611682aa9e0f533e9cde7da22311997263..b3be2492c664406176724afa7772aeddb26c877b 100644 --- a/src/mol-repr/structure/visual/nucleotide-block-mesh.ts +++ b/src/mol-repr/structure/visual/nucleotide-block-mesh.ts @@ -4,7 +4,7 @@ * @author Alexander Rose <alexander.rose@weirdbyte.de> */ -import { Unit, Structure } from 'mol-model/structure'; +import { Unit, Structure, ElementIndex } from 'mol-model/structure'; import { UnitsVisual } from '../representation'; import { Vec3, Mat4 } from 'mol-math/linear-algebra'; import { Segmentation } from 'mol-data/int'; @@ -71,7 +71,7 @@ async function createNucleotideBlockMesh(ctx: VisualContext, unit: Unit, structu if (isNucleic(moleculeType)) { const parentId = modifiedResidues.parentId.get(compId) if (parentId !== undefined) compId = parentId - let idx1 = -1, idx2 = -1, idx3 = -1, idx4 = -1, idx5 = -1, idx6 = -1 + let idx1: ElementIndex | -1 = -1, idx2: ElementIndex | -1 = -1, idx3: ElementIndex | -1 = -1, idx4: ElementIndex | -1 = -1, idx5: ElementIndex | -1 = -1, idx6: ElementIndex | -1 = -1 let width = 4.5, height = 4.5, depth = 2.5 * sizeFactor if (isPurinBase(compId)) { diff --git a/src/mol-repr/structure/visual/polymer-direction-wedge.ts b/src/mol-repr/structure/visual/polymer-direction-wedge.ts index 786ba2992b77deaf50e6ac687e74c8b93096a9c8..3c9dc41c38ed8bc2cbdd94b286962cf4a3e32b5e 100644 --- a/src/mol-repr/structure/visual/polymer-direction-wedge.ts +++ b/src/mol-repr/structure/visual/polymer-direction-wedge.ts @@ -62,7 +62,7 @@ async function createPolymerDirectionWedgeMesh(ctx: VisualContext, unit: Unit, s interpolateCurveSegment(state, v, tension, shift) - if ((isSheet && !v.secStrucChange) || !isSheet) { + if ((isSheet && !v.secStrucLast) || !isSheet) { const size = theme.size.size(v.center) * sizeFactor const depth = depthFactor * size const width = widthFactor * size diff --git a/src/mol-repr/structure/visual/polymer-trace-mesh.ts b/src/mol-repr/structure/visual/polymer-trace-mesh.ts index 65d6b070eacfdc1d0b933048b3da26af57fee671..6890b4853435a6a41d441132cd69346ae1676dda 100644 --- a/src/mol-repr/structure/visual/polymer-trace-mesh.ts +++ b/src/mol-repr/structure/visual/polymer-trace-mesh.ts @@ -52,7 +52,7 @@ async function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure: const isNucleicType = isNucleic(v.moleculeType) const isSheet = SecondaryStructureType.is(v.secStrucType, SecondaryStructureType.Flag.Beta) const isHelix = SecondaryStructureType.is(v.secStrucType, SecondaryStructureType.Flag.Helix) - const tension = (isNucleicType || isSheet) ? 0.5 : 0.9 + const tension = isNucleicType ? 0.5 : 0.9 const shift = isNucleicType ? 0.3 : 0.5 interpolateCurveSegment(state, v, tension, shift) @@ -62,8 +62,8 @@ async function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure: if (isSheet) { const height = width * aspectRatio - const arrowHeight = v.secStrucChange ? height * arrowFactor : 0 - addSheet(builder, curvePoints, normalVectors, binormalVectors, linearSegments, width, height, arrowHeight, true, true) + const arrowHeight = v.secStrucLast ? height * arrowFactor : 0 + addSheet(builder, curvePoints, normalVectors, binormalVectors, linearSegments, width, height, arrowHeight, v.secStrucFirst, v.secStrucLast) } else { let height: number if (isHelix) { @@ -74,7 +74,7 @@ async function createPolymerTraceMesh(ctx: VisualContext, unit: Unit, structure: } else { height = width } - addTube(builder, curvePoints, normalVectors, binormalVectors, linearSegments, radialSegments, width, height, 1, true, true) + addTube(builder, curvePoints, normalVectors, binormalVectors, linearSegments, radialSegments, width, height, 1, v.secStrucFirst, v.secStrucLast) } if (i % 10000 === 0 && ctx.runtime.shouldUpdate) { diff --git a/src/mol-repr/structure/visual/util/polymer/trace-iterator.ts b/src/mol-repr/structure/visual/util/polymer/trace-iterator.ts index 37bfc18d317b5c96c015f13bda2c9c15bafbb37b..4b3e38c4558eb15a02e4a5adea2d173619e913db 100644 --- a/src/mol-repr/structure/visual/util/polymer/trace-iterator.ts +++ b/src/mol-repr/structure/visual/util/polymer/trace-iterator.ts @@ -13,6 +13,7 @@ import SortedRanges from 'mol-data/int/sorted-ranges'; import { CoarseSphereConformation, CoarseGaussianConformation } from 'mol-model/structure/model/properties/coarse'; import { getAtomicMoleculeType, getElementIndexForAtomRole } from 'mol-model/structure/util'; import { getPolymerRanges } from '../polymer'; +import { AtomicConformation } from 'mol-model/structure/model/properties/atomic'; /** * Iterates over individual residues/coarse elements in polymers of a unit while @@ -30,20 +31,22 @@ export function PolymerTraceIterator(unit: Unit): Iterator<PolymerTraceElement> interface PolymerTraceElement { center: StructureElement first: boolean, last: boolean + secStrucFirst: boolean, secStrucLast: boolean secStrucType: SecondaryStructureType - secStrucChange: boolean moleculeType: MoleculeType p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, p4: Vec3 d12: Vec3, d23: Vec3 } +const SecStrucTypeNA = SecondaryStructureType.create(SecondaryStructureType.Flag.NA) + function createPolymerTraceElement (unit: Unit): PolymerTraceElement { return { center: StructureElement.create(unit), first: false, last: false, - secStrucType: SecondaryStructureType.create(SecondaryStructureType.Flag.NA), - secStrucChange: false, + secStrucFirst: false, secStrucLast: false, + secStrucType: SecStrucTypeNA, moleculeType: MoleculeType.unknown, p0: Vec3.zero(), p1: Vec3.zero(), p2: Vec3.zero(), p3: Vec3.zero(), p4: Vec3.zero(), d12: Vec3.create(1, 0, 0), d23: Vec3.create(1, 0, 0), @@ -57,10 +60,15 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement> private polymerIt: SortedRanges.Iterator<ElementIndex, ResidueIndex> private residueIt: Segmentation.SegmentIterator<ResidueIndex> private polymerSegment: Segmentation.Segment<ResidueIndex> + private secondaryStructureType: ArrayLike<SecondaryStructureType> private residueSegmentMin: ResidueIndex private residueSegmentMax: ResidueIndex + private prevSecStrucType: SecondaryStructureType + private currSecStrucType: SecondaryStructureType + private nextSecStrucType: SecondaryStructureType private state: AtomicPolymerTraceIteratorState = AtomicPolymerTraceIteratorState.nextPolymer private residueAtomSegments: Segmentation<ElementIndex, ResidueIndex> + private atomicConformation: AtomicConformation private p0 = Vec3.zero(); private p1 = Vec3.zero(); @@ -78,13 +86,13 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement> hasNext: boolean = false; private pos(target: Vec3, index: number) { - target[0] = this.unit.model.atomicConformation.x[index] - target[1] = this.unit.model.atomicConformation.y[index] - target[2] = this.unit.model.atomicConformation.z[index] + target[0] = this.atomicConformation.x[index] + target[1] = this.atomicConformation.y[index] + target[2] = this.atomicConformation.z[index] } private updateResidueSegmentRange(polymerSegment: Segmentation.Segment<ResidueIndex>) { - const { index } = this.unit.model.atomicHierarchy.residueAtomSegments + const { index } = this.residueAtomSegments this.residueSegmentMin = index[this.unit.elements[polymerSegment.start]] this.residueSegmentMax = index[this.unit.elements[polymerSegment.end - 1]] } @@ -111,8 +119,7 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement> } private setControlPoint(out: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, residueIndex: ResidueIndex) { - const ss = this.unit.model.properties.secondaryStructure.type[residueIndex] - if (SecondaryStructureType.is(ss, SecondaryStructureType.Flag.Beta)) { + if (SecondaryStructureType.is(this.currSecStrucType, SecondaryStructureType.Flag.Beta)) { Vec3.scale(out, Vec3.add(out, p1, Vec3.add(out, p3, Vec3.add(out, p2, p2))), 1/4) } else { Vec3.copy(out, p2) @@ -129,6 +136,8 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement> this.updateResidueSegmentRange(this.polymerSegment) if (residueIt.hasNext) { this.state = AtomicPolymerTraceIteratorState.nextResidue + this.currSecStrucType = SecStrucTypeNA + this.nextSecStrucType = this.secondaryStructureType[this.residueSegmentMin] break } } @@ -136,7 +145,17 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement> if (this.state === AtomicPolymerTraceIteratorState.nextResidue) { const { index: residueIndex } = residueIt.move(); + this.prevSecStrucType = this.currSecStrucType + this.currSecStrucType = this.nextSecStrucType + this.nextSecStrucType = residueIt.hasNext ? this.secondaryStructureType[residueIndex + 1] : SecStrucTypeNA + + value.secStrucType = this.currSecStrucType value.center.element = this.getElementIndex(residueIndex, 'trace') + value.first = residueIndex === this.residueSegmentMin + value.last = residueIndex === this.residueSegmentMax + value.secStrucFirst = this.prevSecStrucType !== this.currSecStrucType + value.secStrucLast = this.currSecStrucType !== this.nextSecStrucType + value.moleculeType = getAtomicMoleculeType(this.unit.model, residueIndex) this.pos(this.p0, this.getElementIndex(residueIndex - 3 as ResidueIndex, 'trace')) this.pos(this.p1, this.getElementIndex(residueIndex - 2 as ResidueIndex, 'trace')) @@ -151,8 +170,6 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement> 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] - this.setControlPoint(value.p0, this.p0, this.p1, this.p2, residueIndex - 2 as ResidueIndex) this.setControlPoint(value.p1, this.p1, this.p2, this.p3, residueIndex - 1 as ResidueIndex) this.setControlPoint(value.p2, this.p2, this.p3, this.p4, residueIndex) @@ -162,11 +179,6 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement> Vec3.copy(value.d12, this.v12) Vec3.copy(value.d23, this.v23) - value.first = residueIndex === this.residueSegmentMin - value.last = residueIndex === this.residueSegmentMax - value.secStrucChange = this.unit.model.properties.secondaryStructure.key[residueIndex] !== this.unit.model.properties.secondaryStructure.key[residueIndex + 1] - value.moleculeType = getAtomicMoleculeType(this.unit.model, residueIndex) - if (!residueIt.hasNext) { this.state = AtomicPolymerTraceIteratorState.nextPolymer } @@ -178,7 +190,9 @@ export class AtomicPolymerTraceIterator implements Iterator<PolymerTraceElement> } constructor(private unit: Unit.Atomic) { + this.atomicConformation = unit.model.atomicConformation this.residueAtomSegments = unit.model.atomicHierarchy.residueAtomSegments + this.secondaryStructureType = unit.model.properties.secondaryStructure.type this.polymerIt = SortedRanges.transientSegments(getPolymerRanges(unit), unit.elements) this.residueIt = Segmentation.transientSegments(this.residueAtomSegments, unit.elements); this.value = createPolymerTraceElement(unit) diff --git a/src/mol-repr/util.ts b/src/mol-repr/util.ts index ad7d101646a55f50ca2820049e251d538b4ff006..dd3acb447f6b4d09febdddc97fc27f098aa7c264 100644 --- a/src/mol-repr/util.ts +++ b/src/mol-repr/util.ts @@ -10,6 +10,7 @@ import { VisualQuality } from 'mol-geo/geometry/geometry'; export interface VisualUpdateState { updateTransform: boolean + updateMatrix: boolean updateColor: boolean updateSize: boolean createGeometry: boolean @@ -18,6 +19,7 @@ export namespace VisualUpdateState { export function create(): VisualUpdateState { return { updateTransform: false, + updateMatrix: false, updateColor: false, updateSize: false, createGeometry: false @@ -25,6 +27,7 @@ export namespace VisualUpdateState { } export function reset(state: VisualUpdateState) { state.updateTransform = false + state.updateMatrix = false state.updateColor = false state.updateSize = false state.createGeometry = false diff --git a/src/mol-repr/volume/direct-volume.ts b/src/mol-repr/volume/direct-volume.ts index 2bdecb1c2e3625dce69ecae78ce65de60d8c4e43..b2e8dea231970d2a3852c9aeffe78c6f5486c31f 100644 --- a/src/mol-repr/volume/direct-volume.ts +++ b/src/mol-repr/volume/direct-volume.ts @@ -6,7 +6,7 @@ import { VolumeData } from 'mol-model/volume' import { RuntimeContext } from 'mol-task' -import { VolumeVisual, VolumeRepresentation } from './representation'; +import { VolumeVisual, VolumeRepresentation, VolumeRepresentationProvider } from './representation'; import { createDirectVolumeRenderObject } from 'mol-gl/render-object'; import { EmptyLoci } from 'mol-model/loci'; import { ParamDefinition as PD } from 'mol-util/param-definition'; @@ -19,7 +19,7 @@ import { createIdentityTransform } from 'mol-geo/geometry/transform-data'; import { DirectVolume } from 'mol-geo/geometry/direct-volume/direct-volume'; import { Geometry, createRenderableState } from 'mol-geo/geometry/geometry'; import { VisualUpdateState } from 'mol-repr/util'; -import { VisualContext } from 'mol-repr/representation'; +import { VisualContext, RepresentationContext, RepresentationParamsGetter } from 'mol-repr/representation'; import { Theme, ThemeRegistryContext } from 'mol-theme/theme'; function getBoundingBox(gridDimension: Vec3, transform: Mat4) { @@ -178,10 +178,19 @@ export function DirectVolumeVisual(): VolumeVisual<DirectVolumeParams> { const state = createRenderableState(props) return createDirectVolumeRenderObject(values, state) }, - updateValues: DirectVolume.updateValues + updateValues: DirectVolume.updateValues, + updateBoundingSphere: DirectVolume.updateBoundingSphere }) } -export function DirectVolumeRepresentation(): VolumeRepresentation<DirectVolumeParams> { - return VolumeRepresentation('Direct Volume', getDirectVolumeParams, DirectVolumeVisual) +export function DirectVolumeRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, DirectVolumeParams>): VolumeRepresentation<DirectVolumeParams> { + return VolumeRepresentation('Direct Volume', ctx, getParams, DirectVolumeVisual) +} + +export const DirectVolumeRepresentationProvider: VolumeRepresentationProvider<DirectVolumeParams> = { + label: 'Direct Volume', + description: 'Direct volume rendering of volumetric data.', + factory: DirectVolumeRepresentation, + getParams: getDirectVolumeParams, + defaultValues: PD.getDefaultValues(DirectVolumeParams) } \ No newline at end of file diff --git a/src/mol-repr/volume/isosurface-mesh.ts b/src/mol-repr/volume/isosurface-mesh.ts index a0387ec443038ed806d3b12d0a299c91a09cd7b8..e2b82d003379f030ea3855a06aa2d2c0c5e69558 100644 --- a/src/mol-repr/volume/isosurface-mesh.ts +++ b/src/mol-repr/volume/isosurface-mesh.ts @@ -6,7 +6,7 @@ */ import { VolumeData } from 'mol-model/volume' -import { VolumeVisual, VolumeRepresentation } from './representation'; +import { VolumeVisual, VolumeRepresentation, VolumeRepresentationProvider } from './representation'; import { createMeshRenderObject } from 'mol-gl/render-object'; import { EmptyLoci } from 'mol-model/loci'; import { ParamDefinition as PD } from 'mol-util/param-definition'; @@ -16,7 +16,7 @@ import { LocationIterator } from 'mol-geo/util/location-iterator'; import { createIdentityTransform } from 'mol-geo/geometry/transform-data'; import { createRenderableState } from 'mol-geo/geometry/geometry'; import { VisualUpdateState } from 'mol-repr/util'; -import { VisualContext } from 'mol-repr/representation'; +import { VisualContext, RepresentationContext, RepresentationParamsGetter } from 'mol-repr/representation'; import { Theme, ThemeRegistryContext } from 'mol-theme/theme'; interface VolumeIsosurfaceProps { @@ -63,10 +63,19 @@ export function IsosurfaceVisual(): VolumeVisual<IsosurfaceParams> { const state = createRenderableState(props) return createMeshRenderObject(values, state) }, - updateValues: Mesh.updateValues + updateValues: Mesh.updateValues, + updateBoundingSphere: Mesh.updateBoundingSphere }) } -export function IsosurfaceRepresentation(): VolumeRepresentation<IsosurfaceParams> { - return VolumeRepresentation('Isosurface', getIsosurfaceParams, IsosurfaceVisual) +export function IsosurfaceRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, IsosurfaceParams>): VolumeRepresentation<IsosurfaceParams> { + return VolumeRepresentation('Isosurface', ctx, getParams, IsosurfaceVisual) +} + +export const IsosurfaceRepresentationProvider: VolumeRepresentationProvider<IsosurfaceParams> = { + label: 'Isosurface', + description: 'Displays an isosurface of volumetric data.', + factory: IsosurfaceRepresentation, + getParams: getIsosurfaceParams, + defaultValues: PD.getDefaultValues(IsosurfaceParams) } \ No newline at end of file diff --git a/src/mol-repr/volume/registry.ts b/src/mol-repr/volume/registry.ts index e91833370ad02b2571af6a1da10d1c292047a347..512463432e26bda845a6ec9d6b784634f8cf50f1 100644 --- a/src/mol-repr/volume/registry.ts +++ b/src/mol-repr/volume/registry.ts @@ -6,6 +6,8 @@ import { RepresentationProvider, RepresentationRegistry } from '../representation'; import { VolumeData } from 'mol-model/volume'; +import { IsosurfaceRepresentationProvider } from './isosurface-mesh'; +import { DirectVolumeRepresentationProvider } from './direct-volume'; export class VolumeRepresentationRegistry extends RepresentationRegistry<VolumeData> { constructor() { @@ -18,7 +20,8 @@ export class VolumeRepresentationRegistry extends RepresentationRegistry<VolumeD } export const BuiltInVolumeRepresentations = { - // TODO + 'isosurface': IsosurfaceRepresentationProvider, + 'direct-volume': DirectVolumeRepresentationProvider, } export type BuiltInVolumeRepresentationsName = keyof typeof BuiltInVolumeRepresentations export const BuiltInVolumeRepresentationsNames = Object.keys(BuiltInVolumeRepresentations) diff --git a/src/mol-repr/volume/representation.ts b/src/mol-repr/volume/representation.ts index 85a467af2ec2a2aa4b63ad115cbf129d72f3d4f4..e274058eb1344c786e6cab7db7501549a5ace2c1 100644 --- a/src/mol-repr/volume/representation.ts +++ b/src/mol-repr/volume/representation.ts @@ -19,7 +19,7 @@ import { LocationIterator } from 'mol-geo/util/location-iterator'; import { NullLocation } from 'mol-model/location'; import { VisualUpdateState } from 'mol-repr/util'; import { ValueCell } from 'mol-util'; -import { Theme, createTheme } from 'mol-theme/theme'; +import { Theme, createEmptyTheme } from 'mol-theme/theme'; import { Subject } from 'rxjs'; export interface VolumeVisual<P extends VolumeParams> extends Visual<VolumeData, P> { } @@ -36,12 +36,13 @@ interface VolumeVisualBuilder<P extends VolumeParams, G extends Geometry> { interface VolumeVisualGeometryBuilder<P extends VolumeParams, G extends Geometry> extends VolumeVisualBuilder<P, G> { createRenderObject(ctx: VisualContext, geometry: G, locationIt: LocationIterator, theme: Theme, currentProps: PD.Values<P>): Promise<VolumeRenderObject> - updateValues(values: RenderableValues, newProps: PD.Values<P>): void + updateValues(values: RenderableValues, newProps: PD.Values<P>): void, + updateBoundingSphere(values: RenderableValues, geometry: G): void } export function VolumeVisual<P extends VolumeParams>(builder: VolumeVisualGeometryBuilder<P, Geometry>): VolumeVisual<P> { const { defaultProps, createGeometry, getLoci, mark, setUpdateState } = builder - const { createRenderObject, updateValues } = builder + const { createRenderObject, updateValues, updateBoundingSphere } = builder const updateState = VisualUpdateState.create() let currentProps: PD.Values<P> @@ -67,6 +68,7 @@ export function VolumeVisual<P extends VolumeParams>(builder: VolumeVisualGeomet if (updateState.createGeometry) { geometry = await createGeometry(ctx, currentVolume, currentProps, geometry) ValueCell.update(renderObject.values.drawCount, Geometry.getDrawCount(geometry)) + updateBoundingSphere(renderObject.values, geometry) } updateValues(renderObject.values, newProps) @@ -143,25 +145,25 @@ export const VolumeParams = { } export type VolumeParams = typeof VolumeParams -export function VolumeRepresentation<P extends VolumeParams>(label: string, getParams: RepresentationParamsGetter<VolumeData, P>, visualCtor: (volume: VolumeData) => VolumeVisual<P>): VolumeRepresentation<P> { +export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<VolumeData, P>, visualCtor: (volume: VolumeData) => VolumeVisual<P>): VolumeRepresentation<P> { let version = 0 const updated = new Subject<number>() + const _state = Representation.createState() let visual: VolumeVisual<P> let _volume: VolumeData let _props: PD.Values<P> let _params: P - let _theme: Theme + let _theme = createEmptyTheme() let busy = false - function createOrUpdate(ctx: RepresentationContext, props: Partial<PD.Values<P>> = {}, volume?: VolumeData) { + function createOrUpdate(props: Partial<PD.Values<P>> = {}, volume?: VolumeData) { if (volume && volume !== _volume) { _params = getParams(ctx, volume) _volume = volume if (!_props) _props = PD.getDefaultValues(_params) } _props = Object.assign({}, _props, props) - _theme = createTheme(ctx, _props, {}, _theme) return Task.create('VolumeRepresentation.create', async runtime => { // TODO queue it somehow @@ -172,11 +174,11 @@ export function VolumeRepresentation<P extends VolumeParams>(label: string, getP } else if (volume && !visual) { busy = true visual = visualCtor(volume) - await visual.createOrUpdate({ ...ctx, runtime }, _theme, _props, volume) + await visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, volume) busy = false } else { busy = true - await visual.createOrUpdate({ ...ctx, runtime }, _theme, _props, volume) + await visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, volume) busy = false } updated.next(version++) @@ -191,16 +193,19 @@ export function VolumeRepresentation<P extends VolumeParams>(label: string, getP return visual ? visual.mark(loci, action) : false } - function destroy() { - if (visual) visual.destroy() + function setState(state: Partial<Representation.State>) { + if (state.visible !== undefined && visual) visual.setVisibility(state.visible) + if (state.pickable !== undefined && visual) visual.setPickable(state.pickable) + + Representation.updateState(_state, state) } - function setVisibility(value: boolean) { - if (visual) visual.setVisibility(value) + function setTheme(theme: Theme) { + _theme = theme } - function setPickable(value: boolean) { - if (visual) visual.setPickable(value) + function destroy() { + if (visual) visual.destroy() } return { @@ -213,12 +218,14 @@ export function VolumeRepresentation<P extends VolumeParams>(label: string, getP }, get props () { return _props }, get params() { return _params }, - get updated() { return updated }, + get state() { return _state }, + get theme() { return _theme }, + updated, createOrUpdate, + setState, + setTheme, getLoci, mark, - setVisibility, - setPickable, destroy } } \ No newline at end of file diff --git a/src/mol-state/action.ts b/src/mol-state/action.ts index 64826d3b64472b546082a6c8ea9e12fdff842d0a..1b178246fbc2fe31d421b4c116db744492117813 100644 --- a/src/mol-state/action.ts +++ b/src/mol-state/action.ts @@ -31,27 +31,29 @@ namespace StateAction { } export interface ApplyParams<A extends StateObject = StateObject, P extends {} = {}> { + ref: string, cell: StateObjectCell, a: A, state: State, params: P } - export interface Definition<A extends StateObject = StateObject, T = any, P extends {} = {}> { - readonly from: StateObject.Ctor[], - readonly display?: { readonly name: string, readonly description?: string }, - + export interface DefinitionBase<A extends StateObject = StateObject, T = any, P extends {} = {}> { /** * Apply an action that modifies the State specified in Params. */ - apply(params: ApplyParams<A, P>, globalCtx: unknown): T | Task<T>, - - params?(a: A, globalCtx: unknown): { [K in keyof P]: PD.Any }, + run(params: ApplyParams<A, P>, globalCtx: unknown): T | Task<T>, /** Test if the transform can be applied to a given node */ isApplicable?(a: A, globalCtx: unknown): boolean } + export interface Definition<A extends StateObject = StateObject, T = any, P extends {} = {}> extends DefinitionBase<A, T, P> { + readonly from: StateObject.Ctor[], + readonly display: { readonly name: string, readonly description?: string }, + params?(a: A, globalCtx: unknown): { [K in keyof P]: PD.Any } + } + export function create<A extends StateObject, T, P extends {} = {}>(definition: Definition<A, T, P>): StateAction<A, T, P> { const action: StateAction<A, T, P> = { create(params) { return { action, params }; }, @@ -67,10 +69,51 @@ namespace StateAction { from: def.from, display: def.display, params: def.params as Transformer.Definition<Transformer.From<T>, any, Transformer.Params<T>>['params'], - apply({ cell, state, params }) { + run({ cell, state, params }) { const tree = state.build().to(cell.transform.ref).apply(transformer, params); return state.update(tree); } }) } + + export namespace Builder { + export interface Type<A extends StateObject.Ctor, P extends { }> { + from?: A | A[], + params?: PD.For<P> | ((a: StateObject.From<A>, globalCtx: any) => PD.For<P>), + display?: string | { name: string, description?: string } + } + + export interface Root { + <A extends StateObject.Ctor, P extends { }>(info: Type<A, P>): Define<StateObject.From<A>, PD.Normalize<P>> + } + + export interface Define<A extends StateObject, P> { + <T>(def: DefinitionBase<A, T, P> | DefinitionBase<A, T, P>['run']): StateAction<A, T, P>, + } + + function root(info: Type<any, any>): Define<any, any> { + return def => create({ + from: info.from instanceof Array + ? info.from + : !!info.from ? [info.from] : [], + display: typeof info.display === 'string' + ? { name: info.display } + : !!info.display + ? info.display + : { name: 'Unnamed State Action' }, + params: typeof info.params === 'object' + ? () => info.params as any + : !!info.params + ? info.params as any + : void 0, + ...(typeof def === 'function' + ? { run: def } + : def) + }); + } + + export const build: Root = (info: any) => root(info); + } + + export const build = Builder.build; } \ No newline at end of file diff --git a/src/mol-state/object.ts b/src/mol-state/object.ts index bdbe5631533fd4915ad7bc080f43d7d329030211..70917c833285e631724179cfbcaae71b7a18cca9 100644 --- a/src/mol-state/object.ts +++ b/src/mol-state/object.ts @@ -23,12 +23,13 @@ namespace StateObject { } export type Type<Cls extends string = string> = { name: string, typeClass: Cls } - export type Ctor = { new(...args: any[]): StateObject, type: any } + export type Ctor<T extends StateObject = StateObject> = { new(...args: any[]): T, type: any } + export type From<C extends Ctor> = C extends Ctor<infer T> ? T : never export function create<Data, T extends Type>(type: T) { - return class implements StateObject<Data, T> { + return class O implements StateObject<Data, T> { static type = type; - static is(obj?: StateObject): obj is StateObject<Data, T> { return !!obj && type === obj.type; } + static is(obj?: StateObject): obj is O { return !!obj && type === obj.type; } id = UUID.create22(); type = type; label: string; diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts index d73d707efaf5280f3f4c0e385b745b8ade465f5e..75f801a682472e0cffe640268c5b49ba8122cbfa 100644 --- a/src/mol-state/state.ts +++ b/src/mol-state/state.ts @@ -104,7 +104,7 @@ class State { if (!cell) throw new Error(`'${ref}' does not exist.`); if (cell.status !== 'ok') throw new Error(`Action cannot be applied to a cell with status '${cell.status}'`); - return runTask(action.definition.apply({ cell, a: cell.obj!, params, state: this }, this.globalContext), ctx); + return runTask(action.definition.run({ ref, cell, a: cell.obj!, params, state: this }, this.globalContext), ctx); }); } @@ -224,7 +224,7 @@ async function update(ctx: UpdateContext) { } if (hasCurrent) { - const newCurrent = findNewCurrent(ctx, current, deletes); + const newCurrent = findNewCurrent(ctx.oldTree, current, deletes, ctx.cells); ctx.parent.setCurrent(newCurrent); } @@ -270,14 +270,14 @@ async function update(ctx: UpdateContext) { await updateSubtree(ctx, root); } - let newCurrent: Transform.Ref | undefined; + let newCurrent: Transform.Ref | undefined = ctx.newCurrent; // Raise object updated events for (const update of ctx.results) { if (update.action === 'created') { ctx.parent.events.object.created.next({ state: ctx.parent, ref: update.ref, obj: update.obj! }); - if (!ctx.hadError) { + if (!ctx.newCurrent) { const transform = ctx.tree.transforms.get(update.ref); - if (!transform.props || !transform.props.isGhost) newCurrent = update.ref; + if (!(transform.props && transform.props.isGhost) && update.obj !== StateObject.Null) newCurrent = update.ref; } } else if (update.action === 'updated') { ctx.parent.events.object.updated.next({ state: ctx.parent, ref: update.ref, action: 'in-place', obj: update.obj }); @@ -286,8 +286,19 @@ async function update(ctx: UpdateContext) { } } - if (ctx.newCurrent) ctx.parent.setCurrent(ctx.newCurrent); - else if (newCurrent) ctx.parent.setCurrent(newCurrent); + if (newCurrent) ctx.parent.setCurrent(newCurrent); + else { + // check if old current or its parent hasn't become null + const current = ctx.parent.current; + const currentCell = ctx.cells.get(current); + if (currentCell && ( + currentCell.obj === StateObject.Null + || (currentCell.status === 'error' && currentCell.errorText === ParentNullErrorText))) { + newCurrent = findNewCurrent(ctx.oldTree, current, [], ctx.cells); + ctx.parent.setCurrent(newCurrent); + } + } + return deletes.length > 0 || roots.length > 0 || ctx.changed; } @@ -304,6 +315,8 @@ function findUpdateRootsVisitor(n: Transform, _: any, s: { roots: Ref[], cells: s.roots.push(n.ref); return false; } + // nothing below a Null object can be an update root + if (cell && cell.obj === StateObject.Null) return false; return true; } @@ -369,12 +382,12 @@ function initCells(ctx: UpdateContext, roots: Ref[]) { return initCtx.added; } -function findNewCurrent(ctx: UpdateContext, start: Ref, deletes: Ref[]) { +function findNewCurrent(tree: StateTree, start: Ref, deletes: Ref[], cells: Map<Ref, StateObjectCell>) { const deleteSet = new Set(deletes); - return _findNewCurrent(ctx.oldTree, start, deleteSet); + return _findNewCurrent(tree, start, deleteSet, cells); } -function _findNewCurrent(tree: StateTree, ref: Ref, deletes: Set<Ref>): Ref { +function _findNewCurrent(tree: StateTree, ref: Ref, deletes: Set<Ref>, cells: Map<Ref, StateObjectCell>): Ref { if (ref === Transform.RootRef) return ref; const node = tree.transforms.get(ref)!; @@ -387,6 +400,10 @@ function _findNewCurrent(tree: StateTree, ref: Ref, deletes: Set<Ref>): Ref { if (s.done) break; if (deletes.has(s.value)) continue; + const cell = cells.get(s.value); + if (!cell || cell.status === 'error' || cell.obj === StateObject.Null) { + continue; + } const t = tree.transforms.get(s.value); if (t.props && t.props.isGhost) continue; @@ -402,17 +419,19 @@ function _findNewCurrent(tree: StateTree, ref: Ref, deletes: Set<Ref>): Ref { } if (prevCandidate) return prevCandidate; - return _findNewCurrent(tree, node.parent, deletes); + return _findNewCurrent(tree, node.parent, deletes, cells); } /** Set status and error text of the cell. Remove all existing objects in the subtree. */ -function doError(ctx: UpdateContext, ref: Ref, errorText: string | undefined) { - ctx.hadError = true; - (ctx.parent as any as { errorFree: boolean }).errorFree = false; +function doError(ctx: UpdateContext, ref: Ref, errorText: string | undefined, silent: boolean) { + if (!silent) { + ctx.hadError = true; + (ctx.parent as any as { errorFree: boolean }).errorFree = false; + } if (errorText) { setCellStatus(ctx, ref, 'error', errorText); - ctx.parent.events.log.next({ type: 'error', timestamp: new Date(), message: errorText }); + if (!silent) ctx.parent.events.log.next({ type: 'error', timestamp: new Date(), message: errorText }); } const cell = ctx.cells.get(ref)!; @@ -428,7 +447,7 @@ function doError(ctx: UpdateContext, ref: Ref, errorText: string | undefined) { while (true) { const next = children.next(); if (next.done) return; - doError(ctx, next.value, void 0); + doError(ctx, next.value, void 0, silent); } } @@ -438,6 +457,8 @@ type UpdateNodeResult = | { ref: Ref, action: 'replaced', oldObj?: StateObject, obj: StateObject } | { action: 'none' } +const ParentNullErrorText = 'Parent is null'; + async function updateSubtree(ctx: UpdateContext, root: Ref) { setCellStatus(ctx, root, 'processing'); @@ -453,30 +474,27 @@ async function updateSubtree(ctx: UpdateContext, root: Ref) { ctx.results.push(update); if (update.action === 'created') { isNull = update.obj === StateObject.Null; - ctx.parent.events.log.next(LogEntry.info(`Created ${update.obj.label} in ${formatTimespan(time)}.`)); + if (!isNull) ctx.parent.events.log.next(LogEntry.info(`Created ${update.obj.label} in ${formatTimespan(time)}.`)); } else if (update.action === 'updated') { isNull = update.obj === StateObject.Null; - ctx.parent.events.log.next(LogEntry.info(`Updated ${update.obj.label} in ${formatTimespan(time)}.`)); + if (!isNull) ctx.parent.events.log.next(LogEntry.info(`Updated ${update.obj.label} in ${formatTimespan(time)}.`)); } else if (update.action === 'replaced') { isNull = update.obj === StateObject.Null; - ctx.parent.events.log.next(LogEntry.info(`Updated ${update.obj.label} in ${formatTimespan(time)}.`)); + if (!isNull) ctx.parent.events.log.next(LogEntry.info(`Updated ${update.obj.label} in ${formatTimespan(time)}.`)); } } catch (e) { ctx.changed = true; if (!ctx.hadError) ctx.newCurrent = root; - doError(ctx, root, '' + e); + doError(ctx, root, '' + e, false); return; } - // Do not continue the updates if the object is null - // TODO: set the states to something "nicer"? - if (isNull) return; - const children = ctx.tree.children.get(root).values(); while (true) { const next = children.next(); if (next.done) return; - await updateSubtree(ctx, next.value); + if (isNull) doError(ctx, next.value, ParentNullErrorText, true); + else await updateSubtree(ctx, next.value); } } diff --git a/src/mol-state/transformer.ts b/src/mol-state/transformer.ts index 950e1b3764613e5ab795bd5ccea307352945b2aa..05663bd8555f5da3b8ddcbaf23c0fd881acd5e74 100644 --- a/src/mol-state/transformer.ts +++ b/src/mol-state/transformer.ts @@ -9,6 +9,7 @@ import { StateObject } from './object'; import { Transform } from './transform'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import { StateAction } from './action'; +import { capitalize } from 'mol-util/string'; export interface Transformer<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = {}> { apply(parent: Transform.Ref, params?: P, props?: Partial<Transform.Options>): Transform<A, B, P>, @@ -44,17 +45,19 @@ export namespace Transformer { cache: unknown } + export interface AutoUpdateParams<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = {}> { + a: A, + b: B, + oldParams: P, + newParams: P + } + export enum UpdateResult { Unchanged, Updated, Recreate } /** Specify default control descriptors for the parameters */ // export type ParamsDefinition<A extends StateObject = StateObject, P = any> = (a: A, globalCtx: unknown) => { [K in keyof P]: PD.Any } - export interface Definition<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = {}> { - readonly name: string, - readonly from: StateObject.Ctor[], - readonly to: StateObject.Ctor[], - readonly display?: { readonly name: string, readonly description?: string }, - + export interface DefinitionBase<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = {}> { /** * Apply the actual transformation. It must be pure (i.e. with no side effects). * Returns a task that produces the result of the result directly. @@ -68,7 +71,8 @@ export namespace Transformer { */ update?(params: UpdateParams<A, B, P>, globalCtx: unknown): Task<UpdateResult> | UpdateResult, - params?(a: A, globalCtx: unknown): { [K in keyof P]: PD.Any }, + /** Determine if the transformer can be applied automatically on UI change. Default is false. */ + canAutoUpdate?(params: AutoUpdateParams<A, B, P>, globalCtx: unknown): boolean, /** Test if the transform can be applied to a given node */ isApplicable?(a: A, globalCtx: unknown): boolean, @@ -80,6 +84,14 @@ export namespace Transformer { readonly customSerialization?: { toJSON(params: P, obj?: B): any, fromJSON(data: any): P } } + export interface Definition<A extends StateObject = StateObject, B extends StateObject = StateObject, P extends {} = {}> extends DefinitionBase<A, B, P> { + readonly name: string, + readonly from: StateObject.Ctor[], + readonly to: StateObject.Ctor[], + readonly display: { readonly name: string, readonly description?: string }, + params?(a: A, globalCtx: unknown): { [K in keyof P]: PD.Any }, + } + const registry = new Map<Id, Transformer<any, any>>(); const fromTypeIndex: Map<StateObject.Type, Transformer[]> = new Map(); @@ -130,10 +142,60 @@ export namespace Transformer { return <A extends StateObject, B extends StateObject, P extends {} = {}>(definition: Definition<A, B, P>) => create(namespace, definition); } + export function builderFactory(namespace: string) { + return Builder.build(namespace); + } + + export namespace Builder { + export interface Type<A extends StateObject.Ctor, B extends StateObject.Ctor, P extends { }> { + name: string, + from: A | A[], + to: B | B[], + params?: PD.For<P> | ((a: StateObject.From<A>, globalCtx: any) => PD.For<P>), + display?: string | { name: string, description?: string } + } + + export interface Root { + <A extends StateObject.Ctor, B extends StateObject.Ctor, P extends { }>(info: Type<A, B, P>): Define<StateObject.From<A>, StateObject.From<B>, PD.Normalize<P>> + } + + export interface Define<A extends StateObject, B extends StateObject, P> { + (def: DefinitionBase<A, B, P>): Transformer<A, B, P> + } + + function root(namespace: string, info: Type<any, any, any>): Define<any, any, any> { + return def => create(namespace, { + name: info.name, + from: info.from instanceof Array ? info.from : [info.from], + to: info.to instanceof Array ? info.to : [info.to], + display: typeof info.display === 'string' + ? { name: info.display } + : !!info.display + ? info.display + : { name: capitalize(info.name.replace(/[-]/g, ' ')) }, + params: typeof info.params === 'object' + ? () => info.params as any + : !!info.params + ? info.params as any + : void 0, + ...def + }); + } + + export function build(namespace: string): Root { + return (info: any) => root(namespace, info); + } + } + + export function build(namespace: string): Builder.Root { + return Builder.build(namespace); + } + export const ROOT = create<any, any, {}>('build-in', { name: 'root', from: [], to: [], + display: { name: 'Root' }, apply() { throw new Error('should never be applied'); }, update() { return UpdateResult.Unchanged; } }) diff --git a/src/mol-state/tree/builder.ts b/src/mol-state/tree/builder.ts index 0ce875ef16c8aa4b52962fb35f9f48b1a519a1fc..fcca72f35476165fe593bcd5e2f6c75623a81ade 100644 --- a/src/mol-state/tree/builder.ts +++ b/src/mol-state/tree/builder.ts @@ -59,9 +59,9 @@ namespace StateTreeBuilder { return new To(this.state, t.ref, this.root); } - update<T extends Transformer<A, any, any>>(transformer: T, params: (old: Transformer.Params<T>) => Transformer.Params<T>): Root + update<T extends Transformer<any, A, any>>(transformer: T, params: (old: Transformer.Params<T>) => Transformer.Params<T>): Root update(params: any): Root - update<T extends Transformer<A, any, any>>(paramsOrTransformer: T, provider?: (old: Transformer.Params<T>) => Transformer.Params<T>) { + update<T extends Transformer<any, A, any>>(paramsOrTransformer: T, provider?: (old: Transformer.Params<T>) => Transformer.Params<T>) { let params: any; if (provider) { const old = this.state.tree.transforms.get(this.ref)!; diff --git a/src/mol-theme/color.ts b/src/mol-theme/color.ts index 31c5b3fd542397ba368eb6e7ed53c751621b69b3..2368ecae72671b1d35a9a7cd9b0bda7da8f61154 100644 --- a/src/mol-theme/color.ts +++ b/src/mol-theme/color.ts @@ -8,7 +8,7 @@ import { Color } from 'mol-util/color'; import { Location } from 'mol-model/location'; import { ColorType } from 'mol-geo/geometry/color-data'; import { CarbohydrateSymbolColorThemeProvider } from './color/carbohydrate-symbol'; -import { UniformColorTheme, UniformColorThemeProvider } from './color/uniform'; +import { UniformColorThemeProvider } from './color/uniform'; import { deepEqual } from 'mol-util'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import { ThemeDataContext } from './theme'; @@ -17,6 +17,7 @@ import { CrossLinkColorThemeProvider } from './color/cross-link'; import { ElementIndexColorThemeProvider } from './color/element-index'; import { ElementSymbolColorThemeProvider } from './color/element-symbol'; import { MoleculeTypeColorThemeProvider } from './color/molecule-type'; +import { PolymerIdColorThemeProvider } from './color/polymer-id'; import { PolymerIndexColorThemeProvider } from './color/polymer-index'; import { ResidueNameColorThemeProvider } from './color/residue-name'; import { SecondaryStructureColorThemeProvider } from './color/secondary-structure'; @@ -31,27 +32,31 @@ export type LocationColor = (location: Location, isSecondary: boolean) => Color export type ColorThemeProps = { [k: string]: any } export { ColorTheme } -interface ColorTheme<P extends ColorThemeProps = {}> { +interface ColorTheme<P extends PD.Params = {}> { + readonly factory: ColorTheme.Factory<P> readonly granularity: ColorType readonly color: LocationColor - readonly props: Readonly<P> + readonly props: Readonly<PD.Values<P>> readonly description?: string readonly legend?: Readonly<ScaleLegend | TableLegend> } namespace ColorTheme { export type Props = { [k: string]: any } - export const Empty = UniformColorTheme({}, { value: Color(0xCCCCCC) }) + export type Factory<P extends PD.Params> = (ctx: ThemeDataContext, props: PD.Values<P>) => ColorTheme<P> + export const EmptyFactory = () => Empty + const EmptyColor = Color(0xCCCCCC) + export const Empty: ColorTheme<{}> = { factory: EmptyFactory, granularity: 'uniform', color: () => EmptyColor, props: {} } export function areEqual(themeA: ColorTheme, themeB: ColorTheme) { - return themeA === themeB && deepEqual(themeA.props, themeB.props) + return themeA.factory === themeB.factory && deepEqual(themeA.props, themeB.props) } export interface Provider<P extends PD.Params> { readonly label: string - readonly factory: (ctx: ThemeDataContext, props: PD.Values<P>) => ColorTheme<PD.Values<P>> + readonly factory: (ctx: ThemeDataContext, props: PD.Values<P>) => ColorTheme<P> readonly getParams: (ctx: ThemeDataContext) => P } - export const EmptyProvider: Provider<{}> = { label: '', factory: () => Empty, getParams: () => ({}) } + export const EmptyProvider: Provider<{}> = { label: '', factory: EmptyFactory, getParams: () => ({}) } export class Registry { private _list: { name: string, provider: Provider<any> }[] = [] @@ -96,6 +101,7 @@ export const BuiltInColorThemes = { 'element-index': ElementIndexColorThemeProvider, 'element-symbol': ElementSymbolColorThemeProvider, 'molecule-type': MoleculeTypeColorThemeProvider, + 'polymer-id': PolymerIdColorThemeProvider, 'polymer-index': PolymerIndexColorThemeProvider, 'residue-name': ResidueNameColorThemeProvider, 'secondary-structure': SecondaryStructureColorThemeProvider, @@ -103,8 +109,4 @@ export const BuiltInColorThemes = { 'shape-group': ShapeGroupColorThemeProvider, 'unit-index': UnitIndexColorThemeProvider, 'uniform': UniformColorThemeProvider, -} -export type BuiltInColorThemeName = keyof typeof BuiltInColorThemes -export const BuiltInColorThemeNames = Object.keys(BuiltInColorThemes) -export const BuiltInColorThemeOptions = BuiltInColorThemeNames.map(n => [n, n] as [BuiltInColorThemeName, string]) -export const getBuiltInColorThemeParams = (name: string, ctx: ThemeDataContext = {}) => PD.Group((BuiltInColorThemes as { [k: string]: ColorTheme.Provider<any> })[name].getParams(ctx)) \ No newline at end of file +} \ No newline at end of file diff --git a/src/mol-theme/color/carbohydrate-symbol.ts b/src/mol-theme/color/carbohydrate-symbol.ts index 5063ca0c788364a4f79a23dee6fe5ae06a247aa1..237dc5caaef0b5c8e35a2df113f36ac2d735884d 100644 --- a/src/mol-theme/color/carbohydrate-symbol.ts +++ b/src/mol-theme/color/carbohydrate-symbol.ts @@ -27,12 +27,12 @@ export const CarbohydrateSymbolColorThemeParams = { // domain: PD.Interval('Color Domain', '', [0, 1]), // value: PD.Color('Color Value', '', DefaultColor), } +export type CarbohydrateSymbolColorThemeParams = typeof CarbohydrateSymbolColorThemeParams export function getCarbohydrateSymbolColorThemeParams(ctx: ThemeDataContext) { return CarbohydrateSymbolColorThemeParams // TODO return copy } -export type CarbohydrateSymbolColorThemeProps = PD.Values<typeof CarbohydrateSymbolColorThemeParams> -export function CarbohydrateSymbolColorTheme(ctx: ThemeDataContext, props: CarbohydrateSymbolColorThemeProps): ColorTheme<CarbohydrateSymbolColorThemeProps> { +export function CarbohydrateSymbolColorTheme(ctx: ThemeDataContext, props: PD.Values<CarbohydrateSymbolColorThemeParams>): ColorTheme<CarbohydrateSymbolColorThemeParams> { let color: LocationColor if (ctx.structure) { @@ -65,6 +65,7 @@ export function CarbohydrateSymbolColorTheme(ctx: ThemeDataContext, props: Carbo } return { + factory: CarbohydrateSymbolColorTheme, granularity: 'group', color: color, props: props, @@ -73,7 +74,7 @@ export function CarbohydrateSymbolColorTheme(ctx: ThemeDataContext, props: Carbo } } -export const CarbohydrateSymbolColorThemeProvider: ColorTheme.Provider<typeof CarbohydrateSymbolColorThemeParams> = { +export const CarbohydrateSymbolColorThemeProvider: ColorTheme.Provider<CarbohydrateSymbolColorThemeParams> = { label: 'Carbohydrate Symbol', factory: CarbohydrateSymbolColorTheme, getParams: getCarbohydrateSymbolColorThemeParams diff --git a/src/mol-theme/color/chain-id.ts b/src/mol-theme/color/chain-id.ts index 2983ce357fef282afe23182257b8e558fc8cb59f..f8c6065f608a9acc568a2c5aab6b441ad0f6b1d2 100644 --- a/src/mol-theme/color/chain-id.ts +++ b/src/mol-theme/color/chain-id.ts @@ -12,6 +12,7 @@ import { ColorTheme, LocationColor } from '../color'; import { ParamDefinition as PD } from 'mol-util/param-definition' import { ThemeDataContext } from 'mol-theme/theme'; import { ColorListOptions, ColorListName } from 'mol-util/color/scale'; +import { Column } from 'mol-data/db'; const DefaultColor = Color(0xCCCCCC) const Description = 'Gives every chain a color based on its `asym_id` value.' @@ -19,10 +20,10 @@ const Description = 'Gives every chain a color based on its `asym_id` value.' export const ChainIdColorThemeParams = { list: PD.Select<ColorListName>('RdYlBu', ColorListOptions), } +export type ChainIdColorThemeParams = typeof ChainIdColorThemeParams export function getChainIdColorThemeParams(ctx: ThemeDataContext) { return ChainIdColorThemeParams // TODO return copy } -export type ChainIdColorThemeProps = PD.Values<typeof ChainIdColorThemeParams> function getAsymId(unit: Unit): StructureElement.Property<string> { switch (unit.kind) { @@ -34,7 +35,18 @@ function getAsymId(unit: Unit): StructureElement.Property<string> { } } -export function ChainIdColorTheme(ctx: ThemeDataContext, props: ChainIdColorThemeProps): ColorTheme<ChainIdColorThemeProps> { +function addAsymIds(map: Map<string, number>, data: Column<string>) { + let j = map.size + for (let o = 0, ol = data.rowCount; o < ol; ++o) { + const k = data.value(o) + if (!map.has(k)) { + map.set(k, j) + j += 1 + } + } +} + +export function ChainIdColorTheme(ctx: ThemeDataContext, props: PD.Values<ChainIdColorThemeParams>): ColorTheme<ChainIdColorThemeParams> { let color: LocationColor const scale = ColorScale.create({ listOrName: props.list, minLabel: 'Start', maxLabel: 'End' }) @@ -42,14 +54,13 @@ export function ChainIdColorTheme(ctx: ThemeDataContext, props: ChainIdColorThem const l = StructureElement.create() const { models } = ctx.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 - } - }) + const m = models[i] + addAsymIds(asymIdSerialMap, m.atomicHierarchy.chains.label_asym_id) + if (m.coarseHierarchy.isDefined) { + addAsymIds(asymIdSerialMap, m.coarseHierarchy.spheres.asym_id) + addAsymIds(asymIdSerialMap, m.coarseHierarchy.gaussians.asym_id) + } } scale.setDomain(0, asymIdSerialMap.size - 1) const scaleColor = scale.color @@ -71,6 +82,7 @@ export function ChainIdColorTheme(ctx: ThemeDataContext, props: ChainIdColorThem } return { + factory: ChainIdColorTheme, granularity: 'group', color, props, @@ -79,7 +91,7 @@ export function ChainIdColorTheme(ctx: ThemeDataContext, props: ChainIdColorThem } } -export const ChainIdColorThemeProvider: ColorTheme.Provider<typeof ChainIdColorThemeParams> = { +export const ChainIdColorThemeProvider: ColorTheme.Provider<ChainIdColorThemeParams> = { label: 'Chain Id', factory: ChainIdColorTheme, getParams: getChainIdColorThemeParams diff --git a/src/mol-theme/color/cross-link.ts b/src/mol-theme/color/cross-link.ts index 74b36b07ee393fe476c8dd1fdf030714164a9eb6..4c43e030a1d8a981f644c87fe6af66a558fb2bed 100644 --- a/src/mol-theme/color/cross-link.ts +++ b/src/mol-theme/color/cross-link.ts @@ -21,19 +21,19 @@ export const CrossLinkColorThemeParams = { domain: PD.Interval([-10, 10]), list: PD.Select<ColorListName>('RdYlBu', ColorListOptions), } +export type CrossLinkColorThemeParams = typeof CrossLinkColorThemeParams export function getCrossLinkColorThemeParams(ctx: ThemeDataContext) { return CrossLinkColorThemeParams // TODO return copy } -export type CrossLinkColorThemeProps = PD.Values<typeof CrossLinkColorThemeParams> const distVecA = Vec3.zero(), distVecB = Vec3.zero() function linkDistance(link: Link.Location) { - link.aUnit.conformation.position(link.aIndex, distVecA) - link.bUnit.conformation.position(link.bIndex, distVecB) + link.aUnit.conformation.position(link.aUnit.elements[link.aIndex], distVecA) + link.bUnit.conformation.position(link.bUnit.elements[link.bIndex], distVecB) return Vec3.distance(distVecA, distVecB) } -export function CrossLinkColorTheme(ctx: ThemeDataContext, props: CrossLinkColorThemeProps): ColorTheme<CrossLinkColorThemeProps> { +export function CrossLinkColorTheme(ctx: ThemeDataContext, props: PD.Values<CrossLinkColorThemeParams>): ColorTheme<CrossLinkColorThemeParams> { let color: LocationColor let scale: ColorScale | undefined = undefined @@ -59,6 +59,7 @@ export function CrossLinkColorTheme(ctx: ThemeDataContext, props: CrossLinkColor } return { + factory: CrossLinkColorTheme, granularity: 'group', color, props, @@ -67,7 +68,7 @@ export function CrossLinkColorTheme(ctx: ThemeDataContext, props: CrossLinkColor } } -export const CrossLinkColorThemeProvider: ColorTheme.Provider<typeof CrossLinkColorThemeParams> = { +export const CrossLinkColorThemeProvider: ColorTheme.Provider<CrossLinkColorThemeParams> = { label: 'Cross Link', factory: CrossLinkColorTheme, getParams: getCrossLinkColorThemeParams diff --git a/src/mol-theme/color/element-index.ts b/src/mol-theme/color/element-index.ts index 2307fd4d78bdb98dd6399c6b9da155cfd1c7c82a..5605cde9aa38612a2cbbbee73b0b0196ef0fafc2 100644 --- a/src/mol-theme/color/element-index.ts +++ b/src/mol-theme/color/element-index.ts @@ -19,12 +19,12 @@ const Description = 'Gives every element (atom or coarse sphere/gaussian) a uniq export const ElementIndexColorThemeParams = { list: PD.Select<ColorListName>('RdYlBu', ColorListOptions), } +export type ElementIndexColorThemeParams = typeof ElementIndexColorThemeParams export function getElementIndexColorThemeParams(ctx: ThemeDataContext) { return ElementIndexColorThemeParams // TODO return copy } -export type ElementIndexColorThemeProps = PD.Values<typeof ElementIndexColorThemeParams> -export function ElementIndexColorTheme(ctx: ThemeDataContext, props: ElementIndexColorThemeProps): ColorTheme<ElementIndexColorThemeProps> { +export function ElementIndexColorTheme(ctx: ThemeDataContext, props: PD.Values<ElementIndexColorThemeParams>): ColorTheme<ElementIndexColorThemeParams> { let color: LocationColor let scale = ColorScale.create({ listOrName: props.list, minLabel: 'Start', maxLabel: 'End' }) @@ -59,6 +59,7 @@ export function ElementIndexColorTheme(ctx: ThemeDataContext, props: ElementInde } return { + factory: ElementIndexColorTheme, granularity: 'groupInstance', color, props, @@ -67,7 +68,7 @@ export function ElementIndexColorTheme(ctx: ThemeDataContext, props: ElementInde } } -export const ElementIndexColorThemeProvider: ColorTheme.Provider<typeof ElementIndexColorThemeParams> = { +export const ElementIndexColorThemeProvider: ColorTheme.Provider<ElementIndexColorThemeParams> = { label: 'Element Index', factory: ElementIndexColorTheme, getParams: getElementIndexColorThemeParams diff --git a/src/mol-theme/color/element-symbol.ts b/src/mol-theme/color/element-symbol.ts index 2e7650413d6985548dd80a1a821fd5c0e611b32c..5ac58ef68fcab3c36f66012a8b2a588967b14264 100644 --- a/src/mol-theme/color/element-symbol.ts +++ b/src/mol-theme/color/element-symbol.ts @@ -22,17 +22,17 @@ const DefaultElementSymbolColor = Color(0xFFFFFF) const Description = 'Assigns a color to every atom according to its chemical element.' export const ElementSymbolColorThemeParams = {} +export type ElementSymbolColorThemeParams = typeof ElementSymbolColorThemeParams export function getElementSymbolColorThemeParams(ctx: ThemeDataContext) { return ElementSymbolColorThemeParams // TODO return copy } -export type ElementSymbolColorThemeProps = PD.Values<typeof ElementSymbolColorThemeParams> export function elementSymbolColor(element: ElementSymbol): Color { const c = (ElementSymbolColors as { [k: string]: Color })[element]; return c === undefined ? DefaultElementSymbolColor : c } -export function ElementSymbolColorTheme(ctx: ThemeDataContext, props: ElementSymbolColorThemeProps): ColorTheme<ElementSymbolColorThemeProps> { +export function ElementSymbolColorTheme(ctx: ThemeDataContext, props: PD.Values<ElementSymbolColorThemeParams>): ColorTheme<ElementSymbolColorThemeParams> { function color(location: Location): Color { if (StructureElement.isLocation(location)) { if (Unit.isAtomic(location.unit)) { @@ -49,6 +49,7 @@ export function ElementSymbolColorTheme(ctx: ThemeDataContext, props: ElementSym } return { + factory: ElementSymbolColorTheme, granularity: 'group', color, props, @@ -59,7 +60,7 @@ export function ElementSymbolColorTheme(ctx: ThemeDataContext, props: ElementSym } } -export const ElementSymbolColorThemeProvider: ColorTheme.Provider<typeof ElementSymbolColorThemeParams> = { +export const ElementSymbolColorThemeProvider: ColorTheme.Provider<ElementSymbolColorThemeParams> = { label: 'Element Symbol', factory: ElementSymbolColorTheme, getParams: getElementSymbolColorThemeParams diff --git a/src/mol-theme/color/molecule-type.ts b/src/mol-theme/color/molecule-type.ts index 11c87cbfb77d3f05de0493a9f4c26872de0e7c0d..e4bc92b93f8dbb4c4b1d27a291dce73411f68017 100644 --- a/src/mol-theme/color/molecule-type.ts +++ b/src/mol-theme/color/molecule-type.ts @@ -28,10 +28,10 @@ const DefaultMoleculeTypeColor = Color(0xffff99) const Description = 'Assigns a color based on the molecule type of a residue.' export const MoleculeTypeColorThemeParams = {} +export type MoleculeTypeColorThemeParams = typeof MoleculeTypeColorThemeParams export function getMoleculeTypeColorThemeParams(ctx: ThemeDataContext) { return MoleculeTypeColorThemeParams // TODO return copy } -export type MoleculeTypeColorThemeProps = PD.Values<typeof MoleculeTypeColorThemeParams> export function moleculeTypeColor(unit: Unit, element: ElementIndex): Color { const moleculeType = getElementMoleculeType(unit, element) @@ -47,7 +47,7 @@ export function moleculeTypeColor(unit: Unit, element: ElementIndex): Color { return DefaultMoleculeTypeColor } -export function MoleculeTypeColorTheme(ctx: ThemeDataContext, props: MoleculeTypeColorThemeProps): ColorTheme<MoleculeTypeColorThemeProps> { +export function MoleculeTypeColorTheme(ctx: ThemeDataContext, props: PD.Values<MoleculeTypeColorThemeParams>): ColorTheme<MoleculeTypeColorThemeParams> { function color(location: Location): Color { if (StructureElement.isLocation(location)) { return moleculeTypeColor(location.unit, location.element) @@ -58,6 +58,7 @@ export function MoleculeTypeColorTheme(ctx: ThemeDataContext, props: MoleculeTyp } return { + factory: MoleculeTypeColorTheme, granularity: 'group', color, props, @@ -68,7 +69,7 @@ export function MoleculeTypeColorTheme(ctx: ThemeDataContext, props: MoleculeTyp } } -export const MoleculeTypeColorThemeProvider: ColorTheme.Provider<typeof MoleculeTypeColorThemeParams> = { +export const MoleculeTypeColorThemeProvider: ColorTheme.Provider<MoleculeTypeColorThemeParams> = { label: 'Molecule Type', factory: MoleculeTypeColorTheme, getParams: getMoleculeTypeColorThemeParams diff --git a/src/mol-theme/color/polymer-id.ts b/src/mol-theme/color/polymer-id.ts new file mode 100644 index 0000000000000000000000000000000000000000..8ef2a42b8a726649b31d89b4d5bb2e8b6f5622c2 --- /dev/null +++ b/src/mol-theme/color/polymer-id.ts @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { Unit, StructureProperties, StructureElement, Link } from 'mol-model/structure'; + +import { ColorScale, Color } from 'mol-util/color'; +import { Location } from 'mol-model/location'; +import { ColorTheme, LocationColor } from '../color'; +import { ParamDefinition as PD } from 'mol-util/param-definition' +import { ThemeDataContext } from 'mol-theme/theme'; +import { ColorListOptions, ColorListName } from 'mol-util/color/scale'; +import { Column } from 'mol-data/db'; +import { Entities } from 'mol-model/structure/model/properties/common'; + +const DefaultColor = Color(0xCCCCCC) +const Description = 'Gives every polymer chain a color based on its `asym_id` value.' + +export const PolymerIdColorThemeParams = { + list: PD.Select<ColorListName>('RdYlBu', ColorListOptions), +} +export type PolymerIdColorThemeParams = typeof PolymerIdColorThemeParams +export function getPolymerIdColorThemeParams(ctx: ThemeDataContext) { + return PolymerIdColorThemeParams // TODO return copy +} + +function getAsymId(unit: Unit): StructureElement.Property<string> { + switch (unit.kind) { + case Unit.Kind.Atomic: + return StructureProperties.chain.label_asym_id + case Unit.Kind.Spheres: + case Unit.Kind.Gaussians: + return StructureProperties.coarse.asym_id + } +} + +function addPolymerAsymIds(map: Map<string, number>, asymId: Column<string>, entityId: Column<string>, entities: Entities) { + let j = map.size + for (let o = 0, ol = asymId.rowCount; o < ol; ++o) { + const e = entityId.value(o) + const eI = entities.getEntityIndex(e) + if (entities.data.type.value(eI) === 'polymer') { + const k = asymId.value(o) + if (!map.has(k)) { + map.set(k, j) + j += 1 + } + } + } +} + +export function PolymerIdColorTheme(ctx: ThemeDataContext, props: PD.Values<PolymerIdColorThemeParams>): ColorTheme<PolymerIdColorThemeParams> { + let color: LocationColor + const scale = ColorScale.create({ listOrName: props.list, minLabel: 'Start', maxLabel: 'End' }) + + if (ctx.structure) { + const l = StructureElement.create() + const { models } = ctx.structure + const polymerAsymIdSerialMap = new Map<string, number>() + for (let i = 0, il = models.length; i <il; ++i) { + for (let i = 0, il = models.length; i <il; ++i) { + const m = models[i] + addPolymerAsymIds(polymerAsymIdSerialMap, m.atomicHierarchy.chains.label_asym_id, m.atomicHierarchy.chains.label_entity_id, m.entities) + if (m.coarseHierarchy.isDefined) { + addPolymerAsymIds(polymerAsymIdSerialMap, m.coarseHierarchy.spheres.asym_id, m.coarseHierarchy.spheres.entity_id, m.entities) + addPolymerAsymIds(polymerAsymIdSerialMap, m.coarseHierarchy.gaussians.asym_id, m.coarseHierarchy.spheres.entity_id, m.entities) + } + } + } + scale.setDomain(0, polymerAsymIdSerialMap.size - 1) + const scaleColor = scale.color + + color = (location: Location): Color => { + if (StructureElement.isLocation(location)) { + const asym_id = getAsymId(location.unit) + return scaleColor(polymerAsymIdSerialMap.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(polymerAsymIdSerialMap.get(asym_id(l)) || 0) + } + return DefaultColor + } + } else { + color = () => DefaultColor + } + + return { + factory: PolymerIdColorTheme, + granularity: 'group', + color, + props, + description: Description, + legend: scale ? scale.legend : undefined + } +} + +export const PolymerIdColorThemeProvider: ColorTheme.Provider<PolymerIdColorThemeParams> = { + label: 'Polymer Id', + factory: PolymerIdColorTheme, + getParams: getPolymerIdColorThemeParams +} \ No newline at end of file diff --git a/src/mol-theme/color/polymer-index.ts b/src/mol-theme/color/polymer-index.ts index ed5cc40d6876374d6e8eac6fff366417f40f8e18..53ac666302efb9bb112c96a955345668539fa3aa 100644 --- a/src/mol-theme/color/polymer-index.ts +++ b/src/mol-theme/color/polymer-index.ts @@ -18,12 +18,13 @@ const Description = 'Gives every polymer a unique color based on the position (i export const PolymerIndexColorThemeParams = { list: PD.Select<ColorListName>('RdYlBu', ColorListOptions), } +export type PolymerIndexColorThemeParams = typeof PolymerIndexColorThemeParams export function getPolymerIndexColorThemeParams(ctx: ThemeDataContext) { return PolymerIndexColorThemeParams // TODO return copy } export type PolymerIndexColorThemeProps = PD.Values<typeof PolymerIndexColorThemeParams> -export function PolymerIndexColorTheme(ctx: ThemeDataContext, props: PolymerIndexColorThemeProps): ColorTheme<PolymerIndexColorThemeProps> { +export function PolymerIndexColorTheme(ctx: ThemeDataContext, props: PD.Values<PolymerIndexColorThemeParams>): ColorTheme<PolymerIndexColorThemeParams> { let color: LocationColor const scale = ColorScale.create({ listOrName: props.list, minLabel: 'Start', maxLabel: 'End' }) @@ -56,6 +57,7 @@ export function PolymerIndexColorTheme(ctx: ThemeDataContext, props: PolymerInde } return { + factory: PolymerIndexColorTheme, granularity: 'instance', color, props, @@ -64,7 +66,7 @@ export function PolymerIndexColorTheme(ctx: ThemeDataContext, props: PolymerInde } } -export const PolymerIndexColorThemeProvider: ColorTheme.Provider<typeof PolymerIndexColorThemeParams> = { +export const PolymerIndexColorThemeProvider: ColorTheme.Provider<PolymerIndexColorThemeParams> = { label: 'Polymer Index', factory: PolymerIndexColorTheme, getParams: getPolymerIndexColorThemeParams diff --git a/src/mol-theme/color/residue-name.ts b/src/mol-theme/color/residue-name.ts index 178d49634e27e9f45da46aee004073620a6ff765..9f4b8efc6bfa03d331feece34820cff8f916c8b7 100644 --- a/src/mol-theme/color/residue-name.ts +++ b/src/mol-theme/color/residue-name.ts @@ -63,10 +63,10 @@ const DefaultResidueNameColor = Color(0xFF00FF) const Description = 'Assigns a color to every residue according to its name.' export const ResidueNameColorThemeParams = {} +export type ResidueNameColorThemeParams = typeof ResidueNameColorThemeParams export function getResidueNameColorThemeParams(ctx: ThemeDataContext) { return ResidueNameColorThemeParams // TODO return copy } -export type ResidueNameColorThemeProps = PD.Values<typeof ResidueNameColorThemeParams> export function residueNameColor(residueName: string): Color { const c = (ResidueNameColors as { [k: string]: Color })[residueName]; @@ -93,7 +93,7 @@ function getCoarseCompId(unit: Unit.Spheres | Unit.Gaussians, element: ElementIn } } -export function ResidueNameColorTheme(ctx: ThemeDataContext, props: ResidueNameColorThemeProps): ColorTheme<ResidueNameColorThemeProps> { +export function ResidueNameColorTheme(ctx: ThemeDataContext, props: PD.Values<ResidueNameColorThemeParams>): ColorTheme<ResidueNameColorThemeParams> { function color(location: Location): Color { if (StructureElement.isLocation(location)) { if (Unit.isAtomic(location.unit)) { @@ -114,6 +114,7 @@ export function ResidueNameColorTheme(ctx: ThemeDataContext, props: ResidueNameC } return { + factory: ResidueNameColorTheme, granularity: 'group', color, props, @@ -124,7 +125,7 @@ export function ResidueNameColorTheme(ctx: ThemeDataContext, props: ResidueNameC } } -export const ResidueNameColorThemeProvider: ColorTheme.Provider<typeof ResidueNameColorThemeParams> = { +export const ResidueNameColorThemeProvider: ColorTheme.Provider<ResidueNameColorThemeParams> = { label: 'Residue Name', factory: ResidueNameColorTheme, getParams: getResidueNameColorThemeParams diff --git a/src/mol-theme/color/secondary-structure.ts b/src/mol-theme/color/secondary-structure.ts index 955846c5f7574ddce3d1a69acf6e2d04c1d0e7e1..3deb17a2127e5799e5f806ca94bfe5b4e0e2a85d 100644 --- a/src/mol-theme/color/secondary-structure.ts +++ b/src/mol-theme/color/secondary-structure.ts @@ -33,10 +33,10 @@ const DefaultSecondaryStructureColor = Color(0x808080) const Description = 'Assigns a color based on the type of secondary structure and basic molecule type.' export const SecondaryStructureColorThemeParams = {} +export type SecondaryStructureColorThemeParams = typeof SecondaryStructureColorThemeParams export function getSecondaryStructureColorThemeParams(ctx: ThemeDataContext) { return SecondaryStructureColorThemeParams // TODO return copy } -export type SecondaryStructureColorThemeProps = PD.Values<typeof SecondaryStructureColorThemeParams> export function secondaryStructureColor(unit: Unit, element: ElementIndex): Color { let secStrucType = SecondaryStructureType.create(SecondaryStructureType.Flag.None) @@ -70,7 +70,7 @@ export function secondaryStructureColor(unit: Unit, element: ElementIndex): Colo return DefaultSecondaryStructureColor } -export function SecondaryStructureColorTheme(ctx: ThemeDataContext, props: SecondaryStructureColorThemeProps): ColorTheme<SecondaryStructureColorThemeProps> { +export function SecondaryStructureColorTheme(ctx: ThemeDataContext, props: PD.Values<SecondaryStructureColorThemeParams>): ColorTheme<SecondaryStructureColorThemeParams> { function color(location: Location): Color { if (StructureElement.isLocation(location)) { return secondaryStructureColor(location.unit, location.element) @@ -81,6 +81,7 @@ export function SecondaryStructureColorTheme(ctx: ThemeDataContext, props: Secon } return { + factory: SecondaryStructureColorTheme, granularity: 'group', color, props, @@ -91,7 +92,7 @@ export function SecondaryStructureColorTheme(ctx: ThemeDataContext, props: Secon } } -export const SecondaryStructureColorThemeProvider: ColorTheme.Provider<typeof SecondaryStructureColorThemeParams> = { +export const SecondaryStructureColorThemeProvider: ColorTheme.Provider<SecondaryStructureColorThemeParams> = { label: 'Secondary Structure', factory: SecondaryStructureColorTheme, getParams: getSecondaryStructureColorThemeParams diff --git a/src/mol-theme/color/sequence-id.ts b/src/mol-theme/color/sequence-id.ts index 4fbfafa637f6c6050b9b488299afe19f851fad85..96bf25a2320d464b067e5b1dfd4034e474cad1d0 100644 --- a/src/mol-theme/color/sequence-id.ts +++ b/src/mol-theme/color/sequence-id.ts @@ -19,10 +19,10 @@ const Description = 'Gives every polymer residue a color based on its `seq_id` v export const SequenceIdColorThemeParams = { list: PD.Select<ColorListName>('rainbow', ColorListOptions), } +export type SequenceIdColorThemeParams = typeof SequenceIdColorThemeParams export function getSequenceIdColorThemeParams(ctx: ThemeDataContext) { return SequenceIdColorThemeParams // TODO return copy } -export type SequenceIdColorThemeProps = PD.Values<typeof SequenceIdColorThemeParams> function getSeqId(unit: Unit, element: ElementIndex): number { const { model } = unit @@ -64,7 +64,7 @@ function getSequenceLength(unit: Unit, element: ElementIndex) { return model.sequence.byEntityKey[entityIndex].sequence.sequence.length } -export function SequenceIdColorTheme(ctx: ThemeDataContext, props: SequenceIdColorThemeProps): ColorTheme<SequenceIdColorThemeProps> { +export function SequenceIdColorTheme(ctx: ThemeDataContext, props: PD.Values<SequenceIdColorThemeParams>): ColorTheme<SequenceIdColorThemeParams> { const scale = ColorScale.create({ listOrName: props.list, minLabel: 'Start', @@ -90,6 +90,7 @@ export function SequenceIdColorTheme(ctx: ThemeDataContext, props: SequenceIdCol } return { + factory: SequenceIdColorTheme, granularity: 'group', color, props, @@ -98,7 +99,7 @@ export function SequenceIdColorTheme(ctx: ThemeDataContext, props: SequenceIdCol } } -export const SequenceIdColorThemeProvider: ColorTheme.Provider<typeof SequenceIdColorThemeParams> = { +export const SequenceIdColorThemeProvider: ColorTheme.Provider<SequenceIdColorThemeParams> = { label: 'Sequence Id', factory: SequenceIdColorTheme, getParams: getSequenceIdColorThemeParams diff --git a/src/mol-theme/color/shape-group.ts b/src/mol-theme/color/shape-group.ts index 4d30c299b6f78e1b1c7f2749ce85c07a3df1115b..052cd6d1ff13c17b390216f7113ea166b1fed9d1 100644 --- a/src/mol-theme/color/shape-group.ts +++ b/src/mol-theme/color/shape-group.ts @@ -15,13 +15,14 @@ const DefaultColor = Color(0xCCCCCC) const Description = 'Assigns colors as defined by the shape object.' export const ShapeGroupColorThemeParams = {} +export type ShapeGroupColorThemeParams = typeof ShapeGroupColorThemeParams export function getShapeGroupColorThemeParams(ctx: ThemeDataContext) { return ShapeGroupColorThemeParams // TODO return copy } -export type ShapeGroupColorThemeProps = PD.Values<typeof ShapeGroupColorThemeParams> -export function ShapeGroupColorTheme(ctx: ThemeDataContext, props: ShapeGroupColorThemeProps): ColorTheme<ShapeGroupColorThemeProps> { +export function ShapeGroupColorTheme(ctx: ThemeDataContext, props: PD.Values<ShapeGroupColorThemeParams>): ColorTheme<ShapeGroupColorThemeParams> { return { + factory: ShapeGroupColorTheme, granularity: 'group', color: (location: Location): Color => { if (Shape.isLocation(location)) { @@ -34,7 +35,7 @@ export function ShapeGroupColorTheme(ctx: ThemeDataContext, props: ShapeGroupCol } } -export const ShapeGroupColorThemeProvider: ColorTheme.Provider<typeof ShapeGroupColorThemeParams> = { +export const ShapeGroupColorThemeProvider: ColorTheme.Provider<ShapeGroupColorThemeParams> = { label: 'Shape Group', factory: ShapeGroupColorTheme, getParams: getShapeGroupColorThemeParams diff --git a/src/mol-theme/color/uniform.ts b/src/mol-theme/color/uniform.ts index 0716bb4ed47e2de68e7b8f42b6f365ff674b1a95..3908ac4e807cfb1d01765fe789b4d91221ca4ae3 100644 --- a/src/mol-theme/color/uniform.ts +++ b/src/mol-theme/color/uniform.ts @@ -16,15 +16,16 @@ const Description = 'Gives everything the same, uniform color.' export const UniformColorThemeParams = { value: PD.Color(DefaultColor), } +export type UniformColorThemeParams = typeof UniformColorThemeParams export function getUniformColorThemeParams(ctx: ThemeDataContext) { return UniformColorThemeParams // TODO return copy } -export type UniformColorThemeProps = PD.Values<typeof UniformColorThemeParams> -export function UniformColorTheme(ctx: ThemeDataContext, props: UniformColorThemeProps): ColorTheme<UniformColorThemeProps> { +export function UniformColorTheme(ctx: ThemeDataContext, props: PD.Values<UniformColorThemeParams>): ColorTheme<UniformColorThemeParams> { const color = props.value || DefaultColor return { + factory: UniformColorTheme, granularity: 'uniform', color: () => color, props: props, @@ -33,7 +34,7 @@ export function UniformColorTheme(ctx: ThemeDataContext, props: UniformColorThem } } -export const UniformColorThemeProvider: ColorTheme.Provider<typeof UniformColorThemeParams> = { +export const UniformColorThemeProvider: ColorTheme.Provider<UniformColorThemeParams> = { label: 'Uniform', factory: UniformColorTheme, getParams: getUniformColorThemeParams diff --git a/src/mol-theme/color/unit-index.ts b/src/mol-theme/color/unit-index.ts index 15494fc8572c20a07da95677eb739b966c8db046..fa179d1bbedadc3debdac4d2575b4e693518905a 100644 --- a/src/mol-theme/color/unit-index.ts +++ b/src/mol-theme/color/unit-index.ts @@ -18,12 +18,12 @@ const Description = 'Gives every unit (single chain or collection of single elem export const UnitIndexColorThemeParams = { list: PD.Select<ColorListName>('RdYlBu', ColorListOptions), } +export type UnitIndexColorThemeParams = typeof UnitIndexColorThemeParams export function getUnitIndexColorThemeParams(ctx: ThemeDataContext) { return UnitIndexColorThemeParams // TODO return copy } -export type UnitIndexColorThemeProps = PD.Values<typeof UnitIndexColorThemeParams> -export function UnitIndexColorTheme(ctx: ThemeDataContext, props: UnitIndexColorThemeProps): ColorTheme<UnitIndexColorThemeProps> { +export function UnitIndexColorTheme(ctx: ThemeDataContext, props: PD.Values<UnitIndexColorThemeParams>): ColorTheme<UnitIndexColorThemeParams> { let color: LocationColor const scale = ColorScale.create({ listOrName: props.list, minLabel: 'Start', maxLabel: 'End' }) @@ -48,6 +48,7 @@ export function UnitIndexColorTheme(ctx: ThemeDataContext, props: UnitIndexColor } return { + factory: UnitIndexColorTheme, granularity: 'instance', color, props, @@ -56,7 +57,7 @@ export function UnitIndexColorTheme(ctx: ThemeDataContext, props: UnitIndexColor } } -export const UnitIndexColorThemeProvider: ColorTheme.Provider<typeof UnitIndexColorThemeParams> = { +export const UnitIndexColorThemeProvider: ColorTheme.Provider<UnitIndexColorThemeParams> = { label: 'Unit Index', factory: UnitIndexColorTheme, getParams: getUnitIndexColorThemeParams diff --git a/src/mol-theme/label.ts b/src/mol-theme/label.ts index 6cfaafb2316802c9ec76a0ed9b24f06466b5d54f..0e961fca8c17953d53ebed5f0a74fa00261176f0 100644 --- a/src/mol-theme/label.ts +++ b/src/mol-theme/label.ts @@ -20,6 +20,8 @@ function setElementLocation(loc: StructureElement, unit: Unit, index: StructureE export function labelFirst(loci: Loci): string { switch (loci.kind) { + case 'structure-loci': + return loci.structure.models.map(m => m.label).join(', ') case 'element-loci': const e = loci.elements[0] if (e) { diff --git a/src/mol-theme/size.ts b/src/mol-theme/size.ts index 22042a4557f7aacc83219ca5e304d4aa2c3bbbc3..66da14adaa82c7640b8f42b852c576b737e213b6 100644 --- a/src/mol-theme/size.ts +++ b/src/mol-theme/size.ts @@ -5,32 +5,36 @@ */ import { SizeType, LocationSize } from 'mol-geo/geometry/size-data'; -import { UniformSizeTheme, UniformSizeThemeProvider } from './size/uniform'; +import { UniformSizeThemeProvider } from './size/uniform'; import { ParamDefinition as PD } from 'mol-util/param-definition'; import { ThemeDataContext } from 'mol-theme/theme'; import { PhysicalSizeThemeProvider } from './size/physical'; import { deepEqual } from 'mol-util'; export { SizeTheme } -interface SizeTheme<P extends SizeTheme.Props = {}> { +interface SizeTheme<P extends PD.Params = {}> { + readonly factory: SizeTheme.Factory<P> readonly granularity: SizeType readonly size: LocationSize - readonly props: Readonly<P> + readonly props: Readonly<PD.Values<P>> readonly description?: string } namespace SizeTheme { export type Props = { [k: string]: any } - export const Empty = UniformSizeTheme({}, { value: 1 }) + export type Factory<P extends PD.Params> = (ctx: ThemeDataContext, props: PD.Values<P>) => SizeTheme<P> + export const EmptyFactory = () => Empty + export const Empty: SizeTheme<{}> = { factory: EmptyFactory, granularity: 'uniform', size: () => 1, props: {} } export function areEqual(themeA: SizeTheme, themeB: SizeTheme) { - return themeA === themeB && deepEqual(themeA.props, themeB.props) + return themeA.factory === themeB.factory && deepEqual(themeA.props, themeB.props) } export interface Provider<P extends PD.Params> { readonly label: string - readonly factory: (ctx: ThemeDataContext, props: PD.Values<P>) => SizeTheme<PD.Values<P>> + readonly factory: Factory<P> readonly getParams: (ctx: ThemeDataContext) => P } + export const EmptyProvider: Provider<{}> = { label: '', factory: EmptyFactory, getParams: () => ({}) } export class Registry { private _list: { name: string, provider: Provider<any> }[] = [] @@ -53,8 +57,8 @@ namespace SizeTheme { this._map.set(name, provider) } - get(id: string) { - return this._map.get(id) + get<P extends PD.Params>(id: string) { + return this._map.get(id) || EmptyProvider as unknown as Provider<P> } create(id: string, ctx: ThemeDataContext, props = {}) { @@ -71,8 +75,4 @@ namespace SizeTheme { export const BuiltInSizeThemes = { 'physical': PhysicalSizeThemeProvider, 'uniform': UniformSizeThemeProvider -} -export type BuiltInSizeThemeName = keyof typeof BuiltInSizeThemes -export const BuiltInSizeThemeNames = Object.keys(BuiltInSizeThemes) -export const BuiltInSizeThemeOptions = BuiltInSizeThemeNames.map(n => [n, n] as [BuiltInSizeThemeName, string]) -export const getBuiltInSizeThemeParams = (name: string, ctx: ThemeDataContext = {}) => PD.Group((BuiltInSizeThemes as { [k: string]: SizeTheme.Provider<any> })[name].getParams(ctx)) \ No newline at end of file +} \ No newline at end of file diff --git a/src/mol-theme/size/physical.ts b/src/mol-theme/size/physical.ts index c7a62dcdb7b0226cd79d8e28a374f12371668b37..43da29365a2bf3e5ab76235a53d1090f55d3222a 100644 --- a/src/mol-theme/size/physical.ts +++ b/src/mol-theme/size/physical.ts @@ -15,10 +15,10 @@ const DefaultSize = 1 const Description = 'Assigns a physical size.' export const PhysicalSizeThemeParams = {} +export type PhysicalSizeThemeParams = typeof PhysicalSizeThemeParams export function getPhysicalSizeThemeParams(ctx: ThemeDataContext) { return PhysicalSizeThemeParams // TODO return copy } -export type PhysicalSizeThemeProps = PD.Values<typeof PhysicalSizeThemeParams> export function getPhysicalRadius(unit: Unit, element: ElementIndex): number { if (Unit.isAtomic(unit)) { @@ -34,7 +34,7 @@ export function getPhysicalRadius(unit: Unit, element: ElementIndex): number { * Create attribute data with the physical size of an element, * i.e. vdw for atoms and radius for coarse spheres */ -export function PhysicalSizeTheme(ctx: ThemeDataContext, props: PhysicalSizeThemeProps): SizeTheme<PhysicalSizeThemeProps> { +export function PhysicalSizeTheme(ctx: ThemeDataContext, props: PD.Values<PhysicalSizeThemeParams>): SizeTheme<PhysicalSizeThemeParams> { function size(location: Location): number { let size: number if (StructureElement.isLocation(location)) { @@ -48,6 +48,7 @@ export function PhysicalSizeTheme(ctx: ThemeDataContext, props: PhysicalSizeThem } return { + factory: PhysicalSizeTheme, granularity: 'group', size, props, @@ -55,7 +56,7 @@ export function PhysicalSizeTheme(ctx: ThemeDataContext, props: PhysicalSizeThem } } -export const PhysicalSizeThemeProvider: SizeTheme.Provider<typeof PhysicalSizeThemeParams> = { +export const PhysicalSizeThemeProvider: SizeTheme.Provider<PhysicalSizeThemeParams> = { label: 'Physical', factory: PhysicalSizeTheme, getParams: getPhysicalSizeThemeParams diff --git a/src/mol-theme/size/uniform.ts b/src/mol-theme/size/uniform.ts index 5040b9675c69a5b8564c86099be55030df6c767c..c60239110b5850b5ea11c182a393cdf728871175 100644 --- a/src/mol-theme/size/uniform.ts +++ b/src/mol-theme/size/uniform.ts @@ -13,15 +13,16 @@ const Description = 'Gives everything the same, uniform size.' export const UniformSizeThemeParams = { value: PD.Numeric(1, { min: 0, max: 20, step: 0.1 }), } +export type UniformSizeThemeParams = typeof UniformSizeThemeParams export function getUniformSizeThemeParams(ctx: ThemeDataContext) { return UniformSizeThemeParams // TODO return copy } -export type UniformSizeThemeProps = PD.Values<typeof UniformSizeThemeParams> -export function UniformSizeTheme(ctx: ThemeDataContext, props: UniformSizeThemeProps): SizeTheme<UniformSizeThemeProps> { +export function UniformSizeTheme(ctx: ThemeDataContext, props: PD.Values<UniformSizeThemeParams>): SizeTheme<UniformSizeThemeParams> { const size = props.value return { + factory: UniformSizeTheme, granularity: 'uniform', size: () => size, props, @@ -29,7 +30,7 @@ export function UniformSizeTheme(ctx: ThemeDataContext, props: UniformSizeThemeP } } -export const UniformSizeThemeProvider: SizeTheme.Provider<typeof UniformSizeThemeParams> = { +export const UniformSizeThemeProvider: SizeTheme.Provider<UniformSizeThemeParams> = { label: 'Uniform', factory: UniformSizeTheme, getParams: getUniformSizeThemeParams diff --git a/src/mol-theme/theme.ts b/src/mol-theme/theme.ts index 4bd6aa1b08f640168484c60c0d04ee6c57dfb4ba..ad2222c388cba75c6509875228e11e6c5226afc3 100644 --- a/src/mol-theme/theme.ts +++ b/src/mol-theme/theme.ts @@ -41,6 +41,6 @@ export function createTheme(ctx: ThemeRegistryContext, data: ThemeDataContext, p return theme } -export function createEmptyTheme() { +export function createEmptyTheme(): Theme { return { color: ColorTheme.Empty, size: SizeTheme.Empty } } \ No newline at end of file diff --git a/src/mol-util/color/tables.ts b/src/mol-util/color/tables.ts index 758fe153507c286071972d72d88fd2340e960322..b4380176ccfb35ad09a8681d2893aa0988ba9b96 100644 --- a/src/mol-util/color/tables.ts +++ b/src/mol-util/color/tables.ts @@ -259,4 +259,13 @@ export const ColorNames = ColorMap({ whitesmoke: 0xf5f5f5, yellow: 0xffff00, yellowgreen: 0x9acd32 -}) \ No newline at end of file +}) +export type ColorNames = typeof ColorNames +export type ColorName = keyof ColorNames +export const ColorNamesValueMap = (function(){ + const map = new Map<Color, ColorName>() + Object.keys(ColorNames).forEach(name => { + map.set(ColorNames[name as ColorName], name as ColorName) + }) + return map +})() diff --git a/src/mol-util/data-source.ts b/src/mol-util/data-source.ts new file mode 100644 index 0000000000000000000000000000000000000000..8545b93979ce9163a34f147389061f8b33955822 --- /dev/null +++ b/src/mol-util/data-source.ts @@ -0,0 +1,187 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + * + * Adapted from LiteMol + */ + +import { Task, RuntimeContext } from 'mol-task'; +import { utf8Read } from 'mol-io/common/utf8'; + +export enum DataCompressionMethod { + None, + Gzip +} + +export interface AjaxGetParams { + url: string, + type: 'string' | 'binary', + title?: string, + compression?: DataCompressionMethod +} + +export function readStringFromFile(file: File) { + return <Task<string>>readFromFileInternal(file, false); +} + +export function readUint8ArrayFromFile(file: File) { + return <Task<Uint8Array>>readFromFileInternal(file, true); +} + +export function readFromFile(file: File, type: 'string' | 'binary') { + return <Task<Uint8Array | string>>readFromFileInternal(file, type === 'binary'); +} + +export function ajaxGetString(url: string, title?: string) { + return <Task<string>>ajaxGetInternal(title, url, false, false); +} + +export function ajaxGetUint8Array(url: string, title?: string) { + return <Task<Uint8Array>>ajaxGetInternal(title, url, true, false); +} + +export function ajaxGet(params: AjaxGetParams) { + return <Task<string | Uint8Array>>ajaxGetInternal(params.title, params.url, params.type === 'binary', params.compression === DataCompressionMethod.Gzip); +} + +function decompress(buffer: Uint8Array): Uint8Array { + // TODO + throw 'nyi'; + // const gzip = new LiteMolZlib.Gunzip(new Uint8Array(buffer)); + // return gzip.decompress(); +} + +async function processFile(ctx: RuntimeContext, asUint8Array: boolean, compressed: boolean, e: any) { + const data = (e.target as FileReader).result; + + if (compressed) { + await ctx.update('Decompressing...'); + + const decompressed = decompress(new Uint8Array(data as ArrayBuffer)); + if (asUint8Array) { + return decompressed; + } else { + return utf8Read(decompressed, 0, decompressed.length); + } + } else { + return asUint8Array ? new Uint8Array(data as ArrayBuffer) : data as string; + } +} + +function readData(ctx: RuntimeContext, action: string, data: XMLHttpRequest | FileReader, asUint8Array: boolean): Promise<any> { + return new Promise<any>((resolve, reject) => { + data.onerror = (e: any) => { + const error = (<FileReader>e.target).error; + reject(error ? error : 'Failed.'); + }; + + data.onabort = () => reject(Task.Aborted('')); + + data.onprogress = (e: ProgressEvent) => { + if (e.lengthComputable) { + ctx.update({ message: action, isIndeterminate: false, current: e.loaded, max: e.total }); + } else { + ctx.update({ message: `${action} ${(e.loaded / 1024 / 1024).toFixed(2)} MB`, isIndeterminate: true }); + } + } + data.onload = (e: any) => resolve(e); + }); +} + +function readFromFileInternal(file: File, asUint8Array: boolean): Task<string | Uint8Array> { + let reader: FileReader | undefined = void 0; + return Task.create('Read File', async ctx => { + try { + reader = new FileReader(); + const isCompressed = /\.gz$/i.test(file.name); + + if (isCompressed || asUint8Array) reader.readAsArrayBuffer(file); + else reader.readAsBinaryString(file); + + ctx.update({ message: 'Opening file...', canAbort: true }); + const e = await readData(ctx, 'Reading...', reader, asUint8Array); + const result = processFile(ctx, asUint8Array, isCompressed, e); + return result; + } finally { + reader = void 0; + } + }, () => { + if (reader) reader.abort(); + }); +} + +class RequestPool { + private static pool: XMLHttpRequest[] = []; + private static poolSize = 15; + + static get() { + if (this.pool.length) { + return this.pool.pop()!; + } + return new XMLHttpRequest(); + } + + static emptyFunc() { } + + static deposit(req: XMLHttpRequest) { + if (this.pool.length < this.poolSize) { + req.onabort = RequestPool.emptyFunc; + req.onerror = RequestPool.emptyFunc; + req.onload = RequestPool.emptyFunc; + req.onprogress = RequestPool.emptyFunc; + this.pool.push(req); + } + } +} + +async function processAjax(ctx: RuntimeContext, asUint8Array: boolean, decompressGzip: boolean, e: any) { + const req = (e.target as XMLHttpRequest); + if (req.status >= 200 && req.status < 400) { + if (asUint8Array) { + const buff = new Uint8Array(e.target.response); + RequestPool.deposit(e.target); + + if (decompressGzip) { + return decompress(buff); + } else { + return buff; + } + } + else { + const text = e.target.responseText; + RequestPool.deposit(e.target); + return text; + } + } else { + const status = req.statusText; + RequestPool.deposit(e.target); + throw status; + } +} + +function ajaxGetInternal(title: string | undefined, url: string, asUint8Array: boolean, decompressGzip: boolean): Task<string | Uint8Array> { + let xhttp: XMLHttpRequest | undefined = void 0; + return Task.create(title ? title : 'Download', async ctx => { + try { + if (!asUint8Array && decompressGzip) { + throw 'Decompress is only available when downloading binary data.'; + } + + xhttp = RequestPool.get(); + + xhttp.open('get', url, true); + xhttp.responseType = asUint8Array ? 'arraybuffer' : 'text'; + xhttp.send(); + + ctx.update({ message: 'Waiting for server...', canAbort: true }); + const e = await readData(ctx, 'Downloading...', xhttp, asUint8Array); + const result = await processAjax(ctx, asUint8Array, decompressGzip, e) + return result; + } finally { + xhttp = void 0; + } + }, () => { + if (xhttp) xhttp.abort(); + }); +} \ No newline at end of file diff --git a/src/mol-util/param-definition.ts b/src/mol-util/param-definition.ts index e508a72c7e8aaffd8d4cc988ceb7fd50b66a9a71..b618052cfe18649afff4fca1d3624f9195b2654d 100644 --- a/src/mol-util/param-definition.ts +++ b/src/mol-util/param-definition.ts @@ -14,23 +14,27 @@ export namespace ParamDefinition { export interface Info { label?: string, description?: string, - isOptional?: boolean, - isHidden?: boolean + isHidden?: boolean, } function setInfo<T extends Info>(param: T, info?: Info): T { if (!info) return param; if (info.description) param.description = info.description; if (info.label) param.label = info.label; - if (info.isOptional) param.isOptional = info.isOptional; if (info.isHidden) param.isHidden = info.isHidden; return param; } export interface Base<T> extends Info { + isOptional?: boolean, defaultValue: T } + export function makeOptional<T>(p: Base<T>): Base<T | undefined> { + p.isOptional = true; + return p; + } + export interface Value<T> extends Base<T> { type: 'value' } @@ -63,11 +67,11 @@ export namespace ParamDefinition { return setInfo<Boolean>({ type: 'boolean', defaultValue }, info) } - export interface Text extends Base<string> { + export interface Text<T extends string = string> extends Base<T> { type: 'text' } - export function Text(defaultValue: string = '', info?: Info): Text { - return setInfo<Text>({ type: 'text', defaultValue }, info) + export function Text<T extends string = string>(defaultValue: string = '', info?: Info): Text<T> { + return setInfo<Text<T>>({ type: 'text', defaultValue: defaultValue as any }, info) } export interface Color extends Base<ColorData> { @@ -84,6 +88,16 @@ export namespace ParamDefinition { return setInfo<Vec3>({ type: 'vec3', defaultValue }, info) } + export interface FileParam extends Base<File> { + type: 'file', + accept?: string + } + export function File(info?: Info & { accept?: string }): FileParam { + const ret = setInfo<FileParam>({ type: 'file', defaultValue: void 0 as any }, info); + if (info && info.accept) ret.accept = info.accept; + return ret; + } + export interface Range { /** If given treat as a range. */ min?: number @@ -126,24 +140,42 @@ export namespace ParamDefinition { export interface Group<T> extends Base<T> { type: 'group', - params: Params + params: Params, + isExpanded?: boolean, + isFlat?: boolean } - export function Group<P extends Params>(params: P, info?: Info): Group<Values<P>> { - return setInfo<Group<Values<P>>>({ type: 'group', defaultValue: getDefaultValues(params) as any, params }, info); + export function Group<P extends Params>(params: P, info?: Info & { isExpanded?: boolean, isFlat?: boolean }): Group<Values<P>> { + const ret = setInfo<Group<Values<P>>>({ type: 'group', defaultValue: getDefaultValues(params) as any, params }, info); + if (info && info.isExpanded) ret.isExpanded = info.isExpanded; + if (info && info.isFlat) ret.isFlat = info.isFlat; + return ret; } - export interface NamedParams<T = any> { name: string, params: T } - export interface Mapped<T> extends Base<NamedParams<T>> { + export interface NamedParams<T = any, K = string> { name: K, params: T } + export type NamedParamUnion<P extends Params, K = keyof P> = K extends any ? NamedParams<P[K]['defaultValue'], K> : never + export interface Mapped<T extends NamedParams<any, any>> extends Base<T> { type: 'mapped', select: Select<string>, map(name: string): Any } - export function Mapped<T>(defaultKey: string, names: [string, string][], map: Mapped<T>['map'], info?: Info): Mapped<T> { - return setInfo<Mapped<T>>({ + export function Mapped<T>(defaultKey: string, names: [string, string][], map: (name: string) => Any, info?: Info): Mapped<NamedParams<T>> { + return setInfo<Mapped<NamedParams<T>>>({ type: 'mapped', defaultValue: { name: defaultKey, params: map(defaultKey).defaultValue as any }, select: Select<string>(defaultKey, names, info), - map }, info); + map + }, info); + } + export function MappedStatic<C extends Params>(defaultKey: keyof C, map: C, info?: Info & { options?: [keyof C, string][] }): Mapped<NamedParamUnion<C>> { + const options: [string, string][] = info && info.options + ? info.options as [string, string][] + : Object.keys(map).map(k => [k, k]) as [string, string][]; + return setInfo<Mapped<NamedParamUnion<C>>>({ + type: 'mapped', + defaultValue: { name: defaultKey, params: map[defaultKey].defaultValue } as any, + select: Select<string>(defaultKey as string, options, info), + map: key => map[key] + }, info); } export interface Converted<T, C> extends Base<T> { @@ -158,11 +190,16 @@ export namespace ParamDefinition { return { type: 'converted', defaultValue: toValue(converted.defaultValue), converted, fromValue, toValue }; } - export type Any = Value<any> | Select<any> | MultiSelect<any> | Boolean | Text | Color | Vec3 | Numeric | Interval | LineGraph | Group<any> | Mapped<any> | Converted<any, any> + export type Any = Value<any> | Select<any> | MultiSelect<any> | Boolean | Text | Color | Vec3 | Numeric | FileParam | Interval | LineGraph | Group<any> | Mapped<any> | Converted<any, any> export type Params = { [k: string]: Any } export type Values<T extends Params> = { [k in keyof T]: T[k]['defaultValue'] } + type Optionals<P> = { [K in keyof P]-?: undefined extends P[K] ? K : never }[keyof P] + type NonOptionals<P> = { [K in keyof P]-?: undefined extends P[K] ? never: K }[keyof P] + export type Normalize<P> = Pick<P, NonOptionals<P>> & Partial<Pick<P, Optionals<P>>> + export type For<P> = { [K in keyof P]-?: Base<P[K]> } + export function getDefaultValues<T extends Params>(params: T) { const d: { [k: string]: any } = {} for (const k of Object.keys(params)) { @@ -213,6 +250,23 @@ export namespace ParamDefinition { if (u.name !== v.name) return false; const map = p.map(u.name); return isParamEqual(map, u.params, v.params); + } else if (p.type === 'multi-select') { + const u = a as MultiSelect<any>['defaultValue'], v = b as MultiSelect<any>['defaultValue']; + if (u.length !== v.length) return false; + if (u.length < 10) { + for (let i = 0, _i = u.length; i < _i; i++) { + if (u[i] === v[i]) continue; + if (v.indexOf(u[i]) < 0) return false; + } + } else { + // TODO: should the value of multiselect be a set? + const vSet = new Set(v); + for (let i = 0, _i = u.length; i < _i; i++) { + if (u[i] === v[i]) continue; + if (!vSet.has(u[i])) return false; + } + } + return true; } else if (p.type === 'interval') { return a[0] === b[0] && a[1] === b[1]; } else if (p.type === 'line-graph') {