Browse Source

add transparency to component theme

Alexander Rose 4 years ago
parent
commit
217e983da8

+ 2 - 4
src/mol-geo/geometry/transparency-data.ts

@@ -7,7 +7,6 @@
 import { ValueCell } from '../../mol-util/value-cell';
 import { Vec2 } from '../../mol-math/linear-algebra';
 import { TextureImage, createTextureImage } from '../../mol-gl/renderable/util';
-import { Transparency } from '../../mol-theme/transparency';
 
 export type TransparencyData = {
     tTransparency: ValueCell<TextureImage<Uint8Array>>
@@ -27,20 +26,19 @@ export function clearTransparency(array: Uint8Array, start: number, end: number)
     array.fill(0, start, end);
 }
 
-export function createTransparency(count: number, variant: Transparency.Variant, transparencyData?: TransparencyData): TransparencyData {
+export function createTransparency(count: number, transparencyData?: TransparencyData): TransparencyData {
     const transparency = createTextureImage(Math.max(1, count), 1, Uint8Array, transparencyData && transparencyData.tTransparency.ref.value.array);
     if (transparencyData) {
         ValueCell.update(transparencyData.tTransparency, transparency);
         ValueCell.update(transparencyData.uTransparencyTexDim, Vec2.create(transparency.width, transparency.height));
         ValueCell.update(transparencyData.dTransparency, count > 0);
-        ValueCell.update(transparencyData.dTransparencyVariant, variant);
         return transparencyData;
     } else {
         return {
             tTransparency: ValueCell.create(transparency),
             uTransparencyTexDim: ValueCell.create(Vec2.create(transparency.width, transparency.height)),
             dTransparency: ValueCell.create(count > 0),
-            dTransparencyVariant: ValueCell.create(variant),
+            dTransparencyVariant: ValueCell.create('single'),
         };
     }
 }

+ 9 - 0
src/mol-gl/renderer.ts

@@ -18,6 +18,7 @@ import { GraphicsRenderVariant } from './webgl/render-item';
 import { ParamDefinition as PD } from '../mol-util/param-definition';
 import { Clipping } from '../mol-theme/clipping';
 import { stringToWords } from '../mol-util/string';
+import { Transparency } from '../mol-theme/transparency';
 
 export interface RendererStats {
     programCount: number
@@ -51,6 +52,7 @@ export const RendererParams = {
 
     // the following are general 'material' parameters
     pickingAlphaThreshold: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }, { description: 'The minimum opacity value needed for an object to be pickable.' }),
+    transparencyVariant: PD.Select('single', PD.arrayToOptions<Transparency.Variant>(['single', 'multi'])),
 
     interiorDarkening: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }),
     interiorColorFlag: PD.Boolean(true, { label: 'Use Interior Color' }),
@@ -239,6 +241,10 @@ namespace Renderer {
                 ValueCell.update(r.values.dClipVariant, clip.variant);
                 definesNeedUpdate = true;
             }
+            if (r.values.dTransparencyVariant.ref.value !== p.transparencyVariant) {
+                ValueCell.update(r.values.dTransparencyVariant, p.transparencyVariant);
+                definesNeedUpdate = true;
+            }
             if (definesNeedUpdate) r.update();
 
             const program = r.getProgram(variant);
@@ -373,6 +379,9 @@ namespace Renderer {
                     p.pickingAlphaThreshold = props.pickingAlphaThreshold;
                     ValueCell.update(globalUniforms.uPickingAlphaThreshold, p.pickingAlphaThreshold);
                 }
