diff --git a/package-lock.json b/package-lock.json index 8f86e5adeac89d096105360c76b4de9fcdd01ed1..154ac30778fb368fba7ab849675bb000faeff7ca 100644 Binary files a/package-lock.json and b/package-lock.json differ diff --git a/src/examples/ply-wrapper/annotation.ts b/src/examples/ply-wrapper/annotation.ts new file mode 100644 index 0000000000000000000000000000000000000000..66bf2ea7c0ceaec0d6eabfd6268bc4364df71713 --- /dev/null +++ b/src/examples/ply-wrapper/annotation.ts @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { CustomElementProperty } from 'mol-model-props/common/custom-element-property'; +import { Model, ElementIndex, ResidueIndex } from 'mol-model/structure'; +import { Color } from 'mol-util/color'; + +const EvolutionaryConservationPalette: Color[] = [ + [255, 255, 129], // insufficient + [160, 37, 96], // 9 + [240, 125, 171], + [250, 201, 222], + [252, 237, 244], + [255, 255, 255], + [234, 255, 255], + [215, 255, 255], + [140, 255, 255], + [16, 200, 209] // 1 +].reverse().map(([r, g, b]) => Color.fromRgb(r, g, b)); +const EvolutionaryConservationDefaultColor = Color(0x999999); + +export const EvolutionaryConservation = CustomElementProperty.create<number>({ + isStatic: true, + name: 'proteopedia-wrapper-evolutionary-conservation', + display: 'Evolutionary Conservation', + async getData(model: Model) { + const id = model.label.toLowerCase(); + const req = await fetch(`https://proteopedia.org/cgi-bin/cnsrf?${id}`); + const json = await req.json(); + const annotations = (json && json.residueAnnotations) || []; + + const conservationMap = new Map<string, number>(); + + for (const e of annotations) { + for (const r of e.ids) { + conservationMap.set(r, e.annotation); + } + } + + const map = new Map<ElementIndex, number>(); + + const { _rowCount: residueCount } = model.atomicHierarchy.residues; + const { offsets: residueOffsets } = model.atomicHierarchy.residueAtomSegments; + const chainIndex = model.atomicHierarchy.chainAtomSegments.index; + + for (let rI = 0 as ResidueIndex; rI < residueCount; rI++) { + const cI = chainIndex[residueOffsets[rI]]; + const key = `${model.atomicHierarchy.chains.auth_asym_id.value(cI)} ${model.atomicHierarchy.residues.auth_seq_id.value(rI)}`; + if (!conservationMap.has(key)) continue; + const ann = conservationMap.get(key)!; + for (let aI = residueOffsets[rI]; aI < residueOffsets[rI + 1]; aI++) { + map.set(aI, ann); + } + } + + return map; + }, + coloring: { + getColor(e: number) { + if (e < 1 || e > 10) return EvolutionaryConservationDefaultColor; + return EvolutionaryConservationPalette[e - 1]; + }, + defaultColor: EvolutionaryConservationDefaultColor + }, + format(e) { + if (e === 10) return `Evolutionary Conservation: InsufficientData`; + return e ? `Evolutionary Conservation: ${e}` : void 0; + } +}); \ No newline at end of file diff --git a/src/examples/ply-wrapper/changelog.md b/src/examples/ply-wrapper/changelog.md new file mode 100644 index 0000000000000000000000000000000000000000..041ecacd3454949a8be9e03ad2a6284c5b9f8381 --- /dev/null +++ b/src/examples/ply-wrapper/changelog.md @@ -0,0 +1,7 @@ +== v2.0 == + +* Changed how state saving works. + +== v1.0 == + +* Initial version. \ No newline at end of file diff --git a/src/examples/ply-wrapper/helpers.ts b/src/examples/ply-wrapper/helpers.ts new file mode 100644 index 0000000000000000000000000000000000000000..e9406e38a0fea12efca0eb9f194ca6807a9b6414 --- /dev/null +++ b/src/examples/ply-wrapper/helpers.ts @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { ResidueIndex, Model } from 'mol-model/structure'; +import { BuiltInStructureRepresentationsName } from 'mol-repr/structure/registry'; +import { BuiltInColorThemeName } from 'mol-theme/color'; +import { AminoAcidNames } from 'mol-model/structure/model/types'; +import { PluginContext } from 'mol-plugin/context'; + +export interface ModelInfo { + hetResidues: { name: string, indices: ResidueIndex[] }[], + assemblies: { id: string, details: string, isPreferred: boolean }[], + preferredAssemblyId: string | undefined +} + +export namespace ModelInfo { + async function getPreferredAssembly(ctx: PluginContext, model: Model) { + if (model.label.length <= 3) return void 0; + try { + const id = model.label.toLowerCase(); + const src = await ctx.runTask(ctx.fetch({ url: `https://www.ebi.ac.uk/pdbe/api/pdb/entry/summary/${id}` })) as string; + const json = JSON.parse(src); + const data = json && json[id]; + + const assemblies = data[0] && data[0].assemblies; + if (!assemblies || !assemblies.length) return void 0; + + for (const asm of assemblies) { + if (asm.preferred) { + return asm.assembly_id; + } + } + return void 0; + } catch (e) { + console.warn('getPreferredAssembly', e); + } + } + + export async function get(ctx: PluginContext, model: Model, checkPreferred: boolean): Promise<ModelInfo> { + const { _rowCount: residueCount } = model.atomicHierarchy.residues; + const { offsets: residueOffsets } = model.atomicHierarchy.residueAtomSegments; + const chainIndex = model.atomicHierarchy.chainAtomSegments.index; + // const resn = SP.residue.label_comp_id, entType = SP.entity.type; + + const pref = checkPreferred + ? getPreferredAssembly(ctx, model) + : void 0; + + const hetResidues: ModelInfo['hetResidues'] = []; + const hetMap = new Map<string, ModelInfo['hetResidues'][0]>(); + + for (let rI = 0 as ResidueIndex; rI < residueCount; rI++) { + const comp_id = model.atomicHierarchy.residues.label_comp_id.value(rI); + if (AminoAcidNames.has(comp_id)) continue; + const mod_parent = model.properties.modifiedResidues.parentId.get(comp_id); + if (mod_parent && AminoAcidNames.has(mod_parent)) continue; + + const cI = chainIndex[residueOffsets[rI]]; + const eI = model.atomicHierarchy.index.getEntityFromChain(cI); + if (model.entities.data.type.value(eI) === 'water') continue; + + let lig = hetMap.get(comp_id); + if (!lig) { + lig = { name: comp_id, indices: [] }; + hetResidues.push(lig); + hetMap.set(comp_id, lig); + } + lig.indices.push(rI); + } + + const preferredAssemblyId = await pref; + + return { + hetResidues: hetResidues, + assemblies: model.symmetry.assemblies.map(a => ({ id: a.id, details: a.details, isPreferred: a.id === preferredAssemblyId })), + preferredAssemblyId + }; + } +} + +export type SupportedFormats = 'cif' | 'pdb' +export interface LoadParams { + plyurl: string, + url: string, + format?: SupportedFormats, + assemblyId?: string, + representationStyle?: RepresentationStyle +} + +export interface RepresentationStyle { + sequence?: RepresentationStyle.Entry, + hetGroups?: RepresentationStyle.Entry, + water?: RepresentationStyle.Entry +} + +export namespace RepresentationStyle { + export type Entry = { kind?: BuiltInStructureRepresentationsName, coloring?: BuiltInColorThemeName } +} \ No newline at end of file diff --git a/src/examples/ply-wrapper/index.html b/src/examples/ply-wrapper/index.html new file mode 100644 index 0000000000000000000000000000000000000000..f59e5745f2dabf5a47cbcc506ceaacd736617b51 --- /dev/null +++ b/src/examples/ply-wrapper/index.html @@ -0,0 +1,218 @@ +<!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* PLY Wrapper</title> + <style> + * { + margin: 0; + padding: 0; + box-sizing: border-box; + } + #app { + position: absolute; + left: 160px; + top: 100px; + width: 600px; + height: 400px; + border: 1px solid #ccc; + } + #select { + position: absolute; + left: 10px; + top: 480px; + } + #diagram { + position: absolute; + left: 10px; + top: 520px; + width: 1210px; + height: 510px; + border: 1px solid #ccc; + } + + #controls { + position: absolute; + width: 130px; + top: 10px; + left: 10px; + } + + #controls > button { + display: block; + width: 100%; + text-align: left; + } + + #controls > hr { + margin: 5px 0; + } + + #controls > input, #controls > select { + width: 100%; + display: block; + } + </style> + <link rel="stylesheet" type="text/css" href="app.css" /> + <script type="text/javascript" src="./index.js"></script> + <link rel="stylesheet" type="text/css" href="/FProject5.3/style.css"> + <!--<link rel="stylesheet" type="text/css" href="/FProject5.3/dist/slimselect.css" />--> + </head> + <body> + <script>var aminoAcid = 68</script> + <div id='controls'> + <h3>Source</h3> + <input type='text' id='plyurl' placeholder='plyurl' style='width: 400px' /> + <input type='text' id='url' placeholder='url' style='width: 400px' /> + <input type='text' id='assemblyId' placeholder='assembly id' /> + <select id='format'> + <option value='cif' selected>CIF</option> + <option value='pdb'>PDB</option> + </select> + </div> + <div id="app"></div> + <script> + // create an instance of the plugin + var PluginWrapper = new MolStarPLYWrapper(); + + console.log('Wrapper version', MolStarPLYWrapper.VERSION_MAJOR); + + function $(id) { return document.getElementById(id); } + + var pdbId = '1tca', assemblyId= 'preferred'; + var url = '/test-data/' + pdbId + '_updated.cif'; + var format = 'cif'; + + var plyName = 'run_0_mesh'; + var plyurl = '/test-data/' + plyName + '.ply'; + + $('plyurl').value = plyurl; + $('plyurl').onchange = function (e) { url = e.target.value; } + $('url').value = url; + $('url').onchange = function (e) { url = e.target.value; } + $('assemblyId').value = assemblyId; + $('assemblyId').onchange = function (e) { assemblyId = e.target.value; } + $('format').value = format; + $('format').onchange = function (e) { format = e.target.value; } + + // var url = 'https://www.ebi.ac.uk/pdbe/entry-files/pdb' + pdbId + '.ent'; + // var format = 'pdb'; + // var assemblyId = 'deposited'; + + PluginWrapper.init('app' /** or document.getElementById('app') */); + PluginWrapper.setBackground(0xffffff); + PluginWrapper.load({ plyurl: plyurl, url: url, format: format, assemblyId: assemblyId }); + PluginWrapper.toggleSpin(); + + PluginWrapper.events.modelInfo.subscribe(function (info) { + console.log('Model Info', info); + }); + + + + addControl('Load Asym Unit', () => PluginWrapper.load({ plyurl: plyurl, url: url, format: format })); + addControl('Load Assembly', () => PluginWrapper.load({ plyurl: plyurl, url: url, format: format, assemblyId: assemblyId })); + + addSeparator(); + + addHeader('Camera'); + addControl('Toggle Spin', () => PluginWrapper.toggleSpin()); + + addSeparator(); + + addHeader('Animation'); + + // adjust this number to make the animation faster or slower + // requires to "restart" the animation if changed + PluginWrapper.animate.modelIndex.maxFPS = 30; + + addControl('Play To End', () => PluginWrapper.animate.modelIndex.onceForward()); + addControl('Play To Start', () => PluginWrapper.animate.modelIndex.onceBackward()); + addControl('Play Palindrome', () => PluginWrapper.animate.modelIndex.palindrome()); + addControl('Play Loop', () => PluginWrapper.animate.modelIndex.loop()); + addControl('Stop', () => PluginWrapper.animate.modelIndex.stop()); + + addSeparator(); + addHeader('Misc'); + + addControl('Apply Evo Cons', () => PluginWrapper.coloring.evolutionaryConservation()); + addControl('Default Visuals', () => PluginWrapper.updateStyle()); + + addSeparator(); + addHeader('State'); + + var snapshot; + addControl('Create Snapshot', () => { + snapshot = PluginWrapper.snapshot.get(); + // could use JSON.stringify(snapshot) and upload the data + }); + addControl('Apply Snapshot', () => { + if (!snapshot) return; + PluginWrapper.snapshot.set(snapshot); + + // or download snapshot using fetch or ajax or whatever + // or PluginWrapper.snapshot.download(url); + }); + + //////////////////////////////////////////////////////// + + function addControl(label, action) { + var btn = document.createElement('button'); + btn.onclick = action; + btn.innerText = label; + $('controls').appendChild(btn); + } + + function addSeparator() { + var hr = document.createElement('hr'); + $('controls').appendChild(hr); + } + + function addHeader(header) { + var h = document.createElement('h3'); + h.innerText = header; + $('controls').appendChild(h); + } + PluginWrapper.klick; + </script> + + <!-- --------- FProject start --------- --> + <select id="select" onchange="iniciar()"> + <option value="/FProject5.3/Contact_density/run_0.json">Run 0</option> + <option value="/FProject5.3/Contact_density/run_1.json">Run 1</option> + <option value="/FProject5.3/Contact_density/run_2.json">Run 2</option> + <option value="/FProject5.3/Contact_density/run_3.json">Run 3</option> + <option value="/FProject5.3/Contact_density/run_4.json">Run 4</option> + <option value="/FProject5.3/Contact_density/run_5.json">Run 5</option> + <option value="/FProject5.3/Contact_density/run_6.json">Run 6</option> + <option value="/FProject5.3/Contact_density/run_7.json">Run 7</option> + <option value="/FProject5.3/Contact_density/run_8.json">Run 8</option> + <option value="/FProject5.3/Contact_density/run_9.json">Run 9</option> + <option value="/FProject5.3/Contact_density/run_total.json">Run total</option> + </select> + <div id="diagram"> + </div> + <!-- + <script> + setTimeout(function() { + new SlimSelect({ + select: '#select' + }) + }, 300) + </script> + <script src="/FProject5.3/dist/slimselect.min.js"></script> + --> + + <!-- load the d3.js library --> + <script src="/FProject5.3/d3.v4.min.js"></script> + <script src="/FProject5.3/jquery-3.3.1.min.js"></script> + <script src="https://d3js.org/d3-path.v1.min.js"></script> + <script src="https://d3js.org/d3-shape.v1.min.js"></script> + + <script src="/FProject5.3/scriptv2.js"> + </script> + <!-- --------- FProject end --------- --> + + </body> +</html> \ No newline at end of file diff --git a/src/examples/ply-wrapper/index.ts b/src/examples/ply-wrapper/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..955ff98e6e9b45d9dd41468961aa41eadba691b5 --- /dev/null +++ b/src/examples/ply-wrapper/index.ts @@ -0,0 +1,248 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { createPlugin, DefaultPluginSpec } from 'mol-plugin'; +import './index.html' +import { PluginContext } from 'mol-plugin/context'; +import { PluginCommands } from 'mol-plugin/command'; +import { StateTransforms } from 'mol-plugin/state/transforms'; +import { StructureRepresentation3DHelpers } from 'mol-plugin/state/transforms/representation'; +import { Color } from 'mol-util/color'; +import { PluginStateObject as PSO, PluginStateObject } from 'mol-plugin/state/objects'; +import { AnimateModelIndex } from 'mol-plugin/state/animation/built-in'; +import {StateBuilder, StateObject} from 'mol-state'; +import { EvolutionaryConservation } from './annotation'; +import { LoadParams, SupportedFormats, RepresentationStyle, ModelInfo } from './helpers'; +import { RxEventHelper } from 'mol-util/rx-event-helper'; +import { ControlsWrapper } from './ui/controls'; +import { PluginState } from 'mol-plugin/state'; +import { Canvas3D } from 'mol-canvas3d/canvas3d'; +require('mol-plugin/skin/light.scss') + + + +class MolStarPLYWrapper { + static VERSION_MAJOR = 2; + static VERSION_MINOR = 0; + + private _ev = RxEventHelper.create(); + + readonly events = { + modelInfo: this._ev<ModelInfo>() + }; + + + plugin: PluginContext; + + init(target: string | HTMLElement) { + this.plugin = createPlugin(typeof target === 'string' ? document.getElementById(target)! : target, { + ...DefaultPluginSpec, + layout: { + initial: { + isExpanded: false, + showControls: false + }, + controls: { + right: ControlsWrapper + } + } + }); + + this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.add(EvolutionaryConservation.Descriptor.name, EvolutionaryConservation.colorTheme!); + this.plugin.lociLabels.addProvider(EvolutionaryConservation.labelProvider); + this.plugin.customModelProperties.register(EvolutionaryConservation.propertyProvider); + } + + get state() { + return this.plugin.state.dataState; + } + + get klick(){ + this.plugin.canvas3d.interaction.click.subscribe(e =>{ + console.log('atomID', e) + aminoAcid = 169; + }) + return 0 + } + + + private download(b: StateBuilder.To<PSO.Root>, url: string) { + return b.apply(StateTransforms.Data.Download, { url, isBinary: false }) + } + + private model(b: StateBuilder.To<PSO.Data.Binary | PSO.Data.String>, format: SupportedFormats, assemblyId: string) { + const parsed = format === 'cif' + ? b.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif) + : b.apply(StateTransforms.Model.TrajectoryFromPDB); + + return parsed + .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 }, { ref: 'model' }); + } + + private plyData(b: StateBuilder.To<PSO.Data.String>) { + return b.apply(StateTransforms.Data.ParsePly) + .apply(StateTransforms.Model.ShapeFromPly) + .apply(StateTransforms.Representation.ShapeRepresentation3D); + } + + private structure(assemblyId: string) { + const model = this.state.build().to('model'); + + return model + .apply(StateTransforms.Model.CustomModelProperties, { properties: [EvolutionaryConservation.Descriptor.name] }, { ref: 'props', props: { isGhost: false } }) + .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: 'asm' }); + } + + private visual(ref: string, style?: RepresentationStyle) { + const structure = this.getObj<PluginStateObject.Molecule.Structure>(ref); + if (!structure) return; + + const root = this.state.build().to(ref); + + root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' }, { ref: 'sequence' }) + .apply(StateTransforms.Representation.StructureRepresentation3D, + StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin, + (style && style.sequence && style.sequence.kind) || 'cartoon', + (style && style.sequence && style.sequence.coloring) || 'unit-index', structure), + { ref: 'sequence-visual' }); + root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' }, { ref: 'het' }) + .apply(StateTransforms.Representation.StructureRepresentation3D, + StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin, + (style && style.hetGroups && style.hetGroups.kind) || 'ball-and-stick', + (style && style.hetGroups && style.hetGroups.coloring), structure), + { ref: 'het-visual' }); + root.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' }, { ref: 'water' }) + .apply(StateTransforms.Representation.StructureRepresentation3D, + StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin, + (style && style.water && style.water.kind) || 'ball-and-stick', + (style && style.water && style.water.coloring), structure, { alpha: 0.51 }), + { ref: 'water-visual' }); + + return root; + } + + private getObj<T extends StateObject>(ref: string): T['data'] { + const state = this.state; + const cell = state.select(ref)[0]; + if (!cell || !cell.obj) return void 0; + return (cell.obj as T).data; + } + + private async doInfo(checkPreferredAssembly: boolean) { + const model = this.getObj<PluginStateObject.Molecule.Model>('model'); + if (!model) return; + + const info = await ModelInfo.get(this.plugin, model, checkPreferredAssembly) + this.events.modelInfo.next(info); + return info; + } + + private applyState(tree: StateBuilder) { + return PluginCommands.State.Update.dispatch(this.plugin, { state: this.plugin.state.dataState, tree }); + } + + private loadedParams: LoadParams = { plyurl: '', url: '', format: 'cif', assemblyId: '' }; + async load({ plyurl, url, format = 'cif', assemblyId = '', representationStyle }: LoadParams) { + let loadType: 'full' | 'update' = 'full'; + + const state = this.plugin.state.dataState; + + if (this.loadedParams.plyurl !== plyurl || this.loadedParams.url !== url || this.loadedParams.format !== format) { + loadType = 'full'; + } else if (this.loadedParams.url === url) { + if (state.select('asm').length > 0) loadType = 'update'; + } + + if (loadType === 'full') { + await PluginCommands.State.RemoveObject.dispatch(this.plugin, { state, ref: state.tree.root.ref }); + // pdb/cif loading + const modelTree = this.model(this.download(state.build().toRoot(), url), format, assemblyId); + await this.applyState(modelTree); + const info = await this.doInfo(true); + const structureTree = this.structure((assemblyId === 'preferred' && info && info.preferredAssemblyId) || assemblyId); + await this.applyState(structureTree); + // ply loading + const modelTreePly = this.plyData(this.download(state.build().toRoot(), plyurl)); + await this.applyState(modelTreePly); + } else { + const tree = state.build(); + tree.to('asm').update(StateTransforms.Model.StructureAssemblyFromModel, p => ({ ...p, id: assemblyId || 'deposited' })); + await this.applyState(tree); + } + + await this.updateStyle(representationStyle); + + this.loadedParams = { plyurl, url, format, assemblyId }; + PluginCommands.Camera.Reset.dispatch(this.plugin, { }); + } + + async updateStyle(style?: RepresentationStyle) { + const tree = this.visual('asm', style); + if (!tree) return; + await PluginCommands.State.Update.dispatch(this.plugin, { state: this.plugin.state.dataState, tree }); + } + + setBackground(color: number) { + PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { backgroundColor: Color(color) } }); + } + + toggleSpin() { + const trackball = this.plugin.canvas3d.props.trackball; + const spinning = trackball.spin; + PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { trackball: { ...trackball, spin: !trackball.spin } } }); + if (!spinning) PluginCommands.Camera.Reset.dispatch(this.plugin, { }); + } + + animate = { + modelIndex: { + maxFPS: 8, + onceForward: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'once', params: { direction: 'forward' } } }) }, + onceBackward: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'once', params: { direction: 'backward' } } }) }, + palindrome: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'palindrome', params: {} } }) }, + loop: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'loop', params: {} } }) }, + stop: () => this.plugin.state.animation.stop() + } + } + + coloring = { + evolutionaryConservation: async () => { + await this.updateStyle({ sequence: { kind: 'spacefill' } }); + + const state = this.state; + + // const visuals = state.selectQ(q => q.ofType(PluginStateObject.Molecule.Structure.Representation3D).filter(c => c.transform.transformer === StateTransforms.Representation.StructureRepresentation3D)); + const tree = state.build(); + const colorTheme = { name: EvolutionaryConservation.Descriptor.name, params: this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.get(EvolutionaryConservation.Descriptor.name).defaultValues }; + + tree.to('sequence-visual').update(StateTransforms.Representation.StructureRepresentation3D, old => ({ ...old, colorTheme })); + // for (const v of visuals) { + // } + + await PluginCommands.State.Update.dispatch(this.plugin, { state, tree }); + } + } + + snapshot = { + get: () => { + return this.plugin.state.getSnapshot(); + }, + set: (snapshot: PluginState.Snapshot) => { + return this.plugin.state.setSnapshot(snapshot); + }, + download: async (url: string) => { + try { + const data = await this.plugin.runTask(this.plugin.fetch({ url })); + const snapshot = JSON.parse(data); + await this.plugin.state.setSnapshot(snapshot); + } catch (e) { + console.log(e); + } + } + + } +} + +(window as any).MolStarPLYWrapper = MolStarPLYWrapper; diff --git a/src/examples/ply-wrapper/ui/controls.tsx b/src/examples/ply-wrapper/ui/controls.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d2a79e61b59b5df991b610381e77c7d9e4fe8ecc --- /dev/null +++ b/src/examples/ply-wrapper/ui/controls.tsx @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import * as React from 'react'; +import { PluginUIComponent } from 'mol-plugin/ui/base'; +import { CurrentObject } from 'mol-plugin/ui/plugin'; +import { AnimationControls } from 'mol-plugin/ui/state/animation'; +import { CameraSnapshots } from 'mol-plugin/ui/camera'; + +export class ControlsWrapper extends PluginUIComponent { + render() { + return <div className='msp-scrollable-container msp-right-controls'> + <CurrentObject /> + <AnimationControls /> + <CameraSnapshots /> + </div>; + } +} \ No newline at end of file diff --git a/src/mol-geo/geometry/mesh/mesh-builder.ts b/src/mol-geo/geometry/mesh/mesh-builder.ts index 2bcaf085159f4597b6ee0c0d915b754c6aeac46c..cfe57234db1d2b4fd8ce2648158cf4d4369e5656 100644 --- a/src/mol-geo/geometry/mesh/mesh-builder.ts +++ b/src/mol-geo/geometry/mesh/mesh-builder.ts @@ -45,7 +45,7 @@ export namespace MeshBuilder { export function addTriangle(state: State, a: Vec3, b: Vec3, c: Vec3) { const { vertices, normals, indices, groups, currentGroup } = state const offset = vertices.elementCount - + // positions ChunkedArray.add3(vertices, a[0], a[1], a[2]); ChunkedArray.add3(vertices, b[0], b[1], b[2]); diff --git a/src/mol-io/common/ascii.ts b/src/mol-io/common/ascii.ts new file mode 100644 index 0000000000000000000000000000000000000000..4a9b2edfa234bee7d2014b574dc8eb744c6e84c6 --- /dev/null +++ b/src/mol-io/common/ascii.ts @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * Adapted from https://github.com/rcsb/mmtf-javascript + * @author Alexander Rose <alexander.rose@weirdbyte.de> + * @author David Sehnal <david.sehnal@gmail.com> + */ +// NOT IN USE ELSEWEHERE !!!!! +export function asciiWrite(data: Uint8Array, offset: number, str: string) { + for (let i = 0, l = str.length; i < l; i++) { + let codePoint = str.charCodeAt(i); + + // One byte of UTF-8 + if (codePoint < 0x80) { + data[offset++] = codePoint >>> 0 & 0x7f | 0x00; + continue; + } + + // Two bytes of UTF-8 + if (codePoint < 0x800) { + data[offset++] = codePoint >>> 6 & 0x1f | 0xc0; + data[offset++] = codePoint >>> 0 & 0x3f | 0x80; + continue; + } + + // Three bytes of UTF-8. + if (codePoint < 0x10000) { + data[offset++] = codePoint >>> 12 & 0x0f | 0xe0; + data[offset++] = codePoint >>> 6 & 0x3f | 0x80; + data[offset++] = codePoint >>> 0 & 0x3f | 0x80; + continue; + } + + // Four bytes of UTF-8 + if (codePoint < 0x110000) { + data[offset++] = codePoint >>> 18 & 0x07 | 0xf0; + data[offset++] = codePoint >>> 12 & 0x3f | 0x80; + data[offset++] = codePoint >>> 6 & 0x3f | 0x80; + data[offset++] = codePoint >>> 0 & 0x3f | 0x80; + continue; + } + throw new Error('bad codepoint ' + codePoint); + } +} + +const __chars = function () { + let data: string[] = []; + for (let i = 0; i < 1024; i++) data[i] = String.fromCharCode(i); + return data; +}(); + +function throwError(err: string) { + throw new Error(err); +} + +export function asciiRead(data: number, offset: number, length: number) { + let chars = __chars; + let str: string | undefined = void 0; + + let byte = data; + // One byte character + if ((byte & 0x80) !== 0x00) throwError('Invalid byte ' + byte.toString(16)); + str = chars[byte]; + return str; +} + +export function asciiByteCount(str: string) { + let count = 0; + for (let i = 0, l = str.length; i < l; i++) { + let codePoint = str.charCodeAt(i); + if (codePoint < 0x80) { + count += 1; + continue; + } + if (codePoint < 0x800) { + count += 2; + continue; + } + if (codePoint < 0x10000) { + count += 3; + continue; + } + if (codePoint < 0x110000) { + count += 4; + continue; + } + throwError('bad codepoint ' + codePoint); + } + return count; +} \ No newline at end of file diff --git a/src/mol-io/reader/_spec/mol2.spec.ts b/src/mol-io/reader/_spec/mol2.spec.ts index dc88b16043caf6777ca6571656f2c3cfab6eb38f..04570831700fc0db83cae496f660bbf50ef654ef 100644 --- a/src/mol-io/reader/_spec/mol2.spec.ts +++ b/src/mol-io/reader/_spec/mol2.spec.ts @@ -265,10 +265,10 @@ describe('mol2 reader', () => { expect(molecule.num_subst).toBe(0); expect(molecule.num_feat).toBe(0); expect(molecule.num_sets).toBe(0); - expect(molecule.mol_type).toBe("SMALL") - expect(molecule.charge_type).toBe("GASTEIGER"); - expect(molecule.status_bits).toBe(""); - expect(molecule.mol_comment).toBe(""); + expect(molecule.mol_type).toBe('SMALL') + expect(molecule.charge_type).toBe('GASTEIGER'); + expect(molecule.status_bits).toBe(''); + expect(molecule.mol_comment).toBe(''); // required atom fields expect(atoms.count).toBe(26); @@ -277,7 +277,7 @@ describe('mol2 reader', () => { expect(atoms.x.value(0)).toBeCloseTo(1.7394, 0.001); expect(atoms.y.value(0)).toBeCloseTo(-2.1169, 0.0001); expect(atoms.z.value(0)).toBeCloseTo(-1.0893, 0.0001); - expect(atoms.atom_type.value(0)).toBe("O.3"); + expect(atoms.atom_type.value(0)).toBe('O.3'); // optional atom fields expect(atoms.subst_id.value(0)).toBe(1); @@ -316,10 +316,10 @@ describe('mol2 reader', () => { expect(molecule.num_subst).toBe(0); expect(molecule.num_feat).toBe(0); expect(molecule.num_sets).toBe(0); - expect(molecule.mol_type).toBe("SMALL") - expect(molecule.charge_type).toBe("GASTEIGER"); - expect(molecule.status_bits).toBe(""); - expect(molecule.mol_comment).toBe(""); + expect(molecule.mol_type).toBe('SMALL') + expect(molecule.charge_type).toBe('GASTEIGER'); + expect(molecule.status_bits).toBe(''); + expect(molecule.mol_comment).toBe(''); // required atom fields expect(atoms.count).toBe(26); @@ -328,7 +328,7 @@ describe('mol2 reader', () => { expect(atoms.x.value(0)).toBeCloseTo(1.7394, 0.001); expect(atoms.y.value(0)).toBeCloseTo(-2.1169, 0.0001); expect(atoms.z.value(0)).toBeCloseTo(-1.0893, 0.0001); - expect(atoms.atom_type.value(0)).toBe("O.3"); + expect(atoms.atom_type.value(0)).toBe('O.3'); // optional atom fields expect(atoms.subst_id.value(0)).toBe(1); @@ -367,10 +367,10 @@ describe('mol2 reader', () => { expect(molecule.num_subst).toBe(0); expect(molecule.num_feat).toBe(0); expect(molecule.num_sets).toBe(0); - expect(molecule.mol_type).toBe("SMALL") - expect(molecule.charge_type).toBe("GASTEIGER"); - expect(molecule.status_bits).toBe(""); - expect(molecule.mol_comment).toBe(""); + expect(molecule.mol_type).toBe('SMALL') + expect(molecule.charge_type).toBe('GASTEIGER'); + expect(molecule.status_bits).toBe(''); + expect(molecule.mol_comment).toBe(''); // required atom fields expect(atoms.count).toBe(26); @@ -379,7 +379,7 @@ describe('mol2 reader', () => { expect(atoms.x.value(0)).toBeCloseTo(1.7394, 0.001); expect(atoms.y.value(0)).toBeCloseTo(-2.1169, 0.0001); expect(atoms.z.value(0)).toBeCloseTo(-1.0893, 0.0001); - expect(atoms.atom_type.value(0)).toBe("O.3"); + expect(atoms.atom_type.value(0)).toBe('O.3'); // optional atom fields expect(atoms.subst_id.value(0)).toBe(0); diff --git a/src/mol-io/reader/_spec/ply.spec.ts b/src/mol-io/reader/_spec/ply.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..75325914641932f2495716166566fbdc9d084521 --- /dev/null +++ b/src/mol-io/reader/_spec/ply.spec.ts @@ -0,0 +1,152 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import Ply from '../ply/parser' +import { PlyTable, PlyList } from '../ply/schema'; + +const plyString = `ply +format ascii 1.0 +comment file created by MegaMol +element vertex 6 +property float x +property float y +property float z +property uchar red +property uchar green +property uchar blue +property uchar alpha +property float nx +property float ny +property float nz +property int atomid +property uchar contactcount_r +property uchar contactcount_g +property uchar contactcount_b +property uchar contactsteps_r +property uchar contactsteps_g +property uchar contactsteps_b +property uchar hbonds_r +property uchar hbonds_g +property uchar hbonds_b +property uchar hbondsteps_r +property uchar hbondsteps_g +property uchar hbondsteps_b +property uchar molcount_r +property uchar molcount_g +property uchar molcount_b +property uchar spots_r +property uchar spots_g +property uchar spots_b +property uchar rmsf_r +property uchar rmsf_g +property uchar rmsf_b +element face 2 +property list uchar int vertex_index +end_header +130.901 160.016 163.033 90 159 210 255 -0.382 -0.895 -0.231 181 21 100 150 24 102 151 20 100 150 20 100 150 30 106 154 20 100 150 171 196 212 +131.372 159.778 162.83 90 159 210 255 -0.618 -0.776 -0.129 178 21 100 150 24 102 151 20 100 150 20 100 150 30 106 154 20 100 150 141 177 199 +131.682 159.385 163.089 90 159 210 255 -0.773 -0.579 -0.259 180 21 100 150 24 102 151 20 100 150 20 100 150 30 106 154 20 100 150 172 196 212 +131.233 160.386 162.11 90 159 210 255 -0.708 -0.383 -0.594 178 21 100 150 24 102 151 20 100 150 20 100 150 30 106 154 20 100 150 141 177 199 +130.782 160.539 162.415 90 159 210 255 -0.482 -0.459 -0.746 181 21 100 150 24 102 151 20 100 150 20 100 150 30 106 154 20 100 150 171 196 212 +131.482 160.483 161.621 90 159 210 255 -0.832 -0.431 -0.349 179 21 100 150 24 102 151 20 100 150 20 100 150 30 106 154 20 100 150 171 196 212 +3 0 2 1 +3 3 5 4 +` + +const plyCubeString = `ply +format ascii 1.0 +comment test cube +element vertex 24 +property float32 x +property float32 y +property float32 z +property uint32 material_index +element face 6 +property list uint8 int32 vertex_indices +element material 6 +property uint8 red +property uint8 green +property uint8 blue +end_header +-1 -1 -1 0 +1 -1 -1 0 +1 1 -1 0 +-1 1 -1 0 +1 -1 1 1 +-1 -1 1 1 +-1 1 1 1 +1 1 1 1 +1 1 1 2 +1 1 -1 2 +1 -1 -1 2 +1 -1 1 2 +-1 1 -1 3 +-1 1 1 3 +-1 -1 1 3 +-1 -1 -1 3 +-1 1 1 4 +-1 1 -1 4 +1 1 -1 4 +1 1 1 4 +1 -1 1 5 +1 -1 -1 5 +-1 -1 -1 5 +-1 -1 1 5 +4 0 1 2 3 +4 4 5 6 7 +4 8 9 10 11 +4 12 13 14 15 +4 16 17 18 19 +4 20 21 22 23 +255 0 0 +0 255 0 +0 0 255 +255 255 0 +0 255 255 +255 0 255 +` + + +describe('ply reader', () => { + it('basic', async () => { + const parsed = await Ply(plyString).run(); + if (parsed.isError) return; + const plyFile = parsed.result; + + const vertex = plyFile.getElement('vertex') as PlyTable + if (!vertex) return + const x = vertex.getProperty('x') + if (!x) return + expect(x.value(0)).toEqual(130.901) + + const face = plyFile.getElement('face') as PlyList + if (!face) return + expect(face.value(0)).toEqual({ count: 3, entries: [0, 2, 1]}) + expect(face.value(1)).toEqual({ count: 3, entries: [3, 5, 4]}) + + expect.assertions(3) + }); + + it('material', async () => { + const parsed = await Ply(plyCubeString).run(); + if (parsed.isError) return; + const plyFile = parsed.result; + + const vertex = plyFile.getElement('vertex') as PlyTable + if (!vertex) return + expect(vertex.rowCount).toBe(24) + + const face = plyFile.getElement('face') as PlyList + if (!face) return + expect(face.rowCount).toBe(6) + + const material = plyFile.getElement('face') as PlyTable + if (!material) return + expect(face.rowCount).toBe(6) + + expect.assertions(3) + }); +}); \ No newline at end of file diff --git a/src/mol-io/reader/cif/data-model.ts b/src/mol-io/reader/cif/data-model.ts index 2800437dc930bd57af8dafe14913f9d5e15fc105..c5778c7b55a847616c5c36ff57dcf300f9af1926 100644 --- a/src/mol-io/reader/cif/data-model.ts +++ b/src/mol-io/reader/cif/data-model.ts @@ -199,7 +199,7 @@ export namespace CifField { export function ofColumn(column: Column<any>): CifField { const { rowCount, valueKind, areValuesEqual } = column; - + let str: CifField['str'] let int: CifField['int'] let float: CifField['float'] @@ -219,7 +219,6 @@ export namespace CifField { default: throw new Error('unsupported') } - return { __array: void 0, diff --git a/src/mol-io/reader/csv/data-model.ts b/src/mol-io/reader/csv/data-model.ts index 401c7aa2d5855cd530131ac22345bc0052823f9f..7c538467e25bf5eb2143ba569af1af41fe02e3e1 100644 --- a/src/mol-io/reader/csv/data-model.ts +++ b/src/mol-io/reader/csv/data-model.ts @@ -9,12 +9,11 @@ import { CifField as CsvColumn } from '../cif/data-model' export { CsvColumn } export interface CsvFile { - readonly name?: string, readonly table: CsvTable } -export function CsvFile(table: CsvTable, name?: string): CsvFile { - return { name, table }; +export function CsvFile(table: CsvTable): CsvFile { + return { table }; } export interface CsvTable { @@ -27,10 +26,4 @@ export function CsvTable(rowCount: number, columnNames: string[], columns: CsvCo return { rowCount, columnNames: [...columnNames], getColumn(name) { return columns[name]; } }; } -export type CsvColumns = { [name: string]: CsvColumn } - -// export namespace CsvTable { -// export function empty(name: string): Table { -// return { rowCount: 0, name, fieldNames: [], getColumn(name: string) { return void 0; } }; -// }; -// } \ No newline at end of file +export type CsvColumns = { [name: string]: CsvColumn } \ No newline at end of file diff --git a/src/mol-io/reader/ply/parser.ts b/src/mol-io/reader/ply/parser.ts new file mode 100644 index 0000000000000000000000000000000000000000..bd9ce4f2d044a9e0cf62efe7c5f06c87d4258658 --- /dev/null +++ b/src/mol-io/reader/ply/parser.ts @@ -0,0 +1,263 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { ReaderResult as Result } from '../result' +import { Task, RuntimeContext } from 'mol-task' +import { PlyFile, PlyType, PlyElement } from './schema'; +import { Tokenizer, TokenBuilder, Tokens } from '../common/text/tokenizer'; +import { Column } from 'mol-data/db'; +import { TokenColumn } from '../common/text/column/token'; + +interface State { + data: string + tokenizer: Tokenizer + runtimeCtx: RuntimeContext + + comments: string[] + elementSpecs: ElementSpec[] + elements: PlyElement[] +} + +function State(data: string, runtimeCtx: RuntimeContext): State { + const tokenizer = Tokenizer(data) + return { + data, + tokenizer, + runtimeCtx, + + comments: [], + elementSpecs: [], + elements: [] + } +} + +type ColumnProperty = { kind: 'column', type: PlyType, name: string } +type ListProperty = { kind: 'list', countType: PlyType, dataType: PlyType, name: string } +type Property = ColumnProperty | ListProperty + +type TableElementSpec = { kind: 'table', name: string, count: number, properties: ColumnProperty[] } +type ListElementSpec = { kind: 'list', name: string, count: number, property: ListProperty } +type ElementSpec = TableElementSpec | ListElementSpec + +function markHeader(tokenizer: Tokenizer) { + const endHeaderIndex = tokenizer.data.indexOf('end_header', tokenizer.position) + if (endHeaderIndex === -1) throw new Error(`no 'end_header' record found`) + // TODO set `tokenizer.lineNumber` correctly + tokenizer.tokenStart = tokenizer.position + tokenizer.tokenEnd = endHeaderIndex + tokenizer.position = endHeaderIndex + Tokenizer.eatLine(tokenizer) +} + +function parseHeader(state: State) { + const { tokenizer, comments, elementSpecs } = state + + markHeader(tokenizer) + const headerLines = Tokenizer.getTokenString(tokenizer).split(/\r?\n/) + + if (headerLines[0] !== 'ply') throw new Error(`data not starting with 'ply'`) + if (headerLines[1] !== 'format ascii 1.0') throw new Error(`format not 'ascii 1.0'`) + + let currentName: string | undefined + let currentCount: number | undefined + let currentProperties: Property[] | undefined + + + function addCurrentElementSchema() { + if (currentName !== undefined && currentCount !== undefined && currentProperties !== undefined) { + let isList = false + for (let i = 0, il = currentProperties.length; i < il; ++i) { + const p = currentProperties[i] + if (p.kind === 'list') { + isList = true + break + } + } + if (isList && currentProperties.length !== 1) throw new Error('expected single list property') + if (isList) { + elementSpecs.push({ + kind: 'list', + name: currentName, + count: currentCount, + property: currentProperties[0] as ListProperty + }) + } else { + elementSpecs.push({ + kind: 'table', + name: currentName, + count: currentCount, + properties: currentProperties as ColumnProperty[] + }) + } + } + } + + for (let i = 2, il = headerLines.length; i < il; ++i) { + const l = headerLines[i] + const ls = l.split(' ') + if (l.startsWith('comment')) { + comments.push(l.substr(8)) + } else if (l.startsWith('element')) { + addCurrentElementSchema() + currentProperties = [] + currentName = ls[1] + currentCount = parseInt(ls[2]) + } else if (l.startsWith('property')) { + if (currentProperties === undefined) throw new Error(`properties outside of element`) + if (ls[1] === 'list') { + currentProperties.push({ + kind: 'list', + countType: PlyType(ls[2]), + dataType: PlyType(ls[3]), + name: ls[4] + }) + } else { + currentProperties.push({ + kind: 'column', + type: PlyType(ls[1]), + name: ls[2] + }) + } + } else if (l.startsWith('end_header')) { + addCurrentElementSchema() + } else { + console.warn('unknown header line') + } + } +} + +function parseElements(state: State) { + const { elementSpecs } = state + for (let i = 0, il = elementSpecs.length; i < il; ++i) { + const spec = elementSpecs[i] + if (spec.kind === 'table') parseTableElement(state, spec) + else if (spec.kind === 'list') parseListElement(state, spec) + } +} + +function getColumnSchema(type: PlyType): Column.Schema { + switch (type) { + case 'char': case 'uchar': case 'int8': case 'uint8': + case 'short': case 'ushort': case 'int16': case 'uint16': + case 'int': case 'uint': case 'int32': case 'uint32': + return Column.Schema.int + case 'float': case 'double': case 'float32': case 'float64': + return Column.Schema.float + } +} + +function parseTableElement(state: State, spec: TableElementSpec) { + const { elements, tokenizer } = state + const { count, properties } = spec + const propertyCount = properties.length + const propertyNames: string[] = [] + const propertyTypes: PlyType[] = [] + const propertyTokens: Tokens[] = [] + const propertyColumns = new Map<string, Column<number>>() + + for (let i = 0, il = propertyCount; i < il; ++i) { + const tokens = TokenBuilder.create(tokenizer.data, count * 2) + propertyTokens.push(tokens) + } + + for (let i = 0, il = count; i < il; ++i) { + for (let j = 0, jl = propertyCount; j < jl; ++j) { + Tokenizer.skipWhitespace(tokenizer) + Tokenizer.markStart(tokenizer) + Tokenizer.eatValue(tokenizer) + TokenBuilder.addUnchecked(propertyTokens[j], tokenizer.tokenStart, tokenizer.tokenEnd) + } + } + + for (let i = 0, il = propertyCount; i < il; ++i) { + const { type, name } = properties[i] + const column = TokenColumn(propertyTokens[i], getColumnSchema(type)) + propertyNames.push(name) + propertyTypes.push(type) + propertyColumns.set(name, column) + } + + elements.push({ + kind: 'table', + rowCount: count, + propertyNames, + propertyTypes, + getProperty: (name: string) => propertyColumns.get(name) + }) +} + +function parseListElement(state: State, spec: ListElementSpec) { + const { elements, tokenizer } = state + const { count, property } = spec + + // initial tokens size assumes triangle index data + const tokens = TokenBuilder.create(tokenizer.data, count * 2 * 3) + + const offsets = new Uint32Array(count + 1) + let entryCount = 0 + + for (let i = 0, il = count; i < il; ++i) { + // skip over row entry count as it is determined by line break + Tokenizer.skipWhitespace(tokenizer) + Tokenizer.eatValue(tokenizer) + + while (Tokenizer.skipWhitespace(tokenizer) !== 10) { + ++entryCount + Tokenizer.markStart(tokenizer) + Tokenizer.eatValue(tokenizer) + TokenBuilder.addToken(tokens, tokenizer) + } + offsets[i + 1] = entryCount + } + + // console.log(tokens.indices) + // console.log(offsets) + + /** holds row value entries transiently */ + const listValue = { + entries: [] as number[], + count: 0 + } + + const column = TokenColumn(tokens, getColumnSchema(property.dataType)) + + elements.push({ + kind: 'list', + rowCount: count, + name: property.name, + type: property.dataType, + value: (row: number) => { + const start = offsets[row] + const end = offsets[row + 1] + for (let i = start; i < end; ++i) { + listValue.entries[i - start] = column.value(i) + } + listValue.count = end - start + return listValue + } + }) +} + +async function parseInternal(data: string, ctx: RuntimeContext): Promise<Result<PlyFile>> { + const state = State(data, ctx); + ctx.update({ message: 'Parsing...', current: 0, max: data.length }); + parseHeader(state) + // console.log(state.comments) + // console.log(JSON.stringify(state.elementSpecs, undefined, 4)) + parseElements(state) + const { elements, elementSpecs, comments } = state + const elementNames = elementSpecs.map(s => s.name) + const result = PlyFile(elements, elementNames, comments) + return Result.success(result); +} + +export function parse(data: string) { + return Task.create<Result<PlyFile>>('Parse PLY', async ctx => { + return await parseInternal(data, ctx) + }) +} + +export default parse; \ No newline at end of file diff --git a/src/mol-io/reader/ply/schema.ts b/src/mol-io/reader/ply/schema.ts new file mode 100644 index 0000000000000000000000000000000000000000..c5fbcb995ae825d0ea7f829ffd3ea96bca73f715 --- /dev/null +++ b/src/mol-io/reader/ply/schema.ts @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { Column } from 'mol-data/db'; + +// http://paulbourke.net/dataformats/ply/ +// https://en.wikipedia.org/wiki/PLY_(file_format) + +export const PlyTypeByteLength = { + 'char': 1, + 'uchar': 1, + 'short': 2, + 'ushort': 2, + 'int': 4, + 'uint': 4, + 'float': 4, + 'double': 8, + + 'int8': 1, + 'uint8': 1, + 'int16': 2, + 'uint16': 2, + 'int32': 4, + 'uint32': 4, + 'float32': 4, + 'float64': 8 +} +export type PlyType = keyof typeof PlyTypeByteLength +export const PlyTypes = new Set(Object.keys(PlyTypeByteLength)) +export function PlyType(str: string) { + if (!PlyTypes.has(str)) throw new Error(`unknown ply type '${str}'`) + return str as PlyType +} + +export interface PlyFile { + readonly comments: ReadonlyArray<string> + readonly elementNames: ReadonlyArray<string> + getElement(name: string): PlyElement | undefined +} + +export function PlyFile(elements: PlyElement[], elementNames: string[], comments: string[]): PlyFile { + const elementMap = new Map<string, PlyElement>() + for (let i = 0, il = elementNames.length; i < il; ++i) { + elementMap.set(elementNames[i], elements[i]) + } + return { + comments, + elementNames, + getElement: (name: string) => { + return elementMap.get(name) + } + }; +} + +export type PlyElement = PlyTable | PlyList + +export interface PlyTable { + readonly kind: 'table' + readonly rowCount: number + readonly propertyNames: ReadonlyArray<string> + readonly propertyTypes: ReadonlyArray<PlyType> + getProperty(name: string): Column<number> | undefined +} + +export interface PlyListValue { + readonly entries: ArrayLike<number> + readonly count: number +} + +export interface PlyList { + readonly kind: 'list' + readonly rowCount: number, + readonly name: string, + readonly type: PlyType, + value: (row: number) => PlyListValue +} \ No newline at end of file diff --git a/src/mol-model-formats/shape/ply.ts b/src/mol-model-formats/shape/ply.ts new file mode 100644 index 0000000000000000000000000000000000000000..edeceded4abdafa8e59e8370fef32b4c0b7f9f56 --- /dev/null +++ b/src/mol-model-formats/shape/ply.ts @@ -0,0 +1,260 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Schäfer, Marco <marco.schaefer@uni-tuebingen.de> + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { RuntimeContext, Task } from 'mol-task'; +import { ShapeProvider } from 'mol-model/shape/provider'; +import { Color } from 'mol-util/color'; +import { PlyFile, PlyTable, PlyList } from 'mol-io/reader/ply/schema'; +import { MeshBuilder } from 'mol-geo/geometry/mesh/mesh-builder'; +import { Mesh } from 'mol-geo/geometry/mesh/mesh'; +import { Shape } from 'mol-model/shape'; +import { ChunkedArray } from 'mol-data/util'; +import { arrayMax, fillSerial } from 'mol-util/array'; +import { Column } from 'mol-data/db'; +import { ParamDefinition as PD } from 'mol-util/param-definition'; +import { ColorNames } from 'mol-util/color/tables'; +import { deepClone } from 'mol-util/object'; + +// TODO support 'edge' element, see https://www.mathworks.com/help/vision/ug/the-ply-format.html +// TODO support missing face element + +function createPlyShapeParams(plyFile?: PlyFile) { + const vertex = plyFile && plyFile.getElement('vertex') as PlyTable + const material = plyFile && plyFile.getElement('material') as PlyTable + + const defaultValues = { group: '', vRed: '', vGreen: '', vBlue: '', mRed: '', mGreen: '', mBlue: '' } + + const groupOptions: [string, string][] = [['', '']] + const colorOptions: [string, string][] = [['', '']] + if (vertex) { + for (let i = 0, il = vertex.propertyNames.length; i < il; ++i) { + const name = vertex.propertyNames[i] + const type = vertex.propertyTypes[i] + if ( + type === 'uchar' || type === 'uint8' || + type === 'ushort' || type === 'uint16' || + type === 'uint' || type === 'uint32' + ) groupOptions.push([ name, name ]) + if (type === 'uchar' || type === 'uint8') colorOptions.push([ name, name ]) + } + + // TODO hardcoded as convenience for data provided by MegaMol + if (vertex.propertyNames.includes('atomid')) defaultValues.group = 'atomid' + else if (vertex.propertyNames.includes('material_index')) defaultValues.group = 'material_index' + + if (vertex.propertyNames.includes('red')) defaultValues.vRed = 'red' + if (vertex.propertyNames.includes('green')) defaultValues.vGreen = 'green' + if (vertex.propertyNames.includes('blue')) defaultValues.vBlue = 'blue' + } + + const materialOptions: [string, string][] = [['', '']] + if (material) { + for (let i = 0, il = material.propertyNames.length; i < il; ++i) { + const name = material.propertyNames[i] + const type = material.propertyTypes[i] + if (type === 'uchar' || type === 'uint8') materialOptions.push([ name, name ]) + } + + if (material.propertyNames.includes('red')) defaultValues.mRed = 'red' + if (material.propertyNames.includes('green')) defaultValues.mGreen = 'green' + if (material.propertyNames.includes('blue')) defaultValues.mBlue = 'blue' + } + + const defaultColoring = defaultValues.vRed && defaultValues.vGreen && defaultValues.vBlue ? 'vertex' : + defaultValues.mRed && defaultValues.mGreen && defaultValues.mBlue ? 'material' : 'uniform' + + return { + ...Mesh.Params, + + coloring: PD.MappedStatic(defaultColoring, { + vertex: PD.Group({ + red: PD.Select(defaultValues.vRed, colorOptions, { label: 'Red Property' }), + green: PD.Select(defaultValues.vGreen, colorOptions, { label: 'Green Property' }), + blue: PD.Select(defaultValues.vBlue, colorOptions, { label: 'Blue Property' }), + }, { isFlat: true }), + material: PD.Group({ + red: PD.Select(defaultValues.mRed, materialOptions, { label: 'Red Property' }), + green: PD.Select(defaultValues.mGreen, materialOptions, { label: 'Green Property' }), + blue: PD.Select(defaultValues.mBlue, materialOptions, { label: 'Blue Property' }), + }, { isFlat: true }), + uniform: PD.Group({ + color: PD.Color(ColorNames.grey) + }, { isFlat: true }) + }), + grouping: PD.MappedStatic(defaultValues.group ? 'vertex' : 'none', { + vertex: PD.Group({ + group: PD.Select(defaultValues.group, groupOptions, { label: 'Group Property' }), + }, { isFlat: true }), + none: PD.Group({ }) + }), + } +} + +export const PlyShapeParams = createPlyShapeParams() +export type PlyShapeParams = typeof PlyShapeParams + +async function getMesh(ctx: RuntimeContext, vertex: PlyTable, face: PlyList, groupIds: ArrayLike<number>, mesh?: Mesh) { + const builderState = MeshBuilder.createState(vertex.rowCount, vertex.rowCount / 4, mesh) + const { vertices, normals, indices, groups } = builderState + + const x = vertex.getProperty('x') + const y = vertex.getProperty('y') + const z = vertex.getProperty('z') + if (!x || !y || !z) throw new Error('missing coordinate properties') + + const nx = vertex.getProperty('nx') + const ny = vertex.getProperty('ny') + const nz = vertex.getProperty('nz') + + const hasNormals = !!nx && !!ny && !!nz + + for (let i = 0, il = vertex.rowCount; i < il; ++i) { + if (i % 100000 === 0 && ctx.shouldUpdate) await ctx.update({ current: i, max: il, message: `adding vertex ${i}` }) + + ChunkedArray.add3(vertices, x.value(i), y.value(i), z.value(i)) + if (hasNormals) ChunkedArray.add3(normals, nx!.value(i), ny!.value(i), nz!.value(i)); + ChunkedArray.add(groups, groupIds[i]) + } + + for (let i = 0, il = face.rowCount; i < il; ++i) { + if (i % 100000 === 0 && ctx.shouldUpdate) await ctx.update({ current: i, max: il, message: `adding face ${i}` }) + + const { entries, count } = face.value(i) + if (count === 3) { + // triangle + ChunkedArray.add3(indices, entries[0], entries[1], entries[2]) + } else if (count === 4) { + // quadrilateral + ChunkedArray.add3(indices, entries[2], entries[1], entries[0]) + ChunkedArray.add3(indices, entries[2], entries[0], entries[3]) + } + } + + const m = MeshBuilder.getMesh(builderState); + m.normalsComputed = hasNormals + await Mesh.computeNormals(m).runInContext(ctx) + + return m +} + +const int = Column.Schema.int + +type Grouping = { ids: ArrayLike<number>, map: ArrayLike<number> } +function getGrouping(vertex: PlyTable, props: PD.Values<PlyShapeParams>): Grouping { + const { grouping } = props + const { rowCount } = vertex + const column = grouping.name === 'vertex' ? vertex.getProperty(grouping.params.group) : undefined + + const ids = column ? column.toArray({ array: Uint32Array }) : fillSerial(new Uint32Array(rowCount)) + const maxId = arrayMax(ids) // assumes uint ids + const map = new Uint32Array(maxId + 1) + for (let i = 0, il = ids.length; i < il; ++i) map[ids[i]] = i + return { ids, map } +} + +type Coloring = { kind: 'vertex' | 'material' | 'uniform', red: Column<number>, green: Column<number>, blue: Column<number> } +function getColoring(vertex: PlyTable, material: PlyTable | undefined, props: PD.Values<PlyShapeParams>): Coloring { + const { coloring } = props + const { rowCount } = vertex + + let red: Column<number>, green: Column<number>, blue: Column<number> + if (coloring.name === 'vertex') { + red = vertex.getProperty(coloring.params.red) || Column.ofConst(127, rowCount, int) + green = vertex.getProperty(coloring.params.green) || Column.ofConst(127, rowCount, int) + blue = vertex.getProperty(coloring.params.blue) || Column.ofConst(127, rowCount, int) + } else if (coloring.name === 'material') { + red = (material && material.getProperty(coloring.params.red)) || Column.ofConst(127, rowCount, int) + green = (material && material.getProperty(coloring.params.green)) || Column.ofConst(127, rowCount, int) + blue = (material && material.getProperty(coloring.params.blue)) || Column.ofConst(127, rowCount, int) + } else { + const [r, g, b] = Color.toRgb(coloring.params.color) + red = Column.ofConst(r, rowCount, int) + green = Column.ofConst(g, rowCount, int) + blue = Column.ofConst(b, rowCount, int) + } + return { kind: coloring.name, red, green, blue } +} + +function createShape(plyFile: PlyFile, mesh: Mesh, coloring: Coloring, grouping: Grouping) { + const { kind, red, green, blue } = coloring + const { ids, map } = grouping + return Shape.create( + 'ply-mesh', plyFile, mesh, + (groupId: number) => { + const idx = kind === 'material' ? groupId : map[groupId] + return Color.fromRgb(red.value(idx), green.value(idx), blue.value(idx)) + }, + () => 1, // size: constant + (groupId: number) => { + return ids[groupId].toString() + } + ) +} + +function makeShapeGetter() { + let _plyFile: PlyFile | undefined + let _props: PD.Values<PlyShapeParams> | undefined + + let _shape: Shape<Mesh> + let _mesh: Mesh + let _coloring: Coloring + let _grouping: Grouping + + const getShape = async (ctx: RuntimeContext, plyFile: PlyFile, props: PD.Values<PlyShapeParams>, shape?: Shape<Mesh>) => { + + const vertex = plyFile.getElement('vertex') as PlyTable + if (!vertex) throw new Error('missing vertex element') + + const face = plyFile.getElement('face') as PlyList + if (!face) throw new Error('missing face element') + + const material = plyFile.getElement('material') as PlyTable + + let newMesh = false + let newColor = false + + if (!_plyFile || _plyFile !== plyFile) { + newMesh = true + } + + if (!_props || !PD.isParamEqual(PlyShapeParams.grouping, _props.grouping, props.grouping)) { + newMesh = true + } + + if (!_props || !PD.isParamEqual(PlyShapeParams.coloring, _props.coloring, props.coloring)) { + newColor = true + } + + if (newMesh) { + _coloring = getColoring(vertex, material, props) + _grouping = getGrouping(vertex, props) + _mesh = await getMesh(ctx, vertex, face, _grouping.ids, shape && shape.geometry) + _shape = createShape(plyFile, _mesh, _coloring, _grouping) + } else if (newColor) { + _coloring = getColoring(vertex, material, props) + _shape = createShape(plyFile, _mesh, _coloring, _grouping) + } + + _plyFile = plyFile + _props = deepClone(props) + + return _shape + } + return getShape +} + +export function shapeFromPly(source: PlyFile, params?: {}) { + return Task.create<ShapeProvider<PlyFile, Mesh, PlyShapeParams>>('Shape Provider', async ctx => { + return { + label: 'Mesh', + data: source, + params: createPlyShapeParams(source), + getShape: makeShapeGetter(), + geometryUtils: Mesh.Utils + } + }) +} \ No newline at end of file diff --git a/src/mol-model-formats/structure/_spec/pdb.spec.ts b/src/mol-model-formats/structure/_spec/pdb.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..8e365ed4c9c8f2fc1b01e447e32e5314eeb6bd45 --- /dev/null +++ b/src/mol-model-formats/structure/_spec/pdb.spec.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { guessElementSymbol } from '../pdb/to-cif'; +import { TokenBuilder } from 'mol-io/reader/common/text/tokenizer'; + +const records = [ + ['ATOM 19 HD23 LEU A 1 151.940 143.340 155.670 0.00 0.00', 'H'], + ['ATOM 38 CA SER A 3 146.430 138.150 162.270 0.00 0.00', 'C'], + ['ATOM 38 NA SER A 3 146.430 138.150 162.270 0.00 0.00', 'NA'], + ['ATOM 38 NAA SER A 3 146.430 138.150 162.270 0.00 0.00', 'N'], +] + +describe('PDB to-cif', () => { + it('guess-element-symbol', () => { + for (let i = 0, il = records.length; i < il; ++i) { + const [ data, element ] = records[i] + const tokens = TokenBuilder.create(data, 2) + guessElementSymbol(tokens, data, 12, 16) + expect(data.substring(tokens.indices[0], tokens.indices[1])).toBe(element) + } + }); +}); \ No newline at end of file diff --git a/src/mol-model-formats/structure/pdb/to-cif.ts b/src/mol-model-formats/structure/pdb/to-cif.ts index 853a1b9319eb97121a5394a55e772dde2621608a..0f699f297a4c3f03095cd1514bfb8d7db7c129b8 100644 --- a/src/mol-model-formats/structure/pdb/to-cif.ts +++ b/src/mol-model-formats/structure/pdb/to-cif.ts @@ -8,7 +8,7 @@ import { substringStartsWith } from 'mol-util/string'; import { CifField, CifCategory, CifFrame } from 'mol-io/reader/cif'; import { mmCIF_Schema } from 'mol-io/reader/cif/schema/mmcif'; -import { TokenBuilder, Tokenizer } from 'mol-io/reader/common/text/tokenizer'; +import { TokenBuilder, Tokenizer, Tokens } from 'mol-io/reader/common/text/tokenizer'; import { PdbFile } from 'mol-io/reader/pdb/schema'; import { parseCryst1, parseRemark350, parseMtrix } from './assembly'; import { WaterNames } from 'mol-model/structure/model/types'; @@ -89,6 +89,43 @@ function getEntityId(residueName: string, isHet: boolean) { return '1'; } +export function guessElementSymbol(tokens: Tokens, str: string, start: number, end: number) { + let s = start, e = end - 1 + + // trim spaces and numbers + let c = str.charCodeAt(s) + while ((c === 32 || (c >= 48 && c <= 57)) && s <= e) c = str.charCodeAt(++s) + c = str.charCodeAt(e) + while ((c === 32 || (c >= 48 && c <= 57)) && e >= s) c = str.charCodeAt(--e) + + ++e + + if (s === e) return TokenBuilder.add(tokens, s, e) // empty + if (s + 1 === e) return TokenBuilder.add(tokens, s, e) // one char + + c = str.charCodeAt(s) + + if (s + 2 === e) { // two chars + const c2 = str.charCodeAt(s + 1) + if ( + ((c === 78 || c === 110) && (c2 === 65 || c2 === 97)) || // NA na Na nA + ((c === 67 || c === 99) && (c2 === 76 || c2 === 108)) || // CL + ((c === 70 || c === 102) && (c2 === 69 || c2 === 101)) // FE + ) return TokenBuilder.add(tokens, s, s + 2) + } + + if ( + c === 67 || c === 99 || // C c + c === 72 || c === 104 || // H h + c === 78 || c === 110 || // N n + c === 79 || c === 111 || // O o + c === 80 || c === 112 || // P p + c === 83 || c === 115 // S s + ) return TokenBuilder.add(tokens, s, s + 1) + + TokenBuilder.add(tokens, s, s) // no reasonable guess, add empty token +} + function addAtom(sites: AtomSiteTemplate, model: string, data: Tokenizer, s: number, e: number, isHet: boolean) { const { data: str } = data; const length = e - s; @@ -162,11 +199,10 @@ function addAtom(sites: AtomSiteTemplate, model: string, data: Tokenizer, s: num if (data.tokenStart < data.tokenEnd) { TokenBuilder.addToken(sites.type_symbol, data); } else { - // "guess" the symbol - TokenBuilder.add(sites.type_symbol, s + 12, s + 13); + guessElementSymbol(sites.type_symbol, str, s + 12, s + 16) } } else { - TokenBuilder.add(sites.type_symbol, s + 12, s + 13); + guessElementSymbol(sites.type_symbol, str, s + 12, s + 16) } sites.label_entity_id[sites.index] = getEntityId(residueName, isHet); diff --git a/src/mol-model/shape/provider.ts b/src/mol-model/shape/provider.ts new file mode 100644 index 0000000000000000000000000000000000000000..dea45e8ba202b2a09da762a1b9a84926e836b64b --- /dev/null +++ b/src/mol-model/shape/provider.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { ShapeGetter } from 'mol-repr/shape/representation'; +import { Geometry, GeometryUtils } from 'mol-geo/geometry/geometry'; + +export interface ShapeProvider<D, G extends Geometry, P extends Geometry.Params<G>> { + label: string + data: D + params: P + getShape: ShapeGetter<D, G, P> + geometryUtils: GeometryUtils<G> +} \ No newline at end of file diff --git a/src/mol-model/shape/shape.ts b/src/mol-model/shape/shape.ts index 98188c58e360606021be9fa5ae25eb9e8421eeef..0d740ef14a47a24fab8dbb37f0a5c994c466645e 100644 --- a/src/mol-model/shape/shape.ts +++ b/src/mol-model/shape/shape.ts @@ -15,6 +15,8 @@ export interface Shape<G extends Geometry = Geometry> { readonly id: UUID /** A name to describe the shape */ readonly name: string + /** The data used to create the shape */ + readonly sourceData: unknown /** The geometry of the shape, e.g. `Mesh` or `Lines` */ readonly geometry: G /** An array of transformation matrices to describe multiple instances of the geometry */ @@ -30,10 +32,11 @@ export interface Shape<G extends Geometry = Geometry> { } export namespace Shape { - export function create<G extends Geometry>(name: string, geometry: G, getColor: Shape['getColor'], getSize: Shape['getSize'], getLabel: Shape['getLabel'], transforms?: Mat4[]): Shape<G> { + export function create<G extends Geometry>(name: string, sourceData: unknown, geometry: G, getColor: Shape['getColor'], getSize: Shape['getSize'], getLabel: Shape['getLabel'], transforms?: Mat4[]): Shape<G> { return { id: UUID.create22(), name, + sourceData, geometry, transforms: transforms || [Mat4.identity()], get groupCount() { return Geometry.getGroupCount(geometry) }, diff --git a/src/mol-model/structure/model/properties/utils/guess-element.ts b/src/mol-model/structure/model/properties/utils/guess-element.ts index 05658249f5aed93f771b66cae471b9a74be22955..54a66a8f99537da61f09ebcdab5c4a30e33237fc 100644 --- a/src/mol-model/structure/model/properties/utils/guess-element.ts +++ b/src/mol-model/structure/model/properties/utils/guess-element.ts @@ -12,7 +12,7 @@ function charAtIsNumber(str: string, index: number) { return code >= 48 && code <= 57 } -export function guessElement (str: string) { +export function guessElement(str: string) { let at = str.trim().toUpperCase() if (charAtIsNumber(at, 0)) at = at.substr(1) diff --git a/src/mol-plugin/behavior/dynamic/labels.ts b/src/mol-plugin/behavior/dynamic/labels.ts index be54651502d57168464b9a568c7a69eb7043cec6..16b43e187b7f1b41e48ea825119088240df1e457 100644 --- a/src/mol-plugin/behavior/dynamic/labels.ts +++ b/src/mol-plugin/behavior/dynamic/labels.ts @@ -107,7 +107,7 @@ export const SceneLabels = PluginBehavior.create<SceneLabelsProps>({ private getLabelsShape = (ctx: RuntimeContext, data: LabelsData, props: SceneLabelsProps, shape?: Shape<Text>) => { this.geo = getLabelsText(data, props, this.geo) - return Shape.create('Scene Labels', this.geo, this.getColor, this.getSize, this.getLabel, data.transforms) + return Shape.create('Scene Labels', data, this.geo, this.getColor, this.getSize, this.getLabel, data.transforms) } /** Update structures to be labeled, returns true if changed */ diff --git a/src/mol-plugin/state/actions/data-format.ts b/src/mol-plugin/state/actions/data-format.ts index c8eff16e37cb84cb2d61fc5174cff5e74f55f7ab..3cb8a05da5a2fae2722df766e895f691050b2d53 100644 --- a/src/mol-plugin/state/actions/data-format.ts +++ b/src/mol-plugin/state/actions/data-format.ts @@ -14,6 +14,7 @@ import { Ccp4Provider, Dsn6Provider, DscifProvider } from './volume'; import { StateTransforms } from '../transforms'; import { MmcifProvider, PdbProvider, GroProvider } from './structure'; import msgpackDecode from 'mol-io/common/msgpack/decode' +import { PlyProvider } from './shape'; export class DataFormatRegistry<D extends PluginStateObject.Data.Binary | PluginStateObject.Data.String> { private _list: { name: string, provider: DataFormatProvider<D> }[] = [] @@ -60,6 +61,7 @@ export class DataFormatRegistry<D extends PluginStateObject.Data.Binary | Plugin this.add('gro', GroProvider) this.add('mmcif', MmcifProvider) this.add('pdb', PdbProvider) + this.add('ply', PlyProvider) }; private _clear() { diff --git a/src/mol-plugin/state/actions/shape.ts b/src/mol-plugin/state/actions/shape.ts new file mode 100644 index 0000000000000000000000000000000000000000..7a1311149ed894a5112b189a1ab0f92f323b79d8 --- /dev/null +++ b/src/mol-plugin/state/actions/shape.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { PluginContext } from 'mol-plugin/context'; +import { State, StateBuilder } from 'mol-state'; +import { Task } from 'mol-task'; +import { FileInfo } from 'mol-util/file-info'; +import { PluginStateObject } from '../objects'; +import { StateTransforms } from '../transforms'; +import { DataFormatProvider } from './data-format'; + +export const PlyProvider: DataFormatProvider<any> = { + label: 'PLY', + description: 'PLY', + stringExtensions: ['ply'], + binaryExtensions: [], + isApplicable: (info: FileInfo, data: string) => { + return info.ext === 'ply' + }, + getDefaultBuilder: (ctx: PluginContext, data: StateBuilder.To<PluginStateObject.Data.String>, state: State) => { + return Task.create('PLY default builder', async taskCtx => { + const tree = data.apply(StateTransforms.Data.ParsePly) + .apply(StateTransforms.Model.ShapeFromPly) + .apply(StateTransforms.Representation.ShapeRepresentation3D) + await state.updateTree(tree).runInContext(taskCtx) + }) + } +} \ No newline at end of file diff --git a/src/mol-plugin/state/objects.ts b/src/mol-plugin/state/objects.ts index c73a2d03df02236ad1204e229ae0bf8a65816ba5..c3a0a8c979c81d6c9fc04c2fc5c3830c571774dd 100644 --- a/src/mol-plugin/state/objects.ts +++ b/src/mol-plugin/state/objects.ts @@ -6,6 +6,7 @@ */ import { CifFile } from 'mol-io/reader/cif'; +import { PlyFile } from 'mol-io/reader/ply/schema'; import { Model as _Model, Structure as _Structure } from 'mol-model/structure'; import { VolumeData } from 'mol-model/volume'; import { PluginBehavior } from 'mol-plugin/behavior/behavior'; @@ -16,6 +17,8 @@ import { StateObject, StateTransformer } from 'mol-state'; import { Ccp4File } from 'mol-io/reader/ccp4/schema'; import { Dsn6File } from 'mol-io/reader/dsn6/schema'; import { ShapeRepresentation } from 'mol-repr/shape/representation'; +import { Shape as _Shape } from 'mol-model/shape'; +import { ShapeProvider } from 'mol-model/shape/provider'; export type TypeClass = 'root' | 'data' | 'prop' @@ -61,6 +64,7 @@ export namespace PluginStateObject { export namespace Format { export class Json extends Create<any>({ name: 'JSON Data', typeClass: 'Data' }) { } export class Cif extends Create<CifFile>({ name: 'CIF File', typeClass: 'Data' }) { } + export class Ply extends Create<PlyFile>({ name: 'PLY File', typeClass: 'Data' }) { } export class Ccp4 extends Create<Ccp4File>({ name: 'CCP4/MRC/MAP File', typeClass: 'Data' }) { } export class Dsn6 extends Create<Dsn6File>({ name: 'DSN6/BRIX File', typeClass: 'Data' }) { } @@ -71,6 +75,7 @@ export namespace PluginStateObject { | { kind: 'cif', data: CifFile } | { kind: 'ccp4', data: Ccp4File } | { kind: 'dsn6', data: Dsn6File } + | { kind: 'ply', data: PlyFile } // For non-build in extensions | { kind: 'custom', data: unknown, tag: string }) export type BlobData = BlobEntry[] @@ -100,6 +105,11 @@ export namespace PluginStateObject { export class Data extends Create<VolumeData>({ name: 'Volume Data', typeClass: 'Object' }) { } export class Representation3D extends CreateRepresentation3D<VolumeRepresentation<any>>({ name: 'Volume 3D' }) { } } + + export namespace Shape { + export class Provider extends Create<ShapeProvider<any, any, any>>({ name: 'Shape Provider', typeClass: 'Object' }) { } + export class Representation3D extends CreateRepresentation3D<ShapeRepresentation<any, any, any>>({ name: 'Shape 3D' }) { } + } } export namespace PluginStateTransform { diff --git a/src/mol-plugin/state/transforms/data.ts b/src/mol-plugin/state/transforms/data.ts index aa0554a568085ad5ac2cb6a93bc0259e33e39b4b..43c82b639de8388404961518f571027401a37003 100644 --- a/src/mol-plugin/state/transforms/data.ts +++ b/src/mol-plugin/state/transforms/data.ts @@ -15,6 +15,7 @@ import { StateTransformer } from 'mol-state'; import { readFromFile, ajaxGetMany } from 'mol-util/data-source'; import * as CCP4 from 'mol-io/reader/ccp4/parser' import * as DSN6 from 'mol-io/reader/dsn6/parser' +import * as PLY from 'mol-io/reader/ply/parser' export { Download } type Download = typeof Download @@ -185,6 +186,23 @@ const ParseCif = PluginStateTransform.BuiltIn({ } }); +export { ParsePly } +type ParsePly = typeof ParsePly +const ParsePly = PluginStateTransform.BuiltIn({ + name: 'parse-ply', + display: { name: 'Parse PLY', description: 'Parse PLY from String data' }, + from: [SO.Data.String], + to: SO.Format.Ply +})({ + apply({ a }) { + return Task.create('Parse PLY', async ctx => { + const parsed = await PLY.parse(a.data).runInContext(ctx); + if (parsed.isError) throw new Error(parsed.message); + return new SO.Format.Ply(parsed.result, { label: parsed.result.comments[0] || 'PLY Data' }); + }); + } +}); + export { ParseCcp4 } type ParseCcp4 = typeof ParseCcp4 const ParseCcp4 = PluginStateTransform.BuiltIn({ diff --git a/src/mol-plugin/state/transforms/model.ts b/src/mol-plugin/state/transforms/model.ts index dd54e9dc49b54ddf352dd5f4e6888dfe33615734..5f890882adf2887c5739111615d8b8632638c2c7 100644 --- a/src/mol-plugin/state/transforms/model.ts +++ b/src/mol-plugin/state/transforms/model.ts @@ -24,6 +24,7 @@ import { trajectoryFromGRO } from 'mol-model-formats/structure/gro'; import { parseGRO } from 'mol-io/reader/gro/parser'; import { parseMolScript } from 'mol-script/language/parser'; import { transpileMolScript } from 'mol-script/script/mol-script/symbols'; +import { shapeFromPly } from 'mol-model-formats/shape/ply'; export { TrajectoryFromBlob }; export { TrajectoryFromMmCif }; @@ -338,7 +339,6 @@ function updateStructureFromQuery(query: QueryFn<Sel>, src: Structure, obj: SO.M return true; } - namespace StructureComplexElement { export type Types = 'atomic-sequence' | 'water' | 'atomic-het' | 'spheres' } @@ -394,4 +394,24 @@ async function attachProps(model: Model, ctx: PluginContext, taskCtx: RuntimeCon const p = ctx.customModelProperties.get(name); await p.attach(model).runInContext(taskCtx); } -} \ No newline at end of file +} + +export { ShapeFromPly } +type ShapeFromPly = typeof ShapeFromPly +const ShapeFromPly = PluginStateTransform.BuiltIn({ + name: 'shape-from-ply', + display: { name: 'Shape from PLY', description: 'Create Shape from PLY data' }, + from: SO.Format.Ply, + to: SO.Shape.Provider, + params(a) { + return { }; + } +})({ + apply({ a, params }) { + return Task.create('Create shape from PLY', async ctx => { + const shape = await shapeFromPly(a.data, params).runInContext(ctx) + const props = { label: 'Shape' }; + return new SO.Shape.Provider(shape, props); + }); + } +}); \ No newline at end of file diff --git a/src/mol-plugin/state/transforms/representation.ts b/src/mol-plugin/state/transforms/representation.ts index 0d99a98fc98cfd9920d8ad993959d72fdbd9ab1c..4309e45f424ebf1b0d079900d9da5d049f246e0c 100644 --- a/src/mol-plugin/state/transforms/representation.ts +++ b/src/mol-plugin/state/transforms/representation.ts @@ -30,6 +30,7 @@ import { Color } from 'mol-util/color'; import { Overpaint } from 'mol-theme/overpaint'; import { Transparency } from 'mol-theme/transparency'; import { getStructureOverpaint, getStructureTransparency } from './helpers'; +import { BaseGeometry } from 'mol-geo/geometry/base'; export { StructureRepresentation3D } export { StructureRepresentation3DHelpers } @@ -514,4 +515,38 @@ const VolumeRepresentation3D = PluginStateTransform.BuiltIn({ return StateTransformer.UpdateResult.Updated; }); } +}); + +// + +export { ShapeRepresentation3D } +type ShapeRepresentation3D = typeof ShapeRepresentation3D +const ShapeRepresentation3D = PluginStateTransform.BuiltIn({ + name: 'shape-representation-3d', + display: '3D Representation', + from: SO.Shape.Provider, + to: SO.Shape.Representation3D, + params: (a, ctx: PluginContext) => { + return a ? a.data.params : BaseGeometry.Params + } +})({ + canAutoUpdate() { + return true; + }, + apply({ a, params }, plugin: PluginContext) { + return Task.create('Shape Representation', async ctx => { + const props = { ...PD.getDefaultValues(a.data.params), params } + const repr = ShapeRepresentation(a.data.getShape, a.data.geometryUtils) + // TODO set initial state, repr.setState({}) + await repr.createOrUpdate(props, a.data.data).runInContext(ctx); + return new SO.Shape.Representation3D({ repr, source: a }, { label: a.data.label }); + }); + }, + update({ a, b, oldParams, newParams }, plugin: PluginContext) { + return Task.create('Shape Representation', async ctx => { + const props = { ...b.data.repr.props, ...newParams } + await b.data.repr.createOrUpdate(props, a.data.data).runInContext(ctx); + return StateTransformer.UpdateResult.Updated; + }); + } }); \ No newline at end of file diff --git a/src/mol-plugin/util/structure-labels.ts b/src/mol-plugin/util/structure-labels.ts index c6e944b29219ac96fed56f47556ad3885780ea36..932569da9046774aca3952c380c7174f7d9f31ca 100644 --- a/src/mol-plugin/util/structure-labels.ts +++ b/src/mol-plugin/util/structure-labels.ts @@ -44,7 +44,7 @@ export async function getLabelRepresentation(ctx: RuntimeContext, structure: Str function getLabelsShape(ctx: RuntimeContext, data: LabelsData, props: PD.Values<Text.Params>, shape?: Shape<Text>) { const geo = getLabelsText(data, props, shape && shape.geometry); - return Shape.create('Scene Labels', geo, () => ColorNames.dimgrey, g => data.sizes[g], () => '') + return Shape.create('Scene Labels', data, geo, () => ColorNames.dimgrey, g => data.sizes[g], () => '') } const boundaryHelper = new BoundaryHelper(); diff --git a/src/mol-repr/shape/representation.ts b/src/mol-repr/shape/representation.ts index ed079f38cfb2430264d257e603b981039ba91a6f..b84a0bffc03e96a677e7fb103f10f52de4b011c3 100644 --- a/src/mol-repr/shape/representation.ts +++ b/src/mol-repr/shape/representation.ts @@ -57,9 +57,7 @@ export function ShapeRepresentation<D, G extends Geometry, P extends Geometry.Pa updateState.createNew = true } else if (shape && _shape && shape.id === _shape.id) { // console.log('same shape') - // trigger color update when shape has not changed - updateState.updateColor = true - updateState.updateTransform = true + // nothing to set } else if (shape && _shape && shape.id !== _shape.id) { // console.log('new shape') updateState.updateTransform = true diff --git a/src/mol-util/array.ts b/src/mol-util/array.ts index 078ab5319b4db6bdd612c7655390a8e4a0e2225f..d9c19419b1ee4a0f91f79f54033f72ee1c56e65e 100644 --- a/src/mol-util/array.ts +++ b/src/mol-util/array.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ @@ -9,7 +9,7 @@ import { NumberArray } from './type-helpers'; // TODO move to mol-math as Vector??? /** Get the maximum value in an array */ -export function arrayMax(array: NumberArray) { +export function arrayMax(array: ArrayLike<number>) { let max = -Infinity for (let i = 0, il = array.length; i < il; ++i) { if (array[i] > max) max = array[i] @@ -18,7 +18,7 @@ export function arrayMax(array: NumberArray) { } /** Get the minimum value in an array */ -export function arrayMin(array: NumberArray) { +export function arrayMin(array: ArrayLike<number>) { let min = Infinity for (let i = 0, il = array.length; i < il; ++i) { if (array[i] < min) min = array[i] @@ -27,7 +27,7 @@ export function arrayMin(array: NumberArray) { } /** Get the sum of values in an array */ -export function arraySum(array: NumberArray, stride = 1, offset = 0) { +export function arraySum(array: ArrayLike<number>, stride = 1, offset = 0) { const n = array.length let sum = 0 for (let i = offset; i < n; i += stride) { @@ -37,12 +37,12 @@ export function arraySum(array: NumberArray, stride = 1, offset = 0) { } /** Get the mean of values in an array */ -export function arrayMean(array: NumberArray, stride = 1, offset = 0) { +export function arrayMean(array: ArrayLike<number>, stride = 1, offset = 0) { return arraySum(array, stride, offset) / (array.length / stride) } /** Get the root mean square of values in an array */ -export function arrayRms(array: NumberArray) { +export function arrayRms(array: ArrayLike<number>) { const n = array.length let sumSq = 0 for (let i = 0; i < n; ++i) { diff --git a/src/mol-util/param-definition.ts b/src/mol-util/param-definition.ts index a7f08f3cf1a3d8227703292d4c4078aeb862e499..bfb9311dcdfa65bf5febb10796124bd71d631643 100644 --- a/src/mol-util/param-definition.ts +++ b/src/mol-util/param-definition.ts @@ -284,7 +284,7 @@ export namespace ParamDefinition { return true; } - function isParamEqual(p: Any, a: any, b: any): boolean { + export function isParamEqual(p: Any, a: any, b: any): boolean { if (a === b) return true; if (!a) return !b; if (!b) return !a; diff --git a/src/tests/browser/index.html b/src/tests/browser/index.html index f28af95b2858709af424bf4ff1cbf49396329846..856790048af4104e6b2fd26af0d6c47ba92dd31b 100644 --- a/src/tests/browser/index.html +++ b/src/tests/browser/index.html @@ -1,38 +1,42 @@ <!DOCTYPE html> <html lang="en"> - <head> +<head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"> <title>Mol* Browser Test</title> <style> - * { - margin: 0; - padding: 0; - box-sizing: border-box; - } - html, body { - width: 100%; - height: 100%; - overflow: hidden; - } + * { + margin: 0; + padding: 0; + box-sizing: border-box; + } + html, body { + width: 100%; + height: 100%; + overflow: hidden; + } </style> - </head> - <body> - <div id="app"></div> - <script type="text/javascript"> - function urlQueryParameter (id) { +</head> +<body> +<div id="app"></div> +<script type="text/javascript"> + function urlQueryParameter (id) { if (typeof window === 'undefined') return undefined const a = new RegExp(`${id}=([^&#=]*)`) const m = a.exec(window.location.search) return m ? decodeURIComponent(m[1]) : undefined - } - - const name = urlQueryParameter('name') - if (name) { + } + const name = urlQueryParameter('name') + if (name) { const script = document.createElement('script') script.src = name + '.js' document.body.appendChild(script) - } - </script> - </body> + } +</script> +<script type="text/javascript" > + const script = document.createElement('script'); + script.src = "render-shape.js"; + document.body.appendChild(script); +</script> +</body> </html> \ No newline at end of file diff --git a/src/tests/browser/render-shape.ts b/src/tests/browser/render-shape.ts index ce513f8132403be892bd02ffa87fe06d5a6f936a..6fd4ace698f131bc5faecdb82ace7a1cd300a1df 100644 --- a/src/tests/browser/render-shape.ts +++ b/src/tests/browser/render-shape.ts @@ -68,7 +68,7 @@ async function getSphereMesh(ctx: RuntimeContext, centers: number[], mesh?: Mesh const builderState = MeshBuilder.createState(centers.length * 128, centers.length * 128 / 2, mesh) const t = Mat4.identity() const v = Vec3.zero() - const sphere = Sphere(2) + const sphere = Sphere(4) builderState.currentGroup = 0 for (let i = 0, il = centers.length / 3; i < il; ++i) { // for production, calls to update should be guarded by `if (ctx.shouldUpdate)` @@ -81,8 +81,8 @@ async function getSphereMesh(ctx: RuntimeContext, centers: number[], mesh?: Mesh } const myData = { - centers: [0, 0, 0, 0, 3, 0], - colors: [ColorNames.tomato, ColorNames.springgreen], + centers: [0, 0, 0, 0, 3, 0, 1, 0 , 4], + colors: [ColorNames.tomato, ColorNames.springgreen, ColorNames.springgreen], labels: ['Sphere 0, Instance A', 'Sphere 1, Instance A', 'Sphere 0, Instance B', 'Sphere 1, Instance B'], transforms: [Mat4.identity(), Mat4.fromTranslation(Mat4.zero(), Vec3.create(3, 0, 0))] } @@ -96,8 +96,8 @@ async function getShape(ctx: RuntimeContext, data: MyData, props: {}, shape?: Sh const { centers, colors, labels, transforms } = data const mesh = await getSphereMesh(ctx, centers, shape && shape.geometry) const groupCount = centers.length / 3 - return shape || Shape.create( - 'test', mesh, + return Shape.create( + 'test', data, mesh, (groupId: number) => colors[groupId], // color: per group, same for instances () => 1, // size: constant (groupId: number, instanceId: number) => labels[instanceId * groupCount + groupId], // label: per group and instance @@ -108,10 +108,9 @@ async function getShape(ctx: RuntimeContext, data: MyData, props: {}, shape?: Sh // Init ShapeRepresentation container const repr = ShapeRepresentation(getShape, Mesh.Utils) -async function init() { +export async function init() { // Create shape from myData and add to canvas3d await repr.createOrUpdate({}, myData).run((p: Progress) => console.log(Progress.format(p))) - console.log(repr) canvas3d.add(repr) canvas3d.resetCamera() @@ -122,4 +121,4 @@ async function init() { await repr.createOrUpdate({}, myData).run() }, 1000) } -init() \ No newline at end of file +export default init(); \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index bda3612a92f256206750a7c20e5422ed44d57fc9..f23cd57bbcb5deef38db87f344634cfd6ebd1252 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -97,6 +97,7 @@ module.exports = [ createApp('viewer'), createApp('basic-wrapper'), createEntry('examples/proteopedia-wrapper/index', 'examples/proteopedia-wrapper', 'index'), + createEntry('examples/ply-wrapper/index', 'examples/ply-wrapper', 'index'), createNodeApp('state-docs'), createNodeEntryPoint('preprocess', 'servers/model', 'model-server'), createApp('model-server-query'),