Forráskód Böngészése

Merge branch 'ply-test'

# Conflicts:
#	src/mol-plugin/state/transforms/representation.ts
Alexander Rose 6 éve
szülő
commit
845a2be7d2
35 módosított fájl, 1784 hozzáadás és 88 törlés
  1. 3 9
      package-lock.json
  2. 72 0
      src/examples/ply-wrapper/annotation.ts
  3. 7 0
      src/examples/ply-wrapper/changelog.md
  4. 101 0
      src/examples/ply-wrapper/helpers.ts
  5. 218 0
      src/examples/ply-wrapper/index.html
  6. 248 0
      src/examples/ply-wrapper/index.ts
  7. 21 0
      src/examples/ply-wrapper/ui/controls.tsx
  8. 1 1
      src/mol-geo/geometry/mesh/mesh-builder.ts
  9. 90 0
      src/mol-io/common/ascii.ts
  10. 15 15
      src/mol-io/reader/_spec/mol2.spec.ts
  11. 152 0
      src/mol-io/reader/_spec/ply.spec.ts
  12. 1 2
      src/mol-io/reader/cif/data-model.ts
  13. 3 10
      src/mol-io/reader/csv/data-model.ts
  14. 263 0
      src/mol-io/reader/ply/parser.ts
  15. 79 0
      src/mol-io/reader/ply/schema.ts
  16. 260 0
      src/mol-model-formats/shape/ply.ts
  17. 26 0
      src/mol-model-formats/structure/_spec/pdb.spec.ts
  18. 40 4
      src/mol-model-formats/structure/pdb/to-cif.ts
  19. 16 0
      src/mol-model/shape/provider.ts
  20. 4 1
      src/mol-model/shape/shape.ts
  21. 1 1
      src/mol-model/structure/model/properties/utils/guess-element.ts
  22. 1 1
      src/mol-plugin/behavior/dynamic/labels.ts
  23. 2 0
      src/mol-plugin/state/actions/data-format.ts
  24. 31 0
      src/mol-plugin/state/actions/shape.ts
  25. 10 0
      src/mol-plugin/state/objects.ts
  26. 18 0
      src/mol-plugin/state/transforms/data.ts
  27. 22 2
      src/mol-plugin/state/transforms/model.ts
  28. 35 0
      src/mol-plugin/state/transforms/representation.ts
  29. 1 1
      src/mol-plugin/util/structure-labels.ts
  30. 1 3
      src/mol-repr/shape/representation.ts
  31. 6 6
      src/mol-util/array.ts
  32. 1 1
      src/mol-util/param-definition.ts
  33. 27 23
      src/tests/browser/index.html
  34. 7 8
      src/tests/browser/render-shape.ts
  35. 1 0
      webpack.config.js

+ 3 - 9
package-lock.json

@@ -14925,7 +14925,6 @@
               "version": "2.3.5",
               "bundled": true,
               "dev": true,
-              "optional": true,
               "requires": {
                 "safe-buffer": "^5.1.2",
                 "yallist": "^3.0.0"
@@ -14944,7 +14943,6 @@
               "version": "0.5.1",
               "bundled": true,
               "dev": true,
-              "optional": true,
               "requires": {
                 "minimist": "0.0.8"
               }
@@ -15038,7 +15036,6 @@
               "version": "1.4.0",
               "bundled": true,
               "dev": true,
-              "optional": true,
               "requires": {
                 "wrappy": "1"
               }
@@ -15124,8 +15121,7 @@
             "safe-buffer": {
               "version": "5.1.2",
               "bundled": true,
-              "dev": true,
-              "optional": true
+              "dev": true
             },
             "safer-buffer": {
               "version": "2.1.2",
@@ -15225,14 +15221,12 @@
             "wrappy": {
               "version": "1.0.2",
               "bundled": true,
-              "dev": true,
-              "optional": true
+              "dev": true
             },
             "yallist": {
               "version": "3.0.3",
               "bundled": true,
-              "dev": true,
-              "optional": true
+              "dev": true
             }
           }
         },

+ 72 - 0
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;
+    }
+});

+ 7 - 0
src/examples/ply-wrapper/changelog.md

@@ -0,0 +1,7 @@
+== v2.0 ==
+
+* Changed how state saving works.
+
+== v1.0 ==
+
+* Initial version.

+ 101 - 0
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 }
+}

+ 218 - 0
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>

+ 248 - 0
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;

+ 21 - 0
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>;
+    }
+}

+ 1 - 1
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]);

+ 90 - 0
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;
+}

+ 15 - 15
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);

+ 152 - 0
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)
+    });
+});

+ 1 - 2
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,

+ 3 - 10
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; } };
-//     };
-// }
+export type CsvColumns = { [name: string]: CsvColumn }

+ 263 - 0
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;

+ 79 - 0
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
+}

+ 260 - 0
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
+        }
+    })
+}

+ 26 - 0
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)
+        }
+    });
+});

+ 40 - 4
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);

+ 16 - 0
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>
+}

+ 4 - 1
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) },

+ 1 - 1
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)

+ 1 - 1
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 */

+ 2 - 0
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() {

+ 31 - 0
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)
+        })
+    }
+}

+ 10 - 0
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 {

+ 18 - 0
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({

+ 22 - 2
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);
     }
-}
+}
+
+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);
+        });
+    }
+});

+ 35 - 0
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;
+        });
+    }
 });

+ 1 - 1
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();

+ 1 - 3
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

+ 6 - 6
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) {

+ 1 - 1
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;

+ 27 - 23
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>

+ 7 - 8
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()
+export default init();

+ 1 - 0
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'),