diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f194ff293a17af9ab8df7dc6aebe8433a1bd2ae..ea2857d409c5e19c8592a994363b1c33db0e0cf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,14 @@ Note that since we don't clearly distinguish between a public and private interf ## [Unreleased] +- Add additional measurement controls: orientation (box, axes, ellipsoid) & plane (best fit) - Improve aromatic bond visuals (add ``aromaticScale``, ``aromaticSpacing``, ``aromaticDashCount`` params) - [Breaking] Change ``adjustCylinderLength`` default to ``false`` (set to true for focus representation) - Fix marker highlight color overriding select color +- CellPack extension update + - add binary model support + - add compartment (including membrane) geometry support + - add latest mycoplasma model example ## [v2.3.5] - 2021-10-19 diff --git a/README.md b/README.md index c9f448128806e06830dee74590d6f360cb1729d0..0ce2d30e7e4e6666c706618f3091ffc146adcea6 100644 --- a/README.md +++ b/README.md @@ -122,9 +122,9 @@ and navigate to `build/viewer` **Convert any CIF to BinaryCIF** - node lib/servers/model/preprocess -i file.cif -ob file.bcif + node lib/commonjs/servers/model/preprocess -i file.cif -ob file.bcif -To see all available commands, use ``node lib/servers/model/preprocess -h``. +To see all available commands, use ``node lib/commonjs/servers/model/preprocess -h``. Or diff --git a/src/extensions/cellpack/color/generate.ts b/src/extensions/cellpack/color/generate.ts index 247324b76fc4d35d411cdf1730cfc3f5e0e12f57..1d115228d9773eefb6f13cbcb24bb4b597e3a5f6 100644 --- a/src/extensions/cellpack/color/generate.ts +++ b/src/extensions/cellpack/color/generate.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ diff --git a/src/extensions/cellpack/color/provided.ts b/src/extensions/cellpack/color/provided.ts index a24ecabedb9f9c105dbd8e1b6a295a0fbb96fd63..6e035e830787e8bd11c4b8a959c58a5a4ac62073 100644 --- a/src/extensions/cellpack/color/provided.ts +++ b/src/extensions/cellpack/color/provided.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ diff --git a/src/extensions/cellpack/curve.ts b/src/extensions/cellpack/curve.ts index 83358584a5ce5c2c098d928a398dc328fbbcc409..5229ef4320d1afc8d09ad18448d8fdf61722b7cf 100644 --- a/src/extensions/cellpack/curve.ts +++ b/src/extensions/cellpack/curve.ts @@ -1,7 +1,7 @@ /** - * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * - * @author Ludovic Autin <autin@scripps.edu> + * @author Ludovic Autin <ludovic.autin@gmail.com> * @author Alexander Rose <alexander.rose@weirdbyte.de> */ diff --git a/src/extensions/cellpack/data.ts b/src/extensions/cellpack/data.ts index f7d8e469c9ed2c76d9003774aba7ee57addaff9b..6764ddc7faaebcfeda0f9dc9c128fae3c8be629b 100644 --- a/src/extensions/cellpack/data.ts +++ b/src/extensions/cellpack/data.ts @@ -13,16 +13,27 @@ export interface CellPack { export interface CellPacking { name: string, - location: 'surface' | 'interior' | 'cytoplasme', + location: 'surface' | 'interior' | 'cytoplasme' ingredients: Packing['ingredients'] + compartment?: CellCompartment } -// +export interface CellCompartment { + filename?: string + geom_type?: 'raw' | 'file' | 'sphere' | 'mb' | 'None' + compartment_primitives?: CompartmentPrimitives +} export interface Cell { recipe: Recipe + options?: RecipeOptions cytoplasme?: Packing compartments?: { [key: string]: Compartment } + mapping_ids?: { [key: number]: [number, string] } +} + +export interface RecipeOptions { + resultfile?: string } export interface Recipe { @@ -35,8 +46,29 @@ export interface Recipe { export interface Compartment { surface?: Packing interior?: Packing + geom?: unknown + geom_type?: 'raw' | 'file' | 'sphere' | 'mb' | 'None' + mb?: CompartmentPrimitives +} + +// Primitives discribing a compartment +export const enum CompartmentPrimitiveType { + MetaBall = 0, + Sphere = 1, + Cube = 2, + Cylinder = 3, + Cone = 4, + Plane = 5, + None = 6 } +export interface CompartmentPrimitives{ + positions?: number[]; + radii?: number[]; + types?: CompartmentPrimitiveType[]; +} + + export interface Packing { ingredients: { [key: string]: Ingredient } } @@ -64,11 +96,13 @@ export interface Ingredient { [curveX: string]: unknown; /** the orientation in the membrane */ principalAxis?: Vec3; + principalVector?: Vec3; /** offset along membrane */ offset?: Vec3; ingtype?: string; color?: Vec3; confidence?: number; + Type?: string; } export interface IngredientSource { diff --git a/src/extensions/cellpack/index.ts b/src/extensions/cellpack/index.ts index e3810eff8a041766af3cd6f0cca2a4c763c07699..962d85c69b668602e1755041c345eb1799a1751e 100644 --- a/src/extensions/cellpack/index.ts +++ b/src/extensions/cellpack/index.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ diff --git a/src/extensions/cellpack/model.ts b/src/extensions/cellpack/model.ts index f52b431332b18dbde1c30a53d6ce6bf7e728995c..5715ce080984519a44ea5cf26662fca30c4b4da4 100644 --- a/src/extensions/cellpack/model.ts +++ b/src/extensions/cellpack/model.ts @@ -2,13 +2,14 @@ * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> + * @author Ludovic Autin <ludovic.autin@gmail.com> */ import { StateAction, StateBuilder, StateTransformer, State } from '../../mol-state'; import { PluginContext } from '../../mol-plugin/context'; import { PluginStateObject as PSO } from '../../mol-plugin-state/objects'; import { ParamDefinition as PD } from '../../mol-util/param-definition'; -import { Ingredient, IngredientSource, CellPacking } from './data'; +import { Ingredient, CellPacking, CompartmentPrimitives } from './data'; import { getFromPdb, getFromCellPackDB, IngredientFiles, parseCif, parsePDBfile, getStructureMean, getFromOPM } from './util'; import { Model, Structure, StructureSymmetry, StructureSelection, QueryContext, Unit, Trajectory } from '../../mol-model/structure'; import { trajectoryFromMmCIF, MmcifFormat } from '../../mol-model-formats/structure/mmcif'; @@ -17,7 +18,7 @@ import { Mat4, Vec3, Quat } from '../../mol-math/linear-algebra'; import { SymmetryOperator } from '../../mol-math/geometry'; import { Task, RuntimeContext } from '../../mol-task'; import { StateTransforms } from '../../mol-plugin-state/transforms'; -import { ParseCellPack, StructureFromCellpack, DefaultCellPackBaseUrl, StructureFromAssemblies } from './state'; +import { ParseCellPack, StructureFromCellpack, DefaultCellPackBaseUrl, StructureFromAssemblies, CreateCompartmentSphere } from './state'; import { MolScriptBuilder as MS } from '../../mol-script/language/builder'; import { getMatFromResamplePoints } from './curve'; import { compile } from '../../mol-script/runtime/query/compiler'; @@ -28,8 +29,9 @@ import { createModels } from '../../mol-model-formats/structure/basic/parser'; import { CellpackPackingPreset, CellpackMembranePreset } from './preset'; import { Asset } from '../../mol-util/assets'; import { Color } from '../../mol-util/color'; -import { readFromFile } from '../../mol-util/data-source'; import { objectForEach } from '../../mol-util/object'; +import { readFromFile } from '../../mol-util/data-source'; +import { ColorNames } from '../../mol-util/color/names'; function getCellPackModelUrl(fileName: string, baseUrl: string) { return `${baseUrl}/results/${fileName}`; @@ -41,10 +43,14 @@ class TrajectoryCache { get(id: string) { return this.map.get(id); } } -async function getModel(plugin: PluginContext, id: string, ingredient: Ingredient, baseUrl: string, trajCache: TrajectoryCache, file?: Asset.File) { +async function getModel(plugin: PluginContext, id: string, ingredient: Ingredient, + baseUrl: string, trajCache: TrajectoryCache, location: string, + file?: Asset.File +) { const assetManager = plugin.managers.asset; const modelIndex = (ingredient.source.model) ? parseInt(ingredient.source.model) : 0; - const surface = (ingredient.ingtype) ? (ingredient.ingtype === 'transmembrane') : false; + let surface = (ingredient.ingtype) ? (ingredient.ingtype === 'transmembrane') : false; + if (location === 'surface') surface = true; let trajectory = trajCache.get(id); const assets: Asset.Wrapper[] = []; if (!trajectory) { @@ -72,6 +78,7 @@ async function getModel(plugin: PluginContext, id: string, ingredient: Ingredien try { const data = await getFromOPM(plugin, id, assetManager); assets.push(data.asset); + data.pdb.id! = id.toUpperCase(); trajectory = await plugin.runTask(trajectoryFromPDB(data.pdb)); } catch (e) { // fallback to getFromPdb @@ -100,7 +107,7 @@ async function getModel(plugin: PluginContext, id: string, ingredient: Ingredien return { model, assets }; } -async function getStructure(plugin: PluginContext, model: Model, source: IngredientSource, props: { assembly?: string } = {}) { +async function getStructure(plugin: PluginContext, model: Model, source: Ingredient, props: { assembly?: string } = {}) { let structure = Structure.ofModel(model); const { assembly } = props; @@ -108,11 +115,12 @@ async function getStructure(plugin: PluginContext, model: Model, source: Ingredi structure = await plugin.runTask(StructureSymmetry.buildAssembly(structure, assembly)); } let query; - if (source.selection) { - const asymIds: string[] = source.selection.replace(' ', '').replace(':', '').split('or'); + if (source.source.selection) { + const sel = source.source.selection; + // selection can have the model ID as well. remove it + const asymIds: string[] = sel.replace(/ /g, '').replace(/:/g, '').split('or').slice(1); query = MS.struct.modifier.union([ MS.struct.generator.atomGroups({ - 'entity-test': MS.core.rel.eq([MS.ammp('entityType'), 'polymer']), 'chain-test': MS.core.set.has([MS.set(...asymIds), MS.ammp('auth_asym_id')]) }) ]); @@ -123,11 +131,11 @@ async function getStructure(plugin: PluginContext, model: Model, source: Ingredi }) ]); } - const compiled = compile<StructureSelection>(query); const result = compiled(new QueryContext(structure)); structure = StructureSelection.unionStructure(result); - + // change here if possible the label ? + // structure.label = source.name; return structure; } @@ -141,9 +149,9 @@ function getTransformLegacy(trans: Vec3, rot: Quat) { } function getTransform(trans: Vec3, rot: Quat) { - const q: Quat = Quat.create(rot[0], rot[1], rot[2], rot[3]); + const q: Quat = Quat.create(-rot[0], rot[1], rot[2], -rot[3]); const m: Mat4 = Mat4.fromQuat(Mat4.zero(), q); - const p: Vec3 = Vec3.create(trans[0], trans[1], trans[2]); + const p: Vec3 = Vec3.create(-trans[0], trans[1], trans[2]); Mat4.setTranslation(m, p); return m; } @@ -168,7 +176,7 @@ function getCurveTransforms(ingredient: Ingredient) { for (let i = 0; i < n; ++i) { const cname = `curve${i}`; if (!(cname in ingredient)) { - // console.warn(`Expected '${cname}' in ingredient`) + console.warn(`Expected '${cname}' in ingredient`); continue; } const _points = ingredient[cname] as Vec3[]; @@ -179,7 +187,7 @@ function getCurveTransforms(ingredient: Ingredient) { // test for resampling const distance: number = Vec3.distance(_points[0], _points[1]); if (distance >= segmentLength + 2.0) { - console.info(distance); + // console.info(distance); resampling = true; } const points = new Float32Array(_points.length * 3); @@ -190,8 +198,8 @@ function getCurveTransforms(ingredient: Ingredient) { return instances; } -function getAssembly(transforms: Mat4[], structure: Structure) { - const builder = Structure.Builder(); +function getAssembly(name: string, transforms: Mat4[], structure: Structure) { + const builder = Structure.Builder({ label: name }); const { units } = structure; for (let i = 0, il = transforms.length; i < il; ++i) { @@ -307,13 +315,13 @@ async function getCurve(plugin: PluginContext, name: string, ingredient: Ingredi }); const curveModel = await plugin.runTask(curveModelTask); - return getStructure(plugin, curveModel, ingredient.source); + // ingredient.source.selection = undefined; + return getStructure(plugin, curveModel, ingredient); } -async function getIngredientStructure(plugin: PluginContext, ingredient: Ingredient, baseUrl: string, ingredientFiles: IngredientFiles, trajCache: TrajectoryCache) { +async function getIngredientStructure(plugin: PluginContext, ingredient: Ingredient, baseUrl: string, ingredientFiles: IngredientFiles, trajCache: TrajectoryCache, location: 'surface' | 'interior' | 'cytoplasme') { const { name, source, results, nbCurve } = ingredient; if (source.pdb === 'None') return; - const file = ingredientFiles[source.pdb]; if (!file) { // TODO can these be added to the library? @@ -325,13 +333,13 @@ async function getIngredientStructure(plugin: PluginContext, ingredient: Ingredi } // model id in case structure is NMR - const { model, assets } = await getModel(plugin, source.pdb || name, ingredient, baseUrl, trajCache, file); + const { model, assets } = await getModel(plugin, source.pdb || name, ingredient, baseUrl, trajCache, location, file); if (!model) return; - let structure: Structure; if (nbCurve) { structure = await getCurve(plugin, name, ingredient, getCurveTransforms(ingredient), model); } else { + if ((!results || results.length === 0)) return; let bu: string|undefined = source.bu ? source.bu : undefined; if (bu) { if (bu === 'AU') { @@ -340,10 +348,11 @@ async function getIngredientStructure(plugin: PluginContext, ingredient: Ingredi bu = bu.slice(2); } } - structure = await getStructure(plugin, model, source, { assembly: bu }); + structure = await getStructure(plugin, model, ingredient, { assembly: bu }); // transform with offset and pcp let legacy: boolean = true; - if (ingredient.offset || ingredient.principalAxis) { + const pcp = ingredient.principalVector ? ingredient.principalVector : ingredient.principalAxis; + if (pcp) { legacy = false; const structureMean = getStructureMean(structure); Vec3.negate(structureMean, structureMean); @@ -351,38 +360,44 @@ async function getIngredientStructure(plugin: PluginContext, ingredient: Ingredi Mat4.setTranslation(m1, structureMean); structure = Structure.transform(structure, m1); if (ingredient.offset) { - if (!Vec3.exactEquals(ingredient.offset, Vec3.zero())) { + const o: Vec3 = Vec3.create(ingredient.offset[0], ingredient.offset[1], ingredient.offset[2]); + if (!Vec3.exactEquals(o, Vec3.zero())) { // -1, 1, 4e-16 ?? + if (location !== 'surface') { + Vec3.negate(o, o); + } const m: Mat4 = Mat4.identity(); - Mat4.setTranslation(m, ingredient.offset); + Mat4.setTranslation(m, o); structure = Structure.transform(structure, m); } } - if (ingredient.principalAxis) { - if (!Vec3.exactEquals(ingredient.principalAxis, Vec3.unitZ)) { + if (pcp) { + const p: Vec3 = Vec3.create(pcp[0], pcp[1], pcp[2]); + if (!Vec3.exactEquals(p, Vec3.unitZ)) { const q: Quat = Quat.identity(); - Quat.rotationTo(q, ingredient.principalAxis, Vec3.unitZ); + Quat.rotationTo(q, p, Vec3.unitZ); const m: Mat4 = Mat4.fromQuat(Mat4.zero(), q); structure = Structure.transform(structure, m); } } } - structure = getAssembly(getResultTransforms(results, legacy), structure); + + structure = getAssembly(name, getResultTransforms(results, legacy), structure); } return { structure, assets }; } + export function createStructureFromCellPack(plugin: PluginContext, packing: CellPacking, baseUrl: string, ingredientFiles: IngredientFiles) { return Task.create('Create Packing Structure', async ctx => { - const { ingredients, name } = packing; + const { ingredients, location, name } = packing; const assets: Asset.Wrapper[] = []; const trajCache = new TrajectoryCache(); const structures: Structure[] = []; const colors: Color[] = []; - let skipColors: boolean = false; for (const iName in ingredients) { if (ctx.shouldUpdate) await ctx.update(iName); - const ingredientStructure = await getIngredientStructure(plugin, ingredients[iName], baseUrl, ingredientFiles, trajCache); + const ingredientStructure = await getIngredientStructure(plugin, ingredients[iName], baseUrl, ingredientFiles, trajCache, location); if (ingredientStructure) { structures.push(ingredientStructure.structure); assets.push(...ingredientStructure.assets); @@ -390,7 +405,7 @@ export function createStructureFromCellPack(plugin: PluginContext, packing: Cell if (c) { colors.push(Color.fromNormalizedRgb(c[0], c[1], c[2])); } else { - skipColors = true; + colors.push(Color.fromNormalizedRgb(1, 0, 0)); } } } @@ -414,21 +429,20 @@ export function createStructureFromCellPack(plugin: PluginContext, packing: Cell } if (ctx.shouldUpdate) await ctx.update(`${name} - structure`); - const structure = Structure.create(units); + const structure = Structure.create(units, { label: name + '.' + location }); for (let i = 0, il = structure.models.length; i < il; ++i) { Model.TrajectoryInfo.set(structure.models[i], { size: il, index: i }); } - return { structure, assets, colors: skipColors ? undefined : colors }; + return { structure, assets, colors: colors }; }); } async function handleHivRna(plugin: PluginContext, packings: CellPacking[], baseUrl: string) { for (let i = 0, il = packings.length; i < il; ++i) { - if (packings[i].name === 'HIV1_capsid_3j3q_PackInner_0_1_0') { + if (packings[i].name === 'HIV1_capsid_3j3q_PackInner_0_1_0' || packings[i].name === 'HIV_capsid') { const url = Asset.getUrlAsset(plugin.managers.asset, `${baseUrl}/extras/rna_allpoints.json`); const json = await plugin.runTask(plugin.managers.asset.resolve(url, 'json', false)); const points = json.data.points as number[]; - const curve0: Vec3[] = []; for (let j = 0, jl = points.length; j < jl; j += 3) { curve0.push(Vec3.fromArray(Vec3(), points, j)); @@ -465,7 +479,8 @@ async function loadMembrane(plugin: PluginContext, name: string, state: State, p } } } - + let legacy_membrane: boolean = false; // temporary variable until all membrane are converted to the new correct cif format + let geometry_membrane: boolean = false; // membrane can be a mesh geometry let b = state.build().toRoot(); if (file) { if (file.name.endsWith('.cif')) { @@ -474,27 +489,82 @@ async function loadMembrane(plugin: PluginContext, name: string, state: State, p b = b.apply(StateTransforms.Data.ReadFile, { file, isBinary: true, label: file.name }, { state: { isGhost: true } }); } } else { - const url = Asset.getUrlAsset(plugin.managers.asset, `${params.baseUrl}/membranes/${name}.bcif`); - b = b.apply(StateTransforms.Data.Download, { url, isBinary: true, label: name }, { state: { isGhost: true } }); + if (name.toLowerCase().endsWith('.bcif')) { + const url = Asset.getUrlAsset(plugin.managers.asset, `${params.baseUrl}/membranes/${name}`); + b = b.apply(StateTransforms.Data.Download, { url, isBinary: true, label: name }, { state: { isGhost: true } }); + } else if (name.toLowerCase().endsWith('.cif')) { + const url = Asset.getUrlAsset(plugin.managers.asset, `${params.baseUrl}/membranes/${name}`); + b = b.apply(StateTransforms.Data.Download, { url, isBinary: false, label: name }, { state: { isGhost: true } }); + } else if (name.toLowerCase().endsWith('.ply')) { + const url = Asset.getUrlAsset(plugin.managers.asset, `${params.baseUrl}/geometries/${name}`); + b = b.apply(StateTransforms.Data.Download, { url, isBinary: false, label: name }, { state: { isGhost: true } }); + geometry_membrane = true; + } else { + const url = Asset.getUrlAsset(plugin.managers.asset, `${params.baseUrl}/membranes/${name}.bcif`); + b = b.apply(StateTransforms.Data.Download, { url, isBinary: true, label: name }, { state: { isGhost: true } }); + legacy_membrane = true; + } } - - const membrane = await b.apply(StateTransforms.Data.ParseCif, undefined, { state: { isGhost: true } }) - .apply(StateTransforms.Model.TrajectoryFromMmCif, undefined, { state: { isGhost: true } }) - .apply(StateTransforms.Model.ModelFromTrajectory, undefined, { state: { isGhost: true } }) - .apply(StructureFromAssemblies, undefined, { state: { isGhost: true } }) - .commit({ revertOnError: true }); - - const membraneParams = { - representation: params.preset.representation, + const props = { + type: { + name: 'assembly' as const, + params: { id: '1' } + } }; + if (legacy_membrane) { + // old membrane + const membrane = await b.apply(StateTransforms.Data.ParseCif, undefined, { state: { isGhost: true } }) + .apply(StateTransforms.Model.TrajectoryFromMmCif, undefined, { state: { isGhost: true } }) + .apply(StateTransforms.Model.ModelFromTrajectory, undefined, { state: { isGhost: true } }) + .apply(StructureFromAssemblies, undefined, { state: { isGhost: true } }) + .commit({ revertOnError: true }); + const membraneParams = { + representation: params.preset.representation, + }; + await CellpackMembranePreset.apply(membrane, membraneParams, plugin); + } else if (geometry_membrane) { + await b.apply(StateTransforms.Data.ParsePly, undefined, { state: { isGhost: true } }) + .apply(StateTransforms.Model.ShapeFromPly) + .apply(StateTransforms.Representation.ShapeRepresentation3D, { xrayShaded: true, + doubleSided: true, coloring: { name: 'uniform', params: { color: ColorNames.orange } } }) + .commit({ revertOnError: true }); + } else { + const membrane = await b.apply(StateTransforms.Data.ParseCif, undefined, { state: { isGhost: true } }) + .apply(StateTransforms.Model.TrajectoryFromMmCif, undefined, { state: { isGhost: true } }) + .apply(StateTransforms.Model.ModelFromTrajectory, undefined, { state: { isGhost: true } }) + .apply(StateTransforms.Model.StructureFromModel, props, { state: { isGhost: true } }) + .commit({ revertOnError: true }); + const membraneParams = { + representation: params.preset.representation, + }; + await CellpackMembranePreset.apply(membrane, membraneParams, plugin); + } +} - await CellpackMembranePreset.apply(membrane, membraneParams, plugin); +async function handleMembraneSpheres(state: State, primitives: CompartmentPrimitives) { + const nSpheres = primitives.positions!.length / 3; + // console.log('ok mb ', nSpheres); + // TODO : take in account the type of the primitives. + for (let j = 0; j < nSpheres; j++) { + await state.build() + .toRoot() + .apply(CreateCompartmentSphere, { + center: Vec3.create( + primitives.positions![j * 3 + 0], + primitives.positions![j * 3 + 1], + primitives.positions![j * 3 + 2] + ), + radius: primitives!.radii![j] + }) + .commit(); + } } async function loadPackings(plugin: PluginContext, runtime: RuntimeContext, state: State, params: LoadCellPackModelParams) { const ingredientFiles = params.ingredients || []; let cellPackJson: StateBuilder.To<PSO.Format.Json, StateTransformer<PSO.Data.String, PSO.Format.Json>>; + let resultsFile: Asset.File | null = params.results; if (params.source.name === 'id') { const url = Asset.getUrlAsset(plugin.managers.asset, getCellPackModelUrl(params.source.params, params.baseUrl)); cellPackJson = state.build().toRoot() @@ -506,29 +576,36 @@ async function loadPackings(plugin: PluginContext, runtime: RuntimeContext, stat return; } - let jsonFile: Asset.File; + let modelFile: Asset.File; if (file.name.toLowerCase().endsWith('.zip')) { const data = await readFromFile(file.file, 'zip').runInContext(runtime); - jsonFile = Asset.File(new File([data['model.json']], 'model.json')); + if (data['model.json']) { + modelFile = Asset.File(new File([data['model.json']], 'model.json')); + } else { + throw new Error('model.json missing from zip file'); + } + if (data['results.bin']) { + resultsFile = Asset.File(new File([data['results.bin']], 'results.bin')); + } objectForEach(data, (v, k) => { if (k === 'model.json') return; + if (k === 'results.bin') return; ingredientFiles.push(Asset.File(new File([v], k))); }); } else { - jsonFile = file; + modelFile = file; } - cellPackJson = state.build().toRoot() - .apply(StateTransforms.Data.ReadFile, { file: jsonFile, isBinary: false, label: jsonFile.name }, { state: { isGhost: true } }); + .apply(StateTransforms.Data.ReadFile, { file: modelFile, isBinary: false, label: modelFile.name }, { state: { isGhost: true } }); } const cellPackBuilder = cellPackJson .apply(StateTransforms.Data.ParseJson, undefined, { state: { isGhost: true } }) - .apply(ParseCellPack); + .apply(ParseCellPack, { resultsFile, baseUrl: params.baseUrl }); const cellPackObject = await state.updateTree(cellPackBuilder).runInContext(runtime); - const { packings } = cellPackObject.obj!.data; + const { packings } = cellPackObject.obj!.data; await handleHivRna(plugin, packings, params.baseUrl); for (let i = 0, il = packings.length; i < il; ++i) { @@ -544,8 +621,30 @@ async function loadPackings(plugin: PluginContext, runtime: RuntimeContext, stat representation: params.preset.representation, }; await CellpackPackingPreset.apply(packing, packingParams, plugin); - if (packings[i].location === 'surface' && params.membrane) { - await loadMembrane(plugin, packings[i].name, state, params); + if (packings[i].compartment) { + if (params.membrane === 'lipids') { + if (packings[i].compartment!.geom_type) { + if (packings[i].compartment!.geom_type === 'file') { + // TODO: load mesh files or vertex,faces data + await loadMembrane(plugin, packings[i].compartment!.filename!, state, params); + } else if (packings[i].compartment!.compartment_primitives) { + await handleMembraneSpheres(state, packings[i].compartment!.compartment_primitives!); + } + } else { + // try loading membrane from repo as a bcif file or from the given list of files. + if (params.membrane === 'lipids') { + await loadMembrane(plugin, packings[i].name, state, params); + } + } + } else if (params.membrane === 'geometry') { + if (packings[i].compartment!.compartment_primitives) { + await handleMembraneSpheres(state, packings[i].compartment!.compartment_primitives!); + } else if (packings[i].compartment!.geom_type === 'file') { + if (packings[i].compartment!.filename!.toLowerCase().endsWith('.ply')) { + await loadMembrane(plugin, packings[i].compartment!.filename!, state, params); + } + } + } } } } @@ -555,19 +654,19 @@ const LoadCellPackModelParams = { 'id': PD.Select('InfluenzaModel2.json', [ ['blood_hiv_immature_inside.json', 'Blood HIV immature'], ['HIV_immature_model.json', 'HIV immature'], - ['BloodHIV1.0_mixed_fixed_nc1.cpr', 'Blood HIV'], - ['HIV-1_0.1.6-8_mixed_radii_pdb.cpr', 'HIV'], + ['Blood_HIV.json', 'Blood HIV'], + ['HIV-1_0.1.6-8_mixed_radii_pdb.json', 'HIV'], ['influenza_model1.json', 'Influenza envelope'], - ['InfluenzaModel2.json', 'Influenza Complete'], + ['InfluenzaModel2.json', 'Influenza complete'], ['ExosomeModel.json', 'Exosome Model'], - ['Mycoplasma1.5_mixed_pdb_fixed.cpr', 'Mycoplasma simple'], - ['MycoplasmaModel.json', 'Mycoplasma WholeCell model'], + ['MycoplasmaGenitalium.json', 'Mycoplasma Genitalium curated model'], ] as const, { description: 'Download the model definition with `id` from the server at `baseUrl.`' }), - 'file': PD.File({ accept: '.json,.cpr,.zip', description: 'Open model definition from .json/.cpr file or open .zip file containing model definition plus ingredients.' }), + 'file': PD.File({ accept: '.json,.cpr,.zip', description: 'Open model definition from .json/.cpr file or open .zip file containing model definition plus ingredients.', label: 'Recipe file' }), }, { options: [['id', 'Id'], ['file', 'File']] }), baseUrl: PD.Text(DefaultCellPackBaseUrl), - membrane: PD.Boolean(true), - ingredients: PD.FileList({ accept: '.cif,.bcif,.pdb', label: 'Ingredients' }), + results: PD.File({ accept: '.bin', description: 'open results file in binary format from cellpackgpu for the specified recipe', label: 'Results file' }), + membrane: PD.Select('lipids', PD.arrayToOptions(['lipids', 'geometry', 'none'])), + ingredients: PD.FileList({ accept: '.cif,.bcif,.pdb', label: 'Ingredient files' }), preset: PD.Group({ traceOnly: PD.Boolean(false), representation: PD.Select('gaussian-surface', PD.arrayToOptions(['spacefill', 'gaussian-surface', 'point', 'orientation'])) @@ -581,4 +680,4 @@ export const LoadCellPackModel = StateAction.build({ from: PSO.Root })(({ state, params }, ctx: PluginContext) => Task.create('CellPack Loader', async taskCtx => { await loadPackings(ctx, taskCtx, state, params); -})); \ No newline at end of file +})); diff --git a/src/extensions/cellpack/preset.ts b/src/extensions/cellpack/preset.ts index 0cd3ae55bcf8daf75fdf58bfe2f6854fe5bbd402..895f2fb1ab7d085a297bcdfe5cc350dcd06e1e43 100644 --- a/src/extensions/cellpack/preset.ts +++ b/src/extensions/cellpack/preset.ts @@ -1,7 +1,8 @@ /** - * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> + * @author Ludovic Autin <ludovic.autin@gmail.com> */ import { StateObjectRef } from '../../mol-state'; @@ -9,8 +10,6 @@ import { StructureRepresentationPresetProvider, presetStaticComponent } from '.. import { ParamDefinition as PD } from '../../mol-util/param-definition'; import { ColorNames } from '../../mol-util/color/names'; import { CellPackGenerateColorThemeProvider } from './color/generate'; -import { CellPackInfoProvider } from './property'; -import { CellPackProvidedColorThemeProvider } from './color/provided'; export const CellpackPackingPresetParams = { traceOnly: PD.Boolean(true), @@ -42,8 +41,8 @@ export const CellpackPackingPreset = StructureRepresentationPresetProvider({ Object.assign(reprProps, { sizeFactor: 2 }); } - const info = structureCell.obj?.data && CellPackInfoProvider.get(structureCell.obj?.data).value; - const color = info?.colors ? CellPackProvidedColorThemeProvider.name : CellPackGenerateColorThemeProvider.name; + // default is generated + const color = CellPackGenerateColorThemeProvider.name; const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, {}); const representations = { @@ -92,4 +91,4 @@ export const CellpackMembranePreset = StructureRepresentationPresetProvider({ return { components, representations }; } -}); \ No newline at end of file +}); diff --git a/src/extensions/cellpack/property.ts b/src/extensions/cellpack/property.ts index c2b8550e588c715fba551d604afcd0c164ac212d..9350e6501e54182138b5b7c3f45bc7488fbe6cc3 100644 --- a/src/extensions/cellpack/property.ts +++ b/src/extensions/cellpack/property.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ @@ -34,4 +34,4 @@ export const CellPackInfoProvider: CustomStructureProperty.Provider<typeof CellP value: { ...CellPackInfoParams.info.defaultValue, ...props.info } }; } -}); \ No newline at end of file +}); diff --git a/src/extensions/cellpack/representation.ts b/src/extensions/cellpack/representation.ts new file mode 100644 index 0000000000000000000000000000000000000000..261e3a3c4f50d52d81fe0cfb8fdc6b318ed832f2 --- /dev/null +++ b/src/extensions/cellpack/representation.ts @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + * @author Ludovic Autin <ludovic.autin@gmail.com> + */ + +import { ShapeRepresentation } from '../../mol-repr/shape/representation'; +import { Shape } from '../../mol-model/shape'; +import { ColorNames } from '../../mol-util/color/names'; +import { RuntimeContext } from '../../mol-task'; +import { ParamDefinition as PD } from '../../mol-util/param-definition'; +import { Mesh } from '../../mol-geo/geometry/mesh/mesh'; +import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder'; +// import { Polyhedron, DefaultPolyhedronProps } from '../../mol-geo/primitive/polyhedron'; +// import { Icosahedron } from '../../mol-geo/primitive/icosahedron'; +import { Sphere } from '../../mol-geo/primitive/sphere'; +import { Mat4, Vec3 } from '../../mol-math/linear-algebra'; +import { RepresentationParamsGetter, Representation, RepresentationContext } from '../../mol-repr/representation'; + + +interface MembraneSphereData { + radius: number + center: Vec3 +} + + +const MembraneSphereParams = { + ...Mesh.Params, + cellColor: PD.Color(ColorNames.orange), + cellScale: PD.Numeric(2, { min: 0.1, max: 5, step: 0.1 }), + radius: PD.Numeric(2, { min: 0.1, max: 5, step: 0.1 }), + center: PD.Vec3(Vec3.create(0, 0, 0)), + quality: { ...Mesh.Params.quality, isEssential: false }, +}; + +type MeshParams = typeof MembraneSphereParams + +const MembraneSphereVisuals = { + 'mesh': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<MembraneSphereData, MeshParams>) => ShapeRepresentation(getMBShape, Mesh.Utils), +}; + +export const MBParams = { + ...MembraneSphereParams +}; +export type MBParams = typeof MBParams +export type UnitcellProps = PD.Values<MBParams> + +function getMBMesh(data: MembraneSphereData, props: UnitcellProps, mesh?: Mesh) { + const state = MeshBuilder.createState(256, 128, mesh); + const radius = props.radius; + const asphere = Sphere(3); + const trans: Mat4 = Mat4.identity(); + Mat4.fromScaling(trans, Vec3.create(radius, radius, radius)); + state.currentGroup = 1; + MeshBuilder.addPrimitive(state, trans, asphere); + const m = MeshBuilder.getMesh(state); + return m; +} + +function getMBShape(ctx: RuntimeContext, data: MembraneSphereData, props: UnitcellProps, shape?: Shape<Mesh>) { + const geo = getMBMesh(data, props, shape && shape.geometry); + const label = 'mb'; + return Shape.create(label, data, geo, () => props.cellColor, () => 1, () => label); +} + +export type MBRepresentation = Representation<MembraneSphereData, MBParams> +export function MBRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<MembraneSphereData, MBParams>): MBRepresentation { + return Representation.createMulti('MB', ctx, getParams, Representation.StateBuilder, MembraneSphereVisuals as unknown as Representation.Def<MembraneSphereData, MBParams>); +} \ No newline at end of file diff --git a/src/extensions/cellpack/state.ts b/src/extensions/cellpack/state.ts index 9be5b9ede505b01e6e22f4961493b226a52d20e0..131c32e90ce73c12257334681bd7fff6314e10b8 100644 --- a/src/extensions/cellpack/state.ts +++ b/src/extensions/cellpack/state.ts @@ -1,7 +1,8 @@ /** - * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> + * @author Ludovic Autin <ludovic.autin@gmail.com> */ import { PluginStateObject as PSO, PluginStateTransform } from '../../mol-plugin-state/objects'; @@ -15,9 +16,13 @@ import { PluginContext } from '../../mol-plugin/context'; import { CellPackInfoProvider } from './property'; import { Structure, StructureSymmetry, Unit, Model } from '../../mol-model/structure'; import { ModelSymmetry } from '../../mol-model-formats/structure/property/symmetry'; +import { Vec3, Quat } from '../../mol-math/linear-algebra'; +import { StateTransformer } from '../../mol-state'; +import { MBRepresentation, MBParams } from './representation'; +import { IsNativeEndianLittle, flipByteOrder } from '../../mol-io/common/binary'; +import { getFloatValue } from './util'; -export const DefaultCellPackBaseUrl = 'https://mesoscope.scripps.edu/data/cellPACK_data/cellPACK_database_1.1.0/'; - +export const DefaultCellPackBaseUrl = 'https://raw.githubusercontent.com/mesoscope/cellPACK_data/master/cellPACK_database_1.1.0'; export class CellPack extends PSO.Create<_CellPack>({ name: 'CellPack', typeClass: 'Object' }) { } export { ParseCellPack }; @@ -26,26 +31,173 @@ const ParseCellPack = PluginStateTransform.BuiltIn({ name: 'parse-cellpack', display: { name: 'Parse CellPack', description: 'Parse CellPack from JSON data' }, from: PSO.Format.Json, - to: CellPack + to: CellPack, + params: a => { + return { + resultsFile: PD.File({ accept: '.bin' }), + baseUrl: PD.Text(DefaultCellPackBaseUrl) + }; + } })({ - apply({ a }) { + apply({ a, params, cache }, plugin: PluginContext) { return Task.create('Parse CellPack', async ctx => { const cell = a.data as Cell; - + let counter_id = 0; + let fiber_counter_id = 0; + let comp_counter = 0; const packings: CellPacking[] = []; const { compartments, cytoplasme } = cell; + if (!cell.mapping_ids) cell.mapping_ids = {}; + if (cytoplasme) { + packings.push({ name: 'Cytoplasme', location: 'cytoplasme', ingredients: cytoplasme.ingredients }); + for (const iName in cytoplasme.ingredients) { + if (cytoplasme.ingredients[iName].ingtype === 'fiber') { + cell.mapping_ids[-(fiber_counter_id + 1)] = [comp_counter, iName]; + if (!cytoplasme.ingredients[iName].nbCurve) cytoplasme.ingredients[iName].nbCurve = 0; + fiber_counter_id++; + } else { + cell.mapping_ids[counter_id] = [comp_counter, iName]; + if (!cytoplasme.ingredients[iName].results) { cytoplasme.ingredients[iName].results = []; } + counter_id++; + } + } + comp_counter++; + } if (compartments) { for (const name in compartments) { const { surface, interior } = compartments[name]; - if (surface) packings.push({ name, location: 'surface', ingredients: surface.ingredients }); - if (interior) packings.push({ name, location: 'interior', ingredients: interior.ingredients }); + let filename = ''; + if (compartments[name].geom_type === 'file') { + filename = (compartments[name].geom) ? compartments[name].geom as string : ''; + } + const compartment = { filename: filename, geom_type: compartments[name].geom_type, compartment_primitives: compartments[name].mb }; + if (surface) { + packings.push({ name, location: 'surface', ingredients: surface.ingredients, compartment: compartment }); + for (const iName in surface.ingredients) { + if (surface.ingredients[iName].ingtype === 'fiber') { + cell.mapping_ids[-(fiber_counter_id + 1)] = [comp_counter, iName]; + if (!surface.ingredients[iName].nbCurve) surface.ingredients[iName].nbCurve = 0; + fiber_counter_id++; + } else { + cell.mapping_ids[counter_id] = [comp_counter, iName]; + if (!surface.ingredients[iName].results) { surface.ingredients[iName].results = []; } + counter_id++; + } + } + comp_counter++; + } + if (interior) { + if (!surface) packings.push({ name, location: 'interior', ingredients: interior.ingredients, compartment: compartment }); + else packings.push({ name, location: 'interior', ingredients: interior.ingredients }); + for (const iName in interior.ingredients) { + if (interior.ingredients[iName].ingtype === 'fiber') { + cell.mapping_ids[-(fiber_counter_id + 1)] = [comp_counter, iName]; + if (!interior.ingredients[iName].nbCurve) interior.ingredients[iName].nbCurve = 0; + fiber_counter_id++; + } else { + cell.mapping_ids[counter_id] = [comp_counter, iName]; + if (!interior.ingredients[iName].results) { interior.ingredients[iName].results = []; } + counter_id++; + } + } + comp_counter++; + } } } - if (cytoplasme) packings.push({ name: 'Cytoplasme', location: 'cytoplasme', ingredients: cytoplasme.ingredients }); + const { options } = cell; + let resultsAsset: Asset.Wrapper<'binary'> | undefined; + if (params.resultsFile) { + resultsAsset = await plugin.runTask(plugin.managers.asset.resolve(params.resultsFile, 'binary', true)); + } else if (options?.resultfile) { + const url = `${params.baseUrl}/results/${options.resultfile}`; + resultsAsset = await plugin.runTask(plugin.managers.asset.resolve(Asset.getUrlAsset(plugin.managers.asset, url), 'binary', true)); + } + if (resultsAsset) { + (cache as any).asset = resultsAsset; + const results = resultsAsset.data; + // flip the byte order if needed + const buffer = IsNativeEndianLittle ? results.buffer : flipByteOrder(results, 4); + const numbers = new DataView(buffer); + const ninst = getFloatValue(numbers, 0); + const npoints = getFloatValue(numbers, 4); + const ncurve = getFloatValue(numbers, 8); + + let offset = 12; + + if (ninst !== 0) { + const pos = new Float32Array(buffer, offset, ninst * 4); + offset += ninst * 4 * 4; + const quat = new Float32Array(buffer, offset, ninst * 4); + offset += ninst * 4 * 4; + + for (let i = 0; i < ninst; i++) { + const x: number = pos[i * 4 + 0]; + const y: number = pos[i * 4 + 1]; + const z: number = pos[i * 4 + 2]; + const ingr_id = pos[i * 4 + 3] as number; + const pid = cell.mapping_ids![ingr_id]; + if (!packings[pid[0]].ingredients[pid[1]].results) { + packings[pid[0]].ingredients[pid[1]].results = []; + } + packings[pid[0]].ingredients[pid[1]].results.push([Vec3.create(x, y, z), + Quat.create(quat[i * 4 + 0], quat[i * 4 + 1], quat[i * 4 + 2], quat[i * 4 + 3])]); + } + } + if (npoints !== 0) { + const ctr_pos = new Float32Array(buffer, offset, npoints * 4); + offset += npoints * 4 * 4; + offset += npoints * 4 * 4; + const ctr_info = new Float32Array(buffer, offset, npoints * 4); + offset += npoints * 4 * 4; + const curve_ids = new Float32Array(buffer, offset, ncurve * 4); + offset += ncurve * 4 * 4; + + let counter = 0; + let ctr_points: Vec3[] = []; + let prev_ctype = 0; + let prev_cid = 0; + + for (let i = 0; i < npoints; i++) { + const x: number = -ctr_pos[i * 4 + 0]; + const y: number = ctr_pos[i * 4 + 1]; + const z: number = ctr_pos[i * 4 + 2]; + const cid: number = ctr_info[i * 4 + 0]; // curve id + const ctype: number = curve_ids[cid * 4 + 0]; // curve type + // cid 148 165 -1 0 + // console.log("cid ",cid,ctype,prev_cid,prev_ctype);//165,148 + if (prev_ctype !== ctype) { + const pid = cell.mapping_ids![-prev_ctype - 1]; + const cname = `curve${counter}`; + packings[pid[0]].ingredients[pid[1]].nbCurve = counter + 1; + packings[pid[0]].ingredients[pid[1]][cname] = ctr_points; + ctr_points = []; + counter = 0; + } else if (prev_cid !== cid) { + ctr_points = []; + const pid = cell.mapping_ids![-prev_ctype - 1]; + const cname = `curve${counter}`; + packings[pid[0]].ingredients[pid[1]][cname] = ctr_points; + counter += 1; + } + ctr_points.push(Vec3.create(x, y, z)); + prev_ctype = ctype; + prev_cid = cid; + } + + // do the last one + const pid = cell.mapping_ids![-prev_ctype - 1]; + const cname = `curve${counter}`; + packings[pid[0]].ingredients[pid[1]].nbCurve = counter + 1; + packings[pid[0]].ingredients[pid[1]][cname] = ctr_points; + } + } return new CellPack({ cell, packings }); }); - } + }, + dispose({ cache }) { + ((cache as any)?.asset as Asset.Wrapper | undefined)?.dispose(); + }, }); export { StructureFromCellpack }; @@ -77,9 +229,8 @@ const StructureFromCellpack = PluginStateTransform.BuiltIn({ await CellPackInfoProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset }, structure, { info: { packingsCount: a.data.packings.length, packingIndex: params.packing, colors } }); - (cache as any).assets = assets; - return new PSO.Molecule.Structure(structure, { label: packing.name }); + return new PSO.Molecule.Structure(structure, { label: packing.name + '.' + packing.location }); }); }, dispose({ b, cache }) { @@ -125,7 +276,7 @@ const StructureFromAssemblies = PluginStateTransform.BuiltIn({ const s = await StructureSymmetry.buildAssembly(initial_structure, a.id).runInContext(ctx); structures.push(s); } - const builder = Structure.Builder(); + const builder = Structure.Builder({ label: 'Membrane' }); let offsetInvariantId = 0; for (const s of structures) { let maxInvariantId = 0; @@ -148,3 +299,28 @@ const StructureFromAssemblies = PluginStateTransform.BuiltIn({ b?.data.customPropertyDescriptors.dispose(); } }); + +const CreateTransformer = StateTransformer.builderFactory('cellPACK'); +export const CreateCompartmentSphere = CreateTransformer({ + name: 'create-compartment-sphere', + display: 'CompartmentSphere', + from: PSO.Root, // or whatever data source + to: PSO.Shape.Representation3D, + params: { + center: PD.Vec3(Vec3()), + radius: PD.Numeric(1), + label: PD.Text(`Compartment Sphere`) + } +})({ + canAutoUpdate({ oldParams, newParams }) { + return true; + }, + apply({ a, params }, plugin: PluginContext) { + return Task.create('Compartment Sphere', async ctx => { + const data = params; + const repr = MBRepresentation({ webgl: plugin.canvas3d?.webgl, ...plugin.representation.structure.themes }, () => (MBParams)); + await repr.createOrUpdate({ ...params, quality: 'custom', xrayShaded: true, doubleSided: true }, data).runInContext(ctx); + return new PSO.Shape.Representation3D({ repr, sourceData: a }, { label: data.label }); + }); + } +}); \ No newline at end of file diff --git a/src/extensions/cellpack/util.ts b/src/extensions/cellpack/util.ts index 2d25a12cdf6d987033e68fa53e6d5c43318d7707..b7c8aea3053bdb970a4b68db0b8b6035fd58909f 100644 --- a/src/extensions/cellpack/util.ts +++ b/src/extensions/cellpack/util.ts @@ -1,7 +1,8 @@ /** - * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> + * @author Ludovic Autin <ludovic.autin@gmail.com> */ import { CIF } from '../../mol-io/reader/cif'; @@ -37,7 +38,7 @@ async function downloadPDB(plugin: PluginContext, url: string, id: string, asset } export async function getFromPdb(plugin: PluginContext, pdbId: string, assetManager: AssetManager) { - const { cif, asset } = await downloadCif(plugin, `https://models.rcsb.org/${pdbId.toUpperCase()}.bcif`, true, assetManager); + const { cif, asset } = await downloadCif(plugin, `https://models.rcsb.org/${pdbId}.bcif`, true, assetManager); return { mmcif: cif.blocks[0], asset }; } @@ -74,4 +75,35 @@ export function getStructureMean(structure: Structure) { } const { elementCount } = structure; return Vec3.create(xSum / elementCount, ySum / elementCount, zSum / elementCount); +} + +export function getFloatValue(value: DataView, offset: number) { + // if the last byte is a negative value (MSB is 1), the final + // float should be too + const negative = value.getInt8(offset + 2) >>> 31; + + // this is how the bytes are arranged in the byte array/DataView + // buffer + const [b0, b1, b2, exponent] = [ + // get first three bytes as unsigned since we only care + // about the last 8 bits of 32-bit js number returned by + // getUint8(). + // Should be the same as: getInt8(offset) & -1 >>> 24 + value.getUint8(offset), + value.getUint8(offset + 1), + value.getUint8(offset + 2), + + // get the last byte, which is the exponent, as a signed int + // since it's already correct + value.getInt8(offset + 3) + ]; + + let mantissa = b0 | (b1 << 8) | (b2 << 16); + if (negative) { + // need to set the most significant 8 bits to 1's since a js + // number is 32 bits but our mantissa is only 24. + mantissa |= 255 << 24; + } + + return mantissa * Math.pow(10, exponent); } \ No newline at end of file diff --git a/src/mol-math/geometry/primitives/axes3d.ts b/src/mol-math/geometry/primitives/axes3d.ts index bb70f70ef48dcb5af3a4883bf6b62e9762790c04..5db8ae4f5cd692322ebc7dcf7b58572334549f38 100644 --- a/src/mol-math/geometry/primitives/axes3d.ts +++ b/src/mol-math/geometry/primitives/axes3d.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ @@ -48,7 +48,7 @@ namespace Axes3D { return out; } - const tmpTransformMat3 = Mat3.zero(); + const tmpTransformMat3 = Mat3(); /** Transform axes with a Mat4 */ export function transform(out: Axes3D, a: Axes3D, m: Mat4): Axes3D { Vec3.transformMat4(out.origin, a.origin, m); @@ -58,6 +58,13 @@ namespace Axes3D { Vec3.transformMat3(out.dirC, a.dirC, n); return out; } + + export function scale(out: Axes3D, a: Axes3D, scale: number): Axes3D { + Vec3.scale(out.dirA, a.dirA, scale); + Vec3.scale(out.dirB, a.dirB, scale); + Vec3.scale(out.dirC, a.dirC, scale); + return out; + } } export { Axes3D }; \ No newline at end of file diff --git a/src/mol-model/structure/structure/element/loci.ts b/src/mol-model/structure/structure/element/loci.ts index 617f869a57916cf5402bb8bd18409376ee0111a2..53c2780c60f4191ab4300bb880b507b9ee490370 100644 --- a/src/mol-model/structure/structure/element/loci.ts +++ b/src/mol-model/structure/structure/element/loci.ts @@ -582,6 +582,20 @@ export namespace Loci { return PrincipalAxes.ofPositions(positions); } + export function getPrincipalAxesMany(locis: Loci[]): PrincipalAxes { + let elementCount = 0; + locis.forEach(l => { + elementCount += size(l); + }); + const positions = new Float32Array(3 * elementCount); + let offset = 0; + locis.forEach(l => { + toPositionsArray(l, positions, offset); + offset += size(l) * 3; + }); + return PrincipalAxes.ofPositions(positions); + } + function sourceIndex(unit: Unit, element: ElementIndex) { return Unit.isAtomic(unit) ? unit.model.atomicHierarchy.atomSourceIndex.value(element) diff --git a/src/mol-plugin-state/builder/structure/representation-preset.ts b/src/mol-plugin-state/builder/structure/representation-preset.ts index de9e8caf73a6b543e37c17dc5c0f7b4cc47dbdf0..6984111236fdc6a20a7e7e6542ded10ebbe4113d 100644 --- a/src/mol-plugin-state/builder/structure/representation-preset.ts +++ b/src/mol-plugin-state/builder/structure/representation-preset.ts @@ -379,6 +379,8 @@ const atomicDetail = StructureRepresentationPresetProvider({ } await update.commit({ revertOnError: true }); + await updateFocusRepr(plugin, structure, params.theme?.focus?.name ?? color, params.theme?.focus?.params ?? colorParams); + return { components, representations }; } }); diff --git a/src/mol-plugin-state/manager/structure/measurement.ts b/src/mol-plugin-state/manager/structure/measurement.ts index 4907eb3e7edf84d536604126b5811b722e8265ec..c18075a8f1319b7dbf93121d12f5347757066621 100644 --- a/src/mol-plugin-state/manager/structure/measurement.ts +++ b/src/mol-plugin-state/manager/structure/measurement.ts @@ -1,7 +1,8 @@ /** - * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> + * @author Alexander Rose <alexander.rose@weirdbyte.de> */ import { StructureElement } from '../../../mol-model/structure'; @@ -15,10 +16,13 @@ import { StatefulPluginComponent } from '../../component'; import { ParamDefinition as PD } from '../../../mol-util/param-definition'; import { MeasurementRepresentationCommonTextParams, LociLabelTextParams } from '../../../mol-repr/shape/loci/common'; import { LineParams } from '../../../mol-repr/structure/representation/line'; +import { Expression } from '../../../mol-script/language/expression'; +import { Color } from '../../../mol-util/color'; export { StructureMeasurementManager }; export const MeasurementGroupTag = 'measurement-group'; +export const MeasurementOrderLabelTag = 'measurement-order-label'; export type StructureMeasurementCell = StateObjectCell<PluginStateObject.Shape.Representation3D, StateTransform<StateTransformer<PluginStateObject.Molecule.Structure.Selections, PluginStateObject.Shape.Representation3D, any>>> @@ -35,6 +39,7 @@ export interface StructureMeasurementManagerState { angles: StructureMeasurementCell[], dihedrals: StructureMeasurementCell[], orientations: StructureMeasurementCell[], + planes: StructureMeasurementCell[], options: StructureMeasurementOptions } @@ -222,19 +227,25 @@ class StructureMeasurementManager extends StatefulPluginComponent<StructureMeasu await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } }); } - async addOrientation(a: StructureElement.Loci) { - const cellA = this.plugin.helpers.substructureParent.get(a.structure); + async addOrientation(locis: StructureElement.Loci[]) { + const selections: { key: string, ref: string, groupId?: string, expression: Expression }[] = []; + const dependsOn: string[] = []; - if (!cellA) return; + for (let i = 0, il = locis.length; i < il; ++i) { + const l = locis[i]; + const cell = this.plugin.helpers.substructureParent.get(l.structure); + if (!cell) continue; - const dependsOn = [cellA.transform.ref]; + arraySetAdd(dependsOn, cell.transform.ref); + selections.push({ key: `l${i}`, ref: cell.transform.ref, expression: StructureElement.Loci.toExpression(l) }); + } + + if (selections.length === 0) return; const update = this.getGroup(); update .apply(StateTransforms.Model.MultiStructureSelectionFromExpression, { - selections: [ - { key: 'a', ref: cellA.transform.ref, expression: StructureElement.Loci.toExpression(a) }, - ], + selections, isTransitive: true, label: 'Orientation' }, { dependsOn }) @@ -244,6 +255,69 @@ class StructureMeasurementManager extends StatefulPluginComponent<StructureMeasu await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } }); } + async addPlane(locis: StructureElement.Loci[]) { + const selections: { key: string, ref: string, groupId?: string, expression: Expression }[] = []; + const dependsOn: string[] = []; + + for (let i = 0, il = locis.length; i < il; ++i) { + const l = locis[i]; + const cell = this.plugin.helpers.substructureParent.get(l.structure); + if (!cell) continue; + + arraySetAdd(dependsOn, cell.transform.ref); + selections.push({ key: `l${i}`, ref: cell.transform.ref, expression: StructureElement.Loci.toExpression(l) }); + } + + if (selections.length === 0) return; + + const update = this.getGroup(); + update + .apply(StateTransforms.Model.MultiStructureSelectionFromExpression, { + selections, + isTransitive: true, + label: 'Plane' + }, { dependsOn }) + .apply(StateTransforms.Representation.StructureSelectionsPlane3D); + + const state = this.plugin.state.data; + await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } }); + } + + async addOrderLabels(locis: StructureElement.Loci[]) { + const update = this.getGroup(); + + const current = this.plugin.state.data.select(StateSelection.Generators.ofType(PluginStateObject.Molecule.Structure.Selections).withTag(MeasurementOrderLabelTag)); + for (const obj of current) + update.delete(obj); + + let order = 1; + for (const loci of locis) { + const cell = this.plugin.helpers.substructureParent.get(loci.structure); + if (!cell) continue; + + const dependsOn = [cell.transform.ref]; + + update + .apply(StateTransforms.Model.MultiStructureSelectionFromExpression, { + selections: [ + { key: 'a', ref: cell.transform.ref, expression: StructureElement.Loci.toExpression(loci) }, + ], + isTransitive: true, + label: 'Order' + }, { dependsOn, tags: MeasurementOrderLabelTag }) + .apply(StateTransforms.Representation.StructureSelectionsLabel3D, { + textColor: Color.fromRgb(255, 255, 255), + borderColor: Color.fromRgb(0, 0, 0), + borderWidth: 0.5, + textSize: 0.33, + customText: `${order++}` + }, { tags: MeasurementOrderLabelTag }); + } + + const state = this.plugin.state.data; + await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotLogTiming: true } }); + } + private _empty: any[] = []; private getTransforms<T extends StateTransformer<A, B, any>, A extends PluginStateObject.Molecule.Structure.Selections, B extends StateObject>(transformer: T) { const state = this.plugin.state.data; @@ -254,18 +328,26 @@ class StructureMeasurementManager extends StatefulPluginComponent<StructureMeasu } private sync() { + const labels = []; + for (const cell of this.getTransforms(StateTransforms.Representation.StructureSelectionsLabel3D) as StructureMeasurementCell[]) { + const tags = (cell.obj as any)['tags'] as string[]; + if (!tags || !tags.includes(MeasurementOrderLabelTag)) + labels.push(cell); + } + const updated = this.updateState({ - labels: this.getTransforms(StateTransforms.Representation.StructureSelectionsLabel3D), + labels, distances: this.getTransforms(StateTransforms.Representation.StructureSelectionsDistance3D), angles: this.getTransforms(StateTransforms.Representation.StructureSelectionsAngle3D), dihedrals: this.getTransforms(StateTransforms.Representation.StructureSelectionsDihedral3D), - orientations: this.getTransforms(StateTransforms.Representation.StructureSelectionsOrientation3D) + orientations: this.getTransforms(StateTransforms.Representation.StructureSelectionsOrientation3D), + planes: this.getTransforms(StateTransforms.Representation.StructureSelectionsPlane3D), }); if (updated) this.stateUpdated(); } constructor(private plugin: PluginContext) { - super({ labels: [], distances: [], angles: [], dihedrals: [], orientations: [], options: DefaultStructureMeasurementOptions }); + super({ labels: [], distances: [], angles: [], dihedrals: [], orientations: [], planes: [], options: DefaultStructureMeasurementOptions }); plugin.state.data.events.changed.subscribe(e => { if (e.inTransaction || plugin.behaviors.state.isAnimating.value) return; diff --git a/src/mol-plugin-state/manager/structure/selection.ts b/src/mol-plugin-state/manager/structure/selection.ts index a8e99b6d0f709cda0a278e8871ab4c72d5e7d531..c043a9e614cde87c2312401549f90828ff847b96 100644 --- a/src/mol-plugin-state/manager/structure/selection.ts +++ b/src/mol-plugin-state/manager/structure/selection.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> * @author Alexander Rose <alexander.rose@weirdbyte.de> @@ -22,6 +22,7 @@ import { PluginStateObject as PSO } from '../../objects'; import { UUID } from '../../../mol-util'; import { StructureRef } from './hierarchy-state'; import { Boundary } from '../../../mol-math/geometry/boundary'; +import { iterableToArray } from '../../../mol-data/util'; interface StructureSelectionManagerState { entries: Map<string, SelectionEntry>, @@ -405,14 +406,8 @@ export class StructureSelectionManager extends StatefulPluginComponent<Structure } getPrincipalAxes(): PrincipalAxes { - const elementCount = this.elementCount(); - const positions = new Float32Array(3 * elementCount); - let offset = 0; - this.entries.forEach(v => { - StructureElement.Loci.toPositionsArray(v.selection, positions, offset); - offset += StructureElement.Loci.size(v.selection) * 3; - }); - return PrincipalAxes.ofPositions(positions); + const values = iterableToArray(this.entries.values()); + return StructureElement.Loci.getPrincipalAxesMany(values.map(v => v.selection)); } modify(modifier: StructureSelectionModifier, loci: Loci) { diff --git a/src/mol-plugin-state/transforms/helpers.ts b/src/mol-plugin-state/transforms/helpers.ts index 55ca620ae5581c1021a46947a0f2b66a63d44f60..9bdd857bd67f5149b7b4f7cb029a676fbc5437d6 100644 --- a/src/mol-plugin-state/transforms/helpers.ts +++ b/src/mol-plugin-state/transforms/helpers.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ @@ -10,6 +10,7 @@ import { LabelData } from '../../mol-repr/shape/loci/label'; import { OrientationData } from '../../mol-repr/shape/loci/orientation'; import { AngleData } from '../../mol-repr/shape/loci/angle'; import { DihedralData } from '../../mol-repr/shape/loci/dihedral'; +import { PlaneData } from '../../mol-repr/shape/loci/plane'; export function getDistanceDataFromStructureSelections(s: ReadonlyArray<PluginStateObject.Molecule.Structure.SelectionEntry>): DistanceData { const lociA = s[0].loci; @@ -38,6 +39,9 @@ export function getLabelDataFromStructureSelections(s: ReadonlyArray<PluginState } export function getOrientationDataFromStructureSelections(s: ReadonlyArray<PluginStateObject.Molecule.Structure.SelectionEntry>): OrientationData { - const loci = s[0].loci; - return { locis: [loci] }; + return { locis: s.map(v => v.loci) }; +} + +export function getPlaneDataFromStructureSelections(s: ReadonlyArray<PluginStateObject.Molecule.Structure.SelectionEntry>): PlaneData { + return { locis: s.map(v => v.loci) }; } \ No newline at end of file diff --git a/src/mol-plugin-state/transforms/model.ts b/src/mol-plugin-state/transforms/model.ts index 204f783e5ddafc3a2af4a14f613d02c37e029101..ea75c491b015fd1f6967dd0b3ac20f1d473d5ee1 100644 --- a/src/mol-plugin-state/transforms/model.ts +++ b/src/mol-plugin-state/transforms/model.ts @@ -645,7 +645,8 @@ const MultiStructureSelectionFromExpression = PluginStateTransform.BuiltIn({ totalSize += StructureElement.Loci.size(loci.loci); continue; - } if (entry.expression !== sel.expression) { + } + if (entry.expression !== sel.expression) { recreate = true; } else { // TODO: properly support "transitive" queries. For that Structure.areUnitAndIndicesEqual needs to be fixed; diff --git a/src/mol-plugin-state/transforms/representation.ts b/src/mol-plugin-state/transforms/representation.ts index 6c1d37b0fd74453bf47878dca70639d0ddc91756..1b257422f4ad930792ac663b4daf07dd5b4d7197 100644 --- a/src/mol-plugin-state/transforms/representation.ts +++ b/src/mol-plugin-state/transforms/representation.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> * @author Alexander Rose <alexander.rose@weirdbyte.de> @@ -28,7 +28,7 @@ import { BaseGeometry } from '../../mol-geo/geometry/base'; import { Script } from '../../mol-script/script'; import { UnitcellParams, UnitcellRepresentation, getUnitcellData } from '../../mol-repr/shape/model/unitcell'; import { DistanceParams, DistanceRepresentation } from '../../mol-repr/shape/loci/distance'; -import { getDistanceDataFromStructureSelections, getLabelDataFromStructureSelections, getOrientationDataFromStructureSelections, getAngleDataFromStructureSelections, getDihedralDataFromStructureSelections } from './helpers'; +import { getDistanceDataFromStructureSelections, getLabelDataFromStructureSelections, getOrientationDataFromStructureSelections, getAngleDataFromStructureSelections, getDihedralDataFromStructureSelections, getPlaneDataFromStructureSelections } from './helpers'; import { LabelParams, LabelRepresentation } from '../../mol-repr/shape/loci/label'; import { OrientationRepresentation, OrientationParams } from '../../mol-repr/shape/loci/orientation'; import { AngleParams, AngleRepresentation } from '../../mol-repr/shape/loci/angle'; @@ -40,6 +40,7 @@ import { Mesh } from '../../mol-geo/geometry/mesh/mesh'; import { getBoxMesh } from './shape'; import { Shape } from '../../mol-model/shape'; import { Box3D } from '../../mol-math/geometry'; +import { PlaneParams, PlaneRepresentation } from '../../mol-repr/shape/loci/plane'; export { StructureRepresentation3D }; export { ExplodeStructureRepresentation3D }; @@ -986,4 +987,37 @@ const StructureSelectionsOrientation3D = PluginStateTransform.BuiltIn({ return StateTransformer.UpdateResult.Updated; }); }, +}); + +export { StructureSelectionsPlane3D }; +type StructureSelectionsPlane3D = typeof StructureSelectionsPlane3D +const StructureSelectionsPlane3D = PluginStateTransform.BuiltIn({ + name: 'structure-selections-plane-3d', + display: '3D Plane', + from: SO.Molecule.Structure.Selections, + to: SO.Shape.Representation3D, + params: () => ({ + ...PlaneParams, + }) +})({ + canAutoUpdate({ oldParams, newParams }) { + return true; + }, + apply({ a, params }, plugin: PluginContext) { + return Task.create('Structure Plane', async ctx => { + const data = getPlaneDataFromStructureSelections(a.data); + const repr = PlaneRepresentation({ webgl: plugin.canvas3d?.webgl, ...plugin.representation.structure.themes }, () => PlaneParams); + await repr.createOrUpdate(params, data).runInContext(ctx); + return new SO.Shape.Representation3D({ repr, sourceData: data }, { label: `Plane` }); + }); + }, + update({ a, b, oldParams, newParams }, plugin: PluginContext) { + return Task.create('Structure Plane', async ctx => { + const props = { ...b.data.repr.props, ...newParams }; + const data = getPlaneDataFromStructureSelections(a.data); + await b.data.repr.createOrUpdate(props, data).runInContext(ctx); + b.data.sourceData = data; + return StateTransformer.UpdateResult.Updated; + }); + }, }); \ No newline at end of file diff --git a/src/mol-plugin-ui/sequence.tsx b/src/mol-plugin-ui/sequence.tsx index 068b4e8ab61783638b19f87f66d61a1701292b9a..44316caf7717d7c5cba6e9942b97cde8a4d2e8f5 100644 --- a/src/mol-plugin-ui/sequence.tsx +++ b/src/mol-plugin-ui/sequence.tsx @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> * @author David Sehnal <david.sehnal@gmail.com> @@ -25,6 +25,9 @@ import { StructureSelectionManager } from '../mol-plugin-state/manager/structure import { arrayEqual } from '../mol-util/array'; const MaxDisplaySequenceLength = 5000; +// TODO: add virtualized Select controls (at best with a search box)? +const MaxSelectOptionsCount = 1000; +const MaxSequenceWrappersCount = 30; function opKey(l: StructureElement.Location) { const ids = SP.unit.pdbx_struct_oper_list_ids(l); @@ -94,7 +97,7 @@ function getSequenceWrapper(state: { structure: Structure, modelEntityId: string } } -function getModelEntityOptions(structure: Structure, polymersOnly = false) { +function getModelEntityOptions(structure: Structure, polymersOnly = false): [string, string][] { const options: [string, string][] = []; const l = StructureElement.Location.create(structure); const seen = new Set<string>(); @@ -118,13 +121,17 @@ function getModelEntityOptions(structure: Structure, polymersOnly = false) { const label = `${id}: ${description}`; options.push([key, label]); seen.add(key); + + if (options.length > MaxSelectOptionsCount) { + return [['', 'Too many entities']]; + } } if (options.length === 0) options.push(['', 'No entities']); return options; } -function getChainOptions(structure: Structure, modelEntityId: string) { +function getChainOptions(structure: Structure, modelEntityId: string): [number, string][] { const options: [number, string][] = []; const l = StructureElement.Location.create(structure); const seen = new Set<number>(); @@ -144,13 +151,17 @@ function getChainOptions(structure: Structure, modelEntityId: string) { options.push([id, label]); seen.add(id); + + if (options.length > MaxSelectOptionsCount) { + return [[-1, 'Too many chains']]; + } } - if (options.length === 0) options.push([-1, 'No units']); + if (options.length === 0) options.push([-1, 'No chains']); return options; } -function getOperatorOptions(structure: Structure, modelEntityId: string, chainGroupId: number) { +function getOperatorOptions(structure: Structure, modelEntityId: string, chainGroupId: number): [string, string][] { const options: [string, string][] = []; const l = StructureElement.Location.create(structure); const seen = new Set<string>(); @@ -168,6 +179,10 @@ function getOperatorOptions(structure: Structure, modelEntityId: string, chainGr const label = unit.conformation.operator.name; options.push([id, label]); seen.add(id); + + if (options.length > MaxSelectOptionsCount) { + return [['', 'Too many operators']]; + } } if (options.length === 0) options.push(['', 'No operators']); @@ -266,6 +281,7 @@ export class SequenceView extends PluginUIComponent<{ defaultMode?: SequenceView }, this.plugin.managers.structure.selection), label: `${cLabel} | ${eLabel}` }); + if (wrappers.length > MaxSequenceWrappersCount) return []; } } } diff --git a/src/mol-plugin-ui/structure/measurements.tsx b/src/mol-plugin-ui/structure/measurements.tsx index b1cdfb2a33ae1bfa5b289a29e7f79959bd4b36cd..b9a8f08f93c8d8d16ac58500a52326663fcf948c 100644 --- a/src/mol-plugin-ui/structure/measurements.tsx +++ b/src/mol-plugin-ui/structure/measurements.tsx @@ -1,5 +1,5 @@ /** - * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> * @author David Sehnal <david.sehnal@gmail.com> @@ -15,7 +15,8 @@ import { AngleData } from '../../mol-repr/shape/loci/angle'; import { DihedralData } from '../../mol-repr/shape/loci/dihedral'; import { DistanceData } from '../../mol-repr/shape/loci/distance'; import { LabelData } from '../../mol-repr/shape/loci/label'; -import { angleLabel, dihedralLabel, distanceLabel, lociLabel } from '../../mol-theme/label'; +import { OrientationData } from '../../mol-repr/shape/loci/orientation'; +import { angleLabel, dihedralLabel, distanceLabel, lociLabel, structureElementLociLabelMany } from '../../mol-theme/label'; import { FiniteArray } from '../../mol-util/type-helpers'; import { CollapsableControls, PurePluginUIComponent } from '../base'; import { ActionMenu } from '../controls/action-menu'; @@ -61,12 +62,13 @@ export class MeasurementList extends PurePluginUIComponent { render() { const measurements = this.plugin.managers.structure.measurement.state; - return <div style={{ marginTop: '6px' }}> {this.renderGroup(measurements.labels, 'Labels')} {this.renderGroup(measurements.distances, 'Distances')} {this.renderGroup(measurements.angles, 'Angles')} {this.renderGroup(measurements.dihedrals, 'Dihedrals')} + {this.renderGroup(measurements.orientations, 'Orientations')} + {this.renderGroup(measurements.planes, 'Planes')} </div>; } } @@ -77,6 +79,7 @@ export class MeasurementControls extends PurePluginUIComponent<{}, { isBusy: boo componentDidMount() { this.subscribe(this.selection.events.additionsHistoryUpdated, () => { this.forceUpdate(); + this.updateOrderLabels(); }); this.subscribe(this.plugin.behaviors.state.isBusy, v => { @@ -84,6 +87,33 @@ export class MeasurementControls extends PurePluginUIComponent<{}, { isBusy: boo }); } + componentWillUnmount() { + this.clearOrderLabels(); + super.componentWillUnmount(); + } + + componentDidUpdate(prevProps: {}, prevState: { isBusy: boolean, action?: 'add' | 'options' }) { + if (this.state.action !== prevState.action) + this.updateOrderLabels(); + } + + clearOrderLabels() { + this.plugin.managers.structure.measurement.addOrderLabels([]); + } + + updateOrderLabels() { + if (this.state.action !== 'add') { + this.clearOrderLabels(); + return; + } + + const locis = []; + const history = this.selection.additionsHistory; + for (let idx = 0; idx < history.length && idx < 4; idx++) + locis.push(history[idx].loci); + this.plugin.managers.structure.measurement.addOrderLabels(locis); + } + get selection() { return this.plugin.managers.structure.selection; } @@ -108,13 +138,31 @@ export class MeasurementControls extends PurePluginUIComponent<{}, { isBusy: boo this.plugin.managers.structure.measurement.addLabel(loci[0].loci); } + addOrientation = () => { + const locis: StructureElement.Loci[] = []; + this.plugin.managers.structure.selection.entries.forEach(v => { + locis.push(v.selection); + }); + this.plugin.managers.structure.measurement.addOrientation(locis); + } + + addPlane = () => { + const locis: StructureElement.Loci[] = []; + this.plugin.managers.structure.selection.entries.forEach(v => { + locis.push(v.selection); + }); + this.plugin.managers.structure.measurement.addPlane(locis); + } + get actions(): ActionMenu.Items { const history = this.selection.additionsHistory; const ret: ActionMenu.Item[] = [ - { kind: 'item', label: `Label ${history.length === 0 ? ' (1 selection required)' : ' (1st selection)'}`, value: this.addLabel, disabled: history.length === 0 }, - { kind: 'item', label: `Distance ${history.length < 2 ? ' (2 selections required)' : ' (top 2 selections)'}`, value: this.measureDistance, disabled: history.length < 2 }, - { kind: 'item', label: `Angle ${history.length < 3 ? ' (3 selections required)' : ' (top 3 selections)'}`, value: this.measureAngle, disabled: history.length < 3 }, - { kind: 'item', label: `Dihedral ${history.length < 4 ? ' (4 selections required)' : ' (top 4 selections)'}`, value: this.measureDihedral, disabled: history.length < 4 }, + { kind: 'item', label: `Label ${history.length === 0 ? ' (1 selection item required)' : ' (1st selection item)'}`, value: this.addLabel, disabled: history.length === 0 }, + { kind: 'item', label: `Distance ${history.length < 2 ? ' (2 selection items required)' : ' (top 2 selection items)'}`, value: this.measureDistance, disabled: history.length < 2 }, + { kind: 'item', label: `Angle ${history.length < 3 ? ' (3 selection items required)' : ' (top 3 items)'}`, value: this.measureAngle, disabled: history.length < 3 }, + { kind: 'item', label: `Dihedral ${history.length < 4 ? ' (4 selection items required)' : ' (top 4 selection items)'}`, value: this.measureDihedral, disabled: history.length < 4 }, + { kind: 'item', label: `Orientation ${history.length === 0 ? ' (selection required)' : ' (current selection)'}`, value: this.addOrientation, disabled: history.length === 0 }, + { kind: 'item', label: `Plane ${history.length === 0 ? ' (selection required)' : ' (current selection)'}`, value: this.addPlane, disabled: history.length === 0 }, ]; return ret; } @@ -142,8 +190,8 @@ export class MeasurementControls extends PurePluginUIComponent<{}, { isBusy: boo historyEntry(e: StructureSelectionHistoryEntry, idx: number) { const history = this.plugin.managers.structure.selection.additionsHistory; - return <div className='msp-flex-row' key={e.id}> - <Button noOverflow title='Click to focus. Hover to highlight.' onClick={() => this.focusLoci(e.loci)} style={{ width: 'auto', textAlign: 'left' }} onMouseEnter={() => this.highlight(e.loci)} onMouseLeave={() => this.plugin.managers.interactivity.lociHighlights.clearHighlights()}> + return <div className='msp-flex-row' key={e.id} onMouseEnter={() => this.highlight(e.loci)} onMouseLeave={() => this.plugin.managers.interactivity.lociHighlights.clearHighlights()}> + <Button noOverflow title='Click to focus. Hover to highlight.' onClick={() => this.focusLoci(e.loci)} style={{ width: 'auto', textAlign: 'left' }}> {idx}. <span dangerouslySetInnerHTML={{ __html: e.label }} /> </Button> {history.length > 1 && <IconButton svg={ArrowUpwardSvg} small={true} className='msp-form-control' onClick={() => this.moveHistory(e, 'up')} flex='20px' title={'Move up'} />} @@ -219,7 +267,7 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen } get selections() { - return this.props.cell.obj?.data.sourceData as Partial<DistanceData & AngleData & DihedralData & LabelData> | undefined; + return this.props.cell.obj?.data.sourceData as Partial<DistanceData & AngleData & DihedralData & LabelData & OrientationData> | undefined; } delete = () => { @@ -266,6 +314,7 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen if (selections.pairs) return selections.pairs[0].loci; if (selections.triples) return selections.triples[0].loci; if (selections.quads) return selections.quads[0].loci; + if (selections.locis) return selections.locis; return []; } @@ -277,6 +326,7 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen if (selections.pairs) return distanceLabel(selections.pairs[0], { condensed: true, unitLabel: this.plugin.managers.structure.measurement.state.options.distanceUnitLabel }); if (selections.triples) return angleLabel(selections.triples[0], { condensed: true }); if (selections.quads) return dihedralLabel(selections.quads[0], { condensed: true }); + if (selections.locis) return structureElementLociLabelMany(selections.locis, { countsOnly: true }); return '<empty>'; } diff --git a/src/mol-repr/representation.ts b/src/mol-repr/representation.ts index 5daed3f5edcd214aba247dead009b2a0b948c09b..9be1c63371d7150c4294aa2b4a32acb56e2514dd 100644 --- a/src/mol-repr/representation.ts +++ b/src/mol-repr/representation.ts @@ -65,12 +65,16 @@ export namespace RepresentationProvider { export type AnyRepresentationProvider = RepresentationProvider<any, {}, Representation.State> -const EmptyRepresentationProvider = { +export const EmptyRepresentationProvider: RepresentationProvider = { + name: '', label: '', description: '', factory: () => Representation.Empty, getParams: () => ({}), - defaultValues: {} + defaultValues: {}, + defaultColorTheme: ColorTheme.EmptyProvider, + defaultSizeTheme: SizeTheme.EmptyProvider, + isApplicable: () => true }; function getTypes(list: { name: string, provider: RepresentationProvider<any, any, any> }[]) { @@ -114,7 +118,7 @@ export class RepresentationRegistry<D, S extends Representation.State> { } get<P extends PD.Params>(name: string): RepresentationProvider<D, P, S> { - return this._map.get(name) || EmptyRepresentationProvider as unknown as RepresentationProvider<D, P, S>; + return this._map.get(name) || EmptyRepresentationProvider; } get list() { diff --git a/src/mol-repr/shape/loci/orientation.ts b/src/mol-repr/shape/loci/orientation.ts index 4a5b120ec8acf1b73722a4c90d56db084290672a..451aff38efdfbee738e3dd54ec5db5f38ef8946c 100644 --- a/src/mol-repr/shape/loci/orientation.ts +++ b/src/mol-repr/shape/loci/orientation.ts @@ -1,10 +1,9 @@ /** - * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ -import { Loci } from '../../../mol-model/loci'; import { RuntimeContext } from '../../../mol-task'; import { ParamDefinition as PD } from '../../../mol-util/param-definition'; import { ColorNames } from '../../../mol-util/color/names'; @@ -13,21 +12,23 @@ import { Representation, RepresentationParamsGetter, RepresentationContext } fro import { Shape } from '../../../mol-model/shape'; import { Mesh } from '../../../mol-geo/geometry/mesh/mesh'; import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder'; -import { lociLabel } from '../../../mol-theme/label'; +import { structureElementLociLabelMany } from '../../../mol-theme/label'; import { addAxes } from '../../../mol-geo/geometry/mesh/builder/axes'; import { addOrientedBox } from '../../../mol-geo/geometry/mesh/builder/box'; import { addEllipsoid } from '../../../mol-geo/geometry/mesh/builder/ellipsoid'; import { Axes3D } from '../../../mol-math/geometry'; import { Vec3 } from '../../../mol-math/linear-algebra'; import { MarkerActions } from '../../../mol-util/marker-action'; +import { StructureElement } from '../../../mol-model/structure'; export interface OrientationData { - locis: Loci[] + locis: StructureElement.Loci[] } const SharedParams = { color: PD.Color(ColorNames.orange), - scale: PD.Numeric(2, { min: 0.1, max: 10, step: 0.1 }) + scaleFactor: PD.Numeric(1, { min: 0.1, max: 10, step: 0.1 }), + radiusScale: PD.Numeric(2, { min: 0.1, max: 10, step: 0.1 }) }; const AxesParams = { @@ -57,97 +58,84 @@ const OrientationVisuals = { export const OrientationParams = { ...AxesParams, ...BoxParams, + ...EllipsoidParams, visuals: PD.MultiSelect(['box'], PD.objectToOptions(OrientationVisuals)), - color: PD.Color(ColorNames.orange), - scale: PD.Numeric(2, { min: 0.1, max: 5, step: 0.1 }) }; export type OrientationParams = typeof OrientationParams export type OrientationProps = PD.Values<OrientationParams> // -function orientationLabel(loci: Loci) { - const label = lociLabel(loci, { countsOnly: true }); +function getAxesName(locis: StructureElement.Loci[]) { + const label = structureElementLociLabelMany(locis, { countsOnly: true }); return `Principal Axes of ${label}`; } -function getOrientationName(data: OrientationData) { - return data.locis.length === 1 ? orientationLabel(data.locis[0]) : `${data.locis.length} Orientations`; -} - -// - function buildAxesMesh(data: OrientationData, props: OrientationProps, mesh?: Mesh): Mesh { const state = MeshBuilder.createState(256, 128, mesh); - for (let i = 0, il = data.locis.length; i < il; ++i) { - const principalAxes = Loci.getPrincipalAxes(data.locis[i]); - if (principalAxes) { - state.currentGroup = i; - addAxes(state, principalAxes.momentsAxes, props.scale, 2, 20); - } - } + const principalAxes = StructureElement.Loci.getPrincipalAxesMany(data.locis); + Axes3D.scale(principalAxes.momentsAxes, principalAxes.momentsAxes, props.scaleFactor); + + state.currentGroup = 0; + addAxes(state, principalAxes.momentsAxes, props.radiusScale, 2, 20); return MeshBuilder.getMesh(state); } function getAxesShape(ctx: RuntimeContext, data: OrientationData, props: OrientationProps, shape?: Shape<Mesh>) { const mesh = buildAxesMesh(data, props, shape && shape.geometry); - const name = getOrientationName(data); - const getLabel = function (groupId: number) { - return orientationLabel(data.locis[groupId]); - }; - return Shape.create(name, data, mesh, () => props.color, () => 1, getLabel); + const name = getAxesName(data.locis); + return Shape.create(name, data, mesh, () => props.color, () => 1, () => name); } // +function getBoxName(locis: StructureElement.Loci[]) { + const label = structureElementLociLabelMany(locis, { countsOnly: true }); + return `Oriented Box of ${label}`; +} + function buildBoxMesh(data: OrientationData, props: OrientationProps, mesh?: Mesh): Mesh { const state = MeshBuilder.createState(256, 128, mesh); - for (let i = 0, il = data.locis.length; i < il; ++i) { - const principalAxes = Loci.getPrincipalAxes(data.locis[i]); - if (principalAxes) { - state.currentGroup = i; - addOrientedBox(state, principalAxes.boxAxes, props.scale, 2, 20); - } - } + const principalAxes = StructureElement.Loci.getPrincipalAxesMany(data.locis); + Axes3D.scale(principalAxes.boxAxes, principalAxes.boxAxes, props.scaleFactor); + + state.currentGroup = 0; + addOrientedBox(state, principalAxes.boxAxes, props.radiusScale, 2, 20); return MeshBuilder.getMesh(state); } function getBoxShape(ctx: RuntimeContext, data: OrientationData, props: OrientationProps, shape?: Shape<Mesh>) { const mesh = buildBoxMesh(data, props, shape && shape.geometry); - const name = getOrientationName(data); - const getLabel = function (groupId: number) { - return orientationLabel(data.locis[groupId]); - }; - return Shape.create(name, data, mesh, () => props.color, () => 1, getLabel); + const name = getBoxName(data.locis); + return Shape.create(name, data, mesh, () => props.color, () => 1, () => name); } // +function getEllipsoidName(locis: StructureElement.Loci[]) { + const label = structureElementLociLabelMany(locis, { countsOnly: true }); + return `Oriented Ellipsoid of ${label}`; +} + function buildEllipsoidMesh(data: OrientationData, props: OrientationProps, mesh?: Mesh): Mesh { const state = MeshBuilder.createState(256, 128, mesh); - for (let i = 0, il = data.locis.length; i < il; ++i) { - const principalAxes = Loci.getPrincipalAxes(data.locis[i]); - if (principalAxes) { - const axes = principalAxes.boxAxes; - const { origin, dirA, dirB } = axes; - const size = Axes3D.size(Vec3(), axes); - Vec3.scale(size, size, 0.5); - const radiusScale = Vec3.create(size[2], size[1], size[0]); - - state.currentGroup = i; - addEllipsoid(state, origin, dirA, dirB, radiusScale, 2); - } - } + const principalAxes = StructureElement.Loci.getPrincipalAxesMany(data.locis); + + const axes = principalAxes.boxAxes; + const { origin, dirA, dirB } = axes; + const size = Axes3D.size(Vec3(), axes); + Vec3.scale(size, size, 0.5 * props.scaleFactor); + const radiusScale = Vec3.create(size[2], size[1], size[0]); + + state.currentGroup = 0; + addEllipsoid(state, origin, dirA, dirB, radiusScale, 2); return MeshBuilder.getMesh(state); } function getEllipsoidShape(ctx: RuntimeContext, data: OrientationData, props: OrientationProps, shape?: Shape<Mesh>) { const mesh = buildEllipsoidMesh(data, props, shape && shape.geometry); - const name = getOrientationName(data); - const getLabel = function (groupId: number) { - return orientationLabel(data.locis[groupId]); - }; - return Shape.create(name, data, mesh, () => props.color, () => 1, getLabel); + const name = getEllipsoidName(data.locis); + return Shape.create(name, data, mesh, () => props.color, () => 1, () => name); } // diff --git a/src/mol-repr/shape/loci/plane.ts b/src/mol-repr/shape/loci/plane.ts new file mode 100644 index 0000000000000000000000000000000000000000..719f3ae19611accf74fde85089eb347c7ff14d2d --- /dev/null +++ b/src/mol-repr/shape/loci/plane.ts @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { RuntimeContext } from '../../../mol-task'; +import { ParamDefinition as PD } from '../../../mol-util/param-definition'; +import { ColorNames } from '../../../mol-util/color/names'; +import { ShapeRepresentation } from '../representation'; +import { Representation, RepresentationParamsGetter, RepresentationContext } from '../../representation'; +import { Shape } from '../../../mol-model/shape'; +import { Mesh } from '../../../mol-geo/geometry/mesh/mesh'; +import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder'; +import { structureElementLociLabelMany } from '../../../mol-theme/label'; +import { Mat4, Vec3 } from '../../../mol-math/linear-algebra'; +import { MarkerActions } from '../../../mol-util/marker-action'; +import { Plane } from '../../../mol-geo/primitive/plane'; +import { StructureElement } from '../../../mol-model/structure'; +import { Axes3D } from '../../../mol-math/geometry'; + +export interface PlaneData { + locis: StructureElement.Loci[] +} + +const _PlaneParams = { + ...Mesh.Params, + color: PD.Color(ColorNames.orange), + scaleFactor: PD.Numeric(1, { min: 0.1, max: 10, step: 0.1 }), +}; +type _PlaneParams = typeof _PlaneParams + +const PlaneVisuals = { + 'plane': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<PlaneData, _PlaneParams>) => ShapeRepresentation(getPlaneShape, Mesh.Utils), +}; + +export const PlaneParams = { + ..._PlaneParams, + visuals: PD.MultiSelect(['plane'], PD.objectToOptions(PlaneVisuals)), +}; +export type PlaneParams = typeof PlaneParams +export type PlaneProps = PD.Values<PlaneParams> + +// + +function getPlaneName(locis: StructureElement.Loci[]) { + const label = structureElementLociLabelMany(locis, { countsOnly: true }); + return `Best Fit Plane of ${label}`; +} + +const tmpMat = Mat4(); +const tmpV = Vec3(); +function buildPlaneMesh(data: PlaneData, props: PlaneProps, mesh?: Mesh): Mesh { + const state = MeshBuilder.createState(256, 128, mesh); + const principalAxes = StructureElement.Loci.getPrincipalAxesMany(data.locis); + const axes = principalAxes.boxAxes; + const plane = Plane(); + + Vec3.add(tmpV, axes.origin, axes.dirC); + Mat4.targetTo(tmpMat, tmpV, axes.origin, axes.dirB); + Mat4.scale(tmpMat, tmpMat, Axes3D.size(tmpV, axes)); + Mat4.scaleUniformly(tmpMat, tmpMat, props.scaleFactor); + Mat4.setTranslation(tmpMat, axes.origin); + + state.currentGroup = 0; + MeshBuilder.addPrimitive(state, tmpMat, plane); + MeshBuilder.addPrimitiveFlipped(state, tmpMat, plane); + return MeshBuilder.getMesh(state); +} + +function getPlaneShape(ctx: RuntimeContext, data: PlaneData, props: PlaneProps, shape?: Shape<Mesh>) { + const mesh = buildPlaneMesh(data, props, shape && shape.geometry); + const name = getPlaneName(data.locis); + return Shape.create(name, data, mesh, () => props.color, () => 1, () => name); +} + +// + +export type PlaneRepresentation = Representation<PlaneData, PlaneParams> +export function PlaneRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<PlaneData, PlaneParams>): PlaneRepresentation { + const repr = Representation.createMulti('Plane', ctx, getParams, Representation.StateBuilder, PlaneVisuals as unknown as Representation.Def<PlaneData, PlaneParams>); + repr.setState({ markerActions: MarkerActions.Highlighting }); + return repr; +} \ No newline at end of file diff --git a/src/mol-theme/label.ts b/src/mol-theme/label.ts index 73c123a859f98f572151db59980b1841fd6500cf..d43df4dd7e87a0d2904d8afb271e9f0303849d2f 100644 --- a/src/mol-theme/label.ts +++ b/src/mol-theme/label.ts @@ -92,6 +92,14 @@ export function structureElementStatsLabel(stats: StructureElement.Stats, option return o.htmlStyling ? label : stripTags(label); } +export function structureElementLociLabelMany(locis: StructureElement.Loci[], options: Partial<LabelOptions> = {}): string { + const stats = StructureElement.Stats.create(); + for (const l of locis) { + StructureElement.Stats.add(stats, stats, StructureElement.Stats.ofLoci(l)); + } + return structureElementStatsLabel(stats, options); +} + function _structureElementStatsLabel(stats: StructureElement.Stats, countsOnly = false, hidePrefix = false, condensed = false, reverse = false): string { const { structureCount, chainCount, residueCount, conformationCount, elementCount } = stats;