+                if (props.transparencyVariant !== undefined && props.transparencyVariant !== p.transparencyVariant) {
+                    p.transparencyVariant = props.transparencyVariant;
+                }
 
                 if (props.interiorDarkening !== undefined && props.interiorDarkening !== p.interiorDarkening) {
                     p.interiorDarkening = props.interiorDarkening;

+ 74 - 0
src/mol-plugin-state/helpers/structure-transparency.ts

@@ -0,0 +1,74 @@
+/**
+ * Copyright (c) 2019-2020 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>
+ */
+
+import { Structure, StructureElement } from '../../mol-model/structure';
+import { PluginStateObject } from '../../mol-plugin-state/objects';
+import { StateTransforms } from '../../mol-plugin-state/transforms';
+import { PluginContext } from '../../mol-plugin/context';
+import { StateBuilder, StateObjectCell, StateSelection, StateTransform } from '../../mol-state';
+import { StructureComponentRef } from '../manager/structure/hierarchy-state';
+import { EmptyLoci, Loci } from '../../mol-model/loci';
+import { Transparency } from '../../mol-theme/transparency';
+
+type TransparencyEachReprCallback = (update: StateBuilder.Root, repr: StateObjectCell<PluginStateObject.Molecule.Structure.Representation3D, StateTransform<typeof StateTransforms.Representation.StructureRepresentation3D>>, transparency?: StateObjectCell<any, StateTransform<typeof StateTransforms.Representation.TransparencyStructureRepresentation3DFromBundle>>) => Promise<void>
+const TransparencyManagerTag = 'transparency-controls';
+
+export async function setStructureTransparency(plugin: PluginContext, components: StructureComponentRef[], value: number, lociGetter: (structure: Structure) => Promise<StructureElement.Loci | EmptyLoci>, types?: string[]) {
+    await eachRepr(plugin, components, async (update, repr, transparencyCell) => {
+        if (types && types.length > 0 && !types.includes(repr.params!.values.type.name)) return;
+
+        const structure = repr.obj!.data.source.data;
+        // always use the root structure to get the loci so the transparency
+        // stays applicable as long as the root structure does not change
+        const loci = await lociGetter(structure.root);
+        if (Loci.isEmpty(loci)) return;
+
+        const layer = {
+            bundle: StructureElement.Bundle.fromLoci(loci),
+            value,
+        };
+
+        if (transparencyCell) {
+            const bundleLayers = [...transparencyCell.params!.values.layers, layer];
+            const filtered = getFilteredBundle(bundleLayers, structure);
+            update.to(transparencyCell).update(Transparency.toBundle(filtered));
+        } else {
+            const filtered = getFilteredBundle([layer], structure);
+            update.to(repr.transform.ref)
+                .apply(StateTransforms.Representation.TransparencyStructureRepresentation3DFromBundle, Transparency.toBundle(filtered), { tags: TransparencyManagerTag });
+        }
+    });
+}
+
+export async function clearStructureTransparency(plugin: PluginContext, components: StructureComponentRef[], types?: string[]) {
+    await eachRepr(plugin, components, async (update, repr, transparencyCell) => {
+        if (types && types.length > 0 && !types.includes(repr.params!.values.type.name)) return;
+        if (transparencyCell) {
+            update.delete(transparencyCell.transform.ref);
+        }
+    });
+}
+
+async function eachRepr(plugin: PluginContext, components: StructureComponentRef[], callback: TransparencyEachReprCallback) {
+    const state = plugin.state.data;
+    const update = state.build();
+    for (const c of components) {
+        for (const r of c.representations) {
+            const transparency = state.select(StateSelection.Generators.ofTransformer(StateTransforms.Representation.TransparencyStructureRepresentation3DFromBundle, r.cell.transform.ref).withTag(TransparencyManagerTag));
+            await callback(update, r.cell, transparency[0]);
+        }
+    }
+
+    return update.commit({ doNotUpdateCurrent: true });
+}
+
+/** filter transparency layers for given structure */
+function getFilteredBundle(layers: Transparency.BundleLayer[], structure: Structure) {
+    const transparency = Transparency.ofBundle(layers, structure.root);
+    const merged = Transparency.merge(transparency);
+    return Transparency.filter(merged, structure);
+}

+ 8 - 1
src/mol-plugin-state/manager/structure/component.ts

@@ -28,6 +28,7 @@ import { StructureRepresentation3D } from '../../transforms/representation';
 import { StructureHierarchyRef, StructureComponentRef, StructureRef, StructureRepresentationRef } from './hierarchy-state';
 import { Clipping } from '../../../mol-theme/clipping';
 import { setStructureClipping } from '../../helpers/structure-clipping';
+import { setStructureTransparency } from '../../helpers/structure-transparency';
 
 export { StructureComponentManager };
 
@@ -372,6 +373,9 @@ class StructureComponentManager extends StatefulPluginComponent<StructureCompone
                 } else if (params.action.name === 'color') {
                     const p = params.action.params;
                     await setStructureOverpaint(this.plugin, s.components, p.color, getLoci, params.representations, p.opacity);
+                } else if (params.action.name === 'transparency') {
+                    const p = params.action.params;
+                    await setStructureTransparency(this.plugin, s.components, p.value, getLoci, params.representations);
                 } else if (params.action.name === 'clipping') {
                     const p = params.action.params;
                     await setStructureClipping(this.plugin, s.components, Clipping.Groups.fromNames(p.excludeGroups), getLoci, params.representations);
@@ -470,7 +474,10 @@ namespace StructureComponentManager {
                     color: PD.Color(ColorNames.blue, { isExpanded: true }),
                     opacity: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
                 }, { isFlat: true }),
