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..235b31ae13af038add4225a0b17b2adfb6ffa1d9 --- /dev/null +++ b/src/examples/ply-wrapper/index.html @@ -0,0 +1,161 @@ +<!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; + } + + #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> + </head> + <body> + <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 = '1rwe', 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); + } + </script> + </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..f08ad8e8e9a810f2a2488ca4bdcb46b34fbcb0cf --- /dev/null +++ b/src/examples/ply-wrapper/index.ts @@ -0,0 +1,235 @@ +/** + * 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'; +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; + } + + 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; \ No newline at end of file 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-io/reader/ply/parser.ts b/src/mol-io/reader/ply/parser.ts index 96963639992da31e8730f92294c6d5d87ab2ee41..a1fbf99fdac19ab7b186b763d1c499b0c89225af 100644 --- a/src/mol-io/reader/ply/parser.ts +++ b/src/mol-io/reader/ply/parser.ts @@ -196,8 +196,8 @@ function moveNextInternal(state: State) { if (state.currentProperty >= 3 && state.currentProperty < 6) { state.colors[state.currentVertex * 3 + state.currentProperty - 3] = fastParseInt(state.tokenizer.data, state.tokenizer.tokenStart, state.tokenizer.tokenEnd); } - if (state.currentProperty >= 6 && state.currentProperty < 9) { - state.normals[state.currentVertex * 3 + state.currentProperty - 6] = fastParseFloat(state.tokenizer.data, state.tokenizer.tokenStart, state.tokenizer.tokenEnd); + if (state.currentProperty >= 7 && state.currentProperty < 10) { + state.normals[state.currentVertex * 3 + state.currentProperty - 7] = fastParseFloat(state.tokenizer.data, state.tokenizer.tokenStart, state.tokenizer.tokenEnd); } state.currentProperty++; if (state.currentProperty === state.propertyCount) { 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'),