Browse Source

Merge branch 'master' of https://github.com/molstar/molstar into shader-tests

dsehnal 3 years ago
parent
commit
a58cbd31ef

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

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

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

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

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

+ 36 - 2
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 {

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

+ 168 - 69
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);
-}));
+}));

+ 5 - 6
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 };
     }
-});
+});

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

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

+ 189 - 13
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 });
+        });
+    }
+});

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

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

+ 14 - 0
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)

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

+ 93 - 11
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;

+ 4 - 9
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) {

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

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

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

+ 21 - 5
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 [];
                 }
             }
         }

+ 60 - 10
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>';
     }
 

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

+ 45 - 57
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);
 }
 
 //

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

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