-                reset: PD.EmptyGroup(),
+                reset: PD.EmptyGroup({ label: 'Reset Color' }),
+                transparency: PD.Group({
+                    value: PD.Numeric(0.5, { min: 0, max: 1, step: 0.01 }),
+                }, { isFlat: true }),
                 clipping: PD.Group({
                     excludeGroups: PD.MultiSelect([] as Clipping.Groups.Names[], PD.objectToOptions(Clipping.Groups.Names)),
                 }, { isFlat: true }),

+ 27 - 14
src/mol-plugin-state/transforms/representation.ts

@@ -369,9 +369,15 @@ const TransparencyStructureRepresentation3DFromScript = PluginStateTransform.Bui
     from: SO.Molecule.Structure.Representation3D,
     to: SO.Molecule.Structure.Representation3DState,
     params: {
-        script: PD.Script(Script('(sel.atom.all)', 'mol-script')),
-        value: PD.Numeric(0.75, { min: 0, max: 1, step: 0.01 }, { label: 'Transparency' }),
-        variant: PD.Select('single', [['single', 'Single-layer'], ['multi', 'Multi-layer']] as ['single' | 'multi', string][])
+        layers: PD.ObjectList({
+            script: PD.Script(Script('(sel.atom.all)', 'mol-script')),
+            value: PD.Numeric(0.5, { min: 0, max: 1, step: 0.01 }, { label: 'Transparency' }),
+        }, e => `Transparency (${e.value})`, {
+            defaultValue: [{
+                script: Script('(sel.atom.all)', 'mol-script'),
+                value: 0.5,
+            }]
+        })
     }
 })({
     canAutoUpdate() {
@@ -379,25 +385,25 @@ const TransparencyStructureRepresentation3DFromScript = PluginStateTransform.Bui
     },
     apply({ a, params }) {
         const structure = a.data.source.data;
-        const transparency = Transparency.ofScript(params.script, params.value, params.variant, structure);
+        const transparency = Transparency.ofScript(params.layers, structure);
 
         return new SO.Molecule.Structure.Representation3DState({
             state: { transparency },
             initialState: { transparency: Transparency.Empty },
             info: structure,
             source: a
-        }, { label: `Transparency (${transparency.value})` });
+        }, { label: `Transparency (${transparency.layers.length} Layers)` });
     },
     update({ a, b, newParams, oldParams }) {
         const structure = b.data.info as Structure;
         if (a.data.source.data !== structure) return StateTransformer.UpdateResult.Recreate;
         const oldTransparency = b.data.state.transparency!;
-        const newTransparency = Transparency.ofScript(newParams.script, newParams.value, newParams.variant, structure);
+        const newTransparency = Transparency.ofScript(newParams.layers, structure);
         if (Transparency.areEqual(oldTransparency, newTransparency)) return StateTransformer.UpdateResult.Unchanged;
 
         b.data.state.transparency = newTransparency;
         b.data.source = a;
-        b.label = `Transparency (${newTransparency.value})`;
+        b.label = `Transparency (${newTransparency.layers.length} Layers)`;
         return StateTransformer.UpdateResult.Updated;
     }
 });
@@ -409,9 +415,16 @@ const TransparencyStructureRepresentation3DFromBundle = PluginStateTransform.Bui
     from: SO.Molecule.Structure.Representation3D,
     to: SO.Molecule.Structure.Representation3DState,
     params: {
-        bundle: PD.Value<StructureElement.Bundle>(StructureElement.Bundle.Empty),
-        value: PD.Numeric(0.75, { min: 0, max: 1, step: 0.01 }, { label: 'Transparency' }),
-        variant: PD.Select('single', [['single', 'Single-layer'], ['multi', 'Multi-layer']] as ['single' | 'multi', string][])
+        layers: PD.ObjectList({
+            bundle: PD.Value<StructureElement.Bundle>(StructureElement.Bundle.Empty),
+            value: PD.Numeric(0.5, { min: 0, max: 1, step: 0.01 }, { label: 'Transparency' }),
+        }, e => `Transparency (${e.value})`, {
+            defaultValue: [{
+                bundle: StructureElement.Bundle.Empty,
+                value: 0.5,
+            }],
+            isHidden: true
+        })
     }
 })({
     canAutoUpdate() {
@@ -419,25 +432,25 @@ const TransparencyStructureRepresentation3DFromBundle = PluginStateTransform.Bui
     },
     apply({ a, params }) {
         const structure = a.data.source.data;
-        const transparency = Transparency.ofBundle(params.bundle, params.value, params.variant, structure);
+        const transparency = Transparency.ofBundle(params.layers, structure);
 
         return new SO.Molecule.Structure.Representation3DState({
             state: { transparency },
             initialState: { transparency: Transparency.Empty },
             info: structure,
             source: a
-        }, { label: `Transparency (${transparency.value})` });
+        }, { label: `Transparency (${transparency.layers.length} Layers)` });
     },
     update({ a, b, newParams, oldParams }) {
         const structure = b.data.info as Structure;
         if (a.data.source.data !== structure) return StateTransformer.UpdateResult.Recreate;
         const oldTransparency = b.data.state.transparency!;
-        const newTransparency = Transparency.ofBundle(newParams.bundle, newParams.value, newParams.variant, structure);
+        const newTransparency = Transparency.ofBundle(newParams.layers, structure);
         if (Transparency.areEqual(oldTransparency, newTransparency)) return StateTransformer.UpdateResult.Unchanged;
 
         b.data.state.transparency = newTransparency;
         b.data.source = a;
-        b.label = `Transparency (${newTransparency.value})`;
+        b.label = `Transparency (${newTransparency.layers.length} Layers)`;
         return StateTransformer.UpdateResult.Updated;
     }
 });

+ 6 - 1
src/mol-repr/structure/complex-representation.ts

@@ -19,6 +19,7 @@ import { MarkerAction, MarkerActions } from '../../mol-util/marker-action';
 import { Overpaint } from '../../mol-theme/overpaint';
 import { StructureParams } from './params';
 import { Clipping } from '../../mol-theme/clipping';
+import { Transparency } from '../../mol-theme/transparency';
 
 export function ComplexRepresentation<P extends StructureParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: (materialId: number) => ComplexVisual<P>): StructureRepresentation<P> {
     let version = 0;
@@ -94,7 +95,11 @@ export function ComplexRepresentation<P extends StructureParams>(label: string,
             const remappedOverpaint = Overpaint.remap(state.overpaint, _structure);
             visual.setOverpaint(remappedOverpaint);
         }
-        if (state.transparency !== undefined && visual) visual.setTransparency(state.transparency);
+        if (state.transparency !== undefined && visual) {
+            // Remap loci from equivalent structure to the current structure
+            const remappedTransparency = Transparency.remap(state.transparency, _structure);
+            visual.setTransparency(remappedTransparency);
+        }
         if (state.clipping !== undefined && visual) {
             // Remap loci from equivalent structure to the current structure
             const remappedClipping = Clipping.remap(state.clipping, _structure);

+ 3 - 1
src/mol-repr/structure/units-representation.ts

@@ -224,7 +224,9 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
             }
         }
         if (transparency !== undefined && !Transparency.areEqual(transparency, _state.transparency)) {
-            newState.transparency = transparency;
+            if (_structure) {
+                newState.transparency = Transparency.remap(transparency, _structure);
+            }
         }
         if (clipping !== undefined && !Clipping.areEqual(clipping, _state.clipping)) {
             if (_structure) {

+ 10 - 10
src/mol-repr/visual.ts

@@ -112,22 +112,22 @@ namespace Visual {
         const { tTransparency, uGroupCount, instanceCount } = renderObject.values;
         const count = uGroupCount.ref.value * instanceCount.ref.value;
 
-        const { loci, value, variant } = transparency;
-
         // ensure texture has right size and variant
-        createTransparency(value && !isEmptyLoci(loci) ? count : 0, variant, renderObject.values);
+        createTransparency(transparency.layers.length ? count : 0, renderObject.values);
         const { array } = tTransparency.ref.value;
 
         // clear if requested
         if (clear) clearTransparency(array, 0, count);
 
-        const apply = (interval: Interval) => {
-            const start = Interval.start(interval);
-            const end = Interval.end(interval);
-            return applyTransparencyValue(array, start, end, value);
-        };
-        lociApply(loci, apply, false);
-
+        for (let i = 0, il = transparency.layers.length; i < il; ++i) {
+            const { loci, value } = transparency.layers[i];
+            const apply = (interval: Interval) => {
+                const start = Interval.start(interval);
+                const end = Interval.end(interval);
+                return applyTransparencyValue(array, start, end, value);
+            };
+            lociApply(loci, apply, false);
+        }
         ValueCell.update(tTransparency, tTransparency.ref.value);
     }
 

+ 101 - 14
src/mol-theme/transparency.ts

@@ -1,37 +1,124 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Loci, EmptyLoci } from '../mol-model/loci';
+import { Loci } from '../mol-model/loci';
 import { StructureElement, Structure } from '../mol-model/structure';
 import { Script } from '../mol-script/script';
 
 export { Transparency };
 
-interface Transparency {
-    readonly loci: Loci
-    readonly value: number
-    readonly variant: Transparency.Variant
+type Transparency = { readonly layers: ReadonlyArray<Transparency.Layer> }
+
+function Transparency(layers: ReadonlyArray<Transparency.Layer>): Transparency {
+    return { layers };
 }
 
 namespace Transparency {
+    export type Layer = { readonly loci: StructureElement.Loci, readonly value: number }
+    export const Empty: Transparency = { layers: [] };
+
     export type Variant = 'single' | 'multi'
-    export const Empty: Transparency = { loci: EmptyLoci, value: 0, variant: 'single' };
 
     export function areEqual(tA: Transparency, tB: Transparency) {
-        if (tA.value !== tB.value) return false;
-        if (tA.variant !== tB.variant) return false;
-        if (!Loci.areEqual(tA.loci, tB.loci)) return false;
+        if (tA.layers.length === 0 && tB.layers.length === 0) return true;
+        if (tA.layers.length !== tB.layers.length) return false;
+        for (let i = 0, il = tA.layers.length; i < il; ++i) {
+            if (tA.layers[i].value !== tB.layers[i].value) return false;
+            if (!Loci.areEqual(tA.layers[i].loci, tB.layers[i].loci)) return false;
+        }
         return true;
     }
 
-    export function ofScript(script: Script, value: number, variant: Variant, structure: Structure): Transparency {
-        return { loci: Script.toLoci(script, structure), value, variant };
+    export function isEmpty(transparency: Transparency) {
+        return transparency.layers.length === 0;
+    }
+
+    export function remap(transparency: Transparency, structure: Structure) {
+        const layers: Transparency.Layer[] = [];
+        for (const layer of transparency.layers) {
+            let { loci, value } = layer;
+            loci = StructureElement.Loci.remap(loci, structure);
+            if (!StructureElement.Loci.isEmpty(loci)) {
+                layers.push({ loci, value });
+            }
+        }
+        return { layers };
+    }
+
+    export function merge(transparency: Transparency): Transparency {
+        if (isEmpty(transparency)) return transparency;
+        const { structure } = transparency.layers[0].loci;
+        const map = new Map<number, StructureElement.Loci>();
+        let shadowed = StructureElement.Loci.none(structure);
+        for (let i = 0, il = transparency.layers.length; i < il; ++i) {
+            let { loci, value } = transparency.layers[il - i - 1]; // process from end
+            loci = StructureElement.Loci.subtract(loci, shadowed);
+            shadowed = StructureElement.Loci.union(loci, shadowed);
+            if (!StructureElement.Loci.isEmpty(loci)) {
+                if (map.has(value)) {
+                    loci = StructureElement.Loci.union(loci, map.get(value)!);
+                }
+                map.set(value, loci);
+            }
+        }
+        const layers: Transparency.Layer[] = [];
+        map.forEach((loci, value) => {
+            layers.push({ loci, value });
+        });
+        return { layers };
+    }
+
+    export function filter(transparency: Transparency, filter: Structure): Transparency {
+        if (isEmpty(transparency)) return transparency;
+        const { structure } = transparency.layers[0].loci;
+        const layers: Transparency.Layer[] = [];
+        for (const layer of transparency.layers) {
+            let { loci, value } = layer;
+            // filter by first map to the `filter` structure and
+            // then map back to the original structure of the transparency loci
+            const filtered = StructureElement.Loci.remap(loci, filter);
+            loci = StructureElement.Loci.remap(filtered, structure);
+            if (!StructureElement.Loci.isEmpty(loci)) {
+                layers.push({ loci, value });
+            }
+        }
+        return { layers };
+    }
+
+    export type ScriptLayer = { script: Script, value: number }
+    export function ofScript(scriptLayers: ScriptLayer[], structure: Structure): Transparency {
+        const layers: Transparency.Layer[] = [];
+        for (let i = 0, il = scriptLayers.length; i < il; ++i) {
+            const { script, value } = scriptLayers[i];
+            const loci = Script.toLoci(script, structure);
+            if (!StructureElement.Loci.isEmpty(loci)) {
+                layers.push({ loci, value });
+            }
+        }
+        return { layers };
+    }
+
+    export type BundleLayer = { bundle: StructureElement.Bundle, value: number }
+    export function ofBundle(bundleLayers: BundleLayer[], structure: Structure): Transparency {
+        const layers: Transparency.Layer[] = [];
+        for (let i = 0, il = bundleLayers.length; i < il; ++i) {
+            const { bundle, value } = bundleLayers[i];
+            const loci = StructureElement.Bundle.toLoci(bundle, structure.root);
+            layers.push({ loci, value });
+        }
+        return { layers };
     }
 
-    export function ofBundle(bundle: StructureElement.Bundle, value: number, variant: Variant, structure: Structure): Transparency {
-        return { loci: StructureElement.Bundle.toLoci(bundle, structure), value, variant };
+    export function toBundle(transparency: Transparency) {
+        const layers: BundleLayer[] = [];
+        for (let i = 0, il = transparency.layers.length; i < il; ++i) {
+            let { loci, value } = transparency.layers[i];
+            const bundle = StructureElement.Bundle.fromLoci(loci);
+            layers.push({ bundle, value });
+        }
+        return { layers };
     }
 }